From 50180750a0d5e3c3127705232ebf94d1543184dc Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:14:50 +0700 Subject: [PATCH 01/13] ftw: impl FilesystemStatistics, symlink_metadata --- ftw/src/lib.rs | 304 ++++++++++++++++++++++++++++++++------ ftw/src/small_c_string.rs | 115 ++++++++++++++ ftw/tests/empty_file.txt | 0 3 files changed, 373 insertions(+), 46 deletions(-) create mode 100644 ftw/src/small_c_string.rs create mode 100644 ftw/tests/empty_file.txt diff --git a/ftw/src/lib.rs b/ftw/src/lib.rs index 50b5dc22..32faeb13 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>(path: P) -> io::Result { + run_path_with_cstr(path.as_ref(), &|p| Self::new_cstr(p)) + } + + /// Get filesystem statistics + pub fn new_cstr(path: &CStr) -> io::Result { + 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>(path: P) -> io::Result { + 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::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>(path: P) -> io::Result { + 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::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,75 @@ 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 { - 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 { + 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 u64 } // 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 +290,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 +308,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(); @@ -195,19 +324,14 @@ impl Metadata { } } - /// Returns `true` if this metadata is for a directory. - pub fn is_dir(&self) -> bool { - self.file_type().is_dir() + #[must_use] + pub fn dev(&self) -> u64 { + self.0.st_dev } - /// 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() + #[must_use] + pub fn rdev(&self) -> u64 { + self.0.st_rdev } } @@ -277,7 +401,10 @@ impl unix::fs::MetadataExt for Metadata { } } -/// 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,20 +417,51 @@ 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 { @@ -1032,3 +1190,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 00000000..0697b151 --- /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(path: &Path, f: &dyn Fn(&CStr) -> io::Result) -> io::Result { + run_with_cstr(path.as_os_str().as_encoded_bytes(), f) +} + +#[inline] +pub fn run_with_cstr(bytes: &[u8], f: &dyn Fn(&CStr) -> io::Result) -> io::Result { + // 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( + bytes: &[u8], + f: &dyn Fn(&CStr) -> io::Result, +) -> io::Result { + 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(bytes: &[u8], f: &dyn Fn(&CStr) -> io::Result) -> io::Result { + 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 00000000..e69de29b From 224817f1591aee3a1c470eb86f8f10da9597df7e Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:16:10 +0700 Subject: [PATCH 02/13] fs/file: add ftw dependencies --- Cargo.lock | 4 +++- file/Cargo.toml | 1 + fs/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51c569a1..0b91a00b 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" @@ -1326,6 +1326,7 @@ name = "posixutils-file" version = "0.2.1" dependencies = [ "clap", + "ftw", "gettext-rs", "libc", "plib", @@ -1339,6 +1340,7 @@ name = "posixutils-fs" version = "0.2.1" dependencies = [ "clap", + "ftw", "gettext-rs", "libc", "plib", diff --git a/file/Cargo.toml b/file/Cargo.toml index 0c5732da..d6aa5705 100644 --- a/file/Cargo.toml +++ b/file/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true [dependencies] plib = { path = "../plib" } +ftw = { path = "../ftw" } clap.workspace = true gettext-rs.workspace = true libc.workspace = true diff --git a/fs/Cargo.toml b/fs/Cargo.toml index 4a9f2a2c..0294fad0 100644 --- a/fs/Cargo.toml +++ b/fs/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true [dependencies] plib = { path = "../plib" } +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" - From e6ae6986e2618f771c47a762e8d3c2ce2a0f8ffb Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:17:19 +0700 Subject: [PATCH 03/13] file: use ftw::symlink_metadata --- file/file.rs | 120 ++++++++++++++++++-------------------------------- file/magic.rs | 15 ++++--- 2 files changed, 51 insertions(+), 84 deletions(-) diff --git a/file/file.rs b/file/file.rs index ddc0f3a9..88ef8021 100644 --- a/file/file.rs +++ b/file/file.rs @@ -10,16 +10,14 @@ mod magic; use crate::magic::{get_type_from_magic_file_dbs, DEFAULT_MAGIC_FILE}; +use ftw::{symlink_metadata, FileType}; use clap::Parser; use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; use plib::PROJECT_NAME; -use std::{ - fs::{self, read_link}, - io, - os::unix::fs::FileTypeExt, - path::PathBuf, -}; +use std::fs::read_link; +use std::io; +use std::path::{Path, PathBuf}; #[derive(Parser)] #[command( @@ -112,83 +110,45 @@ fn get_magic_files(args: &Args) -> Vec { magic_files } -fn analyze_file(mut path: String, args: &Args, magic_files: &Vec) { - 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>(path: P, args: &Args, magic_files: &Vec) -> 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> { @@ -202,7 +162,13 @@ fn main() -> Result<(), Box> { 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 2b29fa8e..c8fd0cfc 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>( + test_file: P, magic_file_dbs: &[PathBuf], ) -> Option { - 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> { let mf_reader = BufReader::new(File::open(magic_file)?); let mut tf_reader = BufReader::new(File::open(test_file)?); From af739bfeb04b0dd135dbd1ae5712cb52a92356f0 Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:29:57 +0700 Subject: [PATCH 04/13] ls/mv/rm: use ftw::symlink_metadata --- tree/ls.rs | 52 +++++++++++++++++++++++----------------------------- tree/mv.rs | 28 +++++++++++++--------------- tree/rm.rs | 49 ++++++++++++++++++++++++------------------------- 3 files changed, 60 insertions(+), 69 deletions(-) diff --git a/tree/ls.rs b/tree/ls.rs index f69b051d..6036f8bb 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}; @@ -927,8 +929,7 @@ fn ls(paths: Vec, config: &Config) -> io::Result { // 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}"); @@ -946,8 +947,8 @@ fn ls(paths: Vec, config: &Config) -> io::Result { // 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}"); @@ -956,13 +957,13 @@ fn ls(paths: Vec, config: &Config) -> io::Result { } } } 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]; @@ -990,12 +991,7 @@ fn ls(paths: Vec, config: &Config) -> io::Result { 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}"); @@ -1110,24 +1106,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() { @@ -1174,7 +1168,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(), @@ -1185,7 +1179,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; @@ -1211,7 +1205,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/mv.rs b/tree/mv.rs index a572322b..9e4e2615 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; @@ -20,7 +21,7 @@ use std::{ ffi::CString, fs, io::{self, IsTerminal}, - os::unix::{ffi::OsStrExt, fs::MetadataExt}, + os::unix::fs::MetadataExt, path::{Path, PathBuf}, }; @@ -104,11 +105,8 @@ fn move_file( inode_map: &mut HashMap<(u64, u64), (ftw::FileDescriptor, CString)>, created_files: Option<&mut HashSet>, ) -> io::Result { - 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 @@ -120,13 +118,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 @@ -138,7 +136,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, }; @@ -153,8 +151,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` @@ -394,7 +392,7 @@ fn main() -> Result<(), Box> { // 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 220d51ec..73515665 100644 --- a/tree/rm.rs +++ b/tree/rm.rs @@ -9,13 +9,13 @@ 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 plib::PROJECT_NAME; use std::{ - ffi::CString, fs, io::{self, IsTerminal}, os::unix::{ffi::OsStrExt, fs::MetadataExt}, @@ -73,8 +73,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!( @@ -94,8 +94,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!( @@ -117,13 +117,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(cfg: &RmConfig, metadata: &ftw::Metadata, filename_fn: F) -> bool +fn should_remove_file(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()) @@ -141,7 +141,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()) @@ -180,13 +180,13 @@ enum DirAction { fn process_directory( cfg: &RmConfig, entry: &ftw::Entry, - metadata: &ftw::Metadata, + meta: &ftw::Metadata, ) -> io::Result { 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(), @@ -219,7 +219,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) @@ -270,10 +270,10 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result { 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), @@ -284,7 +284,7 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result { } } } 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) }; @@ -306,8 +306,8 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result { } }, |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( @@ -390,10 +390,9 @@ fn rm_directory(cfg: &RmConfig, filepath: &Path) -> io::Result { /// 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 { - 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 '{}': {}", @@ -408,8 +407,8 @@ fn rm_file(cfg: &RmConfig, filepath: &Path) -> io::Result { } fn rm_path(cfg: &RmConfig, filepath: &Path) -> io::Result { - 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 { @@ -425,7 +424,7 @@ fn rm_path(cfg: &RmConfig, filepath: &Path) -> io::Result { } }; - if metadata.is_dir() { + if meta.is_dir() { rm_directory(cfg, filepath) } else { rm_file(cfg, filepath) From 4b6fb28c32dd03a16254fb90631f3c79dd55207e Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:31:19 +0700 Subject: [PATCH 05/13] df: use ftw::symlink_metadata and remove unsafe --- fs/df.rs | 357 +++++++++++++++++++++++-------------------------------- 1 file changed, 152 insertions(+), 205 deletions(-) diff --git a/fs/df.rs b/fs/df.rs index ecbb7d5c..36100315 100644 --- a/fs/df.rs +++ b/fs/df.rs @@ -13,6 +13,8 @@ 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}; use plib::PROJECT_NAME; @@ -50,36 +52,6 @@ struct Args { files: Vec, } -/// 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, -} - -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, - } - } - - pub fn get_block_size(&self) -> u64 { - match self { - OutputMode::Posix | OutputMode::Unspecified1K => 1024, - OutputMode::PosixLegacy | OutputMode::Unspecified => 512, - } - } -} - pub enum FieldType { Str, Num, @@ -119,7 +91,6 @@ impl Display for Field { } pub struct Fields { - pub mode: OutputMode, /// file system pub source: Field, /// FS size @@ -137,10 +108,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,33 +132,23 @@ 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, +pub struct Mount { + pub target: CString, + pub source: CString, + pub fsstat: FilesystemStatistics, + pub dev: i64, + pub masked: bool, } -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", , , - // , , , - // - 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) - ) +impl Mount { + pub fn new(dir: CString, fsname: CString, fsstat: FilesystemStatistics) -> Self { + Self { + target: dir, + source: fsname, + fsstat, + dev: -1, + masked: true, + } } } @@ -200,167 +160,141 @@ fn to_cstr(array: &[libc::c_char]) -> &CStr { } } -fn stat(filename: &CString) -> io::Result { - 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, +pub struct MountList { + info: Vec, } -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; - - 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 used = total - free; - - // The percentage value shall be expressed as a positive integer, - // with any fractional result causing it to be rounded to the next highest integer. - let percentage_used = f64::from(used as u32) / f64::from((used + free) as u32); - let percentage_used = percentage_used * 100.0; - let percentage_used = percentage_used.ceil() as u32; +impl MountList { + #[cfg(target_os = "linux")] + pub fn new() -> io::Result { + let mut info = vec![]; - FieldsData { - fields: &fields, - source: &self.devname, - size: total, - used, - avail, - pcent: percentage_used, - target: &self.dir, + 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 }) } -} -struct MountList { - mounts: Vec, - has_masks: bool, -} + #[cfg(target_os = "macos")] + pub fn new() -> io::Result { + let mut info = vec![]; -impl MountList { - fn new() -> MountList { - MountList { - mounts: Vec::new(), - has_masks: false, - } - } + 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()); + } - fn mask_all(&mut self) { - for mount in &mut self.mounts { - mount.masked = true; + 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 }) } - fn ensure_masked(&mut self) { - if !self.has_masks { - self.mask_all(); - self.has_masks = true; + pub fn mask_by_files(&mut self, files: Vec) { + if files.is_empty() { + return; } - } - 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 + for mount in &mut self.info { + mount.dev = if let Ok(m) = symlink_metadata_cstr(&mount.target) { + m.dev() as i64 + } else if let Ok(m) = symlink_metadata_cstr(&mount.source) { + m.rdev() 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 { - 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()); + }; + mount.masked = false; } - 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); + for path in files { + let meta = match metadata(&path) { + Ok(m) => m, + Err(e) => { + eprintln!("{}: {}", path, e); + continue; + } + }; + for mount in &mut self.info { + if mount.dev == meta.dev() as i64 { + mount.masked = true; + } + } } } +} - Ok(info) +pub struct FieldsData<'a> { + fields: &'a Fields, + source: String, + size: u64, + used: u64, + avail: u64, + pcent: u32, + target: String, } -#[cfg(target_os = "linux")] -fn read_mount_info() -> io::Result { - let mut info = MountList::new(); +impl<'a> FieldsData<'a> { + pub fn new(fields: &'a Fields, mount: &Mount, block_size: u64) -> Self { + let blksz = mount.fsstat.bsize(); - 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; - } + 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, + // with any fractional result causing it to be rounded to the next highest integer. + let percentage_used = f64::from(used as u32) / f64::from((used + free) as u32); + let percentage_used = percentage_used * 100.0; + let percentage_used = percentage_used.ceil() as u32; - info.push(&buf, &mount.fsname, &mount.dir); + FieldsData { + fields, + source: String::from(mount.source.to_str().unwrap()), + size: total, + used, + avail, + pcent: percentage_used, + 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", , , + // , , , + // + 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> { @@ -371,29 +305,42 @@ fn main() -> Result<(), Box> { textdomain(PROJECT_NAME)?; bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; - let mut info = read_mount_info()?; - - if args.files.is_empty() { - info.mask_all(); - } else { - for file in &args.files { - mask_fs_by_file(&mut info, file)?; - } - } + // 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 }; - info.ensure_masked(); + let mut info = MountList::new()?; + info.mask_by_files(args.files); - 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(); + info.mask_by_files(vec!["/tmp/".into()]); + + let mut count = 0; + for mount in &info.info { + if mount.masked { + count += 1; + } + } + assert_eq!(count, 1); + } +} From 40595fca30f908d725a40ffc3a5e4408a7277d04 Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:46:36 +0700 Subject: [PATCH 06/13] ftw: fix mismatched types --- ftw/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ftw/src/lib.rs b/ftw/src/lib.rs index 32faeb13..13622848 100644 --- a/ftw/src/lib.rs +++ b/ftw/src/lib.rs @@ -324,14 +324,16 @@ impl Metadata { } } + #[allow(clippy::unnecessary_cast)] #[must_use] pub fn dev(&self) -> u64 { - self.0.st_dev + self.0.st_dev as u64 } + #[allow(clippy::unnecessary_cast)] #[must_use] pub fn rdev(&self) -> u64 { - self.0.st_rdev + self.0.st_rdev as u64 } } From 0b532d5efbf9815643d56470181bc1b016f06faf Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:26:12 +0700 Subject: [PATCH 07/13] ftw: remove duplicate code --- ftw/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/ftw/src/lib.rs b/ftw/src/lib.rs index 13622848..4172c56f 100644 --- a/ftw/src/lib.rs +++ b/ftw/src/lib.rs @@ -277,7 +277,7 @@ impl Metadata { /// This is analogous to [`std::fs::Metadata::len`]. #[must_use] pub fn len(&self) -> u64 { - self.0.st_size as u64 + self.0.st_size as _ } // These are "effective" IDs and not "real" to allow for things like sudo @@ -323,81 +323,103 @@ impl Metadata { self.0.st_mode & libc::S_IXOTH != 0 } } - - #[allow(clippy::unnecessary_cast)] - #[must_use] - pub fn dev(&self) -> u64 { - self.0.st_dev as u64 - } - - #[allow(clippy::unnecessary_cast)] - #[must_use] - pub fn rdev(&self) -> u64 { - self.0.st_rdev as u64 - } } 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 _ } From 144f00f383be6cc39113e596c91ed8c5a70a9c7a Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:26:39 +0700 Subject: [PATCH 08/13] df: mode debug print --- fs/df.rs | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/fs/df.rs b/fs/df.rs index 36100315..21f1415f 100644 --- a/fs/df.rs +++ b/fs/df.rs @@ -20,6 +20,7 @@ use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleC use plib::PROJECT_NAME; #[cfg(target_os = "macos")] use std::ffi::CStr; +use std::os::unix::fs::MetadataExt; use std::{cmp, ffi::CString, fmt::Display, io}; #[derive(Parser)] @@ -132,8 +133,11 @@ impl Display for Fields { } } +#[derive(Debug)] pub struct Mount { + /// mount point pub target: CString, + /// file system pub source: CString, pub fsstat: FilesystemStatistics, pub dev: i64, @@ -207,8 +211,8 @@ impl MountList { Ok(MountList { info }) } - pub fn mask_by_files(&mut self, files: Vec) { - if files.is_empty() { + pub fn mask_by_files(&mut self, files: Files) { + if files.devs.is_empty() { return; } @@ -223,6 +227,25 @@ impl MountList { 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, +} + +impl Files { + pub fn new(files: Vec) -> Self { + let mut devs = vec![]; + for path in files { let meta = match metadata(&path) { Ok(m) => m, @@ -231,12 +254,10 @@ impl MountList { continue; } }; - for mount in &mut self.info { - if mount.dev == meta.dev() as i64 { - mount.masked = true; - } - } + devs.push(meta.dev() as i64); } + + Self {devs} } } @@ -310,7 +331,8 @@ fn main() -> Result<(), Box> { let block_size: u64 = if args.kilo { 1024 } else { 512 }; let mut info = MountList::new()?; - info.mask_by_files(args.files); + let files = Files::new(args.files); + info.mask_by_files(files); let fields = Fields::new(block_size); // Print header @@ -333,11 +355,14 @@ mod tests { #[test] fn test_only_one_row() { let mut info = MountList::new().unwrap(); - info.mask_by_files(vec!["/tmp/".into()]); + 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; } } From 468282af6794fc48765185f0a1287e2d888e15a7 Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:34:48 +0700 Subject: [PATCH 09/13] df: try fixing test_only_one_row --- fs/df.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/df.rs b/fs/df.rs index 21f1415f..f28287e7 100644 --- a/fs/df.rs +++ b/fs/df.rs @@ -217,10 +217,10 @@ impl MountList { } for mount in &mut self.info { - mount.dev = if let Ok(m) = symlink_metadata_cstr(&mount.target) { - m.dev() as i64 - } else if let Ok(m) = symlink_metadata_cstr(&mount.source) { + 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 }; @@ -257,7 +257,7 @@ impl Files { devs.push(meta.dev() as i64); } - Self {devs} + Self { devs } } } From 19a3f2fe37007389b0dfc27b16dab0aa024f8ea1 Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:08:42 +0700 Subject: [PATCH 10/13] df: cleanup --- fs/df.rs | 160 +++++++++++++++++++++++++++---------------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/fs/df.rs b/fs/df.rs index f28287e7..41665430 100644 --- a/fs/df.rs +++ b/fs/df.rs @@ -53,86 +53,6 @@ struct Args { files: Vec, } -pub enum FieldType { - Str, - Num, - Pcent, -} - -pub struct Field { - caption: String, - width: usize, - typ: FieldType, -} - -impl Field { - pub fn new(caption: String, min_width: usize, typ: FieldType) -> Self { - let width = cmp::max(caption.len(), min_width); - Self { - caption, - width, - typ, - } - } - - pub fn format(&self, value: &T) -> String { - match self.typ { - FieldType::Str => format!("{value: format!("{value: >width$}", width = self.width), - FieldType::Pcent => format!("{value: >width$}", width = self.width - 1), - } - } -} - -/// Print header -impl Display for Field { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.format(&self.caption)) - } -} - -pub struct Fields { - /// file system - pub source: Field, - /// FS size - pub size: Field, - /// FS size used - pub used: Field, - /// FS size available - pub avail: Field, - /// percent used - pub pcent: Field, - /// mount point - pub target: Field, - // /// specified file name - // file: Field, -} - -impl Fields { - pub fn new(block_size: u64) -> Self { - let size_caption = format!("{}-{}", block_size, gettext("blocks")); - Self { - source: Field::new(gettext("Filesystem"), 14, FieldType::Str), - size: Field::new(size_caption, 10, FieldType::Num), - used: Field::new(gettext("Used"), 10, FieldType::Num), - avail: Field::new(gettext("Available"), 10, FieldType::Num), - pcent: Field::new(gettext("Capacity"), 5, FieldType::Pcent), - target: Field::new(gettext("Mounted on"), 0, FieldType::Str), - } - } -} - -/// Print header -impl Display for Fields { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} {} {} {} {} {}", - self.source, self.size, self.used, self.avail, self.pcent, self.target - ) - } -} - #[derive(Debug)] pub struct Mount { /// mount point @@ -261,6 +181,86 @@ impl Files { } } +pub enum FieldType { + Str, + Num, + Pcent, +} + +pub struct Field { + caption: String, + width: usize, + typ: FieldType, +} + +impl Field { + pub fn new(caption: String, min_width: usize, typ: FieldType) -> Self { + let width = cmp::max(caption.len(), min_width); + Self { + caption, + width, + typ, + } + } + + pub fn format(&self, value: &T) -> String { + match self.typ { + FieldType::Str => format!("{value: format!("{value: >width$}", width = self.width), + FieldType::Pcent => format!("{value: >width$}", width = self.width - 1), + } + } +} + +/// Print header +impl Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.format(&self.caption)) + } +} + +pub struct Fields { + /// file system + pub source: Field, + /// FS size + pub size: Field, + /// FS size used + pub used: Field, + /// FS size available + pub avail: Field, + /// percent used + pub pcent: Field, + /// mount point + pub target: Field, + // /// specified file name + // file: Field, +} + +impl Fields { + pub fn new(block_size: u64) -> Self { + let size_caption = format!("{}-{}", block_size, gettext("blocks")); + Self { + source: Field::new(gettext("Filesystem"), 14, FieldType::Str), + size: Field::new(size_caption, 10, FieldType::Num), + used: Field::new(gettext("Used"), 10, FieldType::Num), + avail: Field::new(gettext("Available"), 10, FieldType::Num), + pcent: Field::new(gettext("Capacity"), 5, FieldType::Pcent), + target: Field::new(gettext("Mounted on"), 0, FieldType::Str), + } + } +} + +/// Print header +impl Display for Fields { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} {} {} {} {} {}", + self.source, self.size, self.used, self.avail, self.pcent, self.target + ) + } +} + pub struct FieldsData<'a> { fields: &'a Fields, source: String, From 377c03bce30127a58fd6dc6e57df992631ca6110 Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:46:28 +0700 Subject: [PATCH 11/13] ftw: impl struct Permissions --- ftw/src/lib.rs | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/ftw/src/lib.rs b/ftw/src/lib.rs index 4172c56f..e0c9a993 100644 --- a/ftw/src/lib.rs +++ b/ftw/src/lib.rs @@ -280,6 +280,13 @@ impl Metadata { 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) { @@ -489,23 +496,120 @@ impl FileType { } 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, From 21237105eb81ba5502779ea60a263e6465c5feae Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:47:03 +0700 Subject: [PATCH 12/13] ls: use ftw:: Permissions --- tree/ls_util/entry.rs | 54 +++++++++++-------------------------------- 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/tree/ls_util/entry.rs b/tree/ls_util/entry.rs index b97d6c36..3e7cfbaa 100644 --- a/tree/ls_util/entry.rs +++ b/tree/ls_util/entry.rs @@ -612,22 +612,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', @@ -637,37 +629,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', @@ -675,7 +647,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 { '-' From 5f06c609021e4b5343d3c284e34989287dc3420e Mon Sep 17 00:00:00 2001 From: fox0 <15684995+fox0@users.noreply.github.com> Date: Sat, 26 Oct 2024 20:20:09 +0700 Subject: [PATCH 13/13] cargo fmt --- file/file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file/file.rs b/file/file.rs index 6019d3fe..ed65c1fb 100644 --- a/file/file.rs +++ b/file/file.rs @@ -14,8 +14,8 @@ use std::io; use std::path::{Path, PathBuf}; use clap::Parser; -use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; 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};