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)