1use std::collections::HashSet;
2use std::io::{self, Write};
3
4pub struct DfConfig {
10 pub all: bool,
11 pub block_size: u64,
12 pub human_readable: bool,
13 pub si: bool,
14 pub inodes: bool,
15 pub local_only: bool,
16 pub portability: bool,
17 pub print_type: bool,
18 pub total: bool,
19 pub sync_before: bool,
20 pub type_filter: HashSet<String>,
21 pub exclude_type: HashSet<String>,
22 pub output_fields: Option<Vec<String>>,
23 pub files: Vec<String>,
24}
25
26impl Default for DfConfig {
27 fn default() -> Self {
28 Self {
29 all: false,
30 block_size: 1024,
31 human_readable: false,
32 si: false,
33 inodes: false,
34 local_only: false,
35 portability: false,
36 print_type: false,
37 total: false,
38 sync_before: false,
39 type_filter: HashSet::new(),
40 exclude_type: HashSet::new(),
41 output_fields: None,
42 files: Vec::new(),
43 }
44 }
45}
46
47pub struct MountEntry {
53 pub source: String,
54 pub target: String,
55 pub fstype: String,
56}
57
58pub struct FsInfo {
60 pub source: String,
61 pub fstype: String,
62 pub target: String,
63 pub total: u64,
64 pub used: u64,
65 pub available: u64,
66 pub use_percent: f64,
67 pub itotal: u64,
68 pub iused: u64,
69 pub iavail: u64,
70 pub iuse_percent: f64,
71}
72
73const REMOTE_FS_TYPES: &[&str] = &[
75 "nfs",
76 "nfs4",
77 "cifs",
78 "smbfs",
79 "ncpfs",
80 "afs",
81 "coda",
82 "ftpfs",
83 "mfs",
84 "sshfs",
85 "fuse.sshfs",
86 "ncp",
87 "9p",
88];
89
90const PSEUDO_FS_TYPES: &[&str] = &[
92 "sysfs",
93 "proc",
94 "devtmpfs",
95 "devpts",
96 "securityfs",
97 "cgroup",
98 "cgroup2",
99 "pstore",
100 "efivarfs",
101 "bpf",
102 "autofs",
103 "mqueue",
104 "hugetlbfs",
105 "debugfs",
106 "tracefs",
107 "fusectl",
108 "configfs",
109 "ramfs",
110 "binfmt_misc",
111 "rpc_pipefs",
112 "nsfs",
113 "overlay",
114 "squashfs",
115];
116
117pub fn read_mounts() -> Vec<MountEntry> {
123 let content = std::fs::read_to_string("/proc/mounts")
124 .or_else(|_| std::fs::read_to_string("/etc/mtab"))
125 .unwrap_or_default();
126 content
127 .lines()
128 .filter_map(|line| {
129 let parts: Vec<&str> = line.split_whitespace().collect();
130 if parts.len() >= 4 {
131 Some(MountEntry {
132 source: unescape_octal(parts[0]),
133 target: unescape_octal(parts[1]),
134 fstype: parts[2].to_string(),
135 })
136 } else {
137 None
138 }
139 })
140 .collect()
141}
142
143fn unescape_octal(s: &str) -> String {
145 let mut result = String::with_capacity(s.len());
146 let bytes = s.as_bytes();
147 let mut i = 0;
148 while i < bytes.len() {
149 if bytes[i] == b'\\' && i + 3 < bytes.len() {
150 let d1 = bytes[i + 1];
151 let d2 = bytes[i + 2];
152 let d3 = bytes[i + 3];
153 if (b'0'..=b'3').contains(&d1)
154 && (b'0'..=b'7').contains(&d2)
155 && (b'0'..=b'7').contains(&d3)
156 {
157 let val = (d1 - b'0') * 64 + (d2 - b'0') * 8 + (d3 - b'0');
158 result.push(val as char);
159 i += 4;
160 continue;
161 }
162 }
163 result.push(bytes[i] as char);
164 i += 1;
165 }
166 result
167}
168
169#[cfg(unix)]
175fn statvfs_info(mount: &MountEntry) -> Option<FsInfo> {
176 use std::ffi::CString;
177
178 let path = CString::new(mount.target.as_bytes()).ok()?;
179 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
180 let ret = unsafe { libc::statvfs(path.as_ptr(), &mut stat) };
181 if ret != 0 {
182 return None;
183 }
184
185 #[cfg(target_os = "linux")]
186 let block_size = stat.f_frsize as u64;
187 #[cfg(not(target_os = "linux"))]
188 let block_size = stat.f_bsize as u64;
189 let total = stat.f_blocks as u64 * block_size;
190 let free = stat.f_bfree as u64 * block_size;
191 let available = stat.f_bavail as u64 * block_size;
192 let used = total.saturating_sub(free);
193
194 let use_percent = if total == 0 {
195 0.0
196 } else {
197 let denom = used + available;
199 if denom == 0 {
200 0.0
201 } else {
202 (used as f64 / denom as f64) * 100.0
203 }
204 };
205
206 let itotal = stat.f_files as u64;
207 let ifree = stat.f_ffree as u64;
208 let iused = itotal.saturating_sub(ifree);
209 let iuse_percent = if itotal == 0 {
210 0.0
211 } else {
212 (iused as f64 / itotal as f64) * 100.0
213 };
214
215 Some(FsInfo {
216 source: mount.source.clone(),
217 fstype: mount.fstype.clone(),
218 target: mount.target.clone(),
219 total,
220 used,
221 available,
222 use_percent,
223 itotal,
224 iused,
225 iavail: ifree,
226 iuse_percent,
227 })
228}
229
230#[cfg(not(unix))]
231fn statvfs_info(_mount: &MountEntry) -> Option<FsInfo> {
232 None
233}
234
235fn find_mount_for_file<'a>(path: &str, mounts: &'a [MountEntry]) -> Option<&'a MountEntry> {
242 let canonical = std::fs::canonicalize(path).ok()?;
243 let canonical_str = canonical.to_string_lossy();
244 let mut best: Option<&MountEntry> = None;
245 let mut best_len = 0;
246 for mount in mounts {
247 let target = &mount.target;
248 if canonical_str.starts_with(target.as_str())
249 && (canonical_str.len() == target.len()
250 || target == "/"
251 || canonical_str.as_bytes().get(target.len()) == Some(&b'/'))
252 {
253 if target.len() > best_len {
254 best_len = target.len();
255 best = Some(mount);
256 }
257 }
258 }
259 best
260}
261
262fn is_remote(fstype: &str) -> bool {
268 REMOTE_FS_TYPES.contains(&fstype)
269}
270
271fn is_pseudo(fstype: &str) -> bool {
273 PSEUDO_FS_TYPES.contains(&fstype)
274}
275
276pub fn get_filesystems(config: &DfConfig) -> (Vec<FsInfo>, bool) {
279 let mounts = read_mounts();
280 let mut had_error = false;
281
282 if !config.files.is_empty() {
284 let mut result = Vec::new();
285 for file in &config.files {
287 match find_mount_for_file(file, &mounts) {
288 Some(mount) => {
289 if let Some(info) = statvfs_info(mount) {
290 result.push(info);
291 }
292 }
293 None => {
294 eprintln!("df: {}: No such file or directory", file);
295 had_error = true;
296 }
297 }
298 }
299 return (result, had_error);
300 }
301
302 let mut result = Vec::new();
303 let mut seen_sources = HashSet::new();
304
305 for mount in &mounts {
306 if !config.type_filter.is_empty() && !config.type_filter.contains(&mount.fstype) {
308 continue;
309 }
310
311 if config.exclude_type.contains(&mount.fstype) {
313 continue;
314 }
315
316 if config.local_only && is_remote(&mount.fstype) {
318 continue;
319 }
320
321 if !config.all && is_pseudo(&mount.fstype) {
323 continue;
324 }
325
326 if !config.all {
328 if mount.source == "none" || mount.source == "tmpfs" || mount.source == "devtmpfs" {
329 } else if !seen_sources.insert(mount.source.clone()) {
331 continue;
332 }
333 }
334
335 if let Some(info) = statvfs_info(mount) {
336 if !config.all && info.total == 0 && config.type_filter.is_empty() {
338 continue;
339 }
340 result.push(info);
341 }
342 }
343
344 (result, had_error)
345}
346
347pub fn human_readable_1024(bytes: u64) -> String {
353 const UNITS: &[&str] = &["", "K", "M", "G", "T", "P", "E"];
354 if bytes == 0 {
355 return "0".to_string();
356 }
357 let mut value = bytes as f64;
358 for unit in UNITS {
359 if value < 1024.0 {
360 if value < 10.0 && !unit.is_empty() {
361 return format!("{:.1}{}", value, unit);
362 }
363 return format!("{:.0}{}", value, unit);
364 }
365 value /= 1024.0;
366 }
367 format!("{:.0}E", value)
368}
369
370pub fn human_readable_1000(bytes: u64) -> String {
372 const UNITS: &[&str] = &["", "k", "M", "G", "T", "P", "E"];
373 if bytes == 0 {
374 return "0".to_string();
375 }
376 let mut value = bytes as f64;
377 for unit in UNITS {
378 if value < 1000.0 {
379 if value < 10.0 && !unit.is_empty() {
380 return format!("{:.1}{}", value, unit);
381 }
382 return format!("{:.0}{}", value, unit);
383 }
384 value /= 1000.0;
385 }
386 format!("{:.0}E", value)
387}
388
389pub fn format_size(bytes: u64, config: &DfConfig) -> String {
391 if config.human_readable {
392 human_readable_1024(bytes)
393 } else if config.si {
394 human_readable_1000(bytes)
395 } else {
396 format!("{}", bytes / config.block_size)
397 }
398}
399
400fn format_percent(pct: f64) -> String {
402 if pct == 0.0 {
403 return "0%".to_string();
404 }
405 let rounded = pct.ceil() as u64;
407 format!("{}%", rounded)
408}
409
410pub fn parse_block_size(s: &str) -> Result<u64, String> {
416 let s = s.trim();
417 if s.is_empty() {
418 return Err("invalid block size".to_string());
419 }
420
421 let s = s.strip_prefix('\'').unwrap_or(s);
423
424 let (num_str, suffix) = if s
425 .as_bytes()
426 .last()
427 .map_or(false, |b| b.is_ascii_alphabetic())
428 {
429 let last = s.len() - 1;
430 (&s[..last], &s[last..])
431 } else {
432 (s, "")
433 };
434
435 let num: u64 = if num_str.is_empty() {
436 1
437 } else {
438 num_str
439 .parse()
440 .map_err(|_| format!("invalid block size: '{}'", s))?
441 };
442
443 let multiplier = match suffix.to_uppercase().as_str() {
444 "" => 1u64,
445 "K" => 1024,
446 "M" => 1024 * 1024,
447 "G" => 1024 * 1024 * 1024,
448 "T" => 1024 * 1024 * 1024 * 1024,
449 "P" => 1024u64 * 1024 * 1024 * 1024 * 1024,
450 "E" => 1024u64 * 1024 * 1024 * 1024 * 1024 * 1024,
451 _ => return Err(format!("invalid suffix in block size: '{}'", s)),
452 };
453
454 Ok(num * multiplier)
455}
456
457pub const VALID_OUTPUT_FIELDS: &[&str] = &[
463 "source", "fstype", "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
464 "file", "target",
465];
466
467pub fn parse_output_fields(s: &str) -> Result<Vec<String>, String> {
469 let fields: Vec<String> = s.split(',').map(|f| f.trim().to_lowercase()).collect();
470 for field in &fields {
471 if !VALID_OUTPUT_FIELDS.contains(&field.as_str()) {
472 return Err(format!("df: '{}': not a valid field for --output", field));
473 }
474 }
475 Ok(fields)
476}
477
478fn size_header(config: &DfConfig) -> String {
484 if config.human_readable || config.si {
485 "Size".to_string()
486 } else if config.portability {
487 "1024-blocks".to_string()
488 } else if config.block_size == 1024 {
489 "1K-blocks".to_string()
490 } else if config.block_size == 1024 * 1024 {
491 "1M-blocks".to_string()
492 } else {
493 format!("{}-blocks", config.block_size)
494 }
495}
496
497fn build_row(info: &FsInfo, config: &DfConfig) -> Vec<String> {
499 if let Some(ref fields) = config.output_fields {
500 return fields
501 .iter()
502 .map(|f| match f.as_str() {
503 "source" => info.source.clone(),
504 "fstype" => info.fstype.clone(),
505 "itotal" => format!("{}", info.itotal),
506 "iused" => format!("{}", info.iused),
507 "iavail" => format!("{}", info.iavail),
508 "ipcent" => format_percent(info.iuse_percent),
509 "size" => format_size(info.total, config),
510 "used" => format_size(info.used, config),
511 "avail" => format_size(info.available, config),
512 "pcent" => format_percent(info.use_percent),
513 "file" => info.target.clone(),
514 "target" => info.target.clone(),
515 _ => String::new(),
516 })
517 .collect();
518 }
519
520 if config.inodes {
521 vec![
522 info.source.clone(),
523 format!("{}", info.itotal),
524 format!("{}", info.iused),
525 format!("{}", info.iavail),
526 format_percent(info.iuse_percent),
527 info.target.clone(),
528 ]
529 } else if config.print_type {
530 vec![
531 info.source.clone(),
532 info.fstype.clone(),
533 format_size(info.total, config),
534 format_size(info.used, config),
535 format_size(info.available, config),
536 format_percent(info.use_percent),
537 info.target.clone(),
538 ]
539 } else {
540 vec![
541 info.source.clone(),
542 format_size(info.total, config),
543 format_size(info.used, config),
544 format_size(info.available, config),
545 format_percent(info.use_percent),
546 info.target.clone(),
547 ]
548 }
549}
550
551fn build_header_row(config: &DfConfig) -> Vec<String> {
553 if let Some(ref fields) = config.output_fields {
554 return fields
555 .iter()
556 .map(|f| match f.as_str() {
557 "source" => "Filesystem".to_string(),
558 "fstype" => "Type".to_string(),
559 "itotal" => "Inodes".to_string(),
560 "iused" => "IUsed".to_string(),
561 "iavail" => "IFree".to_string(),
562 "ipcent" => "IUse%".to_string(),
563 "size" => size_header(config),
564 "used" => "Used".to_string(),
565 "avail" => "Avail".to_string(),
566 "pcent" => "Use%".to_string(),
567 "file" => "File".to_string(),
568 "target" => "Mounted on".to_string(),
569 _ => f.clone(),
570 })
571 .collect();
572 }
573
574 let pct_header = if config.portability {
575 "Capacity"
576 } else if config.inodes {
577 "IUse%"
578 } else {
579 "Use%"
580 };
581
582 if config.inodes {
583 vec![
584 "Filesystem".to_string(),
585 "Inodes".to_string(),
586 "IUsed".to_string(),
587 "IFree".to_string(),
588 pct_header.to_string(),
589 "Mounted on".to_string(),
590 ]
591 } else if config.print_type {
592 vec![
593 "Filesystem".to_string(),
594 "Type".to_string(),
595 size_header(config),
596 "Used".to_string(),
597 if config.portability {
598 "Available"
599 } else {
600 "Avail"
601 }
602 .to_string(),
603 pct_header.to_string(),
604 "Mounted on".to_string(),
605 ]
606 } else {
607 vec![
608 "Filesystem".to_string(),
609 size_header(config),
610 "Used".to_string(),
611 if config.portability {
612 "Available"
613 } else {
614 "Avail"
615 }
616 .to_string(),
617 pct_header.to_string(),
618 "Mounted on".to_string(),
619 ]
620 }
621}
622
623fn build_total_row(filesystems: &[FsInfo], config: &DfConfig) -> Vec<String> {
625 let total_size: u64 = filesystems.iter().map(|f| f.total).sum();
626 let total_used: u64 = filesystems.iter().map(|f| f.used).sum();
627 let total_avail: u64 = filesystems.iter().map(|f| f.available).sum();
628 let total_itotal: u64 = filesystems.iter().map(|f| f.itotal).sum();
629 let total_iused: u64 = filesystems.iter().map(|f| f.iused).sum();
630 let total_iavail: u64 = filesystems.iter().map(|f| f.iavail).sum();
631
632 let use_pct = {
633 let denom = total_used + total_avail;
634 if denom == 0 {
635 0.0
636 } else {
637 (total_used as f64 / denom as f64) * 100.0
638 }
639 };
640 let iuse_pct = if total_itotal == 0 {
641 0.0
642 } else {
643 (total_iused as f64 / total_itotal as f64) * 100.0
644 };
645
646 if config.inodes {
647 vec![
648 "total".to_string(),
649 format!("{}", total_itotal),
650 format!("{}", total_iused),
651 format!("{}", total_iavail),
652 format_percent(iuse_pct),
653 "-".to_string(),
654 ]
655 } else if config.print_type {
656 vec![
657 "total".to_string(),
658 "-".to_string(),
659 format_size(total_size, config),
660 format_size(total_used, config),
661 format_size(total_avail, config),
662 format_percent(use_pct),
663 "-".to_string(),
664 ]
665 } else {
666 vec![
667 "total".to_string(),
668 format_size(total_size, config),
669 format_size(total_used, config),
670 format_size(total_avail, config),
671 format_percent(use_pct),
672 "-".to_string(),
673 ]
674 }
675}
676
677enum ColAlign {
679 Left,
680 Right,
681 None, }
683
684fn get_col_alignments(config: &DfConfig, num_cols: usize) -> Vec<ColAlign> {
686 if num_cols == 0 {
687 return vec![];
688 }
689 let mut aligns = Vec::with_capacity(num_cols);
690
691 if config.output_fields.is_some() {
692 aligns.push(ColAlign::Left);
694 for _ in 1..num_cols.saturating_sub(1) {
695 aligns.push(ColAlign::Right);
696 }
697 if num_cols > 1 {
698 aligns.push(ColAlign::None);
699 }
700 } else if config.print_type {
701 aligns.push(ColAlign::Left);
703 aligns.push(ColAlign::Left);
704 for _ in 2..num_cols.saturating_sub(1) {
705 aligns.push(ColAlign::Right);
706 }
707 if num_cols > 2 {
708 aligns.push(ColAlign::None);
709 }
710 } else {
711 aligns.push(ColAlign::Left);
713 for _ in 1..num_cols.saturating_sub(1) {
714 aligns.push(ColAlign::Right);
715 }
716 if num_cols > 1 {
717 aligns.push(ColAlign::None);
718 }
719 }
720
721 aligns
722}
723
724fn print_table(
726 header: &[String],
727 rows: &[Vec<String>],
728 config: &DfConfig,
729 out: &mut impl Write,
730) -> io::Result<()> {
731 let num_cols = header.len();
732 if num_cols == 0 {
733 return Ok(());
734 }
735
736 let mut widths = vec![0usize; num_cols];
738 for (i, h) in header.iter().enumerate() {
739 widths[i] = widths[i].max(h.len());
740 }
741 for row in rows {
742 for (i, val) in row.iter().enumerate() {
743 if i < num_cols {
744 widths[i] = widths[i].max(val.len());
745 }
746 }
747 }
748
749 let aligns = get_col_alignments(config, num_cols);
750
751 print_row(header, &widths, &aligns, out)?;
753
754 for row in rows {
756 print_row(row, &widths, &aligns, out)?;
757 }
758
759 Ok(())
760}
761
762fn print_row(
764 row: &[String],
765 widths: &[usize],
766 aligns: &[ColAlign],
767 out: &mut impl Write,
768) -> io::Result<()> {
769 let num_cols = widths.len();
770 for (i, val) in row.iter().enumerate() {
771 if i < num_cols {
772 if i > 0 {
773 write!(out, " ")?;
774 }
775 let w = widths[i];
776 match aligns.get(i).unwrap_or(&ColAlign::Right) {
777 ColAlign::Left => write!(out, "{:<width$}", val, width = w)?,
778 ColAlign::Right => write!(out, "{:>width$}", val, width = w)?,
779 ColAlign::None => write!(out, "{}", val)?,
780 }
781 }
782 }
783 writeln!(out)?;
784 Ok(())
785}
786
787pub fn print_header(config: &DfConfig, out: &mut impl Write) -> io::Result<()> {
789 let header = build_header_row(config);
790 let widths: Vec<usize> = header.iter().map(|h| h.len()).collect();
792 let aligns = get_col_alignments(config, header.len());
793 print_row(&header, &widths, &aligns, out)
794}
795
796pub fn print_fs_line(info: &FsInfo, config: &DfConfig, out: &mut impl Write) -> io::Result<()> {
798 let header = build_header_row(config);
799 let row = build_row(info, config);
800 let num_cols = header.len();
802 let mut widths = vec![0usize; num_cols];
803 for (i, h) in header.iter().enumerate() {
804 widths[i] = widths[i].max(h.len());
805 }
806 for (i, v) in row.iter().enumerate() {
807 if i < num_cols {
808 widths[i] = widths[i].max(v.len());
809 }
810 }
811 let aligns = get_col_alignments(config, num_cols);
812 print_row(&row, &widths, &aligns, out)
813}
814
815pub fn print_total_line(
817 filesystems: &[FsInfo],
818 config: &DfConfig,
819 out: &mut impl Write,
820) -> io::Result<()> {
821 let row = build_total_row(filesystems, config);
822 let header = build_header_row(config);
823 let num_cols = header.len();
824 let mut widths = vec![0usize; num_cols];
825 for (i, h) in header.iter().enumerate() {
826 widths[i] = widths[i].max(h.len());
827 }
828 for (i, v) in row.iter().enumerate() {
829 if i < num_cols {
830 widths[i] = widths[i].max(v.len());
831 }
832 }
833 let aligns = get_col_alignments(config, num_cols);
834 print_row(&row, &widths, &aligns, out)
835}
836
837pub fn run_df(config: &DfConfig) -> i32 {
839 let stdout = io::stdout();
840 let mut out = io::BufWriter::new(stdout.lock());
841
842 let (filesystems, had_error) = get_filesystems(config);
843
844 let header = build_header_row(config);
845 let mut rows: Vec<Vec<String>> = Vec::new();
846 for info in &filesystems {
847 rows.push(build_row(info, config));
848 }
849
850 if config.total {
851 rows.push(build_total_row(&filesystems, config));
852 }
853
854 if let Err(e) = print_table(&header, &rows, config, &mut out) {
855 if e.kind() == io::ErrorKind::BrokenPipe {
856 return 0;
857 }
858 eprintln!("df: write error: {}", e);
859 return 1;
860 }
861
862 let _ = out.flush();
863 if had_error { 1 } else { 0 }
864}