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