1use std::ffi::CString;
2use std::io;
3use std::os::unix::fs::{FileTypeExt, MetadataExt};
4
5pub 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
14fn extract_fsid(fsid: &libc::fsid_t) -> u64 {
16 let bytes: [u8; std::mem::size_of::<libc::fsid_t>()] =
18 unsafe { std::mem::transmute_copy(fsid) };
19 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
25fn 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
44fn 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
59pub 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
70fn 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
99fn 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
123fn 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
198fn 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
224fn 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; #[cfg(target_os = "linux")]
243 let frsize = sfs.f_frsize;
244 #[cfg(not(target_os = "linux"))]
245 let frsize = sfs.f_bsize as u64; 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
263fn 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; #[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 )
301}
302
303fn 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
436fn 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
516pub 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 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 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 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
572pub 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
586fn format_timestamp(secs: i64, nsec: i64) -> String {
588 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
616fn format_birth_time(st: &libc::stat) -> String {
618 #[cfg(target_os = "linux")]
619 {
620 let _ = st;
624 "-".to_string()
625 }
626 #[cfg(not(target_os = "linux"))]
627 {
628 let _ = st;
629 "-".to_string()
630 }
631}
632
633fn format_birth_seconds(st: &libc::stat) -> String {
635 let _ = st;
636 "0".to_string()
637}
638
639fn major(dev: u64) -> u64 {
641 ((dev >> 8) & 0xff) | ((dev >> 32) & !0xffu64)
643}
644
645fn minor(dev: u64) -> u64 {
647 (dev & 0xff) | ((dev >> 12) & !0xffu64)
648}
649
650fn 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
662fn 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
674fn 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
711pub 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 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
762fn fs_type_name(fs_type: u64) -> &'static str {
764 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}