From 2478a238298717b065666981566863aee1b28431 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 6 Feb 2024 21:27:26 +0100 Subject: [PATCH 01/13] feat(archive): Add support for tar archive inspection --- Cargo.lock | 50 +-- Cargo.toml | 1 + src/fs/archive.rs | 488 ++++++++++++++++++++++++++++++ src/fs/fields.rs | 50 +++ src/fs/file.rs | 368 +++++++++++----------- src/fs/filelike.rs | 220 ++++++++++++++ src/fs/filter.rs | 46 +-- src/fs/mod.rs | 6 + src/info/filetype.rs | 21 +- src/info/sources.rs | 24 +- src/main.rs | 126 ++++++-- src/options/archive_inspection.rs | 19 ++ src/options/flags.rs | 3 +- src/options/mod.rs | 6 + src/output/color_scale.rs | 28 +- src/output/details.rs | 42 +-- src/output/file_name.rs | 79 +++-- src/output/grid.rs | 10 +- src/output/grid_details.rs | 22 +- src/output/icons.rs | 24 +- src/output/lines.rs | 12 +- src/output/table.rs | 32 +- src/theme/mod.rs | 43 ++- 23 files changed, 1332 insertions(+), 388 deletions(-) create mode 100644 src/fs/archive.rs create mode 100644 src/fs/filelike.rs create mode 100644 src/options/archive_inspection.rs diff --git a/Cargo.lock b/Cargo.lock index 02b9fcf9a..7656b0f02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,23 +349,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -391,6 +380,7 @@ dependencies = [ "plist", "proc-mounts", "rayon", + "tar", "terminal_size", "timeago", "trycmd", @@ -616,9 +606,9 @@ checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "locale" @@ -992,15 +982,15 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1124,6 +1114,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.8.0" @@ -1602,6 +1603,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "zoneinfo_compiled" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index bc3a08472..6fb7e1093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ percent-encoding = "2.3.1" phf = { version = "0.11.2", features = ["macros"] } plist = { version = "1.6.1", default-features = false } uutils_term_grid = "0.6.0" +tar = "0.4.40" terminal_size = "0.3.0" timeago = { version = "0.4.2", default-features = false } unicode-width = "0.1" diff --git a/src/fs/archive.rs b/src/fs/archive.rs new file mode 100644 index 000000000..8202840d0 --- /dev/null +++ b/src/fs/archive.rs @@ -0,0 +1,488 @@ +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::slice::Iter as SliceIter; + +use chrono::NaiveDateTime; + +use crate::fs::feature::xattr::Attribute; +use crate::fs::fields as f; +use crate::fs::file::FileTarget; +use crate::fs::{Dir, File, Filelike}; + +use super::mounts::MountedFs; + +#[derive(Clone)] +pub struct Owner { + pub id: u64, + pub name: Option, +} + +#[derive(Clone)] +pub struct ArchiveEntry { + name: String, + path: PathBuf, + size: u64, + permissions: Option, + user: Option, + group: Option, + is_directory: bool, + is_link: bool, + link_target: Option, + mtime: Option, + atime: Option, + ctime: Option, +} + +impl Filelike for ArchiveEntry { + fn path(&self) -> &PathBuf { + &self.path + } + + fn name(&self) -> &String { + &self.name + } + + fn extension(&self) -> Option { + File::extension(&self.path) + } + + fn deref_links(&self) -> bool { + false + } + + fn extended_attributes(&self) -> &[Attribute] { + &[] + } + + fn metadata(&self) -> Option<&std::fs::Metadata> { + None + } + + fn parent_directory(&self) -> Option<&Dir> { + None + } + + fn to_dir(&self) -> Option> { + None + } + + fn is_directory(&self) -> bool { + self.is_directory + } + + fn points_to_directory(&self) -> bool { + // symlinks in archive will always be handled as broken links, + // thus no link will ever be a directory + self.is_directory + } + + fn is_file(&self) -> bool { + !self.is_link && !self.is_directory + } + + #[cfg(unix)] + fn is_executable_file(&self) -> bool { + false + } + + fn is_link(&self) -> bool { + self.is_link + } + + #[cfg(unix)] + fn is_pipe(&self) -> bool { + false + } + + #[cfg(unix)] + fn is_char_device(&self) -> bool { + false + } + + #[cfg(unix)] + fn is_block_device(&self) -> bool { + false + } + + #[cfg(unix)] + fn is_socket(&self) -> bool { + false + } + + fn absolute_path(&self) -> Option<&PathBuf> { + // TODO: could be argued that this should also include path to archive; + // but that would be kind of ugly to implement since every ArchiveEntry + // either needs to store the entire path or keep a reference to the + // archive which would then have to have mutable content (since it has + // to be constructed before any entry is created); thus, I think this + // behavior is sufficient + Some(&self.path) + } + + fn is_mount_point(&self) -> bool { + false + } + + fn mount_point_info(&self) -> Option<&MountedFs> { + None + } + + fn link_target<'a>(&self) -> FileTarget<'a> { + if let Some(link_target) = &self.link_target { + FileTarget::Broken(link_target.clone()) + } else { + FileTarget::Err(io::Error::new(io::ErrorKind::Other, "no link target")) + } + } + + fn link_target_recurse<'a>(&self) -> FileTarget<'a> { + self.link_target() + } + + #[cfg(unix)] + fn links(&self) -> f::Links { + f::Links { + count: 0, + multiple: false, + } + } + + #[cfg(unix)] + fn inode(&self) -> f::Inode { + // inode 0 can be used to indicate that there is no inode + f::Inode(0) + } + + #[cfg(unix)] + fn blocksize(&self) -> f::Blocksize { + f::Blocksize::None + } + + #[cfg(unix)] + fn user(&self) -> Option { + self.user.as_ref().map(|o| f::User(o.id as u32)) + } + + #[cfg(unix)] + fn group(&self) -> Option { + self.group.as_ref().map(|o| f::Group(o.id as u32)) + } + + fn size(&self) -> f::Size { + if self.is_directory || self.is_link { + f::Size::None + } else { + f::Size::Some(self.size) + } + } + + fn length(&self) -> u64 { + self.size + } + + fn is_recursive_size(&self) -> bool { + false + } + + fn is_empty_dir(&self) -> bool { + // TODO: could check if there is any other entry in archive with "{path}/" as prefix; + // but kind of expensive for very little benefit + false + } + + fn modified_time(&self) -> Option { + NaiveDateTime::from_timestamp_opt(self.mtime? as i64, 0) + } + + fn changed_time(&self) -> Option { + NaiveDateTime::from_timestamp_opt(self.ctime? as i64, 0) + } + + fn accessed_time(&self) -> Option { + NaiveDateTime::from_timestamp_opt(self.atime? as i64, 0) + } + + fn created_time(&self) -> Option { + None + } + + #[cfg(unix)] + fn type_char(&self) -> f::Type { + if self.is_link { + f::Type::Link + } else if self.is_directory { + f::Type::Directory + } else { + f::Type::File + } + } + + #[cfg(unix)] + fn permissions(&self) -> Option { + self.permissions + } + + #[cfg(windows)] + fn attributes(&self) -> f::Attributes { + f::Attributes { + archive: false, + directory: false, + readonly: true, + hidden: false, + system: false, + reparse_point: false, + } + } + + #[cfg(unix)] + fn security_context(&self) -> f::SecurityContext<'_> { + f::SecurityContext { + context: f::SecurityContextType::None, + } + } + + fn flags(&self) -> f::Flags { + f::Flags(0) + } +} + +impl AsRef for ArchiveEntry { + fn as_ref(&self) -> &ArchiveEntry { + self + } +} + +pub enum ArchiveFormat { + Tar, + Unknown, +} + +trait ArchiveReader { + fn read_dir(path: &Path) -> io::Result>>; +} + +struct TarReader {} + +impl TarReader { + /// Get size of entry; the size written in the header field takes precedence + pub fn size(entry: &tar::Entry<'_, R>) -> u64 { + entry.header().size().unwrap_or(entry.size()) + } + + pub fn path(entry: &tar::Entry<'_, R>) -> io::Result { + let mut path = entry.header().path(); + if path.is_err() { + path = entry.path(); + } + path.map(|p| p.to_path_buf()) + } + + pub fn is_directory(entry: &tar::Entry<'_, R>) -> bool { + entry.header().entry_type().is_dir() + } + + pub fn is_link(entry: &tar::Entry<'_, R>) -> bool { + entry.header().entry_type().is_symlink() + } + + pub fn link_target(entry: &tar::Entry<'_, R>) -> io::Result> { + entry + .header() + .link_name() + .map(|o| o.map(|p| p.to_path_buf())) + } + + pub fn uid(entry: &tar::Entry<'_, R>) -> io::Result { + entry.header().uid() + } + + pub fn gid(entry: &tar::Entry<'_, R>) -> io::Result { + entry.header().gid() + } + + pub fn username( + entry: &tar::Entry<'_, R>, + ) -> Result, std::str::Utf8Error> { + entry.header().username().map(|o| o.map(str::to_owned)) + } + + pub fn groupname( + entry: &tar::Entry<'_, R>, + ) -> Result, std::str::Utf8Error> { + entry.header().groupname().map(|o| o.map(str::to_owned)) + } + + pub fn permissions(entry: &tar::Entry<'_, R>) -> io::Result { + let mode = entry.header().mode()?; + Ok(f::Permissions::from_mode(mode)) + } + + pub fn mtime(entry: &tar::Entry<'_, R>) -> io::Result { + entry.header().mtime() + } + + pub fn atime(entry: &tar::Entry<'_, R>) -> io::Result { + entry + .header() + .as_gnu() + .ok_or(io::Error::new( + io::ErrorKind::Unsupported, + "archive header does not support atime", + )) + .and_then(tar::GnuHeader::atime) + } + + pub fn ctime(entry: &tar::Entry<'_, R>) -> io::Result { + entry + .header() + .as_gnu() + .ok_or(io::Error::new( + io::ErrorKind::Unsupported, + "archive header does not support ctime", + )) + .and_then(tar::GnuHeader::ctime) + } + + pub fn tar_entry(entry: &tar::Entry<'_, R>) -> Result { + let path = TarReader::path(entry); + match path { + Ok(path) => Ok(ArchiveEntry { + name: File::filename(&path), + path, + size: TarReader::size(entry), + user: Some(Owner { + id: TarReader::uid(entry)?, + name: TarReader::username(entry)?, + }), + group: Some(Owner { + id: TarReader::gid(entry)?, + name: TarReader::groupname(entry)?, + }), + permissions: Some(TarReader::permissions(entry)?), + mtime: Some(TarReader::mtime(entry)?), + atime: TarReader::atime(entry).ok(), + ctime: TarReader::ctime(entry).ok(), + link_target: TarReader::link_target(entry)?, + is_link: TarReader::is_link(entry), + is_directory: TarReader::is_directory(entry), + }), + Err(e) => Err(e.into()), + } + } +} + +impl ArchiveReader for TarReader { + fn read_dir(path: &Path) -> io::Result>> { + let mut result = Vec::new(); + let file_content = fs::File::open(path)?; + tar::Archive::new(file_content).entries().map(|entries| { + for entry in entries { + match entry { + Ok(entry) => result.push(TarReader::tar_entry(&entry)), + Err(error) => result.push(Err(error.into())), + } + } + })?; + Ok(result) + } +} + +impl ArchiveFormat { + pub fn from_extension(extension: &str) -> Option { + match extension { + "tar" => Some(ArchiveFormat::Tar), + _ => None, + } + } +} + +pub struct Error { + message: String, +} + +impl From for Error { + fn from(value: E) -> Self { + let full_message = value.to_string(); + let mut lines = full_message.lines(); + let mut message = lines.next().unwrap_or("").to_owned(); + if lines.next().is_some() { + message += "..."; + } + Error { message } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + fmt.write_str(self.message.as_str()) + } +} + +pub struct Archive { + pub format: ArchiveFormat, + pub path: PathBuf, + + contents: Vec>, +} + +#[derive(Clone)] +pub struct ArchiveIterator<'archive> { + inner: SliceIter<'archive, Result>, + /// Path in archive whose content is iterated over + path: PathBuf, +} + +impl<'archive> Iterator for ArchiveIterator<'archive> { + type Item = &'archive Result; + + fn next(&mut self) -> Option { + while let Some(it) = self.inner.next() { + if it.is_err() + || it.as_ref().is_ok_and(|x| { + if let Some(p) = x.path.parent() { + p == self.path + } else { + false + } + }) + { + return Some(it); + } + } + None + } +} + +impl Archive { + pub fn from_path(path: PathBuf) -> io::Result { + let extension = File::extension(path.as_path()).unwrap_or(String::new()); + let format = + ArchiveFormat::from_extension(extension.as_str()).unwrap_or(ArchiveFormat::Unknown); + let contents = match format { + ArchiveFormat::Tar => TarReader::read_dir(&path), + ArchiveFormat::Unknown => { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "Unsupported archive format", + )) + } + }?; + // TODO: could check if any in `contents` is Err and then + // return Err for silent fail + Ok(Archive { + format, + path, + contents, + }) + } + + /// Produce an iterator of IO results of trying to read all the files in + /// this directory. + pub fn files(&self, root: PathBuf) -> ArchiveIterator<'_> { + ArchiveIterator { + inner: self.contents.iter(), + path: root, + } + } +} diff --git a/src/fs/fields.rs b/src/fs/fields.rs index 8a0c1c362..146361e7b 100644 --- a/src/fs/fields.rs +++ b/src/fs/fields.rs @@ -79,6 +79,56 @@ pub struct Permissions { pub setuid: bool, } +/// More readable aliases for the permission bits exposed by libc. +#[allow(trivial_numeric_casts)] +#[cfg(unix)] +mod modes { + + // The `libc::mode_t` type’s actual type varies, but the value returned + // from `metadata.permissions().mode()` is always `u32`. + pub type Mode = u32; + + pub const USER_READ: Mode = libc::S_IRUSR as Mode; + pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; + pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; + + pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; + pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; + pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; + + pub const OTHER_READ: Mode = libc::S_IROTH as Mode; + pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; + pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; + + pub const STICKY: Mode = libc::S_ISVTX as Mode; + pub const SETGID: Mode = libc::S_ISGID as Mode; + pub const SETUID: Mode = libc::S_ISUID as Mode; +} + +impl Permissions { + pub fn from_mode(mode: u32) -> Permissions { + let has_bit = |bit| mode & bit == bit; + + Permissions { + user_read: has_bit(modes::USER_READ), + user_write: has_bit(modes::USER_WRITE), + user_execute: has_bit(modes::USER_EXECUTE), + + group_read: has_bit(modes::GROUP_READ), + group_write: has_bit(modes::GROUP_WRITE), + group_execute: has_bit(modes::GROUP_EXECUTE), + + other_read: has_bit(modes::OTHER_READ), + other_write: has_bit(modes::OTHER_WRITE), + other_execute: has_bit(modes::OTHER_EXECUTE), + + sticky: has_bit(modes::STICKY), + setgid: has_bit(modes::SETGID), + setuid: has_bit(modes::SETUID), + } + } +} + /// The file's `FileAttributes` field, available only on Windows. #[derive(Copy, Clone)] #[rustfmt::skip] diff --git a/src/fs/file.rs b/src/fs/file.rs index ca0be62b5..6de85f6a2 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use std::io; #[cfg(unix)] -use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; @@ -20,11 +20,13 @@ use log::*; #[cfg(unix)] use once_cell::sync::Lazy; +use crate::fs::archive::Archive; use crate::fs::dir::Dir; use crate::fs::feature::xattr; use crate::fs::feature::xattr::{Attribute, FileAttributes}; use crate::fs::fields as f; use crate::fs::fields::SecurityContextType; +use crate::fs::filelike::Filelike; use crate::fs::recursive_size::RecursiveSize; use super::mounts::all_mounts; @@ -122,7 +124,7 @@ impl<'dir> File<'dir> { { let parent_dir = parent_dir.into(); let name = filename.into().unwrap_or_else(|| File::filename(&path)); - let ext = File::ext(&path); + let ext = File::extension(&path); debug!("Statting file {:?}", &path); let metadata = std::fs::symlink_metadata(&path)?; @@ -161,7 +163,7 @@ impl<'dir> File<'dir> { name: &'static str, total_size: bool, ) -> io::Result> { - let ext = File::ext(&path); + let ext = File::extension(&path); debug!("Statting file {:?}", &path); let metadata = std::fs::symlink_metadata(&path)?; @@ -228,7 +230,7 @@ impl<'dir> File<'dir> { /// ASCII lowercasing is used because these extensions are only compared /// against a pre-compiled list of extensions which are known to only exist /// within ASCII, so it’s alright. - fn ext(path: &Path) -> Option { + pub fn extension(path: &Path) -> Option { let name = path.file_name().map(|f| f.to_string_lossy().to_string())?; name.rfind('.').map(|p| name[p + 1..].to_ascii_lowercase()) @@ -258,19 +260,139 @@ impl<'dir> File<'dir> { } } + /// Re-prefixes the path pointed to by this file, if it’s a symlink, to + /// make it an absolute path that can be accessed from whichever + /// directory exa is being run from. + fn reorient_target_path(&self, path: &Path) -> PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else if let Some(dir) = self.parent_dir { + dir.join(path) + } else if let Some(parent) = self.path.parent() { + parent.join(path) + } else { + self.path.join(path) + } + } + + /// Calculate the total directory size recursively. If not a directory `None` + /// will be returned. The directory size is cached for recursive directory + /// listing. + #[cfg(unix)] + fn recursive_directory_size(&self) -> RecursiveSize { + if self.is_directory() { + let key = (self.metadata.dev(), self.metadata.ino()); + if let Some(size) = DIRECTORY_SIZE_CACHE.lock().unwrap().get(&key) { + return RecursiveSize::Some(size.0, size.1); + } + Dir::read_dir(self.path.clone()).map_or(RecursiveSize::Unknown, |dir| { + let mut size = 0; + let mut blocks = 0; + for file in dir + .files(super::DotFilter::Dotfiles, None, false, false, true) + .flatten() + { + match file.recursive_directory_size() { + RecursiveSize::Some(bytes, blks) => { + size += bytes; + blocks += blks; + } + RecursiveSize::Unknown => {} + RecursiveSize::None => { + size += file.metadata.size(); + blocks += file.metadata.blocks(); + } + } + } + DIRECTORY_SIZE_CACHE + .lock() + .unwrap() + .insert(key, (size, blocks)); + RecursiveSize::Some(size, blocks) + }) + } else { + RecursiveSize::None + } + } + + /// Windows version always returns None. The metadata for + /// `volume_serial_number` and `file_index` are marked unstable so we can + /// not cache the sizes. Without caching we could end up walking the + /// directory structure several times. + #[cfg(windows)] + fn recursive_directory_size(&self) -> RecursiveSize { + RecursiveSize::None + } + + /// Checks the contents of the directory to determine if it's empty. + /// + /// This function avoids counting '.' and '..' when determining if the directory is + /// empty. If any other entries are found, it returns `false`. + /// + /// The naive approach, as one would think that this info may have been cached. + /// but as mentioned in the size function comment above, different filesystems + /// make it difficult to get any info about a dir by it's size, so this may be it. + fn is_empty_directory(&self) -> bool { + trace!("is_empty_directory: reading dir"); + match Dir::read_dir(self.path.clone()) { + // . & .. are skipped, if the returned iterator has .next(), it's not empty + Ok(has_files) => has_files + .files(super::DotFilter::Dotfiles, None, false, false, false) + .next() + .is_none(), + Err(_) => false, + } + } + + /// Interpret file as archive + pub fn to_archive(&self) -> Option { + Archive::from_path(self.path.clone()).ok() + } +} + +impl<'dir> Filelike for File<'dir> { + fn path(&self) -> &PathBuf { + &self.path + } + + fn name(&self) -> &String { + &self.name + } + + fn extension(&self) -> Option { + self.ext.clone() // TODO: too many clones? + } + + fn deref_links(&self) -> bool { + self.deref_links + } + + fn metadata(&self) -> Option<&std::fs::Metadata> { + Some(&self.metadata) + } + + fn parent_directory(&self) -> Option<&'dir Dir> { + self.parent_dir + } + + fn to_dir(&self) -> Option> { + trace!("to_dir: reading dir"); + Some(Dir::read_dir(self.path.clone())) + } + /// Get the extended attributes of a file path on demand. - pub fn extended_attributes(&self) -> &Vec { + fn extended_attributes(&self) -> &[Attribute] { self.extended_attributes .get_or_init(|| self.gather_extended_attributes()) } /// Whether this file is a directory on the filesystem. - pub fn is_directory(&self) -> bool { + fn is_directory(&self) -> bool { self.metadata.is_dir() } /// Whether this file is a directory, or a symlink pointing to a directory. - pub fn points_to_directory(&self) -> bool { + fn points_to_directory(&self) -> bool { if self.is_directory() { return true; } @@ -285,20 +407,9 @@ impl<'dir> File<'dir> { false } - /// If this file is a directory on the filesystem, then clone its - /// `PathBuf` for use in one of our own `Dir` values, and read a list of - /// its contents. - /// - /// Returns an IO error upon failure, but this shouldn’t be used to check - /// if a `File` is a directory or not! For that, just use `is_directory()`. - pub fn to_dir(&self) -> io::Result { - trace!("to_dir: reading dir"); - Dir::read_dir(self.path.clone()) - } - /// Whether this file is a regular file on the filesystem — that is, not a /// directory, a link, or anything else treated specially. - pub fn is_file(&self) -> bool { + fn is_file(&self) -> bool { self.metadata.is_file() } @@ -306,42 +417,41 @@ impl<'dir> File<'dir> { /// current user. An executable file has a different purpose from an /// executable directory, so they should be highlighted differently. #[cfg(unix)] - pub fn is_executable_file(&self) -> bool { - let bit = modes::USER_EXECUTE; - self.is_file() && (self.metadata.permissions().mode() & bit) == bit + fn is_executable_file(&self) -> bool { + self.is_file() && self.permissions().is_some_and(|p| p.user_execute) } /// Whether this file is a symlink on the filesystem. - pub fn is_link(&self) -> bool { + fn is_link(&self) -> bool { self.metadata.file_type().is_symlink() } /// Whether this file is a named pipe on the filesystem. #[cfg(unix)] - pub fn is_pipe(&self) -> bool { + fn is_pipe(&self) -> bool { self.metadata.file_type().is_fifo() } /// Whether this file is a char device on the filesystem. #[cfg(unix)] - pub fn is_char_device(&self) -> bool { + fn is_char_device(&self) -> bool { self.metadata.file_type().is_char_device() } /// Whether this file is a block device on the filesystem. #[cfg(unix)] - pub fn is_block_device(&self) -> bool { + fn is_block_device(&self) -> bool { self.metadata.file_type().is_block_device() } /// Whether this file is a socket on the filesystem. #[cfg(unix)] - pub fn is_socket(&self) -> bool { + fn is_socket(&self) -> bool { self.metadata.file_type().is_socket() } /// Determine the full path resolving all symbolic links on demand. - pub fn absolute_path(&self) -> Option<&PathBuf> { + fn absolute_path(&self) -> Option<&PathBuf> { self.absolute_path .get_or_init(|| { if self.is_link() && self.link_target().is_broken() { @@ -360,7 +470,7 @@ impl<'dir> File<'dir> { } /// Whether this file is a mount point - pub fn is_mount_point(&self) -> bool { + fn is_mount_point(&self) -> bool { cfg!(any(target_os = "linux", target_os = "macos")) && self.is_directory() && self @@ -369,28 +479,13 @@ impl<'dir> File<'dir> { } /// The filesystem device and type for a mount point - pub fn mount_point_info(&self) -> Option<&MountedFs> { + fn mount_point_info(&self) -> Option<&MountedFs> { if cfg!(any(target_os = "linux", target_os = "macos")) { return self.absolute_path().and_then(|p| all_mounts().get(p)); } None } - /// Re-prefixes the path pointed to by this file, if it’s a symlink, to - /// make it an absolute path that can be accessed from whichever - /// directory exa is being run from. - fn reorient_target_path(&self, path: &Path) -> PathBuf { - if path.is_absolute() { - path.to_path_buf() - } else if let Some(dir) = self.parent_dir { - dir.join(path) - } else if let Some(parent) = self.path.parent() { - parent.join(path) - } else { - self.path.join(path) - } - } - /// Again assuming this file is a symlink, follows that link and returns /// the result of following it. /// @@ -401,7 +496,7 @@ impl<'dir> File<'dir> { /// For a broken symlink, returns where the file *would* be, if it /// existed. If this file cannot be read at all, returns the error that /// we got when we tried to read it. - pub fn link_target(&self) -> FileTarget<'dir> { + fn link_target<'a>(&self) -> FileTarget<'a> { // We need to be careful to treat the path actually pointed to by // this file — which could be absolute or relative — to the path // we actually look up and turn into a `File` — which needs to be @@ -418,7 +513,7 @@ impl<'dir> File<'dir> { // follow links. match std::fs::metadata(&absolute_path) { Ok(metadata) => { - let ext = File::ext(&path); + let ext = File::extension(&path); let name = File::filename(&path); let extended_attributes = OnceLock::new(); let absolute_path_cell = OnceLock::from(Some(absolute_path)); @@ -453,7 +548,7 @@ impl<'dir> File<'dir> { /// For a broken symlink, returns where the file *would* be, if it /// existed. If this file cannot be read at all, returns the error that /// we got when we tried to read it. - pub fn link_target_recurse(&self) -> FileTarget<'dir> { + fn link_target_recurse<'a>(&self) -> FileTarget<'a> { let target = self.link_target(); if let FileTarget::Ok(f) = target { if f.is_link() { @@ -472,7 +567,7 @@ impl<'dir> File<'dir> { /// with multiple links much more often. Thus, it should get highlighted /// more attentively. #[cfg(unix)] - pub fn links(&self) -> f::Links { + fn links(&self) -> f::Links { let count = self.metadata.nlink(); f::Links { @@ -483,13 +578,13 @@ impl<'dir> File<'dir> { /// This file’s inode. #[cfg(unix)] - pub fn inode(&self) -> f::Inode { + fn inode(&self) -> f::Inode { f::Inode(self.metadata.ino()) } /// This actual size the file takes up on disk, in bytes. #[cfg(unix)] - pub fn blocksize(&self) -> f::Blocksize { + fn blocksize(&self) -> f::Blocksize { if self.deref_links && self.is_link() { match self.link_target() { FileTarget::Ok(f) => f.blocksize(), @@ -513,7 +608,7 @@ impl<'dir> File<'dir> { /// The ID of the user that own this file. If dereferencing links, the links /// may be broken, in which case `None` will be returned. #[cfg(unix)] - pub fn user(&self) -> Option { + fn user(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.user(), @@ -525,7 +620,7 @@ impl<'dir> File<'dir> { /// The ID of the group that owns this file. #[cfg(unix)] - pub fn group(&self) -> Option { + fn group(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.group(), @@ -547,7 +642,7 @@ impl<'dir> File<'dir> { /// Links will return the size of their target (recursively through other /// links) if dereferencing is enabled, otherwise None. #[cfg(unix)] - pub fn size(&self) -> f::Size { + fn size(&self) -> f::Size { if self.deref_links && self.is_link() { match self.link_target() { FileTarget::Ok(f) => f.size(), @@ -583,7 +678,7 @@ impl<'dir> File<'dir> { /// For Windows platforms, the size of directories is not computed and will /// return `Size::None`. #[cfg(windows)] - pub fn size(&self) -> f::Size { + fn size(&self) -> f::Size { if self.is_directory() { f::Size::None } else { @@ -591,65 +686,16 @@ impl<'dir> File<'dir> { } } - /// Calculate the total directory size recursively. If not a directory `None` - /// will be returned. The directory size is cached for recursive directory - /// listing. - #[cfg(unix)] - fn recursive_directory_size(&self) -> RecursiveSize { - if self.is_directory() { - let key = (self.metadata.dev(), self.metadata.ino()); - if let Some(size) = DIRECTORY_SIZE_CACHE.lock().unwrap().get(&key) { - return RecursiveSize::Some(size.0, size.1); - } - Dir::read_dir(self.path.clone()).map_or(RecursiveSize::Unknown, |dir| { - let mut size = 0; - let mut blocks = 0; - for file in dir - .files(super::DotFilter::Dotfiles, None, false, false, true) - .flatten() - { - match file.recursive_directory_size() { - RecursiveSize::Some(bytes, blks) => { - size += bytes; - blocks += blks; - } - RecursiveSize::Unknown => {} - RecursiveSize::None => { - size += file.metadata.size(); - blocks += file.metadata.blocks(); - } - } - } - DIRECTORY_SIZE_CACHE - .lock() - .unwrap() - .insert(key, (size, blocks)); - RecursiveSize::Some(size, blocks) - }) - } else { - RecursiveSize::None - } - } - - /// Windows version always returns None. The metadata for - /// `volume_serial_number` and `file_index` are marked unstable so we can - /// not cache the sizes. Without caching we could end up walking the - /// directory structure several times. - #[cfg(windows)] - fn recursive_directory_size(&self) -> RecursiveSize { - RecursiveSize::None - } - /// Returns the same value as `self.metadata.len()` or the recursive size /// of a directory when `total_size` is used. #[inline] - pub fn length(&self) -> u64 { + fn length(&self) -> u64 { self.recursive_size.unwrap_bytes_or(self.metadata.len()) } /// Is the file is using recursive size calculation #[inline] - pub fn is_recursive_size(&self) -> bool { + fn is_recursive_size(&self) -> bool { !self.recursive_size.is_none() } @@ -663,7 +709,7 @@ impl<'dir> File<'dir> { /// directly, as certain filesystems make it difficult to infer emptiness /// based on directory size alone. #[cfg(unix)] - pub fn is_empty_dir(&self) -> bool { + fn is_empty_dir(&self) -> bool { if self.is_directory() { if self.metadata.nlink() > 2 { // Directories will have a link count of two if they do not have any subdirectories. @@ -686,7 +732,7 @@ impl<'dir> File<'dir> { /// to determine if it's empty. Since certain filesystems on Windows make it /// challenging to infer emptiness based on directory size, this approach is used. #[cfg(windows)] - pub fn is_empty_dir(&self) -> bool { + fn is_empty_dir(&self) -> bool { if self.is_directory() { self.is_empty_directory() } else { @@ -694,28 +740,8 @@ impl<'dir> File<'dir> { } } - /// Checks the contents of the directory to determine if it's empty. - /// - /// This function avoids counting '.' and '..' when determining if the directory is - /// empty. If any other entries are found, it returns `false`. - /// - /// The naive approach, as one would think that this info may have been cached. - /// but as mentioned in the size function comment above, different filesystems - /// make it difficult to get any info about a dir by it's size, so this may be it. - fn is_empty_directory(&self) -> bool { - trace!("is_empty_directory: reading dir"); - match Dir::read_dir(self.path.clone()) { - // . & .. are skipped, if the returned iterator has .next(), it's not empty - Ok(has_files) => has_files - .files(super::DotFilter::Dotfiles, None, false, false, false) - .next() - .is_none(), - Err(_) => false, - } - } - /// This file’s last modified timestamp, if available on this platform. - pub fn modified_time(&self) -> Option { + fn modified_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.modified_time(), @@ -730,7 +756,7 @@ impl<'dir> File<'dir> { /// This file’s last changed timestamp, if available on this platform. #[cfg(unix)] - pub fn changed_time(&self) -> Option { + fn changed_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.changed_time(), @@ -741,12 +767,12 @@ impl<'dir> File<'dir> { } #[cfg(windows)] - pub fn changed_time(&self) -> Option { + fn changed_time(&self) -> Option { self.modified_time() } /// This file’s last accessed timestamp, if available on this platform. - pub fn accessed_time(&self) -> Option { + fn accessed_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.accessed_time(), @@ -760,7 +786,7 @@ impl<'dir> File<'dir> { } /// This file’s created timestamp, if available on this platform. - pub fn created_time(&self) -> Option { + fn created_time(&self) -> Option { if self.is_link() && self.deref_links { return match self.link_target_recurse() { FileTarget::Ok(f) => f.created_time(), @@ -779,7 +805,7 @@ impl<'dir> File<'dir> { /// The file type can usually be guessed from the colour of the file, but /// ls puts this character there. #[cfg(unix)] - pub fn type_char(&self) -> f::Type { + fn type_char(&self) -> f::Type { if self.is_file() { f::Type::File } else if self.is_directory() { @@ -800,7 +826,7 @@ impl<'dir> File<'dir> { } #[cfg(windows)] - pub fn type_char(&self) -> f::Type { + fn type_char(&self) -> f::Type { if self.is_file() { f::Type::File } else if self.is_directory() { @@ -812,7 +838,7 @@ impl<'dir> File<'dir> { /// This file’s permissions, with flags for each bit. #[cfg(unix)] - pub fn permissions(&self) -> Option { + fn permissions(&self) -> Option { if self.is_link() && self.deref_links { // If the chain of links is broken, we instead fall through and // return the permissions of the original link, as would have been @@ -823,29 +849,11 @@ impl<'dir> File<'dir> { }; } let bits = self.metadata.mode(); - let has_bit = |bit| bits & bit == bit; - - Some(f::Permissions { - user_read: has_bit(modes::USER_READ), - user_write: has_bit(modes::USER_WRITE), - user_execute: has_bit(modes::USER_EXECUTE), - - group_read: has_bit(modes::GROUP_READ), - group_write: has_bit(modes::GROUP_WRITE), - group_execute: has_bit(modes::GROUP_EXECUTE), - - other_read: has_bit(modes::OTHER_READ), - other_write: has_bit(modes::OTHER_WRITE), - other_execute: has_bit(modes::OTHER_EXECUTE), - - sticky: has_bit(modes::STICKY), - setgid: has_bit(modes::SETGID), - setuid: has_bit(modes::SETUID), - }) + Some(f::Permissions::from_mode(bits)) } #[cfg(windows)] - pub fn attributes(&self) -> f::Attributes { + fn attributes(&self) -> f::Attributes { let bits = self.metadata.file_attributes(); let has_bit = |bit| bits & bit == bit; @@ -862,7 +870,7 @@ impl<'dir> File<'dir> { /// This file’s security context field. #[cfg(unix)] - pub fn security_context(&self) -> f::SecurityContext<'_> { + fn security_context(&self) -> f::SecurityContext<'_> { let context = match self .extended_attributes() .iter() @@ -882,7 +890,7 @@ impl<'dir> File<'dir> { } #[cfg(windows)] - pub fn security_context(&self) -> f::SecurityContext<'_> { + fn security_context(&self) -> f::SecurityContext<'_> { f::SecurityContext { context: SecurityContextType::None, } @@ -896,7 +904,7 @@ impl<'dir> File<'dir> { target_os = "openbsd", target_os = "dragonfly" ))] - pub fn flags(&self) -> f::Flags { + fn flags(&self) -> f::Flags { #[cfg(target_os = "dragonfly")] use std::os::dragonfly::fs::MetadataExt; #[cfg(target_os = "freebsd")] @@ -911,7 +919,7 @@ impl<'dir> File<'dir> { } #[cfg(windows)] - pub fn flags(&self) -> f::Flags { + fn flags(&self) -> f::Flags { f::Flags(self.metadata.file_attributes()) } @@ -923,7 +931,7 @@ impl<'dir> File<'dir> { target_os = "dragonfly", target_os = "windows" )))] - pub fn flags(&self) -> f::Flags { + fn flags(&self) -> f::Flags { f::Flags(0) } } @@ -960,32 +968,6 @@ impl<'dir> FileTarget<'dir> { } } -/// More readable aliases for the permission bits exposed by libc. -#[allow(trivial_numeric_casts)] -#[cfg(unix)] -mod modes { - - // The `libc::mode_t` type’s actual type varies, but the value returned - // from `metadata.permissions().mode()` is always `u32`. - pub type Mode = u32; - - pub const USER_READ: Mode = libc::S_IRUSR as Mode; - pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; - pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; - - pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; - pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; - pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; - - pub const OTHER_READ: Mode = libc::S_IROTH as Mode; - pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; - pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; - - pub const STICKY: Mode = libc::S_ISVTX as Mode; - pub const SETGID: Mode = libc::S_ISGID as Mode; - pub const SETUID: Mode = libc::S_ISUID as Mode; -} - #[cfg(test)] mod ext_test { use super::File; @@ -993,17 +975,23 @@ mod ext_test { #[test] fn extension() { - assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat"))); + assert_eq!( + Some("dat".to_string()), + File::extension(Path::new("fester.dat")) + ); } #[test] fn dotfile() { - assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc"))); + assert_eq!( + Some("vimrc".to_string()), + File::extension(Path::new(".vimrc")) + ); } #[test] fn no_extension() { - assert_eq!(None, File::ext(Path::new("jarlsberg"))); + assert_eq!(None, File::extension(Path::new("jarlsberg"))); } } diff --git a/src/fs/filelike.rs b/src/fs/filelike.rs new file mode 100644 index 000000000..176933efb --- /dev/null +++ b/src/fs/filelike.rs @@ -0,0 +1,220 @@ +use chrono::NaiveDateTime; +use std::fs::Metadata; +use std::io; +use std::path::PathBuf; + +use crate::fs::dir::Dir; +use crate::fs::feature::xattr::Attribute; +use crate::fs::fields as f; +use crate::fs::file::FileTarget; +use crate::fs::mounts::MountedFs; + +pub trait Filelike { + /// Path + fn path(&self) -> &PathBuf; + + /// File name + fn name(&self) -> &String; + + /// File extension + fn extension(&self) -> Option; + + /// Whether to dereference symbolic links when querying for information. + /// + /// For instance, when querying the size of a symbolic link, if + /// dereferencing is enabled, the size of the target will be displayed + /// instead. + fn deref_links(&self) -> bool; + + /// Get the extended attributes of a file + fn extended_attributes(&self) -> &[Attribute]; + + /// Metadata for file in filesystem + fn metadata(&self) -> Option<&Metadata>; + + /// A reference to the directory that contains this file, if any. + /// + /// Filenames that get passed in on the command-line directly will have no + /// parent directory reference — although they technically have one on the + /// filesystem, we’ll never need to look at it, so it’ll be `None`. + /// However, *directories* that get passed in will produce files that + /// contain a reference to it, which is used in certain operations (such + /// as looking up compiled files). + fn parent_directory(&self) -> Option<&Dir>; + + /// If this file is a directory on the filesystem, then clone its + /// `PathBuf` for use in one of our own `Dir` values, and read a list of + /// its contents. + /// + /// Returns an IO error upon failure, but this shouldn’t be used to check + /// if it is a directory or not! For that, just use `is_directory()`. + /// + /// If this file is not representable in the filesystem, `None` will be + /// returned. + fn to_dir(&self) -> Option>; + + /// Whether this file is a directory on the filesystem. + fn is_directory(&self) -> bool; + + /// Whether this file is a directory, or a symlink pointing to a directory. + fn points_to_directory(&self) -> bool; + + /// Whether this file is a regular file on the filesystem — that is, not a + /// directory, a link, or anything else treated specially. + fn is_file(&self) -> bool; + + /// Whether this file is both a regular file *and* executable for the + /// current user. An executable file has a different purpose from an + /// executable directory, so they should be highlighted differently. + #[cfg(unix)] + fn is_executable_file(&self) -> bool; + + /// Whether this file is a symlink on the filesystem. + fn is_link(&self) -> bool; + + /// Whether this file is a named pipe on the filesystem. + #[cfg(unix)] + fn is_pipe(&self) -> bool; + + /// Whether this file is a char device on the filesystem. + #[cfg(unix)] + fn is_char_device(&self) -> bool; + + /// Whether this file is a block device on the filesystem. + #[cfg(unix)] + fn is_block_device(&self) -> bool; + + /// Whether this file is a socket on the filesystem. + #[cfg(unix)] + fn is_socket(&self) -> bool; + + /// Determine the full path resolving all symbolic links on demand. + fn absolute_path(&self) -> Option<&PathBuf>; + + /// Whether this file is a mount point + fn is_mount_point(&self) -> bool; + + /// The filesystem device and type for a mount point + fn mount_point_info(&self) -> Option<&MountedFs>; + + /// Again assuming this file is a symlink, follows that link and returns + /// the result of following it. + /// + /// For a working symlink that the user is allowed to follow, + /// this will be the `File` object at the other end, which can then have + /// its name, colour, and other details read. + /// + /// For a broken symlink, returns where the file *would* be, if it + /// existed. If this file cannot be read at all, returns the error that + /// we got when we tried to read it. + fn link_target<'a>(&self) -> FileTarget<'a>; + + /// Assuming this file is a symlink, follows that link and any further + /// links recursively, returning the result from following the trail. + /// + /// For a working symlink that the user is allowed to follow, + /// this will be the `File` object at the other end, which can then have + /// its name, colour, and other details read. + /// + /// For a broken symlink, returns where the file *would* be, if it + /// existed. If this file cannot be read at all, returns the error that + /// we got when we tried to read it. + fn link_target_recurse<'a>(&self) -> FileTarget<'a>; + + /// This file’s number of hard links. + /// + /// It also reports whether this is both a regular file, and a file with + /// multiple links. This is important, because a file with multiple links + /// is uncommon, while you come across directories and other types + /// with multiple links much more often. Thus, it should get highlighted + /// more attentively. + #[cfg(unix)] + fn links(&self) -> f::Links; + + /// This file’s inode. + #[cfg(unix)] + fn inode(&self) -> f::Inode; + + /// This actual size the file takes up on disk, in bytes. + #[cfg(unix)] + fn blocksize(&self) -> f::Blocksize; + + /// The ID of the user that own this file. If dereferencing links, the links + /// may be broken, in which case `None` will be returned. + #[cfg(unix)] + fn user(&self) -> Option; + + /// The ID of the group that owns this file. + #[cfg(unix)] + fn group(&self) -> Option; + + /// This file’s size, if it’s a regular file. + /// + /// For directories, the recursive size or no size is given depending on + /// flags. Although they do have a size on some filesystems, I’ve never + /// looked at one of those numbers and gained any information from it. + /// + /// Block and character devices return their device IDs, because they + /// usually just have a file size of zero. + /// + /// Links will return the size of their target (recursively through other + /// links) if dereferencing is enabled, otherwise None. + /// + /// For Windows platforms, the size of directories is not computed and will + /// return `Size::None`. + fn size(&self) -> f::Size; + + /// Returns the same value as `self.metadata.len()` or the recursive size + /// of a directory when `total_size` is used. + fn length(&self) -> u64; + + /// Is the file is using recursive size calculation + fn is_recursive_size(&self) -> bool; + + /// Determines if the directory is empty or not. + /// + /// For Unix platforms, this function first checks the link count to quickly + /// determine non-empty directories. On most UNIX filesystems the link count + /// is two plus the number of subdirectories. If the link count is less than + /// or equal to 2, it then checks the directory contents to determine if + /// it's truly empty. The naive approach used here checks the contents + /// directly, as certain filesystems make it difficult to infer emptiness + /// based on directory size alone. + /// + /// For Windows platforms, this function checks the directory contents directly + /// to determine if it's empty. Since certain filesystems on Windows make it + /// challenging to infer emptiness based on directory size, this approach is used. + fn is_empty_dir(&self) -> bool; + + /// This file’s last modified timestamp, if available on this platform. + fn modified_time(&self) -> Option; + + /// This file’s last changed timestamp, if available on this platform. + fn changed_time(&self) -> Option; + + /// This file’s last accessed timestamp, if available on this platform. + fn accessed_time(&self) -> Option; + + /// This file’s created timestamp, if available on this platform. + fn created_time(&self) -> Option; + + /// This file’s ‘type’. + /// + /// This is used a the leftmost character of the permissions column. + /// The file type can usually be guessed from the colour of the file, but + /// ls puts this character there. + fn type_char(&self) -> f::Type; + + /// This file’s permissions, with flags for each bit. + #[cfg(unix)] + fn permissions(&self) -> Option; + + #[cfg(windows)] + fn attributes(&self) -> f::Attributes; + + /// This file’s security context field. + fn security_context(&self) -> f::SecurityContext<'_>; + + /// User file flags. + fn flags(&self) -> f::Flags; +} diff --git a/src/fs/filter.rs b/src/fs/filter.rs index 162cf0ac0..8eb6d4bfb 100644 --- a/src/fs/filter.rs +++ b/src/fs/filter.rs @@ -7,6 +7,7 @@ use std::os::unix::fs::MetadataExt; use crate::fs::DotFilter; use crate::fs::File; +use crate::fs::Filelike; /// Flags used to manage the **file filter** process #[derive(PartialEq, Eq, Debug, Clone)] @@ -73,10 +74,10 @@ pub struct FileFilter { impl FileFilter { /// Remove every file in the given vector that does *not* pass the /// filter predicate for files found inside a directory. - pub fn filter_child_files(&self, files: &mut Vec>) { + pub fn filter_child_files(&self, files: &mut Vec) { use FileFilterFlags::{OnlyDirs, OnlyFiles}; - files.retain(|f| !self.ignore_patterns.is_ignored(&f.name)); + files.retain(|f| !self.ignore_patterns.is_ignored(f.name())); match ( self.flags.contains(&OnlyDirs), @@ -84,11 +85,11 @@ impl FileFilter { ) { (true, false) => { // On pass -'-only-dirs' flag only - files.retain(File::is_directory); + files.retain(Filelike::is_directory); } (false, true) => { // On pass -'-only-files' flag only - files.retain(File::is_file); + files.retain(Filelike::is_file); } _ => {} } @@ -108,9 +109,9 @@ impl FileFilter { } /// Sort the files in the given vector based on the sort field option. - pub fn sort_files<'a, F>(&self, files: &mut [F]) + pub fn sort_files(&self, files: &mut [E]) where - F: AsRef>, + E: AsRef, { files.sort_by(|a, b| self.sort_field.compare_files(a.as_ref(), b.as_ref())); @@ -230,20 +231,27 @@ impl SortField { /// into groups between letters and numbers, and then sorts those blocks /// together, so `file10` will sort after `file9`, instead of before it /// because of the `1`. - pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering { + pub fn compare_files(self, a: &F, b: &F) -> Ordering { use self::SortCase::{ABCabc, AaBbCc}; #[rustfmt::skip] return match self { Self::Unsorted => Ordering::Equal, - Self::Name(ABCabc) => natord::compare(&a.name, &b.name), - Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), + Self::Name(ABCabc) => natord::compare(a.name(), b.name()), + Self::Name(AaBbCc) => natord::compare_ignore_case(a.name(), b.name()), Self::Size => a.length().cmp(&b.length()), #[cfg(unix)] - Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), + Self::FileInode => { + match (a.metadata(), b.metadata()) { + (Some(metadata_a), Some(metadata_b)) => metadata_a.ino().cmp(&metadata_b.ino()), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => Ordering::Equal, + } + }, Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()), Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), Self::ChangedDate => a.changed_time().cmp(&b.changed_time()), @@ -251,27 +259,27 @@ impl SortField { Self::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a Self::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes - Ordering::Equal => natord::compare(&a.name, &b.name), + Ordering::Equal => natord::compare(a.name(), b.name()), order => order, }, - Self::Extension(ABCabc) => match a.ext.cmp(&b.ext) { - Ordering::Equal => natord::compare(&a.name, &b.name), + Self::Extension(ABCabc) => match a.extension().cmp(&b.extension()) { + Ordering::Equal => natord::compare(a.name(), b.name()), order => order, }, - Self::Extension(AaBbCc) => match a.ext.cmp(&b.ext) { - Ordering::Equal => natord::compare_ignore_case(&a.name, &b.name), + Self::Extension(AaBbCc) => match a.extension().cmp(&b.extension()) { + Ordering::Equal => natord::compare_ignore_case(a.name(), b.name()), order => order, }, Self::NameMixHidden(ABCabc) => natord::compare( - Self::strip_dot(&a.name), - Self::strip_dot(&b.name) + Self::strip_dot(a.name()), + Self::strip_dot(b.name()) ), Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case( - Self::strip_dot(&a.name), - Self::strip_dot(&b.name) + Self::strip_dot(a.name()), + Self::strip_dot(b.name()) ), }; } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 0f0d908b2..5cd1ebd69 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -4,6 +4,12 @@ pub use self::dir::{Dir, DotFilter}; mod file; pub use self::file::{File, FileTarget}; +mod filelike; +pub use self::filelike::Filelike; + +mod archive; +pub use self::archive::{Archive, ArchiveEntry}; + pub mod dir_action; pub mod feature; pub mod fields; diff --git a/src/info/filetype.rs b/src/info/filetype.rs index 7395a2787..47d8aaf1a 100644 --- a/src/info/filetype.rs +++ b/src/info/filetype.rs @@ -9,7 +9,8 @@ use phf::{phf_map, Map}; -use crate::fs::File; +use crate::fs::Filelike; +use crate::info::sources::GetSourceFiles; #[derive(Debug, Clone)] pub enum FileType { @@ -392,21 +393,27 @@ impl FileType { /// Lookup the file type based on the file's name, by the file name /// lowercase extension, or if the file could be compiled from related /// source code. - pub(crate) fn get_file_type(file: &File<'_>) -> Option { + pub(crate) fn get_file_type(file: &F) -> Option { // Case-insensitive readme is checked first for backwards compatibility. - if file.name.to_lowercase().starts_with("readme") { + if file.name().to_lowercase().starts_with("readme") { return Some(Self::Build); } - if let Some(file_type) = FILENAME_TYPES.get(&file.name) { + if let Some(file_type) = FILENAME_TYPES.get(file.name()) { return Some(file_type.clone()); } - if let Some(file_type) = file.ext.as_ref().and_then(|ext| EXTENSION_TYPES.get(ext)) { + if let Some(file_type) = file + .extension() + .as_ref() + .and_then(|ext| EXTENSION_TYPES.get(ext)) + { return Some(file_type.clone()); } - if file.name.ends_with('~') || (file.name.starts_with('#') && file.name.ends_with('#')) { + if file.name().ends_with('~') + || (file.name().starts_with('#') && file.name().ends_with('#')) + { return Some(Self::Temp); } - if let Some(dir) = file.parent_dir { + if let Some(dir) = file.parent_directory() { if file .get_source_files() .iter() diff --git a/src/info/sources.rs b/src/info/sources.rs index 72e036a72..c808f333e 100644 --- a/src/info/sources.rs +++ b/src/info/sources.rs @@ -1,8 +1,12 @@ use std::path::PathBuf; -use crate::fs::File; +use crate::fs::Filelike; -impl<'a> File<'a> { +pub trait GetSourceFiles { + fn get_source_files(&self) -> Vec; +} + +impl GetSourceFiles for T { /// For this file, return a vector of alternate file paths that, if any of /// them exist, mean that *this* file should be coloured as “compiled”. /// @@ -11,14 +15,14 @@ impl<'a> File<'a> { /// For example, `foo.js` is perfectly valid without `foo.coffee`, so we /// don’t want to always blindly highlight `*.js` as compiled. /// (See also `FileType`) - pub fn get_source_files(&self) -> Vec { - if let Some(ext) = &self.ext { + fn get_source_files(&self) -> Vec { + if let Some(ext) = &self.extension() { match &ext[..] { - "css" => vec![self.path.with_extension("sass"), self.path.with_extension("scss"), // SASS, SCSS - self.path.with_extension("styl"), self.path.with_extension("less")], // Stylus, Less - "mjs" => vec![self.path.with_extension("mts")], // JavaScript ES Modules source - "cjs" => vec![self.path.with_extension("cts")], // JavaScript Commonjs Modules source - "js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript + "css" => vec![self.path().with_extension("sass"), self.path().with_extension("scss"), // SASS, SCSS + self.path().with_extension("styl"), self.path().with_extension("less")], // Stylus, Less + "mjs" => vec![self.path().with_extension("mts")], // JavaScript ES Modules source + "cjs" => vec![self.path().with_extension("cts")], // JavaScript Commonjs Modules source + "js" => vec![self.path().with_extension("coffee"), self.path().with_extension("ts")], // CoffeeScript, TypeScript "aux" | // TeX: auxiliary file "bbl" | // BibTeX bibliography file "bcf" | // biblatex control file @@ -31,7 +35,7 @@ impl<'a> File<'a> { "lot" | // TeX list of tables "out" | // hyperref list of bookmarks "toc" | // TeX table of contents - "xdv" => vec![self.path.with_extension("tex")], // XeTeX dvi + "xdv" => vec![self.path().with_extension("tex")], // XeTeX dvi _ => vec![], // No source files if none of the above } diff --git a/src/main.rs b/src/main.rs index 162e950cc..762f14a1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,8 @@ use nu_ansi_term::{AnsiStrings as ANSIStrings, Style}; use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; -use crate::fs::{Dir, File}; +use crate::fs::{Archive, ArchiveEntry, Dir, File, Filelike}; +use crate::options::archive_inspection::ArchiveInspection; use crate::options::stdin::FilesInput; use crate::options::{vars, Options, OptionsResult, Vars}; use crate::output::{details, escape, file_name, grid, grid_details, lines, Mode, View}; @@ -250,6 +251,7 @@ impl<'args> Exa<'args> { debug!("Running with options: {:#?}", self.options); let mut files = Vec::new(); + let mut archives = Vec::new(); let mut dirs = Vec::new(); let mut exit_status = 0; @@ -261,6 +263,9 @@ impl<'args> Exa<'args> { self.options.view.deref_links, self.options.view.total_size, ) { + // TODO: check for all path components if it is an archive? Then allow user to + // inspect only selected directories/files in archive? + // => probably separate PR for that feature Err(e) => { exit_status = 2; writeln!(io::stderr(), "{file_path:?}: {e}")?; @@ -270,12 +275,13 @@ impl<'args> Exa<'args> { if f.points_to_directory() && !self.options.dir_action.treat_dirs_as_files() { trace!("matching on to_dir"); match f.to_dir() { - Ok(d) => dirs.push(d), - Err(e) if e.kind() == ErrorKind::PermissionDenied => { + Some(Ok(d)) => dirs.push(d), + Some(Err(e)) if e.kind() == ErrorKind::PermissionDenied => { eprintln!("{file_path:?}: {e}"); exit(exits::PERMISSION_DENIED); } - Err(e) => writeln!(io::stderr(), "{file_path:?}: {e}")?, + Some(Err(e)) => writeln!(io::stderr(), "{file_path:?}: {e}")?, + None => {} } } else { files.push(f); @@ -284,17 +290,31 @@ impl<'args> Exa<'args> { } } + if matches!(self.options.archive_inspection, ArchiveInspection::Always) + && !self.options.dir_action.treat_dirs_as_files() + { + for f in &files { + if let Some(archive) = f.to_archive() { + archives.push(archive); + } + } + } + // We want to print a directory’s name before we list it, *except* in // the case where it’s the only directory, *except* if there are any // files to print as well. (It’s a double negative) let no_files = files.is_empty(); - let is_only_dir = dirs.len() == 1 && no_files; + let no_archives = archives.is_empty(); + let is_only_dir = dirs.len() == 1 && no_files && no_archives; self.options.filter.filter_argument_files(&mut files); self.print_files(None, files)?; - self.print_dirs(dirs, no_files, is_only_dir, exit_status) + for archive in archives { + self.print_archive(&archive, &PathBuf::new())?; + } + self.print_dirs(dirs, is_only_dir, is_only_dir, exit_status) } fn print_dirs( @@ -318,15 +338,7 @@ impl<'args> Exa<'args> { } if !is_only_dir { - let mut bits = Vec::new(); - escape( - dir.path.display().to_string(), - &mut bits, - Style::default(), - Style::default(), - quote_style, - ); - writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?; + self.print_dir_marker(dir.path.display().to_string(), quote_style)?; } let mut children = Vec::new(); @@ -361,14 +373,15 @@ impl<'args> Exa<'args> { .filter(|f| f.is_directory() && !f.is_all_all) { match child_dir.to_dir() { - Ok(d) => child_dirs.push(d), - Err(e) => { + Some(Ok(d)) => child_dirs.push(d), + Some(Err(e)) => { writeln!(io::stderr(), "{}: {}", child_dir.path.display(), e)?; } + None => {} } } - self.print_files(Some(&dir), children)?; + self.print_files(Some(&dir.path), children)?; match self.print_dirs(child_dirs, false, false, exit_status) { Ok(_) => (), Err(e) => return Err(e), @@ -377,14 +390,18 @@ impl<'args> Exa<'args> { } } - self.print_files(Some(&dir), children)?; + self.print_files(Some(&dir.path), children)?; } Ok(exit_status) } /// Prints the list of files using whichever view is selected. - fn print_files(&mut self, dir: Option<&Dir>, files: Vec>) -> io::Result<()> { + fn print_files>( + &mut self, + dir_path: Option<&PathBuf>, + files: Vec, + ) -> io::Result<()> { if files.is_empty() { return Ok(()); } @@ -429,7 +446,7 @@ impl<'args> Exa<'args> { let git = self.git.as_ref(); let git_repos = self.git_repos; let r = details::Render { - dir, + dir_path, files, theme, file_style, @@ -453,7 +470,7 @@ impl<'args> Exa<'args> { let git_repos = self.git_repos; let r = grid_details::Render { - dir, + dir_path, files, theme, file_style, @@ -477,7 +494,7 @@ impl<'args> Exa<'args> { let git_repos = self.git_repos; let r = details::Render { - dir, + dir_path, files, theme, file_style, @@ -492,6 +509,69 @@ impl<'args> Exa<'args> { } } } + + fn print_archive(&mut self, archive: &Archive, root: &PathBuf) -> io::Result<()> { + let View { + file_style: file_name::Options { quote_style, .. }, + .. + } = self.options.view; + + // Put a gap between directory listings and between the list of files and + // the first directory. + // Before an archive, there will always be a list of files. + writeln!(&mut self.writer)?; + + let parent_path = archive.path.join(root).display().to_string(); + self.print_dir_marker( + parent_path + .strip_suffix(std::path::is_separator) + .map(str::to_string) + .unwrap_or(parent_path), + quote_style, + )?; + + let mut children = Vec::::new(); + for entry in archive.files(root.clone()) { + match entry { + Ok(entry) => children.push(entry.clone()), + Err(error) => writeln!(io::stderr(), "[{error}]")?, + } + } + + self.options.filter.filter_child_files(&mut children); + self.options.filter.sort_files(&mut children); + + if let Some(recurse_opts) = self.options.dir_action.recurse_options() { + let depth = root.components().count(); + + if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) { + self.print_files(Some(root), children.clone())?; + for child_dir in children.iter().filter(|f| f.is_directory()) { + self.print_archive(archive, child_dir.path())?; + } + return Ok(()); + } + } + + self.print_files(Some(root), children) + } + + fn print_dir_marker( + &mut self, + dir_name: String, + quote_style: file_name::QuoteStyle, + ) -> Result<(), std::io::Error> { + let mut bits = Vec::new(); + escape( + dir_name, + &mut bits, + Style::default(), + Style::default(), + quote_style, + ); + writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?; + Ok(()) + } } mod exits { diff --git a/src/options/archive_inspection.rs b/src/options/archive_inspection.rs new file mode 100644 index 000000000..fb178257a --- /dev/null +++ b/src/options/archive_inspection.rs @@ -0,0 +1,19 @@ +use crate::options::parser::MatchedFlags; +use crate::options::{flags, OptionsError}; + +#[derive(Debug, PartialEq)] +pub enum ArchiveInspection { + Always, + Never, + // TODO: option to limit file size (especially for compressed archives) +} + +impl ArchiveInspection { + pub fn deduce(matches: &MatchedFlags<'_>) -> Result { + Ok(if matches.has(&flags::INSPECT_ARCHIVES)? { + ArchiveInspection::Always + } else { + ArchiveInspection::Never + }) + } +} diff --git a/src/options/flags.rs b/src/options/flags.rs index 3d6d8359b..41b13ce5f 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -84,6 +84,7 @@ pub static OCTAL: Arg = Arg { short: Some(b'o'), long: "octal-permis pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", takes_value: TakesValue::Forbidden }; pub static STDIN: Arg = Arg { short: None, long: "stdin", takes_value: TakesValue::Forbidden }; pub static FILE_FLAGS: Arg = Arg { short: Some(b'O'), long: "flags", takes_value: TakesValue::Forbidden }; +pub static INSPECT_ARCHIVES: Arg = Arg { short: Some(b'q'), long: "quasi", takes_value: TakesValue::Forbidden }; pub static ALL_ARGS: Args = Args(&[ &VERSION, &HELP, @@ -100,5 +101,5 @@ pub static ALL_ARGS: Args = Args(&[ &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP, &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, - &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS + &EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS, &INSPECT_ARCHIVES ]); diff --git a/src/options/mod.rs b/src/options/mod.rs index c95821522..3eb8adfd5 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -72,6 +72,7 @@ use std::ffi::OsStr; use crate::fs::dir_action::DirAction; use crate::fs::filter::{FileFilter, GitIgnore}; +use crate::options::archive_inspection::ArchiveInspection; use crate::options::stdin::FilesInput; use crate::output::{details, grid_details, Mode, View}; use crate::theme::Options as ThemeOptions; @@ -96,6 +97,7 @@ use self::parser::MatchedFlags; pub mod vars; pub use self::vars::Vars; +pub mod archive_inspection; pub mod stdin; mod version; @@ -123,6 +125,8 @@ pub struct Options { /// Whether to read file names from stdin instead of the command-line pub stdin: FilesInput, + + pub archive_inspection: ArchiveInspection, } impl Options { @@ -206,6 +210,7 @@ impl Options { let filter = FileFilter::deduce(matches)?; let theme = ThemeOptions::deduce(matches, vars)?; let stdin = FilesInput::deduce(matches, vars)?; + let archive_inspection = ArchiveInspection::deduce(matches)?; Ok(Self { dir_action, @@ -213,6 +218,7 @@ impl Options { view, theme, stdin, + archive_inspection, }) } } diff --git a/src/output/color_scale.rs b/src/output/color_scale.rs index 94a513ab1..d31af0e35 100644 --- a/src/output/color_scale.rs +++ b/src/output/color_scale.rs @@ -3,7 +3,9 @@ use nu_ansi_term::{Color as Colour, Style}; use palette::{FromColor, LinSrgb, Oklab, Srgb}; use crate::{ - fs::{dir_action::RecurseOptions, feature::git::GitCache, fields::Size, DotFilter, File}, + fs::{ + dir_action::RecurseOptions, feature::git::GitCache, fields::Size, DotFilter, File, Filelike, + }, output::{table::TimeType, tree::TreeDepth}, }; @@ -35,9 +37,9 @@ pub struct ColorScaleInformation { } impl ColorScaleInformation { - pub fn from_color_scale( + pub fn from_color_scale( color_scale: ColorScaleOptions, - files: &[File<'_>], + files: &[F], dot_filter: DotFilter, git: Option<&GitCache>, git_ignoring: bool, @@ -86,7 +88,12 @@ impl ColorScaleInformation { style } - pub fn apply_time_gradient(&self, style: Style, file: &File<'_>, time_type: TimeType) -> Style { + pub fn apply_time_gradient( + &self, + style: Style, + file: &F, + time_type: TimeType, + ) -> Style { let range = match time_type { TimeType::Modified => self.modified, TimeType::Changed => self.changed, @@ -102,9 +109,9 @@ impl ColorScaleInformation { } } -fn update_information_recursively( +fn update_information_recursively( information: &mut ColorScaleInformation, - files: &[File<'_>], + files: &[F], dot_filter: DotFilter, git: Option<&GitCache>, git_ignoring: bool, @@ -147,11 +154,11 @@ fn update_information_recursively( // the dot_filter. if file.is_directory() && r.is_some_and(|x| !x.is_too_deep(depth.0)) - && file.name != "." - && file.name != ".." + && file.name() != "." + && file.name() != ".." { match file.to_dir() { - Ok(dir) => { + Some(Ok(dir)) => { let files: Vec> = dir .files(dot_filter, git, git_ignoring, false, false) .flatten() @@ -167,7 +174,8 @@ fn update_information_recursively( r, ); } - Err(e) => trace!("Unable to access directory {}: {}", file.name, e), + Some(Err(e)) => trace!("Unable to access directory {}: {}", file.name(), e), + None => {} } }; } diff --git a/src/output/details.rs b/src/output/details.rs index f7e0bc0e0..05e5d2932 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -73,10 +73,10 @@ use crate::fs::feature::git::GitCache; use crate::fs::feature::xattr::Attribute; use crate::fs::fields::SecurityContextType; use crate::fs::filter::FileFilter; -use crate::fs::{Dir, File}; +use crate::fs::{Dir, Filelike}; use crate::output::cell::TextCell; use crate::output::color_scale::{ColorScaleInformation, ColorScaleOptions}; -use crate::output::file_name::Options as FileStyle; +use crate::output::file_name::{GetStyle, Options as FileStyle}; use crate::output::table::{Options as TableOptions, Row as TableRow, Table}; use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk}; use crate::theme::Theme; @@ -117,9 +117,9 @@ pub struct Options { pub color_scale: ColorScaleOptions, } -pub struct Render<'a> { - pub dir: Option<&'a Dir>, - pub files: Vec>, +pub struct Render<'a, F: Filelike> { + pub dir_path: Option<&'a PathBuf>, + pub files: Vec, pub theme: &'a Theme, pub file_style: &'a FileStyle, pub opts: &'a Options, @@ -141,21 +141,21 @@ pub struct Render<'a> { } #[rustfmt::skip] -struct Egg<'a> { +struct Egg<'a, F: Filelike + GetStyle> { table_row: Option, xattrs: &'a [Attribute], errors: Vec<(io::Error, Option)>, dir: Option, - file: &'a File<'a>, + file: &'a F, } -impl<'a> AsRef> for Egg<'a> { - fn as_ref(&self) -> &File<'a> { +impl<'a, F: Filelike + super::file_name::GetStyle> AsRef for Egg<'a, F> { + fn as_ref(&self) -> &F { self.file } } -impl<'a> Render<'a> { +impl<'a, F: Filelike + std::marker::Sync + super::file_name::GetStyle> Render<'a, F> { pub fn render(mut self, w: &mut W) -> io::Result<()> { let mut rows = Vec::new(); @@ -169,14 +169,14 @@ impl<'a> Render<'a> { ); if let Some(ref table) = self.opts.table { - match (self.git, self.dir) { - (Some(g), Some(d)) => { - if !g.has_anything_for(&d.path) { + match (self.git, self.dir_path) { + (Some(g), Some(path)) => { + if !g.has_anything_for(path) { self.git = None; } } (Some(g), None) => { - if !self.files.iter().any(|f| g.has_anything_for(&f.path)) { + if !self.files.iter().any(|f| g.has_anything_for(f.path())) { self.git = None; } } @@ -223,7 +223,7 @@ impl<'a> Render<'a> { } /// Whether to show the extended attribute hint - pub fn show_xattr_hint(&self, file: &File<'_>) -> bool { + pub fn show_xattr_hint(&self, file: &T) -> bool { // Do not show the hint '@' if the only extended attribute is the security // attribute and the security attribute column is active. let xattr_count = file.extended_attributes().len(); @@ -237,11 +237,11 @@ impl<'a> Render<'a> { /// Adds files to the table, possibly recursively. This is easily /// parallelisable, and uses a pool of threads. - fn add_files_to_table<'dir>( + fn add_files_to_table( &self, table: &mut Option>, rows: &mut Vec, - src: &[File<'dir>], + src: &[T], depth: TreeDepth, color_scale_info: Option, ) { @@ -289,12 +289,13 @@ impl<'a> Render<'a> { if file.is_directory() && r.tree && !r.is_too_deep(depth.0) { trace!("matching on to_dir"); match file.to_dir() { - Ok(d) => { + Some(Ok(d)) => { dir = Some(d); } - Err(e) => { + Some(Err(e)) => { errors.push((e, None)); } + None => {} } } }; @@ -309,7 +310,6 @@ impl<'a> Render<'a> { }) .collect(); - // this is safe because all entries have been initialized above self.filter.sort_files(&mut file_eggs); for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) { @@ -341,7 +341,7 @@ impl<'a> Render<'a> { self.filter.dot_filter, self.git, self.git_ignoring, - egg.file.deref_links, + egg.file.deref_links(), egg.file.is_recursive_size(), ) { match file_to_add { diff --git a/src/output/file_name.rs b/src/output/file_name.rs index 30c88a93c..3f1c1ce9b 100644 --- a/src/output/file_name.rs +++ b/src/output/file_name.rs @@ -5,11 +5,12 @@ use nu_ansi_term::{AnsiString as ANSIString, Style}; use path_clean; use unicode_width::UnicodeWidthStr; -use crate::fs::{File, FileTarget}; +use crate::fs::{ArchiveEntry, File, FileTarget, Filelike}; use crate::output::cell::TextCellContents; use crate::output::escape; use crate::output::icons::{icon_for_file, iconify_style}; use crate::output::render::FiletypeColours; +use crate::theme::Theme; /// Basically a file name factory. #[derive(Debug, Copy, Clone)] @@ -36,21 +37,16 @@ pub struct Options { impl Options { /// Create a new `FileName` that prints the given file’s name, painting it /// with the remaining arguments. - pub fn for_file<'a, 'dir, C>( + pub fn for_file<'a, C, F: Filelike + GetStyle>( self, - file: &'a File<'dir>, + file: &'a F, colours: &'a C, - ) -> FileName<'a, 'dir, C> { + ) -> FileName<'a, C, F> { FileName { file, colours, link_style: LinkStyle::JustFilenames, options: self, - target: if file.is_link() { - Some(file.link_target()) - } else { - None - }, mount_style: MountStyle::JustDirectoryNames, } } @@ -139,16 +135,13 @@ pub enum QuoteStyle { /// A **file name** holds all the information necessary to display the name /// of the given file. This is used in all of the views. -pub struct FileName<'a, 'dir, C> { +pub struct FileName<'a, C, F: Filelike + GetStyle> { /// A reference to the file that we’re getting the name of. - file: &'a File<'dir>, + file: &'a F, /// The colours used to paint the file name and its surrounding text. colours: &'a C, - /// The file that this file points to if it’s a link. - target: Option>, // todo: remove? - /// How to handle displaying links. link_style: LinkStyle, @@ -158,7 +151,7 @@ pub struct FileName<'a, 'dir, C> { mount_style: MountStyle, } -impl<'a, 'dir, C> FileName<'a, 'dir, C> { +impl<'a, C, F: Filelike + GetStyle> FileName<'a, C, F> { /// Sets the flag on this file name to display link targets with an /// arrow followed by their path. pub fn with_link_paths(mut self) -> Self { @@ -178,7 +171,7 @@ impl<'a, 'dir, C> FileName<'a, 'dir, C> { } } -impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { +impl<'a, C: Colours, F: Filelike + GetStyle> FileName<'a, C, F> { /// Paints the name of the file using the colours, resulting in a vector /// of coloured cells that can be printed to the terminal. /// @@ -207,13 +200,14 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { bits.push(style.paint(" ".repeat(spaces_count as usize))); } - if self.file.parent_dir.is_none() { - if let Some(parent) = self.file.path.parent() { + // TODO: what is the reason for this? + if self.file.parent_directory().is_none() { + if let Some(parent) = self.file.path().parent() { self.add_parent_bits(&mut bits, parent); } } - if !self.file.name.is_empty() { + if !self.file.name().is_empty() { // The “missing file” colour seems like it should be used here, // but it’s not! In a grid view, where there’s no space to display // link targets, the filename has to have a different style to @@ -225,7 +219,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { } } - if let (LinkStyle::FullLinkPaths, Some(target)) = (self.link_style, self.target.as_ref()) { + if let (LinkStyle::FullLinkPaths, target) = (self.link_style, self.file.link_target()) { match target { FileTarget::Ok(target) => { bits.push(Style::default().paint(" ")); @@ -247,9 +241,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { }; let target_name = FileName { - file: target, + file: target.as_ref(), colours: self.colours, - target: None, link_style: LinkStyle::FullLinkPaths, options: target_options, mount_style: MountStyle::JustDirectoryNames, @@ -260,7 +253,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { } if should_add_classify_char { - if let Some(class) = self.classify_char(target) { + if let Some(class) = self.classify_char(&*target) { bits.push(Style::default().paint(class)); } } @@ -335,7 +328,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { /// The character to be displayed after a file when classifying is on, if /// the file’s type has one associated with it. #[cfg(unix)] - pub(crate) fn classify_char(&self, file: &File<'_>) -> Option<&'static str> { + pub(crate) fn classify_char(&self, file: &T) -> Option<&'static str> { if file.is_executable_file() { Some("*") } else if file.is_directory() { @@ -352,7 +345,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { } #[cfg(windows)] - pub(crate) fn classify_char(&self, file: &File<'_>) -> Option<&'static str> { + pub(crate) fn classify_char(&self, file: &F) -> Option<&'static str> { if file.is_directory() { Some("/") } else if file.is_link() { @@ -425,7 +418,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { fn display_name(&self) -> String { match self.options.absolute { Absolute::On => std::env::current_dir().ok().and_then(|p| { - path_clean::clean(p.join(&self.file.path)) + path_clean::clean(p.join(self.file.path())) .to_str() .map(std::borrow::ToOwned::to_owned) }), @@ -436,7 +429,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { .map(std::borrow::ToOwned::to_owned), Absolute::Off => None, } - .unwrap_or(self.file.name.clone()) + .unwrap_or(self.file.name().clone()) } /// Figures out which colour to paint the filename part of the output, @@ -445,10 +438,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { /// if there’s nowhere else for that fact to be shown.) pub fn style(&self) -> Style { if let LinkStyle::JustFilenames = self.link_style { - if let Some(ref target) = self.target { - if target.is_broken() { - return self.colours.broken_symlink(); - } + if let FileTarget::Broken(_) = self.file.link_target() { + return self.colours.broken_symlink(); } } @@ -474,7 +465,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { /// For grid's use, to cover the case of hyperlink escape sequences pub fn bare_utf8_width(&self) -> usize { - UnicodeWidthStr::width(self.file.name.as_str()) + UnicodeWidthStr::width(self.file.name().as_str()) } } @@ -508,5 +499,27 @@ pub trait Colours: FiletypeColours { /// The style to paint a directory that has a filesystem mounted on it. fn mount_point(&self) -> Style; - fn colour_file(&self, file: &File<'_>) -> Style; + fn colour_file(&self, file: &F) -> Style; +} + +pub trait GetStyle { + fn get_style(&self, theme: &Theme) -> Style; +} + +impl GetStyle for File<'_> { + fn get_style(&self, theme: &Theme) -> Style { + theme + .exts + .get_style(self, theme) + .unwrap_or(theme.ui.filekinds.normal) + } +} + +impl GetStyle for ArchiveEntry { + fn get_style(&self, theme: &Theme) -> Style { + theme + .exts + .get_style_for_archive(self, theme) + .unwrap_or(theme.ui.filekinds.normal) + } } diff --git a/src/output/grid.rs b/src/output/grid.rs index ad30a199d..8bf4ff3c3 100644 --- a/src/output/grid.rs +++ b/src/output/grid.rs @@ -3,8 +3,8 @@ use std::io::{self, Write}; use term_grid::{Direction, Filling, Grid, GridOptions}; use crate::fs::filter::FileFilter; -use crate::fs::File; -use crate::output::file_name::Options as FileStyle; +use crate::fs::Filelike; +use crate::output::file_name::{GetStyle, Options as FileStyle}; use crate::theme::Theme; #[derive(PartialEq, Eq, Debug, Copy, Clone)] @@ -22,8 +22,8 @@ impl Options { } } -pub struct Render<'a> { - pub files: Vec>, +pub struct Render<'a, F: Filelike> { + pub files: Vec, pub theme: &'a Theme, pub file_style: &'a FileStyle, pub opts: &'a Options, @@ -31,7 +31,7 @@ pub struct Render<'a> { pub filter: &'a FileFilter, } -impl<'a> Render<'a> { +impl<'a, F: Filelike + AsRef + GetStyle> Render<'a, F> { pub fn render(mut self, w: &mut W) -> io::Result<()> { self.filter.sort_files(&mut self.files); diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index 71c6bb8bb..3cf0f2b9e 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -8,11 +8,11 @@ use term_grid as grid; use crate::fs::feature::git::GitCache; use crate::fs::filter::FileFilter; -use crate::fs::{Dir, File}; +use crate::fs::{File, Filelike}; use crate::output::cell::TextCell; use crate::output::color_scale::ColorScaleInformation; use crate::output::details::{Options as DetailsOptions, Render as DetailsRender}; -use crate::output::file_name::Options as FileStyle; +use crate::output::file_name::{GetStyle, Options as FileStyle}; use crate::output::table::{Options as TableOptions, Table}; use crate::theme::Theme; @@ -45,14 +45,14 @@ pub enum RowThreshold { AlwaysGrid, } -pub struct Render<'a> { +pub struct Render<'a, F: Filelike> { /// The directory that’s being rendered here. /// We need this to know which columns to put in the output. - pub dir: Option<&'a Dir>, + pub dir_path: Option<&'a std::path::PathBuf>, /// The files that have been read from the directory. They should all /// hold a reference to it. - pub files: Vec>, + pub files: Vec, /// How to colour various pieces of text. pub theme: &'a Theme, @@ -82,17 +82,17 @@ pub struct Render<'a> { pub git_repos: bool, } -impl<'a> Render<'a> { +impl<'a, F: Filelike + GetStyle + std::marker::Sync> Render<'a, F> { /// Create a temporary Details render that gets used for the columns of /// the grid-details render that’s being generated. /// /// This includes an empty files vector because the files get added to /// the table in *this* file, not in details: we only want to insert every /// *n* files into each column’s table, not all of them. - fn details_for_column(&self) -> DetailsRender<'a> { + fn details_for_column(&self) -> DetailsRender<'a, File<'a>> { #[rustfmt::skip] return DetailsRender { - dir: self.dir, + dir_path: self.dir_path, files: Vec::new(), theme: self.theme, file_style: self.file_style, @@ -199,14 +199,14 @@ impl<'a> Render<'a> { } fn make_table(&mut self, options: &'a TableOptions) -> Table<'a> { - match (self.git, self.dir) { + match (self.git, self.dir_path) { (Some(g), Some(d)) => { - if !g.has_anything_for(&d.path) { + if !g.has_anything_for(d) { self.git = None; } } (Some(g), None) => { - if !self.files.iter().any(|f| g.has_anything_for(&f.path)) { + if !self.files.iter().any(|f| g.has_anything_for(f.path())) { self.git = None; } } diff --git a/src/output/icons.rs b/src/output/icons.rs index 5707eb68e..877781820 100644 --- a/src/output/icons.rs +++ b/src/output/icons.rs @@ -1,7 +1,7 @@ use nu_ansi_term::Style; use phf::{phf_map, Map}; -use crate::fs::File; +use crate::fs::Filelike; #[non_exhaustive] struct Icons; @@ -818,18 +818,20 @@ pub fn iconify_style(style: Style) -> Style { /// Lookup the icon for a file based on the file's name, if the entry is a /// directory, or by the lowercase file extension. -pub fn icon_for_file(file: &File<'_>) -> char { +pub fn icon_for_file(file: &F) -> char { if file.points_to_directory() { - *DIRECTORY_ICONS.get(file.name.as_str()).unwrap_or_else(|| { - if file.is_empty_dir() { - &Icons::FOLDER_OPEN //  - } else { - &Icons::FOLDER //  - } - }) - } else if let Some(icon) = FILENAME_ICONS.get(file.name.as_str()) { + *DIRECTORY_ICONS + .get(file.name().as_str()) + .unwrap_or_else(|| { + if file.is_empty_dir() { + &Icons::FOLDER_OPEN //  + } else { + &Icons::FOLDER //  + } + }) + } else if let Some(icon) = FILENAME_ICONS.get(file.name().as_str()) { *icon - } else if let Some(ext) = file.ext.as_ref() { + } else if let Some(ext) = file.extension().as_ref() { *EXTENSION_ICONS.get(ext.as_str()).unwrap_or(&Icons::FILE) //  } else { Icons::FILE_OUTLINE //  diff --git a/src/output/lines.rs b/src/output/lines.rs index bade1ed72..2f10a2f11 100644 --- a/src/output/lines.rs +++ b/src/output/lines.rs @@ -3,20 +3,20 @@ use std::io::{self, Write}; use nu_ansi_term::AnsiStrings as ANSIStrings; use crate::fs::filter::FileFilter; -use crate::fs::File; +use crate::fs::Filelike; use crate::output::cell::TextCellContents; -use crate::output::file_name::Options as FileStyle; +use crate::output::file_name::{GetStyle, Options as FileStyle}; use crate::theme::Theme; /// The lines view literally just displays each file, line-by-line. -pub struct Render<'a> { - pub files: Vec>, +pub struct Render<'a, F: Filelike> { + pub files: Vec, pub theme: &'a Theme, pub file_style: &'a FileStyle, pub filter: &'a FileFilter, } -impl<'a> Render<'a> { +impl<'a, F: Filelike + GetStyle + AsRef> Render<'a, F> { pub fn render(mut self, w: &mut W) -> io::Result<()> { self.filter.sort_files(&mut self.files); for file in &self.files { @@ -27,7 +27,7 @@ impl<'a> Render<'a> { Ok(()) } - fn render_file<'f>(&self, file: &'f File<'a>) -> TextCellContents { + fn render_file(&self, file: &F) -> TextCellContents { self.file_style .for_file(file, self.theme) .with_link_paths() diff --git a/src/output/table.rs b/src/output/table.rs index fa28a9b23..edb85421a 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -11,7 +11,7 @@ use once_cell::sync::Lazy; use uzers::UsersCache; use crate::fs::feature::git::GitCache; -use crate::fs::{fields as f, File}; +use crate::fs::{fields as f, Filelike}; use crate::options::vars::EZA_WINDOWS_ATTRIBUTES; use crate::options::Vars; use crate::output::cell::TextCell; @@ -292,7 +292,7 @@ impl TimeType { } /// Returns the corresponding time from [File] - pub fn get_corresponding_time(self, file: &File<'_>) -> Option { + pub fn get_corresponding_time(self, file: &F) -> Option { match self { TimeType::Modified => file.modified_time(), TimeType::Changed => file.changed_time(), @@ -457,16 +457,16 @@ impl<'a> Table<'a> { Row { cells } } - pub fn row_for_file( + pub fn row_for_file( &self, - file: &File<'_>, + file: &F, xattrs: bool, color_scale_info: Option, ) -> Row { let cells = self .columns .iter() - .map(|c| self.display(file, *c, xattrs, color_scale_info)) + .map(|c| self.display_file(file, *c, xattrs, color_scale_info)) .collect(); Row { cells } @@ -477,7 +477,7 @@ impl<'a> Table<'a> { } #[cfg(unix)] - fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> Option { + fn permissions_plus(&self, file: &F, xattrs: bool) -> Option { file.permissions().map(|p| f::PermissionsPlus { file_type: file.type_char(), permissions: p, @@ -487,7 +487,7 @@ impl<'a> Table<'a> { #[allow(clippy::unnecessary_wraps)] // Needs to match Unix function #[cfg(windows)] - fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> Option { + fn permissions_plus(&self, file: &F, xattrs: bool) -> Option { Some(f::PermissionsPlus { file_type: file.type_char(), #[cfg(windows)] @@ -497,14 +497,14 @@ impl<'a> Table<'a> { } #[cfg(unix)] - fn octal_permissions(&self, file: &File<'_>) -> Option { + fn octal_permissions(&self, file: &F) -> Option { file.permissions() .map(|p| f::OctalPermissions { permissions: p }) } - fn display( + fn display_file( &self, - file: &File<'_>, + file: &F, column: Column, xattrs: bool, color_scale_info: Option, @@ -564,19 +564,19 @@ impl<'a> Table<'a> { } } - fn git_status(&self, file: &File<'_>) -> f::Git { - debug!("Getting Git status for file {:?}", file.path); + fn git_status(&self, file: &F) -> f::Git { + debug!("Getting Git status for file {:?}", file.path()); self.git - .map(|g| g.get(&file.path, file.is_directory())) + .map(|g| g.get(file.path(), file.is_directory())) .unwrap_or_default() } - fn subdir_git_repo(&self, file: &File<'_>, status: bool) -> f::SubdirGitRepo { - debug!("Getting subdir repo status for path {:?}", file.path); + fn subdir_git_repo(&self, file: &F, status: bool) -> f::SubdirGitRepo { + debug!("Getting subdir repo status for path {:?}", file.path()); if file.is_directory() { - return f::SubdirGitRepo::from_path(&file.path, status); + return f::SubdirGitRepo::from_path(file.path(), status); } f::SubdirGitRepo::default() } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 744a3216e..3e2ea83cb 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,9 +1,9 @@ use nu_ansi_term::Style; -use crate::fs::File; +use crate::fs::{ArchiveEntry, File, Filelike}; use crate::info::filetype::FileType; use crate::output::color_scale::ColorScaleOptions; -use crate::output::file_name::Colours as FileNameColours; +use crate::output::file_name::{Colours as FileNameColours, GetStyle}; use crate::output::render; mod ui_styles; @@ -137,6 +137,7 @@ pub trait FileStyle: Sync { /// Return the style to paint the filename text for `file` from the given /// `theme`. fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option