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