Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/archive/rar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use unrar::Archive;
use crate::{
error::{Error, Result},
info,
list::FileInArchive,
list::{FileInArchive, FileType},
utils::BytesFmt,
};

Expand Down Expand Up @@ -55,7 +55,10 @@ pub fn list_archive(
let is_dir = item.is_directory();
let path = item.filename;

Ok(FileInArchive { path, is_dir })
Ok(FileInArchive {
path,
file_type: if is_dir { FileType::Directory } else { FileType::File },
})
}))
}

Expand Down
8 changes: 6 additions & 2 deletions src/archive/sevenz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
Result,
error::{Error, FinalError},
info,
list::FileInArchive,
list::{FileInArchive, FileType},
utils::{
BytesFmt, FileVisibilityPolicy, PathFmt, cd_into_same_dir_as, ensure_parent_dir_exists, is_same_file_as_output,
},
Expand Down Expand Up @@ -88,7 +88,11 @@ where
let entry_extract_fn = |entry: &ArchiveEntry, _: &mut dyn Read, _: &PathBuf| {
files.push(Ok(FileInArchive {
path: entry.name().into(),
is_dir: entry.is_directory(),
file_type: if entry.is_directory() {
FileType::Directory
} else {
FileType::File
},
}));
Ok(true)
};
Expand Down
21 changes: 18 additions & 3 deletions src/archive/tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
Result,
error::FinalError,
info,
list::FileInArchive,
list::{FileInArchive, FileType as ListFileType},
utils::{
self, BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonicalize, create_symlink, is_same_file_as_output,
read_file_type, set_permission_mode,
Expand Down Expand Up @@ -103,13 +103,28 @@ pub fn list_archive(mut archive: tar::Archive<impl Read>) -> Result<impl Iterato
let entries = archive.entries()?.map(|file| {
let file = file?;
let path = file.path()?.into_owned();
let is_dir = file.header().entry_type().is_dir();
Ok(FileInArchive { path, is_dir })
let file_type = get_file_type(file.header(), &file)?;
Ok(FileInArchive { path, file_type })
});

Ok(entries.collect::<Vec<_>>().into_iter())
}

fn get_file_type(header: &tar::Header, file: &tar::Entry<impl Read>) -> Result<ListFileType> {
Ok(match header.entry_type() {
tar::EntryType::Directory => ListFileType::Directory,
tar::EntryType::Symlink => file
.link_name()?
.map(|t| ListFileType::Symlink { target: t.into_owned() })
.unwrap_or(ListFileType::File),
tar::EntryType::Link => file
.link_name()?
.map(|t| ListFileType::Hardlink { target: t.into_owned() })
.unwrap_or(ListFileType::File),
_ => ListFileType::File,
})
}

/// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
pub fn build_archive<W>(
explicit_paths: &[PathBuf],
Expand Down
24 changes: 20 additions & 4 deletions src/archive/zip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::{
Result,
error::FinalError,
info, info_accessible,
list::FileInArchive,
list::{FileInArchive, FileType as ListFileType},
utils::{
BytesFmt, FileType, FileVisibilityPolicy, PathFmt, canonicalize, cd_into_same_dir_as, create_symlink,
ensure_parent_dir_exists, get_invalid_utf8_paths, is_same_file_as_output, pretty_format_list_of_paths,
Expand Down Expand Up @@ -119,15 +119,31 @@ where
None => archive.by_index(idx),
};

let file = match zip_result {
let mut file = match zip_result {
Ok(f) => f,
Err(e) => return Err(e.into()),
};

let path = file.enclosed_name().unwrap_or_else(|| file.mangled_name()).to_owned();
let is_dir = file.is_dir();

Ok(FileInArchive { path, is_dir })
let file_type = if file.is_dir() {
ListFileType::Directory
} else if let Some(target) = file
.unix_mode()
.filter(|mode| mode & 0o170000 == 0o120000)
.and_then(|_| {
let mut s = Vec::new();
file.read_to_end(&mut s)
.ok()
.map(|_| PathBuf::from(String::from_utf8_lossy(&s).into_owned()))
})
{
ListFileType::Symlink { target }
} else {
ListFileType::File
};

Ok(FileInArchive { path, file_type })
})
}

Expand Down
7 changes: 5 additions & 2 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,13 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
// Ensure we were not told to list the content of a non-archive compressed file
check::check_for_non_archive_formats(&files, &formats)?;

let list_options = ListOptions { tree };
let list_options = ListOptions {
tree,
quiet: args.quiet,
};

