1use std::collections::HashMap;
6use std::fs::{self, Metadata};
7use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
8use std::path::Path;
9use std::time::UNIX_EPOCH;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum StatElement {
14 Device,
15 Inode,
16 Mode,
17 Nlink,
18 Uid,
19 Gid,
20 Rdev,
21 Size,
22 Atime,
23 Mtime,
24 Ctime,
25 Blksize,
26 Blocks,
27 Link,
28}
29
30impl StatElement {
31 pub fn from_name(name: &str) -> Option<Self> {
32 let elements = Self::all();
33 let matches: Vec<_> = elements
34 .iter()
35 .filter(|(n, _)| n.starts_with(name))
36 .collect();
37
38 if matches.len() == 1 {
39 Some(matches[0].1)
40 } else {
41 None
42 }
43 }
44
45 pub fn name(&self) -> &'static str {
46 match self {
47 Self::Device => "device",
48 Self::Inode => "inode",
49 Self::Mode => "mode",
50 Self::Nlink => "nlink",
51 Self::Uid => "uid",
52 Self::Gid => "gid",
53 Self::Rdev => "rdev",
54 Self::Size => "size",
55 Self::Atime => "atime",
56 Self::Mtime => "mtime",
57 Self::Ctime => "ctime",
58 Self::Blksize => "blksize",
59 Self::Blocks => "blocks",
60 Self::Link => "link",
61 }
62 }
63
64 pub fn all() -> Vec<(&'static str, Self)> {
65 vec![
66 ("device", Self::Device),
67 ("inode", Self::Inode),
68 ("mode", Self::Mode),
69 ("nlink", Self::Nlink),
70 ("uid", Self::Uid),
71 ("gid", Self::Gid),
72 ("rdev", Self::Rdev),
73 ("size", Self::Size),
74 ("atime", Self::Atime),
75 ("mtime", Self::Mtime),
76 ("ctime", Self::Ctime),
77 ("blksize", Self::Blksize),
78 ("blocks", Self::Blocks),
79 ("link", Self::Link),
80 ]
81 }
82
83 pub fn list_names() -> Vec<&'static str> {
84 Self::all().into_iter().map(|(n, _)| n).collect()
85 }
86}
87
88#[derive(Debug, Default, Clone)]
90pub struct StatFlags {
91 pub show_name: bool,
92 pub show_file: bool,
93 pub string_format: bool,
94 pub raw_format: bool,
95 pub octal_mode: bool,
96 pub use_gmt: bool,
97 pub use_lstat: bool,
98}
99
100#[derive(Debug, Clone)]
102pub struct FileStat {
103 pub device: u64,
104 pub inode: u64,
105 pub mode: u32,
106 pub nlink: u64,
107 pub uid: u32,
108 pub gid: u32,
109 pub rdev: u64,
110 pub size: u64,
111 pub atime: i64,
112 pub mtime: i64,
113 pub ctime: i64,
114 pub blksize: u64,
115 pub blocks: u64,
116 pub link_target: Option<String>,
117 pub file_type: FileType,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum FileType {
122 Regular,
123 Directory,
124 Symlink,
125 BlockDevice,
126 CharDevice,
127 Fifo,
128 Socket,
129 Unknown,
130}
131
132impl FileType {
133 pub fn from_metadata(meta: &Metadata) -> Self {
134 let ft = meta.file_type();
135 if ft.is_file() {
136 Self::Regular
137 } else if ft.is_dir() {
138 Self::Directory
139 } else if ft.is_symlink() {
140 Self::Symlink
141 } else if ft.is_block_device() {
142 Self::BlockDevice
143 } else if ft.is_char_device() {
144 Self::CharDevice
145 } else if ft.is_fifo() {
146 Self::Fifo
147 } else if ft.is_socket() {
148 Self::Socket
149 } else {
150 Self::Unknown
151 }
152 }
153
154 pub fn mode_char(&self) -> char {
155 match self {
156 Self::Regular => '-',
157 Self::Directory => 'd',
158 Self::Symlink => 'l',
159 Self::BlockDevice => 'b',
160 Self::CharDevice => 'c',
161 Self::Fifo => 'p',
162 Self::Socket => 's',
163 Self::Unknown => '?',
164 }
165 }
166}
167
168impl FileStat {
169 pub fn from_path(path: &Path, use_lstat: bool) -> std::io::Result<Self> {
170 let meta = if use_lstat {
171 fs::symlink_metadata(path)?
172 } else {
173 fs::metadata(path)?
174 };
175
176 let link_target = if meta.file_type().is_symlink() {
177 fs::read_link(path)
178 .ok()
179 .map(|p| p.to_string_lossy().to_string())
180 } else {
181 None
182 };
183
184 Ok(Self::from_metadata(&meta, link_target))
185 }
186
187 pub fn from_metadata(meta: &Metadata, link_target: Option<String>) -> Self {
188 let atime = meta
189 .accessed()
190 .ok()
191 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
192 .map(|d| d.as_secs() as i64)
193 .unwrap_or(0);
194
195 let mtime = meta
196 .modified()
197 .ok()
198 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
199 .map(|d| d.as_secs() as i64)
200 .unwrap_or(0);
201
202 Self {
203 device: meta.dev(),
204 inode: meta.ino(),
205 mode: meta.mode(),
206 nlink: meta.nlink(),
207 uid: meta.uid(),
208 gid: meta.gid(),
209 rdev: meta.rdev(),
210 size: meta.size(),
211 atime,
212 mtime,
213 ctime: meta.ctime(),
214 blksize: meta.blksize(),
215 blocks: meta.blocks(),
216 link_target,
217 file_type: FileType::from_metadata(meta),
218 }
219 }
220
221 pub fn get_element(&self, elem: StatElement, flags: &StatFlags) -> String {
222 match elem {
223 StatElement::Device => format!("{}", self.device),
224 StatElement::Inode => format!("{}", self.inode),
225 StatElement::Mode => self.format_mode(flags),
226 StatElement::Nlink => format!("{}", self.nlink),
227 StatElement::Uid => self.format_uid(flags),
228 StatElement::Gid => self.format_gid(flags),
229 StatElement::Rdev => format!("{}", self.rdev),
230 StatElement::Size => format!("{}", self.size),
231 StatElement::Atime => self.format_time(self.atime, flags),
232 StatElement::Mtime => self.format_time(self.mtime, flags),
233 StatElement::Ctime => self.format_time(self.ctime, flags),
234 StatElement::Blksize => format!("{}", self.blksize),
235 StatElement::Blocks => format!("{}", self.blocks),
236 StatElement::Link => self.link_target.clone().unwrap_or_default(),
237 }
238 }
239
240 fn format_mode(&self, flags: &StatFlags) -> String {
241 let mut result = String::new();
242
243 if flags.raw_format {
244 if flags.octal_mode {
245 result.push_str(&format!("0{:o}", self.mode));
246 } else {
247 result.push_str(&format!("{}", self.mode));
248 }
249 if flags.string_format {
250 result.push_str(" (");
251 }
252 }
253
254 if flags.string_format {
255 result.push(self.file_type.mode_char());
256
257 let perms = [
258 (self.mode & 0o400 != 0, 'r'),
259 (self.mode & 0o200 != 0, 'w'),
260 (
261 self.mode & 0o100 != 0,
262 if self.mode & 0o4000 != 0 { 's' } else { 'x' },
263 ),
264 (self.mode & 0o040 != 0, 'r'),
265 (self.mode & 0o020 != 0, 'w'),
266 (
267 self.mode & 0o010 != 0,
268 if self.mode & 0o2000 != 0 { 's' } else { 'x' },
269 ),
270 (self.mode & 0o004 != 0, 'r'),
271 (self.mode & 0o002 != 0, 'w'),
272 (
273 self.mode & 0o001 != 0,
274 if self.mode & 0o1000 != 0 { 't' } else { 'x' },
275 ),
276 ];
277
278 for (set, ch) in perms {
279 if set {
280 result.push(ch);
281 } else if ch == 's' || ch == 't' {
282 result.push(ch.to_ascii_uppercase());
283 } else {
284 result.push('-');
285 }
286 }
287
288 if !set_bit(self.mode, 0o100) && self.mode & 0o4000 != 0 {
289 let chars: Vec<char> = result.chars().collect();
290 let mut r: String = chars[..3].iter().collect();
291 r.push('S');
292 r.push_str(&chars[4..].iter().collect::<String>());
293 result = r;
294 }
295
296 if flags.raw_format {
297 result.push(')');
298 }
299 }
300
301 if !flags.raw_format && !flags.string_format {
302 if flags.octal_mode {
303 result = format!("0{:o}", self.mode);
304 } else {
305 result = format!("{}", self.mode);
306 }
307 }
308
309 result
310 }
311
312 fn format_uid(&self, flags: &StatFlags) -> String {
313 let mut result = String::new();
314
315 if flags.raw_format {
316 result.push_str(&format!("{}", self.uid));
317 if flags.string_format {
318 result.push_str(" (");
319 }
320 }
321
322 if flags.string_format {
323 #[cfg(unix)]
324 {
325 if let Some(name) = get_username(self.uid) {
326 result.push_str(&name);
327 } else {
328 result.push_str(&format!("{}", self.uid));
329 }
330 }
331 #[cfg(not(unix))]
332 {
333 result.push_str(&format!("{}", self.uid));
334 }
335
336 if flags.raw_format {
337 result.push(')');
338 }
339 }
340
341 if !flags.raw_format && !flags.string_format {
342 result = format!("{}", self.uid);
343 }
344
345 result
346 }
347
348 fn format_gid(&self, flags: &StatFlags) -> String {
349 let mut result = String::new();
350
351 if flags.raw_format {
352 result.push_str(&format!("{}", self.gid));
353 if flags.string_format {
354 result.push_str(" (");
355 }
356 }
357
358 if flags.string_format {
359 #[cfg(unix)]
360 {
361 if let Some(name) = get_groupname(self.gid) {
362 result.push_str(&name);
363 } else {
364 result.push_str(&format!("{}", self.gid));
365 }
366 }
367 #[cfg(not(unix))]
368 {
369 result.push_str(&format!("{}", self.gid));
370 }
371
372 if flags.raw_format {
373 result.push(')');
374 }
375 }
376
377 if !flags.raw_format && !flags.string_format {
378 result = format!("{}", self.gid);
379 }
380
381 result
382 }
383
384 fn format_time(&self, timestamp: i64, flags: &StatFlags) -> String {
385 let mut result = String::new();
386
387 if flags.raw_format {
388 result.push_str(&format!("{}", timestamp));
389 if flags.string_format {
390 result.push_str(" (");
391 }
392 }
393
394 if flags.string_format {
395 use chrono::{Local, TimeZone, Utc};
396
397 let dt = if flags.use_gmt {
398 Utc.timestamp_opt(timestamp, 0)
399 .single()
400 .map(|dt| dt.format("%a %b %e %k:%M:%S %Z %Y").to_string())
401 } else {
402 Local
403 .timestamp_opt(timestamp, 0)
404 .single()
405 .map(|dt| dt.format("%a %b %e %k:%M:%S %Z %Y").to_string())
406 };
407
408 result.push_str(&dt.unwrap_or_else(|| format!("{}", timestamp)));
409
410 if flags.raw_format {
411 result.push(')');
412 }
413 }
414
415 if !flags.raw_format && !flags.string_format {
416 result = format!("{}", timestamp);
417 }
418
419 result
420 }
421
422 pub fn to_hash(&self, flags: &StatFlags) -> HashMap<String, String> {
423 let mut map = HashMap::new();
424 for (name, elem) in StatElement::all() {
425 map.insert(name.to_string(), self.get_element(elem, flags));
426 }
427 map
428 }
429
430 pub fn to_array(&self, flags: &StatFlags) -> Vec<String> {
431 StatElement::all()
432 .into_iter()
433 .map(|(_, elem)| self.get_element(elem, flags))
434 .collect()
435 }
436}
437
438fn set_bit(mode: u32, bit: u32) -> bool {
439 mode & bit != 0
440}
441
442#[cfg(unix)]
443fn get_username(uid: u32) -> Option<String> {
444 use std::ffi::CStr;
445 unsafe {
446 let pwd = libc::getpwuid(uid);
447 if pwd.is_null() {
448 None
449 } else {
450 CStr::from_ptr((*pwd).pw_name)
451 .to_str()
452 .ok()
453 .map(|s| s.to_string())
454 }
455 }
456}
457
458#[cfg(unix)]
459fn get_groupname(gid: u32) -> Option<String> {
460 use std::ffi::CStr;
461 unsafe {
462 let grp = libc::getgrgid(gid);
463 if grp.is_null() {
464 None
465 } else {
466 CStr::from_ptr((*grp).gr_name)
467 .to_str()
468 .ok()
469 .map(|s| s.to_string())
470 }
471 }
472}
473
474#[derive(Debug, Default)]
476pub struct StatOptions {
477 pub list_elements: bool,
478 pub use_lstat: bool,
479 pub use_gmt: bool,
480 pub show_name: bool,
481 pub hide_name: bool,
482 pub show_type: bool,
483 pub hide_type: bool,
484 pub raw_format: bool,
485 pub string_format: bool,
486 pub octal_mode: bool,
487 pub element: Option<StatElement>,
488 pub array_name: Option<String>,
489 pub hash_name: Option<String>,
490 pub time_format: Option<String>,
491}
492
493pub fn builtin_stat(args: &[&str], options: &StatOptions) -> (i32, String) {
495 let mut output = String::new();
496
497 if options.list_elements {
498 let names = StatElement::list_names();
499 output.push_str(&names.join(" "));
500 output.push('\n');
501 return (0, output);
502 }
503
504 if args.is_empty() {
505 return (1, "stat: no files given\n".to_string());
506 }
507
508 let flags = StatFlags {
509 show_name: options.show_type && !options.hide_type,
510 show_file: (options.show_name || args.len() > 1) && !options.hide_name,
511 string_format: options.string_format || options.use_gmt,
512 raw_format: options.raw_format || !options.string_format,
513 octal_mode: options.octal_mode,
514 use_gmt: options.use_gmt,
515 use_lstat: options.use_lstat || options.element == Some(StatElement::Link),
516 };
517
518 let mut ret = 0;
519
520 for path_str in args {
521 let path = Path::new(path_str);
522
523 let stat_result = FileStat::from_path(path, flags.use_lstat);
524
525 match stat_result {
526 Ok(stat) => {
527 if flags.show_file {
528 if options.element.is_some() {
529 output.push_str(&format!("{} ", path_str));
530 } else {
531 output.push_str(&format!("{}:\n", path_str));
532 }
533 }
534
535 if let Some(elem) = options.element {
536 let value = stat.get_element(elem, &flags);
537 if flags.show_name {
538 output.push_str(&format!("{} {}\n", elem.name(), value));
539 } else {
540 output.push_str(&format!("{}\n", value));
541 }
542 } else {
543 for (name, elem) in StatElement::all() {
544 let value = stat.get_element(elem, &flags);
545 if flags.show_name {
546 output.push_str(&format!("{:<8} {}\n", name, value));
547 } else {
548 output.push_str(&format!("{}\n", value));
549 }
550 }
551 }
552 }
553 Err(e) => {
554 output.push_str(&format!("stat: {}: {}\n", path_str, e));
555 ret = 1;
556 }
557 }
558 }
559
560 (ret, output)
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use std::fs::File;
567 use std::io::Write;
568 use tempfile::TempDir;
569
570 #[test]
571 fn test_stat_element_from_name() {
572 assert_eq!(StatElement::from_name("dev"), Some(StatElement::Device));
573 assert_eq!(StatElement::from_name("device"), Some(StatElement::Device));
574 assert_eq!(StatElement::from_name("mode"), Some(StatElement::Mode));
575 assert_eq!(StatElement::from_name("size"), Some(StatElement::Size));
576 assert_eq!(StatElement::from_name("link"), Some(StatElement::Link));
577 assert_eq!(StatElement::from_name("nonexistent"), None);
578 }
579
580 #[test]
581 fn test_stat_element_list() {
582 let names = StatElement::list_names();
583 assert!(names.contains(&"device"));
584 assert!(names.contains(&"inode"));
585 assert!(names.contains(&"mode"));
586 assert!(names.contains(&"size"));
587 assert_eq!(names.len(), 14);
588 }
589
590 #[test]
591 fn test_file_type_mode_char() {
592 assert_eq!(FileType::Regular.mode_char(), '-');
593 assert_eq!(FileType::Directory.mode_char(), 'd');
594 assert_eq!(FileType::Symlink.mode_char(), 'l');
595 assert_eq!(FileType::BlockDevice.mode_char(), 'b');
596 assert_eq!(FileType::CharDevice.mode_char(), 'c');
597 }
598
599 #[test]
600 fn test_file_stat_from_path() {
601 let dir = TempDir::new().unwrap();
602 let file_path = dir.path().join("test.txt");
603
604 {
605 let mut f = File::create(&file_path).unwrap();
606 f.write_all(b"hello world").unwrap();
607 }
608
609 let stat = FileStat::from_path(&file_path, false).unwrap();
610 assert_eq!(stat.size, 11);
611 assert_eq!(stat.file_type, FileType::Regular);
612 assert!(stat.inode > 0);
613 }
614
615 #[test]
616 fn test_format_mode_string() {
617 let stat = FileStat {
618 device: 0,
619 inode: 0,
620 mode: 0o100644,
621 nlink: 1,
622 uid: 0,
623 gid: 0,
624 rdev: 0,
625 size: 0,
626 atime: 0,
627 mtime: 0,
628 ctime: 0,
629 blksize: 0,
630 blocks: 0,
631 link_target: None,
632 file_type: FileType::Regular,
633 };
634
635 let flags = StatFlags {
636 string_format: true,
637 ..Default::default()
638 };
639
640 let mode_str = stat.format_mode(&flags);
641 assert!(mode_str.starts_with('-'));
642 assert!(mode_str.contains('r'));
643 assert!(mode_str.contains('w'));
644 }
645
646 #[test]
647 fn test_format_mode_octal() {
648 let stat = FileStat {
649 device: 0,
650 inode: 0,
651 mode: 0o100755,
652 nlink: 1,
653 uid: 0,
654 gid: 0,
655 rdev: 0,
656 size: 0,
657 atime: 0,
658 mtime: 0,
659 ctime: 0,
660 blksize: 0,
661 blocks: 0,
662 link_target: None,
663 file_type: FileType::Regular,
664 };
665
666 let flags = StatFlags {
667 raw_format: true,
668 octal_mode: true,
669 ..Default::default()
670 };
671
672 let mode_str = stat.format_mode(&flags);
673 assert!(mode_str.starts_with("0"));
674 assert!(mode_str.contains("755"));
675 }
676
677 #[test]
678 fn test_stat_to_hash() {
679 let stat = FileStat {
680 device: 1,
681 inode: 12345,
682 mode: 0o100644,
683 nlink: 1,
684 uid: 1000,
685 gid: 1000,
686 rdev: 0,
687 size: 100,
688 atime: 1700000000,
689 mtime: 1700000000,
690 ctime: 1700000000,
691 blksize: 4096,
692 blocks: 8,
693 link_target: None,
694 file_type: FileType::Regular,
695 };
696
697 let flags = StatFlags::default();
698 let hash = stat.to_hash(&flags);
699
700 assert!(hash.contains_key("device"));
701 assert!(hash.contains_key("size"));
702 assert_eq!(hash.get("size"), Some(&"100".to_string()));
703 }
704
705 #[test]
706 fn test_builtin_stat_list() {
707 let options = StatOptions {
708 list_elements: true,
709 ..Default::default()
710 };
711
712 let (status, output) = builtin_stat(&[], &options);
713 assert_eq!(status, 0);
714 assert!(output.contains("device"));
715 assert!(output.contains("inode"));
716 assert!(output.contains("mode"));
717 }
718
719 #[test]
720 fn test_builtin_stat_no_args() {
721 let options = StatOptions::default();
722 let (status, output) = builtin_stat(&[], &options);
723 assert_eq!(status, 1);
724 assert!(output.contains("no files given"));
725 }
726
727 #[test]
728 fn test_builtin_stat_file() {
729 let dir = TempDir::new().unwrap();
730 let file_path = dir.path().join("test.txt");
731
732 {
733 let mut f = File::create(&file_path).unwrap();
734 f.write_all(b"test content").unwrap();
735 }
736
737 let options = StatOptions {
738 show_type: true,
739 ..Default::default()
740 };
741
742 let (status, output) = builtin_stat(&[file_path.to_str().unwrap()], &options);
743 assert_eq!(status, 0);
744 assert!(output.contains("device"));
745 assert!(output.contains("size"));
746 }
747}