diff --git a/Cargo.lock b/Cargo.lock index ad1ee090f..3da256602 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -1371,6 +1371,7 @@ name = "posixutils-file" version = "0.2.2" dependencies = [ "clap", + "ftw", "gettext-rs", "libc", "plib", @@ -1384,6 +1385,7 @@ name = "posixutils-fs" version = "0.2.2" dependencies = [ "clap", + "ftw", "gettext-rs", "libc", ] diff --git a/file/Cargo.toml b/file/Cargo.toml index 533e1630c..38debeaf5 100644 --- a/file/Cargo.toml +++ b/file/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] plib = { path = "../plib" } +ftw = { path = "../ftw" } clap.workspace = true gettext-rs.workspace = true libc.workspace = true diff --git a/file/file.rs b/file/file.rs index dae907fde..ed65c1fb1 100644 --- a/file/file.rs +++ b/file/file.rs @@ -10,11 +10,11 @@ mod magic; use std::fs::read_link; -use std::os::unix::fs::FileTypeExt; -use std::path::PathBuf; -use std::{fs, io}; +use std::io; +use std::path::{Path, PathBuf}; use clap::Parser; +use ftw::{symlink_metadata, FileType}; use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; use crate::magic::{get_type_from_magic_file_dbs, DEFAULT_MAGIC_FILE}; @@ -110,83 +110,45 @@ fn get_magic_files(args: &Args) -> Vec<PathBuf> { magic_files } -fn analyze_file(mut path: String, args: &Args, magic_files: &Vec<PathBuf>) { - if path == "-" { - path = String::new(); - io::stdin().read_line(&mut path).unwrap(); - path = path.trim().to_string(); - } - - let met = match fs::symlink_metadata(&path) { - Ok(met) => met, - Err(_) => { - println!("{path}: cannot open"); - return; - } +fn analyze_file<P: AsRef<Path>>(path: P, args: &Args, magic_files: &Vec<PathBuf>) -> String { + let meta = match symlink_metadata(&path) { + Ok(m) => m, + Err(_) => return gettext("cannot open"), }; - let file_type = met.file_type(); - - if file_type.is_symlink() { - if args.identify_as_symbolic_link { - println!("{path}: symbolic link"); - return; - } - match read_link(&path) { - Ok(file_p) => { - // trace the file pointed by symbolic link - if file_p.exists() { - println!("{path}: symbolic link to {}", file_p.to_str().unwrap()); - } else { - println!( - "{path}: broken symbolic link to {}", - file_p.to_str().unwrap() - ); - } + match meta.file_type() { + FileType::Socket => gettext("socket"), + FileType::BlockDevice => gettext("block special"), + FileType::Directory => gettext("directory"), + FileType::CharacterDevice => gettext("character special"), + FileType::Fifo => gettext("fifo"), + FileType::SymbolicLink => { + if args.identify_as_symbolic_link { + return gettext("symbolic link"); } - Err(_) => { - println!("{path}: symbolic link"); + match read_link(&path) { + Ok(file_p) => { + // trace the file pointed by symbolic link + if file_p.exists() { + gettext!("symbolic link to {}", file_p.to_str().unwrap()) + } else { + gettext!("broken symbolic link to {}", file_p.to_str().unwrap()) + } + } + Err(_) => gettext("symbolic link"), } } - return; - } - if file_type.is_char_device() { - println!("{path}: character special"); - return; - } - if file_type.is_dir() { - println!("{path}: directory"); - return; - } - if file_type.is_fifo() { - println!("{path}: fifo"); - return; - } - if file_type.is_socket() { - println!("{path}: socket"); - return; - } - if file_type.is_block_device() { - println!("{path}: block special"); - return; - } - if file_type.is_file() { - if args.no_further_file_classification { - assert!(magic_files.is_empty()); - println!("{path}: regular file"); - return; - } - if met.len() == 0 { - println!("{path}: empty"); - return; - } - match get_type_from_magic_file_dbs(&PathBuf::from(&path), magic_files) { - Some(f_type) => println!("{path}: {f_type}"), - None => println!("{path}: data"), + FileType::RegularFile => { + if args.no_further_file_classification { + assert!(magic_files.is_empty()); + return gettext("regular file"); + } + if meta.is_empty() { + return gettext("empty"); + } + get_type_from_magic_file_dbs(path, &magic_files).unwrap_or_else(|| gettext("data")) } - return; } - unreachable!(); } fn main() -> Result<(), Box<dyn std::error::Error>> { @@ -199,7 +161,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { let magic_files = get_magic_files(&args); for file in &args.files { - analyze_file(file.clone(), &args, &magic_files); + let mut file = file.clone(); + if file == "-" { + file = String::new(); + io::stdin().read_line(&mut file).unwrap(); + file = file.trim().to_string(); + } + println!("{}: {}", &file, analyze_file(&file, &args, &magic_files)); } Ok(()) diff --git a/file/magic.rs b/file/magic.rs index 2b29fa8ea..c8fd0cfc2 100644 --- a/file/magic.rs +++ b/file/magic.rs @@ -13,7 +13,7 @@ use std::{ fmt, fs::File, io::{self, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom}, - path::PathBuf, + path::{Path, PathBuf}, }; #[cfg(target_os = "macos")] @@ -24,13 +24,14 @@ pub const DEFAULT_MAGIC_FILE: &str = "/usr/share/file/magic/magic"; pub const DEFAULT_MAGIC_FILE: &str = "/etc/magic"; /// Get type for the file from the magic file databases (traversed in order of argument) -pub fn get_type_from_magic_file_dbs( - test_file: &PathBuf, +pub fn get_type_from_magic_file_dbs<P: AsRef<Path>>( + test_file: P, magic_file_dbs: &[PathBuf], ) -> Option<String> { - magic_file_dbs.iter().find_map(|magic_file| { - parse_magic_file_and_test(&PathBuf::from(magic_file), &PathBuf::from(test_file)).ok() - }) + let test_file = test_file.as_ref(); + magic_file_dbs + .iter() + .find_map(|magic_file| parse_magic_file_and_test(test_file, magic_file).ok()) } /// Errors that can occur during parsing of a raw magic line. @@ -478,8 +479,8 @@ impl RawMagicFileLine { /// line by line. It parses each line of the magic database file and tests it against /// the content of the test file. fn parse_magic_file_and_test( + test_file: &Path, magic_file: &PathBuf, - test_file: &PathBuf, ) -> Result<String, Box<dyn std::error::Error>> { let mf_reader = BufReader::new(File::open(magic_file)?); let mut tf_reader = BufReader::new(File::open(test_file)?); diff --git a/fs/Cargo.toml b/fs/Cargo.toml index 971fa60b0..2ad34cf23 100644 --- a/fs/Cargo.toml +++ b/fs/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +ftw = { path = "../ftw" } clap.workspace = true gettext-rs.workspace = true libc.workspace = true @@ -18,4 +19,3 @@ workspace = true [[bin]] name = "df" path = "./df.rs" - diff --git a/fs/df.rs b/fs/df.rs index 6b37eb2cb..6668fb361 100644 --- a/fs/df.rs +++ b/fs/df.rs @@ -13,10 +13,13 @@ mod mntent; #[cfg(target_os = "linux")] use crate::mntent::MountTable; +use ftw::{metadata, symlink_metadata_cstr, FilesystemStatistics}; + use clap::Parser; use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; #[cfg(target_os = "macos")] use std::ffi::CStr; +use std::os::unix::fs::MetadataExt; use std::{cmp, ffi::CString, fmt::Display, io}; #[derive(Parser)] @@ -49,33 +52,131 @@ struct Args { files: Vec<String>, } -/// Display modes -pub enum OutputMode { - /// When both the -k and -P options are specified - Posix, - /// When the -P option is specified without the -k option - PosixLegacy, - /// The format of the default output from df is unspecified, - /// but all space figures are reported in 512-byte units - Unspecified, - Unspecified1K, +#[derive(Debug)] +pub struct Mount { + /// mount point + pub target: CString, + /// file system + pub source: CString, + pub fsstat: FilesystemStatistics, + pub dev: i64, + pub masked: bool, +} + +impl Mount { + pub fn new(dir: CString, fsname: CString, fsstat: FilesystemStatistics) -> Self { + Self { + target: dir, + source: fsname, + fsstat, + dev: -1, + masked: true, + } + } +} + +#[cfg(target_os = "macos")] +fn to_cstr(array: &[libc::c_char]) -> &CStr { + unsafe { + // Assuming the array is null-terminated, as it should be for C strings. + CStr::from_ptr(array.as_ptr()) + } +} + +pub struct MountList { + info: Vec<Mount>, } -impl OutputMode { - pub fn new(kilo: bool, portable: bool) -> Self { - match (kilo, portable) { - (true, true) => Self::Posix, - (true, false) => Self::Unspecified1K, - (false, true) => Self::PosixLegacy, - (false, false) => Self::Unspecified, +impl MountList { + #[cfg(target_os = "linux")] + pub fn new() -> io::Result<Self> { + let mut info = vec![]; + + let mounts = MountTable::open_system()?; + for mount in mounts { + let fsstat = match FilesystemStatistics::new_cstr(mount.dir.as_c_str()) { + Ok(f) => f, + Err(e) => { + eprintln!("{}: {}", mount.dir.to_str().unwrap(), e); + continue; + } + }; + info.push(Mount::new(mount.dir, mount.fsname, fsstat)); + } + + Ok(MountList { info }) + } + + #[cfg(target_os = "macos")] + pub fn new() -> io::Result<Self> { + let mut info = vec![]; + + unsafe { + let mut mounts: *mut libc::statfs = std::ptr::null_mut(); + let n_mnt = libc::getmntinfo(&mut mounts, libc::MNT_WAIT); + if n_mnt < 0 { + return Err(io::Error::last_os_error()); + } + + let mounts: &[libc::statfs] = std::slice::from_raw_parts(mounts as _, n_mnt as _); + for mount in mounts { + let dir = to_cstr(&mount.f_mntonname).into(); + let fsname = to_cstr(&mount.f_mntfromname).into(); + let fsstat = FilesystemStatistics::from(mount); + info.push(Mount::new(dir, fsname, fsstat)); + } + } + + Ok(MountList { info }) + } + + pub fn mask_by_files(&mut self, files: Files) { + if files.devs.is_empty() { + return; + } + + for mount in &mut self.info { + mount.dev = if let Ok(m) = symlink_metadata_cstr(&mount.source) { + m.rdev() as i64 + } else if let Ok(m) = symlink_metadata_cstr(&mount.target) { + m.dev() as i64 + } else { + -1 + }; + mount.masked = false; + } + + for dev in files.devs { + for mount in &mut self.info { + if mount.dev == dev { + mount.masked = true; + } + } } } +} + +#[derive(Debug)] +pub struct Files { + pub devs: Vec<i64>, +} - pub fn get_block_size(&self) -> u64 { - match self { - OutputMode::Posix | OutputMode::Unspecified1K => 1024, - OutputMode::PosixLegacy | OutputMode::Unspecified => 512, +impl Files { + pub fn new(files: Vec<String>) -> Self { + let mut devs = vec![]; + + for path in files { + let meta = match metadata(&path) { + Ok(m) => m, + Err(e) => { + eprintln!("{}: {}", path, e); + continue; + } + }; + devs.push(meta.dev() as i64); } + + Self { devs } } } @@ -118,7 +219,6 @@ impl Display for Field { } pub struct Fields { - pub mode: OutputMode, /// file system pub source: Field, /// FS size @@ -136,10 +236,9 @@ pub struct Fields { } impl Fields { - pub fn new(mode: OutputMode) -> Self { - let size_caption = format!("{}-{}", mode.get_block_size(), gettext("blocks")); + pub fn new(block_size: u64) -> Self { + let size_caption = format!("{}-{}", block_size, gettext("blocks")); Self { - mode, source: Field::new(gettext("Filesystem"), 14, FieldType::Str), size: Field::new(size_caption, 10, FieldType::Num), used: Field::new(gettext("Used"), 10, FieldType::Num), @@ -162,73 +261,22 @@ impl Display for Fields { } pub struct FieldsData<'a> { - pub fields: &'a Fields, - pub source: &'a String, - pub size: u64, - pub used: u64, - pub avail: u64, - pub pcent: u32, - pub target: &'a String, -} - -impl Display for FieldsData<'_> { - // The remaining output with -P shall consist of one line of information - // for each specified file system. These lines shall be formatted as follows: - // "%s %d %d %d %d%% %s\n", <file system name>, <total space>, - // <space used>, <space free>, <percentage used>, - // <file system root> - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} {} {} {} {}% {}", - self.fields.source.format(self.source), - self.fields.size.format(&self.size), - self.fields.used.format(&self.used), - self.fields.avail.format(&self.avail), - self.fields.pcent.format(&self.pcent), - self.fields.target.format(self.target) - ) - } -} - -#[cfg(target_os = "macos")] -fn to_cstr(array: &[libc::c_char]) -> &CStr { - unsafe { - // Assuming the array is null-terminated, as it should be for C strings. - CStr::from_ptr(array.as_ptr()) - } -} - -fn stat(filename: &CString) -> io::Result<libc::stat> { - unsafe { - let mut st: libc::stat = std::mem::zeroed(); - let rc = libc::stat(filename.as_ptr(), &mut st); - if rc == 0 { - Ok(st) - } else { - Err(io::Error::last_os_error()) - } - } -} - -struct Mount { - devname: String, - dir: String, - dev: i64, - masked: bool, - cached_statfs: libc::statfs, + fields: &'a Fields, + source: String, + size: u64, + used: u64, + avail: u64, + pcent: u32, + target: String, } -impl Mount { - fn to_row<'a>(&'a self, fields: &'a Fields) -> FieldsData<'a> { - let sf = self.cached_statfs; - - let block_size = fields.mode.get_block_size(); - let blksz = sf.f_bsize as u64; +impl<'a> FieldsData<'a> { + pub fn new(fields: &'a Fields, mount: &Mount, block_size: u64) -> Self { + let blksz = mount.fsstat.bsize(); - let total = (sf.f_blocks * blksz) / block_size; - let avail = (sf.f_bavail * blksz) / block_size; - let free = (sf.f_bfree * blksz) / block_size; + let total = (mount.fsstat.blocks() * blksz) / block_size; + let avail = (mount.fsstat.bavail() * blksz) / block_size; + let free = (mount.fsstat.bfree() * blksz) / block_size; let used = total - free; // The percentage value shall be expressed as a positive integer, @@ -238,128 +286,35 @@ impl Mount { let percentage_used = percentage_used.ceil() as u32; FieldsData { - fields: fields, - source: &self.devname, + fields, + source: String::from(mount.source.to_str().unwrap()), size: total, used, avail, pcent: percentage_used, - target: &self.dir, - } - } -} - -struct MountList { - mounts: Vec<Mount>, - has_masks: bool, -} - -impl MountList { - fn new() -> MountList { - MountList { - mounts: Vec::new(), - has_masks: false, - } - } - - fn mask_all(&mut self) { - for mount in &mut self.mounts { - mount.masked = true; - } - } - - fn ensure_masked(&mut self) { - if !self.has_masks { - self.mask_all(); - self.has_masks = true; - } - } - - fn push(&mut self, fsstat: &libc::statfs, devname: &CString, dirname: &CString) { - let dev = { - if let Ok(st) = stat(devname) { - st.st_rdev as i64 - } else if let Ok(st) = stat(dirname) { - st.st_dev as i64 - } else { - -1 - } - }; - - self.mounts.push(Mount { - devname: String::from(devname.to_str().unwrap()), - dir: String::from(dirname.to_str().unwrap()), - dev, - masked: false, - cached_statfs: *fsstat, - }); - } -} - -#[cfg(target_os = "macos")] -fn read_mount_info() -> io::Result<MountList> { - let mut info = MountList::new(); - - unsafe { - let mut mounts: *mut libc::statfs = std::ptr::null_mut(); - let n_mnt = libc::getmntinfo(&mut mounts, libc::MNT_WAIT); - if n_mnt < 0 { - return Err(io::Error::last_os_error()); - } - - let mounts: &[libc::statfs] = std::slice::from_raw_parts(mounts as _, n_mnt as _); - for mount in mounts { - let devname = to_cstr(&mount.f_mntfromname).into(); - let dirname = to_cstr(&mount.f_mntonname).into(); - info.push(mount, &devname, &dirname); - } - } - - Ok(info) -} - -#[cfg(target_os = "linux")] -fn read_mount_info() -> io::Result<MountList> { - let mut info = MountList::new(); - - let mounts = MountTable::open_system()?; - for mount in mounts { - unsafe { - let mut buf: libc::statfs = std::mem::zeroed(); - let rc = libc::statfs(mount.dir.as_ptr(), &mut buf); - if rc < 0 { - eprintln!( - "{}: {}", - mount.dir.to_str().unwrap(), - io::Error::last_os_error() - ); - continue; - } - - info.push(&buf, &mount.fsname, &mount.dir); + target: String::from(mount.target.to_str().unwrap()), } } - - Ok(info) } -fn mask_fs_by_file(info: &mut MountList, filename: &str) -> io::Result<()> { - let c_filename = CString::new(filename).expect("`filename` contains an internal 0 byte"); - let stat_res = stat(&c_filename); - if let Err(e) = stat_res { - eprintln!("{}: {}", filename, e); - return Err(e); - } - let stat = stat_res.unwrap(); - - for mount in &mut info.mounts { - if stat.st_dev as i64 == mount.dev { - info.has_masks = true; - mount.masked = true; - } +impl Display for FieldsData<'_> { + // The remaining output with -P shall consist of one line of information + // for each specified file system. These lines shall be formatted as follows: + // "%s %d %d %d %d%% %s\n", <file system name>, <total space>, + // <space used>, <space free>, <percentage used>, + // <file system root> + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} {} {} {} {}% {}", + self.fields.source.format(&self.source), + self.fields.size.format(&self.size), + self.fields.used.format(&self.used), + self.fields.avail.format(&self.avail), + self.fields.pcent.format(&self.pcent), + self.fields.target.format(&self.target) + ) } - - Ok(()) } fn main() -> Result<(), Box<dyn std::error::Error>> { @@ -369,29 +324,46 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { let args = Args::parse(); - let mut info = read_mount_info()?; + // The format of the default output from df is unspecified, + // but all space figures are reported in 512-byte units + let block_size: u64 = if args.kilo { 1024 } else { 512 }; - if args.files.is_empty() { - info.mask_all(); - } else { - for file in &args.files { - mask_fs_by_file(&mut info, file)?; - } - } + let mut info = MountList::new()?; + let files = Files::new(args.files); + info.mask_by_files(files); - info.ensure_masked(); - - let mode = OutputMode::new(args.kilo, args.portable); - let fields = Fields::new(mode); + let fields = Fields::new(block_size); // Print header println!("{}", fields); - for mount in &info.mounts { + for mount in &info.info { if mount.masked { - let row = mount.to_row(&fields); + let row = FieldsData::new(&fields, mount, block_size); println!("{}", row); } } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_only_one_row() { + let mut info = MountList::new().unwrap(); + let files = Files::new(vec!["/tmp/".into()]); + dbg!(&files); + info.mask_by_files(files); + + let mut count = 0; + for mount in &info.info { + if mount.masked { + dbg!(&mount); + count += 1; + } + } + assert_eq!(count, 1); + } +} diff --git a/ftw/src/lib.rs b/ftw/src/lib.rs index 50b5dc222..e0c9a9933 100644 --- a/ftw/src/lib.rs +++ b/ftw/src/lib.rs @@ -1,6 +1,12 @@ +// #![feature(coverage_attribute)] + mod dir; -use dir::{DeferredDir, HybridDir, OwnedDir}; +mod small_c_string; + +use crate::dir::{DeferredDir, HybridDir, OwnedDir}; +use crate::small_c_string::run_path_with_cstr; + use std::{ ffi::{CStr, CString, OsStr}, fmt, io, @@ -107,7 +113,96 @@ impl AsRawFd for FileDescriptor { } } -/// Metadata of an entry. This is analogous to `std::fs::Metadata`. +pub struct FilesystemStatistics(libc::statfs); + +impl fmt::Debug for FilesystemStatistics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("FilesystemStatistics") + } +} + +impl FilesystemStatistics { + /// Get filesystem statistics + pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> { + run_path_with_cstr(path.as_ref(), &|p| Self::new_cstr(p)) + } + + /// Get filesystem statistics + pub fn new_cstr(path: &CStr) -> io::Result<Self> { + let mut buf = unsafe { std::mem::zeroed() }; + let ret = unsafe { libc::statfs(path.as_ptr(), &mut buf) }; + if ret != 0 { + return Err(io::Error::last_os_error()); + } + Ok(Self(buf)) + } + + #[must_use] + pub fn bsize(&self) -> u64 { + self.0.f_bsize as u64 + } + + #[must_use] + pub fn blocks(&self) -> u64 { + self.0.f_blocks + } + + #[must_use] + pub fn bavail(&self) -> u64 { + self.0.f_bavail + } + + #[must_use] + pub fn bfree(&self) -> u64 { + self.0.f_bfree + } +} + +impl From<&libc::statfs> for FilesystemStatistics { + fn from(value: &libc::statfs) -> Self { + Self(*value) + } +} + +/// Given a path, queries the file system to get information about a file, +/// directory, etc. +/// +/// This function will traverse symbolic links to query information about the +/// destination file. +/// +/// This is analogous to [`std::fs::metadata`]. +pub fn metadata<P: AsRef<Path>>(path: P) -> io::Result<Metadata> { + run_path_with_cstr(path.as_ref(), &|p| Metadata::new(libc::AT_FDCWD, p, true)) +} + +/// Given a path, queries the file system to get information about a file, +/// directory, etc. +/// +/// This function will traverse symbolic links to query information about the +/// destination file. +/// +/// This is analogous to [`std::fs::metadata`]. +pub fn metadata_cstr(path: &CStr) -> io::Result<Metadata> { + Metadata::new(libc::AT_FDCWD, path, true) +} + +/// Queries the metadata about a file without following symlinks. +/// +/// This is analogous to [`std::fs::symlink_metadata`]. +pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> io::Result<Metadata> { + run_path_with_cstr(path.as_ref(), &|p| Metadata::new(libc::AT_FDCWD, p, false)) +} + +/// Queries the metadata about a file without following symlinks. +/// +/// This is analogous to [`std::fs::symlink_metadata`]. +pub fn symlink_metadata_cstr(path: &CStr) -> io::Result<Metadata> { + Metadata::new(libc::AT_FDCWD, path, false) +} + +/// Metadata information about a file. +/// +/// This is analogous to [`std::fs::Metadata`]. #[derive(Clone)] pub struct Metadata(libc::stat); @@ -118,43 +213,82 @@ impl fmt::Debug for Metadata { } impl Metadata { - /// Create a new `Metadata`. + /// Create a new [`Metadata`]. /// - /// `dirfd` could be the special value `libc::AT_FDCWD` to query the metadata of a file at the + /// `dirfd` could be the special value [`libc::AT_FDCWD`] to query the metadata of a file at the /// process' current working directory. - pub fn new( - dirfd: libc::c_int, - file_name: &CStr, - follow_symlinks: bool, - ) -> io::Result<Metadata> { - let mut statbuf = MaybeUninit::uninit(); - let flags = if follow_symlinks { - 0 - } else { - libc::AT_SYMLINK_NOFOLLOW - }; - let ret = unsafe { libc::fstatat(dirfd, file_name.as_ptr(), statbuf.as_mut_ptr(), flags) }; + pub fn new(dirfd: libc::c_int, pathname: &CStr, follow_symlinks: bool) -> io::Result<Metadata> { + let mut buf = MaybeUninit::uninit(); + let mut flags = 0; + if !follow_symlinks { + flags |= libc::AT_SYMLINK_NOFOLLOW; + } + let ret = unsafe { libc::fstatat(dirfd, pathname.as_ptr(), buf.as_mut_ptr(), flags) }; if ret != 0 { return Err(io::Error::last_os_error()); } - Ok(Metadata(unsafe { statbuf.assume_init() })) + Ok(Metadata(unsafe { buf.assume_init() })) } - /// Query the file type. + /// Returns the file type for this metadata. + /// + /// This is analogous to [`std::fs::Metadata::file_type`]. pub fn file_type(&self) -> FileType { - match self.0.st_mode & libc::S_IFMT { - libc::S_IFSOCK => FileType::Socket, - libc::S_IFLNK => FileType::SymbolicLink, - libc::S_IFREG => FileType::RegularFile, - libc::S_IFBLK => FileType::BlockDevice, - libc::S_IFDIR => FileType::Directory, - libc::S_IFCHR => FileType::CharacterDevice, - libc::S_IFIFO => FileType::Fifo, - _ => unreachable!(), - } + FileType::new(self.0.st_mode) + } + + /// Returns `true` if this metadata is for a directory. The + /// result is mutually exclusive to the result of + /// [`Metadata::is_file`], and will be false for symlink metadata + /// obtained from [`symlink_metadata`]. + /// + /// This is analogous to [`std::fs::Metadata::is_dir`]. + #[must_use] + pub fn is_dir(&self) -> bool { + self.file_type().is_dir() + } + + /// Returns `true` if this metadata is for a regular file. The + /// result is mutually exclusive to the result of + /// [`Metadata::is_dir`], and will be false for symlink metadata + /// obtained from [`symlink_metadata`]. + /// + /// This is analogous to [`std::fs::Metadata::is_file`]. + #[must_use] + pub fn is_file(&self) -> bool { + self.file_type().is_file() + } + + /// Returns `true` if this metadata is for a symbolic link. + /// + /// This is analogous to [`std::fs::Metadata::is_symlink`]. + #[must_use] + pub fn is_symlink(&self) -> bool { + self.file_type().is_symlink() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the size of the file, in bytes, this metadata is for. + /// + /// This is analogous to [`std::fs::Metadata::len`]. + #[must_use] + pub fn len(&self) -> u64 { + self.0.st_size as _ + } + + /// Returns the permissions of the file this metadata is for. + /// + /// This is analogous to [`std::fs::Metadata::permissions`]. + pub fn permissions(&self) -> Permissions { + Permissions(self.0.st_mode) } // These are "effective" IDs and not "real" to allow for things like sudo + #[must_use] fn get_uid_and_gid(&self) -> (libc::uid_t, libc::gid_t) { let uid = unsafe { libc::geteuid() }; let gid = unsafe { libc::getegid() }; @@ -163,6 +297,7 @@ impl Metadata { /// Check if the current process has write permission for the file that this `Metadata` refers /// to. + #[must_use] pub fn is_writable(&self) -> bool { let (uid, gid) = self.get_uid_and_gid(); @@ -180,6 +315,7 @@ impl Metadata { /// Check if the current process has execute or search permission for the file that this /// `Metadata` refers to. + #[must_use] pub fn is_executable(&self) -> bool { let (uid, gid) = self.get_uid_and_gid(); @@ -194,90 +330,112 @@ impl Metadata { self.0.st_mode & libc::S_IXOTH != 0 } } - - /// Returns `true` if this metadata is for a directory. - pub fn is_dir(&self) -> bool { - self.file_type().is_dir() - } - - /// Returns `true` if this metadata is for a regular file. - pub fn is_file(&self) -> bool { - self.file_type().is_file() - } - - /// Returns `true` if this metadata is for a symbolic link. - pub fn is_symlink(&self) -> bool { - self.file_type().is_symlink() - } } impl unix::fs::MetadataExt for Metadata { + /// Returns the ID of the device containing the file. + #[must_use] fn dev(&self) -> u64 { self.0.st_dev as _ } + /// Returns the inode number. + #[must_use] fn ino(&self) -> u64 { self.0.st_ino } + /// Returns the rights applied to this file. + #[must_use] fn mode(&self) -> u32 { self.0.st_mode as _ } + /// Returns the number of hard links pointing to this file. + #[must_use] fn nlink(&self) -> u64 { self.0.st_nlink as _ } + /// Returns the user ID of the owner of this file. + #[must_use] fn uid(&self) -> u32 { self.0.st_uid } + /// Returns the group ID of the owner of this file. + #[must_use] fn gid(&self) -> u32 { self.0.st_gid } + /// Returns the device ID of this file (if it is a special one). + #[must_use] fn rdev(&self) -> u64 { self.0.st_rdev as _ } + /// Returns the total size of this file in bytes. + #[must_use] fn size(&self) -> u64 { self.0.st_size as _ } + /// Returns the last access time of the file, in seconds since Unix Epoch. + #[must_use] fn atime(&self) -> i64 { self.0.st_atime } + /// Returns the last access time of the file, in nanoseconds since [`atime`]. + #[must_use] fn atime_nsec(&self) -> i64 { self.0.st_atime_nsec } + /// Returns the last modification time of the file, in seconds since Unix Epoch. + #[must_use] fn mtime(&self) -> i64 { self.0.st_mtime } + /// Returns the last modification time of the file, in nanoseconds since [`mtime`]. + #[must_use] fn mtime_nsec(&self) -> i64 { self.0.st_mtime_nsec } + /// Returns the last status change time of the file, in seconds since Unix Epoch. + #[must_use] fn ctime(&self) -> i64 { self.0.st_ctime } + /// Returns the last status change time of the file, in nanoseconds since [`ctime`]. + #[must_use] fn ctime_nsec(&self) -> i64 { self.0.st_ctime_nsec } + /// Returns the block size for filesystem I/O. + #[must_use] fn blksize(&self) -> u64 { self.0.st_blksize as _ } + /// Returns the number of blocks allocated to the file, in 512-byte units. + /// + /// Please note that this may be smaller than `st_size / 512` when the file has holes. + #[must_use] fn blocks(&self) -> u64 { self.0.st_blocks as _ } } -/// File type of an entry. Returned by `Metadata::file_type`. +/// File type of an entry. Returned by [`Metadata::file_type`]. +/// +/// This is analogous to [`std::fs::FileType`]. +#[must_use] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FileType { Socket, @@ -290,40 +448,168 @@ pub enum FileType { } impl FileType { - /// Tests whether this file type represents a directory. - pub fn is_dir(&self) -> bool { - *self == FileType::Directory + fn new(mode: libc::mode_t) -> Self { + match mode & libc::S_IFMT { + libc::S_IFSOCK => FileType::Socket, + libc::S_IFLNK => FileType::SymbolicLink, + libc::S_IFREG => FileType::RegularFile, + libc::S_IFBLK => FileType::BlockDevice, + libc::S_IFDIR => FileType::Directory, + libc::S_IFCHR => FileType::CharacterDevice, + libc::S_IFIFO => FileType::Fifo, + _ => unreachable!(), + } } - /// Tests whether this file type represents a symbolic link. - pub fn is_symlink(&self) -> bool { - *self == FileType::SymbolicLink + /// Tests whether this file type represents a directory. The + /// result is mutually exclusive to the results of + /// [`is_file`] and [`is_symlink`]; only zero or one of these + /// tests may pass. + /// + /// This is analogous to [`std::fs::FileType::is_dir`]. + #[must_use] + pub fn is_dir(&self) -> bool { + *self == FileType::Directory } /// Tests whether this file type represents a regular file. + /// The result is mutually exclusive to the results of + /// [`is_dir`] and [`is_symlink`]; only zero or one of these + /// tests may pass. + /// + /// This is analogous to [`std::fs::FileType::is_file`]. + #[must_use] pub fn is_file(&self) -> bool { *self == FileType::RegularFile } + + /// Tests whether this file type represents a symbolic link. + /// The result is mutually exclusive to the results of + /// [`is_dir`] and [`is_file`]; only zero or one of these + /// tests may pass. + /// + /// This is analogous to [`std::fs::FileType::is_symlink`]. + #[must_use] + pub fn is_symlink(&self) -> bool { + *self == FileType::SymbolicLink + } } impl unix::fs::FileTypeExt for FileType { + /// Returns `true` if this file type is a block device. + /// + /// This is analogous to [`std::fs::FileType::is_block_device`]. + #[must_use] fn is_block_device(&self) -> bool { *self == FileType::BlockDevice } + /// Returns `true` if this file type is a char device. + /// + /// This is analogous to [`std::fs::FileType::is_char_device`]. + #[must_use] fn is_char_device(&self) -> bool { *self == FileType::CharacterDevice } + /// Returns `true` if this file type is a fifo. + /// + /// This is analogous to [`std::fs::FileType::is_fifo`]. + #[must_use] fn is_fifo(&self) -> bool { *self == FileType::Fifo } + /// Returns `true` if this file type is a socket. + /// + /// This is analogous to [`std::fs::FileType::is_socket`]. + #[must_use] fn is_socket(&self) -> bool { *self == FileType::Socket } } +/// Representation of the various permissions on a file. +/// +/// Returned by [`Metadata::permissions`]. +#[must_use] +pub struct Permissions(libc::mode_t); + +impl Permissions { + /// Read permission bit for the owner of the file. + #[must_use] + pub fn is_read_owner(&self) -> bool { + self.0 & libc::S_IRUSR != 0 + } + + /// Write permission bit for the owner of the file. + #[must_use] + pub fn is_write_owner(&self) -> bool { + self.0 & libc::S_IWUSR != 0 + } + + /// Execute (for ordinary files) or search (for directories) + /// permission bit for the owner of the file. + #[must_use] + pub fn is_executable_owner(&self) -> bool { + self.0 & libc::S_IXUSR != 0 + } + + /// Read permission bit for the group owner of the file. + #[must_use] + pub fn is_read_group(&self) -> bool { + self.0 & libc::S_IRGRP != 0 + } + + /// Write permission bit for the group owner of the file. + #[must_use] + pub fn is_write_group(&self) -> bool { + self.0 & libc::S_IWGRP != 0 + } + + /// Execute or search permission bit for the group owner of the file. + #[must_use] + pub fn is_executable_group(&self) -> bool { + self.0 & libc::S_IXGRP != 0 + } + + /// Read permission bit for other users. + #[must_use] + pub fn is_read_other(&self) -> bool { + self.0 & libc::S_IROTH != 0 + } + + /// Write permission bit for other users. + #[must_use] + pub fn is_write_other(&self) -> bool { + self.0 & libc::S_IWOTH != 0 + } + + /// Execute or search permission bit for other users. + #[must_use] + pub fn is_executable_other(&self) -> bool { + self.0 & libc::S_IXOTH != 0 + } + + /// This is the set-user-ID on execute bit. + #[must_use] + pub fn is_set_user_id(&self) -> bool { + self.0 & libc::S_ISUID != 0 + } + + /// This is the set-group-ID on execute bit. + #[must_use] + pub fn is_set_group_id(&self) -> bool { + self.0 & libc::S_ISGID != 0 + } + + /// This is the sticky bit. + #[must_use] + pub fn is_sticky(&self) -> bool { + self.0 & libc::S_ISVTX != 0 + } +} + #[derive(Debug)] struct TreeNode { dir: HybridDir, @@ -1032,3 +1318,57 @@ fn build_path(path_stack: &[Rc<[libc::c_char]>], filename: &Rc<[libc::c_char]>) pathbuf } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_symlink_metadata_not_exist() { + let file = "tests/not_exist"; + let meta = symlink_metadata(file); + let meta = meta.unwrap_err(); + assert_eq!(meta.kind(), std::io::ErrorKind::NotFound); + } + + #[test] + fn test_symlink_metadata_is_dir() { + let file = "tests"; + let meta = symlink_metadata(file).unwrap(); + assert!(meta.is_dir()); + assert_eq!(meta.file_type(), FileType::Directory); + } + + #[test] + fn test_symlink_metadata_cstr_is_dir() { + let file = c"tests"; + let meta = symlink_metadata_cstr(file).unwrap(); + assert!(meta.is_dir()); + assert_eq!(meta.file_type(), FileType::Directory); + } + + #[test] + fn test_symlink_metadata_is_file() { + let file = "tests/empty_file.txt"; + let meta = symlink_metadata(file).unwrap(); + assert!(meta.is_file()); + assert_eq!(meta.file_type(), FileType::RegularFile); + } + + #[test] + fn test_symlink_metadata_is_empty() { + let file = "tests/empty_file.txt"; + let meta = symlink_metadata(file).unwrap(); + assert!(meta.is_empty()); + } + + #[test] + fn test_symlink_metadata_is_char_device() { + use unix::fs::FileTypeExt; + + let file = "/dev/null"; + let file_type = symlink_metadata(file).unwrap().file_type(); + assert!(file_type.is_char_device()); + assert_eq!(file_type, FileType::CharacterDevice); + } +} diff --git a/ftw/src/small_c_string.rs b/ftw/src/small_c_string.rs new file mode 100644 index 000000000..0697b1511 --- /dev/null +++ b/ftw/src/small_c_string.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT + +//! Copied from [`std::sys::common::small_c_string`] + +use std::ffi::{CStr, CString}; +use std::mem::MaybeUninit; +use std::path::Path; +use std::{io, ptr, slice}; + +// Make sure to stay under 4096 so the compiler doesn't insert a probe frame: +// https://docs.rs/compiler_builtins/latest/compiler_builtins/probestack/index.html +#[cfg(not(target_os = "espidf"))] +const MAX_STACK_ALLOCATION: usize = 384; +#[cfg(target_os = "espidf")] +const MAX_STACK_ALLOCATION: usize = 32; + +// const NUL_ERR: io::Error = +// io::const_io_error!(io::ErrorKind::InvalidInput, "file name contained an unexpected NUL byte"); + +#[inline] +pub fn run_path_with_cstr<T>(path: &Path, f: &dyn Fn(&CStr) -> io::Result<T>) -> io::Result<T> { + run_with_cstr(path.as_os_str().as_encoded_bytes(), f) +} + +#[inline] +pub fn run_with_cstr<T>(bytes: &[u8], f: &dyn Fn(&CStr) -> io::Result<T>) -> io::Result<T> { + // Dispatch and dyn erase the closure type to prevent mono bloat. + // See https://github.com/rust-lang/rust/pull/121101. + if bytes.len() >= MAX_STACK_ALLOCATION { + run_with_cstr_allocating(bytes, f) + } else { + unsafe { run_with_cstr_stack(bytes, f) } + } +} + +/// # Safety +/// +/// `bytes` must have a length less than `MAX_STACK_ALLOCATION`. +unsafe fn run_with_cstr_stack<T>( + bytes: &[u8], + f: &dyn Fn(&CStr) -> io::Result<T>, +) -> io::Result<T> { + let mut buf = MaybeUninit::<[u8; MAX_STACK_ALLOCATION]>::uninit(); + let buf_ptr = buf.as_mut_ptr() as *mut u8; + + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), buf_ptr, bytes.len()); + buf_ptr.add(bytes.len()).write(0); + } + + match CStr::from_bytes_with_nul(unsafe { slice::from_raw_parts(buf_ptr, bytes.len() + 1) }) { + Ok(s) => f(s), + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "file name contained an unexpected NUL byte", + )), + } +} + +#[cold] +#[inline(never)] +fn run_with_cstr_allocating<T>(bytes: &[u8], f: &dyn Fn(&CStr) -> io::Result<T>) -> io::Result<T> { + match CString::new(bytes) { + Ok(s) => f(&s), + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "file name contained an unexpected NUL byte", + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_run_path_with_cstr() { + let file = "tests/not_exist"; + let result = run_path_with_cstr(file.as_ref(), &|p| { + assert_eq!(p, c"tests/not_exist"); + Ok(()) + }); + assert!(result.is_ok()); + } + + // #[coverage(off)] + #[test] + fn test_run_path_with_cstr_nul() { + let file = "tests\0not_exist"; + let result = run_path_with_cstr(file.as_ref(), &|_p| Ok(())); + assert!(result.is_err()); + let result = result.unwrap_err(); + assert_eq!(result.kind(), std::io::ErrorKind::InvalidInput); + } + + #[test] + fn test_run_path_with_cstr_alloc() { + let file = "e".repeat(MAX_STACK_ALLOCATION + 1); + let result = run_path_with_cstr(file.as_ref(), &|p| { + assert_eq!(p, CString::new(file.as_str()).unwrap().as_c_str()); + Ok(()) + }); + assert!(result.is_ok()); + } + + // #[coverage(off)] + #[test] + fn test_run_path_with_cstr_alloc_nul() { + let file = "\0".repeat(MAX_STACK_ALLOCATION + 1); + let result = run_path_with_cstr(file.as_ref(), &|_p| Ok(())); + assert!(result.is_err()); + let result = result.unwrap_err(); + assert_eq!(result.kind(), std::io::ErrorKind::InvalidInput); + } +} diff --git a/ftw/tests/empty_file.txt b/ftw/tests/empty_file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tree/ls.rs b/tree/ls.rs index 1f53cc392..46f91ebc5 100644 --- a/tree/ls.rs +++ b/tree/ls.rs @@ -9,6 +9,8 @@ mod ls_util; +use ftw::{metadata, symlink_metadata, Metadata}; + use self::ls_util::{ls_from_utf8_lossy, Entry, LongFormatPadding, MultiColumnPadding}; use clap::{CommandFactory, FromArgMatches, Parser}; use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; @@ -923,8 +925,7 @@ fn ls(paths: Vec<PathBuf>, config: &Config) -> io::Result<u8> { // Files get processed first let mut file_entries = Vec::new(); for path in files { - let path_cstr = CString::new(path.as_os_str().as_bytes()).unwrap(); - let metadata = match ftw::Metadata::new(libc::AT_FDCWD, &path_cstr, false) { + let meta = match symlink_metadata(&path) { Ok(m) => m, Err(e) => { eprintln!("ls: {e}"); @@ -942,8 +943,8 @@ fn ls(paths: Vec<PathBuf>, config: &Config) -> io::Result<u8> { // If -H or -L are enabled, the metadata to be reported is from the file // that the symbolic link points to. - let metadata = if metadata.is_symlink() && dereference_symlink { - match ftw::Metadata::new(libc::AT_FDCWD, &path_cstr, true) { + let meta = if meta.is_symlink() && dereference_symlink { + match metadata(&path) { Ok(m) => m, Err(e) => { eprintln!("ls: {e}"); @@ -952,13 +953,13 @@ fn ls(paths: Vec<PathBuf>, config: &Config) -> io::Result<u8> { } } } else { - metadata + meta }; // Target of the symlink let target_path = { let mut target_path = None; - if metadata.is_symlink() && !dereference_symlink { + if meta.is_symlink() && !dereference_symlink { if let OutputFormat::Long(_) = &config.output_format { let mut buf = vec![0u8; libc::PATH_MAX as usize]; @@ -986,12 +987,7 @@ fn ls(paths: Vec<PathBuf>, config: &Config) -> io::Result<u8> { target_path }; - let entry = match Entry::new( - target_path, - path.as_os_str().to_os_string(), - &metadata, - config, - ) { + let entry = match Entry::new(target_path, path.as_os_str().to_os_string(), &meta, config) { Ok(x) => x, Err(e) => { eprintln!("ls: {e}"); @@ -1106,24 +1102,22 @@ fn process_single_dir( return Ok(false); } - let metadata = dir_entry.metadata().unwrap(); + let meta = dir_entry.metadata().unwrap(); let is_dot_or_double_dot = dir_entry.is_dot_or_double_dot(); // Get the metadata of the file, equivalent to `std::fs::symlink_metadata` let marker = { - let metadata = - match ftw::Metadata::new(dir_entry.dir_fd(), dir_entry.file_name(), false) { - Ok(md) => md, - Err(e) => { - let path_str = ls_from_utf8_lossy( - dir_entry.path().as_inner().as_os_str().as_bytes(), - ); - let err_str = gettext!("cannot access '{}': {}", path_str, e); - errors.push(io::Error::other(err_str)); - return Ok(false); - } - }; - (metadata.dev(), metadata.ino()) + let meta = match Metadata::new(dir_entry.dir_fd(), dir_entry.file_name(), false) { + Ok(m) => m, + Err(e) => { + let path_str = + ls_from_utf8_lossy(dir_entry.path().as_inner().as_os_str().as_bytes()); + let err_str = gettext!("cannot access '{}': {}", path_str, e); + errors.push(io::Error::other(err_str)); + return Ok(false); + } + }; + (meta.dev(), meta.ino()) }; if current_dir.is_none() { @@ -1170,7 +1164,7 @@ fn process_single_dir( }; let mut target_path = None; - if metadata.is_symlink() && !dereference_symlink { + if meta.is_symlink() && !dereference_symlink { if let OutputFormat::Long(_) = &config.output_format { target_path = Some(ls_from_utf8_lossy( dir_entry.read_link().unwrap().to_bytes(), @@ -1181,7 +1175,7 @@ fn process_single_dir( target_path }; - let entry = Entry::new(target_path, file_name_raw, metadata, config) + let entry = Entry::new(target_path, file_name_raw, meta, config) .map_err(|e| io::Error::other(format!("'{path_str}': {e}")))?; let mut include_entry = false; @@ -1207,7 +1201,7 @@ fn process_single_dir( entries.push(entry); if config.recursive { - if metadata.is_dir() { + if meta.is_dir() { return Ok(true); } } diff --git a/tree/ls_util/entry.rs b/tree/ls_util/entry.rs index 7f612f022..c9d5f9643 100644 --- a/tree/ls_util/entry.rs +++ b/tree/ls_util/entry.rs @@ -587,22 +587,14 @@ fn get_file_mode_string(metadata: &ftw::Metadata) -> String { _ => '-', }); - let mode = metadata.mode(); + let perm = metadata.permissions(); // Owner permissions - file_mode.push(if mode & (libc::S_IRUSR as u32) != 0 { - 'r' - } else { - '-' - }); - file_mode.push(if mode & (libc::S_IWUSR as u32) != 0 { - 'w' - } else { - '-' - }); + file_mode.push(if perm.is_read_owner() { 'r' } else { '-' }); + file_mode.push(if perm.is_write_owner() { 'w' } else { '-' }); file_mode.push({ - let executable = mode & (libc::S_IXUSR as u32) != 0; - let set_user_id = mode & (libc::S_ISUID as u32) != 0; + let executable = perm.is_executable_owner(); + let set_user_id = perm.is_set_user_id(); match (executable, set_user_id) { (true, true) => 's', (true, false) => 'x', @@ -612,37 +604,17 @@ fn get_file_mode_string(metadata: &ftw::Metadata) -> String { }); // Group permissions - file_mode.push(if mode & (libc::S_IRGRP as u32) != 0 { - 'r' - } else { - '-' - }); - file_mode.push(if mode & (libc::S_IWGRP as u32) != 0 { - 'w' - } else { - '-' - }); - file_mode.push(if mode & (libc::S_IXGRP as u32) != 0 { - 'x' - } else { - '-' - }); + file_mode.push(if perm.is_read_group() { 'r' } else { '-' }); + file_mode.push(if perm.is_write_group() { 'w' } else { '-' }); + file_mode.push(if perm.is_executable_group() { 'x' } else { '-' }); // Other permissions - file_mode.push(if mode & (libc::S_IROTH as u32) != 0 { - 'r' - } else { - '-' - }); - file_mode.push(if mode & (libc::S_IWOTH as u32) != 0 { - 'w' - } else { - '-' - }); + file_mode.push(if perm.is_read_other() { 'r' } else { '-' }); + file_mode.push(if perm.is_write_other() { 'w' } else { '-' }); file_mode.push({ if file_type.is_dir() { - let searchable = mode & (libc::S_IXOTH as u32) != 0; - let restricted_deletion = mode & (libc::S_ISVTX as u32) != 0; + let searchable = perm.is_executable_other(); + let restricted_deletion = perm.is_sticky(); match (searchable, restricted_deletion) { (true, true) => 't', (true, false) => 'x', @@ -650,7 +622,7 @@ fn get_file_mode_string(metadata: &ftw::Metadata) -> String { (false, false) => '-', } } else { - if mode & (libc::S_IXOTH as u32) != 0 { + if perm.is_executable_other() { 'x' } else { '-' diff --git a/tree/mv.rs b/tree/mv.rs index b29639674..818fa69da 100644 --- a/tree/mv.rs +++ b/tree/mv.rs @@ -6,10 +6,11 @@ // file in the root directory of this project. // SPDX-License-Identifier: MIT // -// mod common; +use ftw::{metadata, symlink_metadata}; + use self::common::{copy_file, error_string}; use clap::Parser; use common::CopyConfig; @@ -19,7 +20,7 @@ use std::{ ffi::CString, fs, io::{self, IsTerminal}, - os::unix::{ffi::OsStrExt, fs::MetadataExt}, + os::unix::fs::MetadataExt, path::{Path, PathBuf}, }; @@ -103,11 +104,8 @@ fn move_file( inode_map: &mut HashMap<(u64, u64), (ftw::FileDescriptor, CString)>, created_files: Option<&mut HashSet<PathBuf>>, ) -> io::Result<bool> { - let source_filename = CString::new(source.as_os_str().as_bytes()).unwrap(); - let target_filename = CString::new(target.as_os_str().as_bytes()).unwrap(); - - let target_md = match ftw::Metadata::new(libc::AT_FDCWD, &target_filename, true) { - Ok(md) => Some(md), + let target_md = match metadata(&target) { + Ok(m) => Some(m), Err(e) => { if e.kind() == io::ErrorKind::NotFound { None @@ -119,13 +117,13 @@ fn move_file( }; let target_exists = target_md.is_some(); let target_is_dir = match &target_md { - Some(md) => md.file_type() == ftw::FileType::Directory, + Some(m) => m.file_type() == ftw::FileType::Directory, None => false, }; - let target_is_writable = target_md.map(|md| md.is_writable()).unwrap_or(false); + let target_is_writable = target_md.map(|m| m.is_writable()).unwrap_or(false); - let source_md = match ftw::Metadata::new(libc::AT_FDCWD, &source_filename, true) { - Ok(md) => Some(md), + let source_md = match metadata(&source) { + Ok(m) => Some(m), Err(e) => { if e.kind() == io::ErrorKind::NotFound { None @@ -137,7 +135,7 @@ fn move_file( }; let source_exists = source_md.is_some(); let source_is_dir = match &source_md { - Some(md) => md.file_type() == ftw::FileType::Directory, + Some(m) => m.file_type() == ftw::FileType::Directory, None => false, }; @@ -152,8 +150,8 @@ fn move_file( // 2. source and target are same dirent if let (Ok(smd), Ok(tmd), Some(deref_smd)) = ( - ftw::Metadata::new(libc::AT_FDCWD, &source_filename, false), - ftw::Metadata::new(libc::AT_FDCWD, &target_filename, false), + symlink_metadata(&source), + symlink_metadata(&target), &source_md, ) { // `true` for hard links to the same file and when `source == target` @@ -391,7 +389,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { // choose mode based on whether target is a directory let dir_exists = { match fs::metadata(target) { - Ok(md) => md.is_dir(), + Ok(m) => m.is_dir(), Err(e) => { if e.kind() == io::ErrorKind::NotFound { false diff --git a/tree/rm.rs b/tree/rm.rs index 0f3830ba7..a6652972d 100644 --- a/tree/rm.rs +++ b/tree/rm.rs @@ -9,12 +9,12 @@ mod common; +use ftw::{symlink_metadata, traverse_directory}; + use self::common::error_string; use clap::Parser; -use ftw::{self, traverse_directory}; use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; use std::{ - ffi::CString, fs, io::{self, IsTerminal}, os::unix::{ffi::OsStrExt, fs::MetadataExt}, @@ -72,8 +72,8 @@ fn ask_for_prompt(cfg: &RmConfig, writable: bool) -> bool { !cfg.args.force && ((!writable && cfg.is_tty) || cfg.args.interactive) } -fn descend_into_directory(cfg: &RmConfig, entry: &ftw::Entry, metadata: &ftw::Metadata) -> bool { - let writable = metadata.is_writable(); +fn descend_into_directory(cfg: &RmConfig, entry: &ftw::Entry, meta: &ftw::Metadata) -> bool { + let writable = meta.is_writable(); if ask_for_prompt(cfg, writable) { let prompt = if writable { gettext!( @@ -93,8 +93,8 @@ fn descend_into_directory(cfg: &RmConfig, entry: &ftw::Entry, metadata: &ftw::Me true } -fn should_remove_directory(cfg: &RmConfig, entry: &ftw::Entry, metadata: &ftw::Metadata) -> bool { - let writable = metadata.is_writable(); +fn should_remove_directory(cfg: &RmConfig, entry: &ftw::Entry, meta: &ftw::Metadata) -> bool { + let writable = meta.is_writable(); if ask_for_prompt(cfg, writable) { let prompt = if writable { gettext!( @@ -116,13 +116,13 @@ fn should_remove_directory(cfg: &RmConfig, entry: &ftw::Entry, metadata: &ftw::M // The signature of `filename_fn` is to prevent unnecessarily building the filename when a prompt // is not required. -fn should_remove_file<F>(cfg: &RmConfig, metadata: &ftw::Metadata, filename_fn: F) -> bool +fn should_remove_file<F>(cfg: &RmConfig, meta: &ftw::Metadata, filename_fn: F) -> bool where F: Fn() -> String, { - let writable = metadata.is_writable(); + let writable = meta.is_writable(); if ask_for_prompt(cfg, writable) { - let file_type = metadata.file_type(); + let file_type = meta.file_type(); let prompt = match file_type { ftw::FileType::Socket => { gettext!("remove socket '{}'?", filename_fn()) @@ -140,7 +140,7 @@ where gettext!("remove fifo '{}'?", filename_fn()) } ftw::FileType::RegularFile => { - let is_empty = metadata.size() == 0; + let is_empty = meta.size() == 0; if writable { if is_empty { gettext!("remove regular empty file '{}'?", filename_fn()) @@ -179,13 +179,13 @@ enum DirAction { fn process_directory( cfg: &RmConfig, entry: &ftw::Entry, - metadata: &ftw::Metadata, + meta: &ftw::Metadata, ) -> io::Result<DirAction> { let dir_is_empty = entry.is_empty_dir(); // If directory is empty or the directory is inaccessible, try to remove it directly if (dir_is_empty.is_ok() && dir_is_empty.as_ref().unwrap() == &true) || dir_is_empty.is_err() { - if should_remove_directory(cfg, entry, metadata) { + if should_remove_directory(cfg, entry, meta) { let ret = unsafe { libc::unlinkat( entry.dir_fd(), @@ -218,7 +218,7 @@ fn process_directory( // Else, manually traverse the directory to remove the contents one-by-one } else { - if descend_into_directory(cfg, entry, metadata) { + if descend_into_directory(cfg, entry, meta) { Ok(DirAction::Entered) } else { Ok(DirAction::Skipped) @@ -269,10 +269,10 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { let success = traverse_directory( filepath, |entry| { - let md = entry.metadata().unwrap(); + let m = entry.metadata().unwrap(); - if md.file_type() == ftw::FileType::Directory { - match process_directory(cfg, &entry, md) { + if m.file_type() == ftw::FileType::Directory { + match process_directory(cfg, &entry, m) { Ok(dir_action) => match dir_action { DirAction::Entered => Ok(true), DirAction::Removed | DirAction::Skipped => Ok(false), @@ -283,7 +283,7 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { } } } else { - if should_remove_file(cfg, md, || entry.path().clean_trailing_slashes()) { + if should_remove_file(cfg, m, || entry.path().clean_trailing_slashes()) { // Remove the file let ret = unsafe { libc::unlinkat(entry.dir_fd(), entry.file_name().as_ptr(), 0) }; @@ -305,8 +305,8 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { } }, |entry| { - let md = entry.metadata().unwrap(); - if should_remove_directory(cfg, &entry, md) { + let m = entry.metadata().unwrap(); + if should_remove_directory(cfg, &entry, m) { // Remove the directory let ret = unsafe { libc::unlinkat( @@ -389,10 +389,9 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { /// This function returns `Ok(true)` on success. This never returns `Ok(false)` and the function /// signature is only to match `rm_directory`. fn rm_file(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { - let filename_cstr = CString::new(filepath.as_os_str().as_bytes())?; - let metadata = ftw::Metadata::new(libc::AT_FDCWD, &filename_cstr, false)?; + let meta = symlink_metadata(&filepath)?; - if should_remove_file(cfg, &metadata, || display_cleaned(filepath)) { + if should_remove_file(cfg, &meta, || display_cleaned(filepath)) { fs::remove_file(filepath).map_err(|e| { let err_str = gettext!( "cannot remove '{}': {}", @@ -407,8 +406,8 @@ fn rm_file(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { } fn rm_path(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { - let metadata = match fs::symlink_metadata(filepath) { - Ok(md) => md, + let meta = match fs::symlink_metadata(filepath) { + Ok(m) => m, Err(e) => { // Not an error with -f in the case of operands that do not exist if e.kind() == io::ErrorKind::NotFound && cfg.args.force { @@ -424,7 +423,7 @@ fn rm_path(cfg: &RmConfig, filepath: &Path) -> io::Result<bool> { } }; - if metadata.is_dir() { + if meta.is_dir() { rm_directory(cfg, filepath) } else { rm_file(cfg, filepath)