Skip to main content

coreutils_rs/stat/
core.rs

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