for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
if i > 0 {
if i > 0 && !args.quiet {
println!();
}
let formats = extension::flatten_compression_formats(&formats);
Expand Down
106 changes: 76 additions & 30 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ use crate::{Result, accessible::is_running_in_accessible_mode, utils::PathFmt};
pub struct ListOptions {
/// Whether to show a tree view
pub tree: bool,

/// Whether to suppress extra output like symlink targets (for scripting)
pub quiet: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileType {
File,
Directory,
Symlink { target: PathBuf },
Hardlink { target: PathBuf },
}

/// Represents a single file in an archive, used in `list::list_files()`
Expand All @@ -22,8 +33,8 @@ pub struct FileInArchive {
/// The file path
pub path: PathBuf,

/// Whether this file is a directory
pub is_dir: bool,
/// The type of file
pub file_type: FileType,
}

/// Actually print the files
Expand All @@ -34,47 +45,76 @@ pub fn list_files(
list_options: ListOptions,
) -> Result<()> {
let mut out = BufWriter::new(stdout().lock());
let _ = writeln!(out, "Archive: {}", PathFmt(archive));

if !list_options.quiet {
let _ = writeln!(out, "Archive: {}", PathFmt(archive));
}

if list_options.tree {
let tree = files.into_iter().collect::<Result<Tree>>()?;
tree.print(&mut out);
} else {
for file in files {
let FileInArchive { path, is_dir } = file?;
print_entry(&mut out, path.display(), is_dir);
let FileInArchive { path, file_type } = file?;
print_entry(&mut out, path.display(), &file_type, list_options.quiet);
}
}
Ok(())
}

/// Print an entry and highlight directories, either by coloring them
/// if that's supported or by adding a trailing /
fn print_entry(out: &mut impl Write, name: impl fmt::Display, is_dir: bool) {
fn print_entry(out: &mut impl Write, name: impl fmt::Display, file_type: &FileType, quiet: bool) {
use crate::utils::colors::*;

if !is_dir {
// Not a directory -> just print the file name
let _ = writeln!(out, "{name}");
return;
}
match file_type {
FileType::File => {
let _ = writeln!(out, "{name}");
}
FileType::Symlink { target } | FileType::Hardlink { target } => {
if quiet {
// In quiet mode, just print the name (like a regular file)
// This allows scripts to process the list without parsing arrows
let _ = writeln!(out, "{}{name}{}", *CYAN, *ALL_RESET);
return;
}

// Handle directory display
let name_str = name.to_string();
let display_name = name_str.strip_suffix('/').unwrap_or(&name_str);
let suffix = if matches!(file_type, FileType::Hardlink { .. }) {
" (hardlink)"
} else {
""
};

let output = if BLUE.is_empty() {
// Colors are deactivated, print final / to mark directories
format!("{display_name}/")
} else if is_running_in_accessible_mode() {
// Accessible mode: use colors but print final / for screen readers
format!("{}{}{}/{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
} else {
// Normal mode: use colors without trailing slash
format!("{}{}{}{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
};
if is_running_in_accessible_mode() {
let _ = writeln!(out, "{name} -> {}{suffix}", target.display());
} else {
let _ = writeln!(
out,
"{c}{name}{r} {c}-> {c}{target}{suffix}{r}",
c = *CYAN,
r = *ALL_RESET,
target = target.display()
);
}
}
FileType::Directory => {
let name_str = name.to_string();
let display_name = name_str.strip_suffix('/').unwrap_or(&name_str);

let output = if BLUE.is_empty() {
// Colors are deactivated, print final / to mark directories
format!("{display_name}/")
} else if is_running_in_accessible_mode() {
// Accessible mode: use colors but print final / for screen readers
format!("{}{}{}/{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
} else {
// Normal mode: use colors without trailing slash
format!("{}{}{}{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
};

let _ = writeln!(out, "{output}");
let _ = writeln!(out, "{output}");
}
}
}

/// Since archives store files as a list of entries -> without direct
Expand All @@ -91,7 +131,7 @@ mod tree {
use bstr::{ByteSlice, ByteVec};
use linked_hash_map::LinkedHashMap;

use super::FileInArchive;
use super::{FileInArchive, FileType};
use crate::{utils::NoQuotePathFmt, warning};

/// Directory tree
Expand Down Expand Up @@ -151,11 +191,17 @@ mod tree {
};

let _ = write!(out, "{prefix}{final_part}");
let is_dir = match self.file {
Some(FileInArchive { is_dir, .. }) => is_dir,
None => true,
let file_type = match &self.file {
Some(FileInArchive { file_type, .. }) => file_type.clone(),
// If we don't have a file entry but have children, it's an implicit directory
None => FileType::Directory,
};
super::print_entry(out, <Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(), is_dir);
super::print_entry(
out,
<Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(),
&file_type,
false, // Always show targets in tree view, regardless of --quiet flag
);

// Construct prefix for children, adding either a line if this isn't
// the last entry in the parent dir or empty space if it is.
Expand Down
Loading