Skip to main content

coreutils_rs/stat/
core.rs

1use std::ffi::CString;
2use std::io;
3use std::os::unix::fs::{FileTypeExt, MetadataExt};
4
5/// Configuration for the stat command.
6pub struct StatConfig {
7    pub dereference: bool,
8    pub filesystem: bool,
9    pub format: Option<String>,
10    pub printf_format: Option<String>,
11    pub terse: bool,
12}
13
14/// Extract the fsid value from a libc::fsid_t as a u64.
15fn extract_fsid(fsid: &libc::fsid_t) -> u64 {
16    // fsid_t is an opaque type; safely read its raw bytes
17    let bytes: [u8; std::mem::size_of::<libc::fsid_t>()] =
18        unsafe { std::mem::transmute_copy(fsid) };
19    // Interpret as two u32 values in native endian (matching __val[0] and __val[1])
20    let val0 = u32::from_ne_bytes(bytes[0..4].try_into().unwrap()) as u64;
21    let val1 = u32::from_ne_bytes(bytes[4..8].try_into().unwrap()) as u64;
22    // GNU stat formats as val[0] in high bits, val[1] in low bits
23    (val0 << 32) | val1
24}
25
26/// Perform a libc stat/lstat call and return the raw `libc::stat` structure.
27fn raw_stat(path: &str, dereference: bool) -> Result<libc::stat, io::Error> {
28    let c_path = CString::new(path)
29        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
30    unsafe {
31        let mut st: libc::stat = std::mem::zeroed();
32        let rc = if dereference {
33            libc::stat(c_path.as_ptr(), &mut st)
34        } else {
35            libc::lstat(c_path.as_ptr(), &mut st)
36        };
37        if rc != 0 {
38            Err(io::Error::last_os_error())
39        } else {
40            Ok(st)
41        }
42    }
43}
44
45/// Perform a libc statfs call and return the raw `libc::statfs` structure.
46fn raw_statfs(path: &str) -> Result<libc::statfs, io::Error> {
47    let c_path = CString::new(path)
48        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
49    unsafe {
50        let mut sfs: libc::statfs = std::mem::zeroed();
51        let rc = libc::statfs(c_path.as_ptr(), &mut sfs);
52        if rc != 0 {
53            Err(io::Error::last_os_error())
54        } else {
55            Ok(sfs)
56        }
57    }
58}
59
60/// Display file or filesystem status.
61///
62/// Returns the formatted output string, or an error if the file cannot be accessed.
63pub fn stat_file(path: &str, config: &StatConfig) -> Result<String, io::Error> {
64    if config.filesystem {
65        stat_filesystem(path, config)
66    } else {
67        stat_regular(path, config)
68    }
69}
70
71// ──────────────────────────────────────────────────
72// Regular file stat
73// ──────────────────────────────────────────────────
74
75fn stat_regular(path: &str, config: &StatConfig) -> Result<String, io::Error> {
76    let meta = if config.dereference {
77        std::fs::metadata(path)?
78    } else {
79        std::fs::symlink_metadata(path)?
80    };
81    let st = raw_stat(path, config.dereference)?;
82
83    if let Some(ref fmt) = config.printf_format {
84        let expanded = expand_backslash_escapes(fmt);
85        return Ok(format_file_specifiers(
86            &expanded,
87            path,
88            &meta,
89            &st,
90            config.dereference,
91        ));
92    }
93
94    if let Some(ref fmt) = config.format {
95        let result = format_file_specifiers(fmt, path, &meta, &st, config.dereference);
96        return Ok(result + "\n");
97    }
98
99    if config.terse {
100        return Ok(format_file_terse(path, &meta, &st, config.dereference));
101    }
102
103    Ok(format_file_default(path, &meta, &st, config.dereference))
104}
105
106// ──────────────────────────────────────────────────
107// Filesystem stat
108// ──────────────────────────────────────────────────
109
110fn stat_filesystem(path: &str, config: &StatConfig) -> Result<String, io::Error> {
111    let sfs = raw_statfs(path)?;
112
113    if let Some(ref fmt) = config.printf_format {
114        let expanded = expand_backslash_escapes(fmt);
115        return Ok(format_fs_specifiers(&expanded, path, &sfs));
116    }
117
118    if let Some(ref fmt) = config.format {
119        let result = format_fs_specifiers(fmt, path, &sfs);
120        return Ok(result + "\n");
121    }
122
123    if config.terse {
124        return Ok(format_fs_terse(path, &sfs));
125    }
126
127    Ok(format_fs_default(path, &sfs))
128}
129
130// ──────────────────────────────────────────────────
131// Default file format
132// ──────────────────────────────────────────────────
133
134fn format_file_default(
135    path: &str,
136    meta: &std::fs::Metadata,
137    st: &libc::stat,
138    dereference: bool,
139) -> String {
140    let mode = meta.mode();
141    let file_type_str = file_type_label(mode);
142    let perms_str = mode_to_human(mode);
143    let uid = meta.uid();
144    let gid = meta.gid();
145    let uname = lookup_username(uid);
146    let gname = lookup_groupname(gid);
147    let dev = meta.dev();
148    let dev_major = major(dev);
149    let dev_minor = minor(dev);
150
151    let name_display = if meta.file_type().is_symlink() {
152        match std::fs::read_link(path) {
153            Ok(target) => format!("{} -> {}", path, target.display()),
154            Err(_) => path.to_string(),
155        }
156    } else {
157        path.to_string()
158    };
159
160    let size_line = if meta.file_type().is_block_device() || meta.file_type().is_char_device() {
161        let rdev = meta.rdev();
162        let rmaj = major(rdev);
163        let rmin = minor(rdev);
164        format!(
165            "  Size: {:<10}\tBlocks: {:<10} IO Block: {:<6} {}",
166            format!("{}, {}", rmaj, rmin),
167            meta.blocks(),
168            meta.blksize(),
169            file_type_str
170        )
171    } else {
172        format!(
173            "  Size: {:<10}\tBlocks: {:<10} IO Block: {:<6} {}",
174            meta.size(),
175            meta.blocks(),
176            meta.blksize(),
177            file_type_str
178        )
179    };
180
181    let device_line = format!(
182        "Device: {},{}\tInode: {:<11} Links: {}",
183        dev_major,
184        dev_minor,
185        meta.ino(),
186        meta.nlink()
187    );
188
189    let access_line = format!(
190        "Access: ({:04o}/{})  Uid: ({:5}/{:>8})   Gid: ({:5}/{:>8})",
191        mode & 0o7777,
192        perms_str,
193        uid,
194        uname,
195        gid,
196        gname
197    );
198
199    let atime = format_timestamp(st.st_atime, st.st_atime_nsec);
200    let mtime = format_timestamp(st.st_mtime, st.st_mtime_nsec);
201    let ctime = format_timestamp(st.st_ctime, st.st_ctime_nsec);
202    let birth = format_birth_time_for_path(path, dereference);
203
204    format!(
205        "  File: {}\n{}\n{}\n{}\nAccess: {}\nModify: {}\nChange: {}\n Birth: {}\n",
206        name_display, size_line, device_line, access_line, atime, mtime, ctime, birth
207    )
208}
209
210// ──────────────────────────────────────────────────
211// Terse file format
212// ──────────────────────────────────────────────────
213
214fn format_file_terse(
215    path: &str,
216    meta: &std::fs::Metadata,
217    st: &libc::stat,
218    dereference: bool,
219) -> String {
220    let dev = meta.dev();
221    let birth_secs = get_birth_time(path, dereference)
222        .map(|(s, _)| s)
223        .unwrap_or(0);
224    format!(
225        "{} {} {} {:x} {} {} {:x} {} {} {:x} {:x} {} {} {} {} {}\n",
226        path,
227        meta.size(),
228        meta.blocks(),
229        meta.mode(),
230        meta.uid(),
231        meta.gid(),
232        dev,
233        meta.ino(),
234        meta.nlink(),
235        major(meta.rdev()),
236        minor(meta.rdev()),
237        st.st_atime,
238        st.st_mtime,
239        st.st_ctime,
240        birth_secs,
241        meta.blksize()
242    )
243}
244
245// ──────────────────────────────────────────────────
246// Default filesystem format
247// ──────────────────────────────────────────────────
248
249fn format_fs_default(path: &str, sfs: &libc::statfs) -> String {
250    #[cfg(target_os = "linux")]
251    let fs_type = sfs.f_type;
252    #[cfg(not(target_os = "linux"))]
253    let fs_type = 0u32;
254    let fs_type_name = fs_type_name(fs_type as u64);
255    let fsid = sfs.f_fsid;
256    let fsid_val = extract_fsid(&fsid);
257
258    #[cfg(target_os = "linux")]
259    let namelen = sfs.f_namelen;
260    #[cfg(not(target_os = "linux"))]
261    let namelen = 255i64; // macOS doesn't expose f_namelen
262
263    #[cfg(target_os = "linux")]
264    let frsize = sfs.f_frsize;
265    #[cfg(not(target_os = "linux"))]
266    let frsize = sfs.f_bsize as u64; // fallback to bsize
267
268    format!(
269        "  File: \"{}\"\n    ID: {:x} Namelen: {}     Type: {}\nBlock size: {:<10} Fundamental block size: {}\nBlocks: Total: {:<10} Free: {:<10} Available: {}\nInodes: Total: {:<10} Free: {}\n",
270        path,
271        fsid_val,
272        namelen,
273        fs_type_name,
274        sfs.f_bsize,
275        frsize,
276        sfs.f_blocks,
277        sfs.f_bfree,
278        sfs.f_bavail,
279        sfs.f_files,
280        sfs.f_ffree
281    )
282}
283
284// ──────────────────────────────────────────────────
285// Terse filesystem format
286// ──────────────────────────────────────────────────
287
288fn format_fs_terse(path: &str, sfs: &libc::statfs) -> String {
289    let fsid = sfs.f_fsid;
290    let fsid_val = extract_fsid(&fsid);
291
292    #[cfg(target_os = "linux")]
293    let namelen = sfs.f_namelen;
294    #[cfg(not(target_os = "linux"))]
295    let namelen = 255i64;
296
297    #[cfg(target_os = "linux")]
298    let fs_type = sfs.f_type;
299    #[cfg(not(target_os = "linux"))]
300    let fs_type = 0u32; // macOS doesn't have f_type
301
302    #[cfg(target_os = "linux")]
303    let frsize = sfs.f_frsize;
304    #[cfg(not(target_os = "linux"))]
305    let frsize = sfs.f_bsize as u64;
306
307    format!(
308        "{} {} {} {} {} {} {} {} {} {} {} {}\n",
309        path,
310        fsid_val,
311        namelen,
312        fs_type,
313        sfs.f_bsize,
314        frsize,
315        sfs.f_blocks,
316        sfs.f_bfree,
317        sfs.f_bavail,
318        sfs.f_files,
319        sfs.f_ffree,
320        0 // flags placeholder
321    )
322}
323
324// ──────────────────────────────────────────────────
325// Custom format specifiers for files
326// ──────────────────────────────────────────────────
327
328fn format_file_specifiers(
329    fmt: &str,
330    path: &str,
331    meta: &std::fs::Metadata,
332    st: &libc::stat,
333    dereference: bool,
334) -> String {
335    let mut result = String::new();
336    let chars: Vec<char> = fmt.chars().collect();
337    let mut i = 0;
338
339    while i < chars.len() {
340        if chars[i] == '%' && i + 1 < chars.len() {
341            i += 1;
342            match chars[i] {
343                'a' => {
344                    result.push_str(&format!("{:o}", meta.mode() & 0o7777));
345                }
346                'A' => {
347                    result.push_str(&mode_to_human(meta.mode()));
348                }
349                'b' => {
350                    result.push_str(&meta.blocks().to_string());
351                }
352                'B' => {
353                    result.push_str("512");
354                }
355                'd' => {
356                    result.push_str(&meta.dev().to_string());
357                }
358                'D' => {
359                    result.push_str(&format!("{:x}", meta.dev()));
360                }
361                'f' => {
362                    result.push_str(&format!("{:x}", meta.mode()));
363                }
364                'F' => {
365                    result.push_str(file_type_label(meta.mode()));
366                }
367                'g' => {
368                    result.push_str(&meta.gid().to_string());
369                }
370                'G' => {
371                    result.push_str(&lookup_groupname(meta.gid()));
372                }
373                'h' => {
374                    result.push_str(&meta.nlink().to_string());
375                }
376                'i' => {
377                    result.push_str(&meta.ino().to_string());
378                }
379                'm' => {
380                    result.push_str(&find_mount_point(path));
381                }
382                'n' => {
383                    result.push_str(path);
384                }
385                'N' => {
386                    if meta.file_type().is_symlink() {
387                        match std::fs::read_link(path) {
388                            Ok(target) => {
389                                result.push_str(&format!("'{}' -> '{}'", path, target.display()));
390                            }
391                            Err(_) => {
392                                result.push_str(&format!("'{}'", path));
393                            }
394                        }
395                    } else {
396                        result.push_str(&format!("'{}'", path));
397                    }
398                }
399                'o' => {
400                    result.push_str(&meta.blksize().to_string());
401                }
402                's' => {
403                    result.push_str(&meta.size().to_string());
404                }
405                't' => {
406                    result.push_str(&format!("{:x}", major(meta.rdev())));
407                }
408                'T' => {
409                    result.push_str(&format!("{:x}", minor(meta.rdev())));
410                }
411                'u' => {
412                    result.push_str(&meta.uid().to_string());
413                }
414                'U' => {
415                    result.push_str(&lookup_username(meta.uid()));
416                }
417                'w' => {
418                    result.push_str(&format_birth_time_for_path(path, dereference));
419                }
420                'W' => {
421                    result.push_str(&format_birth_seconds_for_path(path, dereference));
422                }
423                'x' => {
424                    result.push_str(&format_timestamp(st.st_atime, st.st_atime_nsec));
425                }
426                'X' => {
427                    result.push_str(&st.st_atime.to_string());
428                }
429                'y' => {
430                    result.push_str(&format_timestamp(st.st_mtime, st.st_mtime_nsec));
431                }
432                'Y' => {
433                    result.push_str(&st.st_mtime.to_string());
434                }
435                'z' => {
436                    result.push_str(&format_timestamp(st.st_ctime, st.st_ctime_nsec));
437                }
438                'Z' => {
439                    result.push_str(&st.st_ctime.to_string());
440                }
441                '%' => {
442                    result.push('%');
443                }
444                other => {
445                    result.push('%');
446                    result.push(other);
447                }
448            }
449        } else {
450            result.push(chars[i]);
451        }
452        i += 1;
453    }
454
455    result
456}
457
458// ──────────────────────────────────────────────────
459// Custom format specifiers for filesystems
460// ──────────────────────────────────────────────────
461
462fn format_fs_specifiers(fmt: &str, path: &str, sfs: &libc::statfs) -> String {
463    let mut result = String::new();
464    let chars: Vec<char> = fmt.chars().collect();
465    let mut i = 0;
466    let fsid = sfs.f_fsid;
467    let fsid_val = extract_fsid(&fsid);
468
469    while i < chars.len() {
470        if chars[i] == '%' && i + 1 < chars.len() {
471            i += 1;
472            match chars[i] {
473                'a' => {
474                    result.push_str(&sfs.f_bavail.to_string());
475                }
476                'b' => {
477                    result.push_str(&sfs.f_blocks.to_string());
478                }
479                'c' => {
480                    result.push_str(&sfs.f_files.to_string());
481                }
482                'd' => {
483                    result.push_str(&sfs.f_ffree.to_string());
484                }
485                'f' => {
486                    result.push_str(&sfs.f_bfree.to_string());
487                }
488                'i' => {
489                    result.push_str(&format!("{:x}", fsid_val));
490                }
491                'l' => {
492                    #[cfg(target_os = "linux")]
493                    result.push_str(&sfs.f_namelen.to_string());
494                    #[cfg(not(target_os = "linux"))]
495                    result.push_str("255");
496                }
497                'n' => {
498                    result.push_str(path);
499                }
500                's' => {
501                    result.push_str(&sfs.f_bsize.to_string());
502                }
503                'S' => {
504                    #[cfg(target_os = "linux")]
505                    result.push_str(&sfs.f_frsize.to_string());
506                    #[cfg(not(target_os = "linux"))]
507                    result.push_str(&sfs.f_bsize.to_string());
508                }
509                't' => {
510                    #[cfg(target_os = "linux")]
511                    result.push_str(&format!("{:x}", sfs.f_type));
512                    #[cfg(not(target_os = "linux"))]
513                    result.push('0');
514                }
515                'T' => {
516                    #[cfg(target_os = "linux")]
517                    result.push_str(fs_type_name(sfs.f_type as u64));
518                    #[cfg(not(target_os = "linux"))]
519                    result.push_str("unknown");
520                }
521                '%' => {
522                    result.push('%');
523                }
524                other => {
525                    result.push('%');
526                    result.push(other);
527                }
528            }
529        } else {
530            result.push(chars[i]);
531        }
532        i += 1;
533    }
534
535    result
536}
537
538// ──────────────────────────────────────────────────
539// Helpers
540// ──────────────────────────────────────────────────
541
542/// Convert a Unix file mode to a human-readable permission string like `-rwxr-xr-x`.
543pub fn mode_to_human(mode: u32) -> String {
544    let file_char = match mode & (libc::S_IFMT as u32) {
545        m if m == libc::S_IFREG as u32 => '-',
546        m if m == libc::S_IFDIR as u32 => 'd',
547        m if m == libc::S_IFLNK as u32 => 'l',
548        m if m == libc::S_IFBLK as u32 => 'b',
549        m if m == libc::S_IFCHR as u32 => 'c',
550        m if m == libc::S_IFIFO as u32 => 'p',
551        m if m == libc::S_IFSOCK as u32 => 's',
552        _ => '?',
553    };
554
555    let mut s = String::with_capacity(10);
556    s.push(file_char);
557
558    // Owner
559    s.push(if mode & 0o400 != 0 { 'r' } else { '-' });
560    s.push(if mode & 0o200 != 0 { 'w' } else { '-' });
561    s.push(if mode & (libc::S_ISUID as u32) != 0 {
562        if mode & 0o100 != 0 { 's' } else { 'S' }
563    } else if mode & 0o100 != 0 {
564        'x'
565    } else {
566        '-'
567    });
568
569    // Group
570    s.push(if mode & 0o040 != 0 { 'r' } else { '-' });
571    s.push(if mode & 0o020 != 0 { 'w' } else { '-' });
572    s.push(if mode & (libc::S_ISGID as u32) != 0 {
573        if mode & 0o010 != 0 { 's' } else { 'S' }
574    } else if mode & 0o010 != 0 {
575        'x'
576    } else {
577        '-'
578    });
579
580    // Others
581    s.push(if mode & 0o004 != 0 { 'r' } else { '-' });
582    s.push(if mode & 0o002 != 0 { 'w' } else { '-' });
583    s.push(if mode & (libc::S_ISVTX as u32) != 0 {
584        if mode & 0o001 != 0 { 't' } else { 'T' }
585    } else if mode & 0o001 != 0 {
586        'x'
587    } else {
588        '-'
589    });
590
591    s
592}
593
594/// Return a human-readable label for the file type portion of a mode value.
595pub fn file_type_label(mode: u32) -> &'static str {
596    match mode & (libc::S_IFMT as u32) {
597        m if m == libc::S_IFREG as u32 => "regular file",
598        m if m == libc::S_IFDIR as u32 => "directory",
599        m if m == libc::S_IFLNK as u32 => "symbolic link",
600        m if m == libc::S_IFBLK as u32 => "block special file",
601        m if m == libc::S_IFCHR as u32 => "character special file",
602        m if m == libc::S_IFIFO as u32 => "fifo",
603        m if m == libc::S_IFSOCK as u32 => "socket",
604        _ => "unknown",
605    }
606}
607
608/// Format a Unix timestamp as `YYYY-MM-DD HH:MM:SS.NNNNNNNNN +ZZZZ`.
609fn format_timestamp(secs: i64, nsec: i64) -> String {
610    // Use libc localtime_r for timezone-aware formatting
611    let t = secs as libc::time_t;
612    let mut tm: libc::tm = unsafe { std::mem::zeroed() };
613    unsafe {
614        libc::localtime_r(&t, &mut tm);
615    }
616
617    let offset_secs = tm.tm_gmtoff;
618    let offset_sign = if offset_secs >= 0 { '+' } else { '-' };
619    let offset_abs = offset_secs.unsigned_abs();
620    let offset_hours = offset_abs / 3600;
621    let offset_mins = (offset_abs % 3600) / 60;
622
623    format!(
624        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {}{:02}{:02}",
625        tm.tm_year + 1900,
626        tm.tm_mon + 1,
627        tm.tm_mday,
628        tm.tm_hour,
629        tm.tm_min,
630        tm.tm_sec,
631        nsec,
632        offset_sign,
633        offset_hours,
634        offset_mins
635    )
636}
637
638/// Get birth time using statx() syscall. Returns (seconds, nanoseconds) or None.
639#[cfg(target_os = "linux")]
640fn get_birth_time(path: &str, dereference: bool) -> Option<(i64, i64)> {
641    use std::mem::MaybeUninit;
642
643    let c_path = CString::new(path).ok()?;
644    unsafe {
645        let mut statx_buf: libc::statx = MaybeUninit::zeroed().assume_init();
646        let flags = if dereference {
647            0
648        } else {
649            libc::AT_SYMLINK_NOFOLLOW
650        };
651        let rc = libc::statx(
652            libc::AT_FDCWD,
653            c_path.as_ptr(),
654            flags,
655            libc::STATX_BTIME,
656            &mut statx_buf,
657        );
658        if rc == 0 && (statx_buf.stx_mask & libc::STATX_BTIME) != 0 {
659            Some((
660                statx_buf.stx_btime.tv_sec,
661                statx_buf.stx_btime.tv_nsec as i64,
662            ))
663        } else {
664            None
665        }
666    }
667}
668
669#[cfg(not(target_os = "linux"))]
670fn get_birth_time(_path: &str, _dereference: bool) -> Option<(i64, i64)> {
671    None
672}
673
674/// Format birth time. Returns "-" if unavailable.
675fn format_birth_time_for_path(path: &str, dereference: bool) -> String {
676    if let Some((secs, nsec)) = get_birth_time(path, dereference) {
677        format_timestamp(secs, nsec)
678    } else {
679        "-".to_string()
680    }
681}
682
683/// Format birth time as seconds since epoch. Returns "0" if unavailable.
684fn format_birth_seconds_for_path(path: &str, dereference: bool) -> String {
685    if let Some((secs, _nsec)) = get_birth_time(path, dereference) {
686        secs.to_string()
687    } else {
688        "0".to_string()
689    }
690}
691
692/// Extract the major device number from a dev_t.
693fn major(dev: u64) -> u64 {
694    // Linux major/minor encoding
695    ((dev >> 8) & 0xff) | ((dev >> 32) & !0xffu64)
696}
697
698/// Extract the minor device number from a dev_t.
699fn minor(dev: u64) -> u64 {
700    (dev & 0xff) | ((dev >> 12) & !0xffu64)
701}
702
703/// Look up a username by UID. Returns the numeric UID as string if lookup fails.
704fn lookup_username(uid: u32) -> String {
705    unsafe {
706        let pw = libc::getpwuid(uid);
707        if pw.is_null() {
708            return uid.to_string();
709        }
710        let name = std::ffi::CStr::from_ptr((*pw).pw_name);
711        name.to_string_lossy().into_owned()
712    }
713}
714
715/// Look up a group name by GID. Returns the numeric GID as string if lookup fails.
716fn lookup_groupname(gid: u32) -> String {
717    unsafe {
718        let gr = libc::getgrgid(gid);
719        if gr.is_null() {
720            return gid.to_string();
721        }
722        let name = std::ffi::CStr::from_ptr((*gr).gr_name);
723        name.to_string_lossy().into_owned()
724    }
725}
726
727/// Find the mount point for a given path by walking up the directory tree.
728fn find_mount_point(path: &str) -> String {
729    use std::path::PathBuf;
730
731    let abs = match std::fs::canonicalize(path) {
732        Ok(p) => p,
733        Err(_) => PathBuf::from(path),
734    };
735
736    let mut current = abs.as_path();
737    let dev = match std::fs::metadata(current) {
738        Ok(m) => m.dev(),
739        Err(_) => return "/".to_string(),
740    };
741
742    loop {
743        match current.parent() {
744            Some(parent) => {
745                match std::fs::metadata(parent) {
746                    Ok(pm) => {
747                        if pm.dev() != dev {
748                            return current.to_string_lossy().into_owned();
749                        }
750                    }
751                    Err(_) => {
752                        return current.to_string_lossy().into_owned();
753                    }
754                }
755                current = parent;
756            }
757            None => {
758                return current.to_string_lossy().into_owned();
759            }
760        }
761    }
762}
763
764/// Expand backslash escape sequences in a format string (for --printf).
765pub fn expand_backslash_escapes(s: &str) -> String {
766    let mut result = String::with_capacity(s.len());
767    let chars: Vec<char> = s.chars().collect();
768    let mut i = 0;
769
770    while i < chars.len() {
771        if chars[i] == '\\' && i + 1 < chars.len() {
772            i += 1;
773            match chars[i] {
774                'n' => result.push('\n'),
775                't' => result.push('\t'),
776                'r' => result.push('\r'),
777                'a' => result.push('\x07'),
778                'b' => result.push('\x08'),
779                'f' => result.push('\x0C'),
780                'v' => result.push('\x0B'),
781                '\\' => result.push('\\'),
782                '"' => result.push('"'),
783                '0' => {
784                    // Octal escape: \0NNN (up to 3 octal digits after the leading 0)
785                    let mut val: u32 = 0;
786                    let mut count = 0;
787                    while i + 1 < chars.len() && count < 3 {
788                        let next = chars[i + 1];
789                        if next >= '0' && next <= '7' {
790                            val = val * 8 + (next as u32 - '0' as u32);
791                            i += 1;
792                            count += 1;
793                        } else {
794                            break;
795                        }
796                    }
797                    if let Some(c) = char::from_u32(val) {
798                        result.push(c);
799                    }
800                }
801                other => {
802                    result.push('\\');
803                    result.push(other);
804                }
805            }
806        } else {
807            result.push(chars[i]);
808        }
809        i += 1;
810    }
811
812    result
813}
814
815/// Map a filesystem type magic number to a human-readable name.
816fn fs_type_name(fs_type: u64) -> &'static str {
817    // Common Linux filesystem magic numbers
818    match fs_type {
819        0xEF53 => "ext2/ext3",
820        0x6969 => "nfs",
821        0x58465342 => "xfs",
822        0x2FC12FC1 => "zfs",
823        0x9123683E => "btrfs",
824        0x01021994 => "tmpfs",
825        0x28cd3d45 => "cramfs",
826        0x3153464a => "jfs",
827        0x52654973 => "reiserfs",
828        0x7275 => "romfs",
829        0x858458f6 => "ramfs",
830        0x73717368 => "squashfs",
831        0x62646576 => "devfs",
832        0x64626720 => "debugfs",
833        0x1cd1 => "devpts",
834        0xf15f => "ecryptfs",
835        0x794c7630 => "overlayfs",
836        0xFF534D42 => "cifs",
837        0xfe534d42 => "smb2",
838        0x137F => "minix",
839        0x4d44 => "msdos",
840        0x4006 => "fat",
841        0x65735546 => "fuse",
842        0x65735543 => "fusectl",
843        0x9fa0 => "proc",
844        0x62656572 => "sysfs",
845        0x27e0eb => "cgroup",
846        0x63677270 => "cgroup2",
847        0x19800202 => "mqueue",
848        0x50495045 => "pipefs",
849        0x74726163 => "tracefs",
850        0x68756773 => "hugetlbfs",
851        0xBAD1DEA => "futexfs",
852        0x5346544e => "ntfs",
853        0x00011954 => "ufs",
854        _ => "UNKNOWN",
855    }
856}