Skip to content
Open
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_broken_symlink_error, is_same_file_as_output,
Expand Down Expand Up @@ -89,7 +89,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},
utils::{
self, BytesFmt, FileVisibilityPolicy, PathFmt, create_symlink, is_broken_symlink_error, is_same_file_as_output,
set_permission_mode,
Expand Down Expand Up @@ -102,13 +102,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<FileType> {
Ok(match header.entry_type() {
tar::EntryType::Directory => FileType::Directory,
tar::EntryType::Symlink => file
.link_name()?
.map(|t| FileType::Symlink { target: t.into_owned() })
.unwrap_or(FileType::File),
tar::EntryType::Link => file
.link_name()?
.map(|t| FileType::Hardlink { target: t.into_owned() })
.unwrap_or(FileType::File),
_ => FileType::File,
})
}

/// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
pub fn build_archive<W>(
input_filenames: &[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 @@ -18,7 +18,7 @@ use crate::{
Result,
error::FinalError,
info, info_accessible,
list::FileInArchive,
list::{FileInArchive, FileType},
utils::{
BytesFmt, FileVisibilityPolicy, PathFmt, cd_into_same_dir_as, create_symlink, ensure_parent_dir_exists,
get_invalid_utf8_paths, is_broken_symlink_error, is_same_file_as_output, pretty_format_list_of_paths,
Expand Down Expand Up @@ -118,15 +118,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() {
FileType::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()))
})
{
FileType::Symlink { target }
} else {
FileType::File
};

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

Expand Down
89 changes: 58 additions & 31 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ pub struct ListOptions {
pub tree: 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()`
#[derive(Debug, Clone)]
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 @@ -41,40 +49,58 @@ pub fn list_files(
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);
}
}
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) {
use crate::utils::colors::*;

if !is_dir {
// Not a directory -> just print the file name
let _ = writeln!(out, "{name}");
return;
}

// Handle directory display
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)
};
match file_type {
FileType::File => {
let _ = writeln!(out, "{name}");
}
FileType::Symlink { target } | FileType::Hardlink { target } => {
if is_running_in_accessible_mode() {
// Accessible mode: use "->" for screen readers
let _ = writeln!(out, "{} -> {}", name, target.display());
Copy link
Copy Markdown
Member

@marcospb19 marcospb19 Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we added ->, now ouch list can't be used in scripts to provide a file list 🤔 .

Can you add a check for, when --quiet is set but --tree isn't, we don't print out the arrow nor the target? This way you can still ouch list -q | xargs rm -i even if there is a symlink in it.

Let me know if you think there is a better and simpler approach, to allow for -> but keeping this compatible with scripts.

} else {
// Normal mode: use "->" with colors
let _ = writeln!(
out,
"{}{}{} -> {}{}{}",
Copy link
Copy Markdown
Member

@marcospb19 marcospb19 Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if for hardlink we do:

A -> B (hardlink)

instead of

A -> B

To be explicit, I can't think of symbols that could replace the " (hardlink)" suffix

Copy link
Copy Markdown
Contributor Author

@tommady tommady Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we display hard links with =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not the best idea since only me and you would be able to tell that -> means symlink and => means hardlink.

specially assuming the user will see one or the other, usually, and not both, so they'll just assume it's a symlink I believe

Copy link
Copy Markdown
Contributor Author

@tommady tommady Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since tar itself uses link to to represent hard links:

tar -tvf example.tar
drwxr-xr-x 1000/1000         0 2026-03-06 05:17 example
-rw-r--r-- 1000/1000         0 2026-03-05 08:42 example/hard_link
h--------- 0/0               0 1970-01-01 00:00 example/file link to example/hard_link

maybe we could just stick with the same wording.
WDYT?

Copy link
Copy Markdown
Member

@marcospb19 marcospb19 Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still prefer the -> symbol for symlinks and -> + (hardlink) :v sorry

Similar to what tree -a does in the CLI (the one I always download from pacman).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

roger that~

*CYAN,
name,
*ALL_RESET,
*CYAN,
target.display(),
*ALL_RESET
);
}
}
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 +117,7 @@ mod tree {
use bstr::{ByteSlice, ByteVec};
use linked_hash_map::LinkedHashMap;

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

/// Directory tree
Expand Down Expand Up @@ -151,11 +177,12 @@ 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);

// 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