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 {
197 -1.0
198 } else {
199 let denom = used + available;
201 if denom == 0 {
202 0.0
203 } else {
204 (used as f64 / denom as f64) * 100.0
205 }
206 };
207
208 let itotal = stat.f_files as u64;
209 let ifree = stat.f_ffree as u64;
210 let iused = itotal.saturating_sub(ifree);
211 let iuse_percent = if itotal == 0 {
212 -1.0
213 } else {
214 (iused as f64 / itotal as f64) * 100.0
215 };
216
217 Some(FsInfo {
218 source: mount.source.clone(),
219 fstype: mount.fstype.clone(),
220 target: mount.target.clone(),
221 total,
222 used,
223 available,
224 use_percent,
225 itotal,
226 iused,
227 iavail: ifree,
228 iuse_percent,
229 })
230}
231
232#[cfg(not(unix))]
233fn statvfs_info(_mount: &MountEntry) -> Option<FsInfo> {
234 None
235}
236
237fn find_mount_for_file<'a>(path: &str, mounts: &'a [MountEntry]) -> Option<&'a MountEntry> {
244 let canonical = std::fs::canonicalize(path).ok()?;
245 let canonical_str = canonical.to_string_lossy();
246 let mut best: Option<&MountEntry> = None;
247 let mut best_len = 0;
248 for mount in mounts {
249 let target = &mount.target;
250 if canonical_str.starts_with(target.as_str())
251 && (canonical_str.len() == target.len()
252 || target == "/"
253 || canonical_str.as_bytes().get(target.len()) == Some(&b'/'))
254 {
255 if target.len() > best_len {
256 best_len = target.len();
257 best = Some(mount);
258 }
259 }
260 }
261 best
262}
263
264fn is_remote(fstype: &str) -> bool {
270 REMOTE_FS_TYPES.contains(&fstype)
271}
272
273fn is_pseudo(fstype: &str) -> bool {
275 PSEUDO_FS_TYPES.contains(&fstype)
276}
277
278pub fn get_filesystems(config: &DfConfig) -> (Vec<FsInfo>, bool) {
281 let mounts = read_mounts();
282 let mut had_error = false;
283
284 if !config.files.is_empty() {
286 let mut result = Vec::new();
287 for file in &config.files {
289 match find_mount_for_file(file, &mounts) {
290 Some(mount) => {
291 if let Some(info) = statvfs_info(mount) {
292 result.push(info);
293 }
294 }
295 None => {
296 eprintln!("df: {}: No such file or directory", file);
297 had_error = true;
298 }
299 }
300 }
301 return (result, had_error);
302 }
303
304 let mut result = Vec::new();
305 let mut seen_sources = HashSet::new();
306
307 for mount in &mounts {
308 if !config.type_filter.is_empty() && !config.type_filter.contains(&mount.fstype) {
310 continue;
311 }
312
313 if config.exclude_type.contains(&mount.fstype) {
315 continue;
316 }
317
318 if config.local_only && is_remote(&mount.fstype) {
320 continue;
321 }
322
323 if !config.all && is_pseudo(&mount.fstype) {
325 continue;
326 }
327
328 if !config.all {
330 if mount.source == "none" || mount.source == "tmpfs" || mount.source == "devtmpfs" {
331 } else if !seen_sources.insert(mount.source.clone()) {
333 continue;
334 }
335 }
336
337 if let Some(info) = statvfs_info(mount) {
338 if !config.all && info.total == 0 && config.type_filter.is_empty() {
340 continue;
341 }
342 result.push(info);
343 }
344 }
345
346 (result, had_error)
347}
348
349pub fn human_readable_1024(bytes: u64) -> String {
356 const UNITS: &[&str] = &["", "K", "M", "G", "T", "P", "E"];
357 if bytes == 0 {
358 return "0".to_string();
359 }
360 let mut value = bytes as f64;
361 for unit in UNITS {
362 if value < 1024.0 {
363 if value < 10.0 && !unit.is_empty() {
364 let rounded = (value * 10.0).ceil() / 10.0;
366 if rounded >= 10.0 {
367 return format!("{:.0}{}", rounded.ceil(), unit);
368 }
369 return format!("{:.1}{}", rounded, unit);
370 }
371 return format!("{:.0}{}", value.ceil(), unit);
372 }
373 value /= 1024.0;
374 }
375 format!("{:.0}E", value.ceil())
376}
377
378pub fn human_readable_1000(bytes: u64) -> String {
381 const UNITS: &[&str] = &["", "k", "M", "G", "T", "P", "E"];
382 if bytes == 0 {
383 return "0".to_string();
384 }
385 let mut value = bytes as f64;
386 for unit in UNITS {
387 if value < 1000.0 {
388 if value < 10.0 && !unit.is_empty() {
389 let rounded = (value * 10.0).ceil() / 10.0;
390 if rounded >= 10.0 {
391 return format!("{:.0}{}", rounded.ceil(), unit);
392 }
393 return format!("{:.1}{}", rounded, unit);
394 }
395 return format!("{:.0}{}", value.ceil(), unit);
396 }
397 value /= 1000.0;
398 }
399 format!("{:.0}E", value.ceil())
400}
401
402pub fn format_size(bytes: u64, config: &DfConfig) -> String {
404 if config.human_readable {
405 human_readable_1024(bytes)
406 } else if config.si {
407 human_readable_1000(bytes)
408 } else {
409 format!("{}", (bytes + config.block_size - 1) / config.block_size)
411 }
412}
413
414fn format_percent(pct: f64) -> String {
417 if pct < 0.0 {
418 return "-".to_string();
419 }
420 if pct == 0.0 {
421 return "0%".to_string();
422 }
423 let rounded = pct.ceil() as u64;
425 format!("{}%", rounded)
426}
427
428pub fn parse_block_size(s: &str) -> Result<u64, String> {
434 let s = s.trim();
435 if s.is_empty() {
436 return Err("invalid block size".to_string());
437 }
438
439 let s = s.strip_prefix('\'').unwrap_or(s);
441
442 let (num_str, suffix) = if s
443 .as_bytes()
444 .last()
445 .map_or(false, |b| b.is_ascii_alphabetic())
446 {
447 let last = s.len() - 1;
448 (&s[..last], &s[last..])
449 } else {
450 (s, "")
451 };
452
453 let num: u64 = if num_str.is_empty() {
454 1
455 } else {
456 num_str
457 .parse()
458 .map_err(|_| format!("invalid block size: '{}'", s))?
459 };
460
461 let multiplier = match suffix.to_uppercase().as_str() {
462 "" => 1u64,
463 "K" => 1024,
464 "M" => 1024 * 1024,
465 "G" => 1024 * 1024 * 1024,
466 "T" => 1024 * 1024 * 1024 * 1024,
467 "P" => 1024u64 * 1024 * 1024 * 1024 * 1024,
468 "E" => 1024u64 * 1024 * 1024 * 1024 * 1024 * 1024,
469 _ => return Err(format!("invalid suffix in block size: '{}'", s)),
470 };
471
472 Ok(num * multiplier)
473}
474
475pub const VALID_OUTPUT_FIELDS: &[&str] = &[
481 "source", "fstype", "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
482 "file", "target",
483];
484
485pub fn parse_output_fields(s: &str) -> Result<Vec<String>, String> {
487 let fields: Vec<String> = s.split(',').map(|f| f.trim().to_lowercase()).collect();
488 for field in &fields {
489 if !VALID_OUTPUT_FIELDS.contains(&field.as_str()) {
490 return Err(format!("df: '{}': not a valid field for --output", field));
491 }
492 }
493 Ok(fields)
494}
495
496fn size_header(config: &DfConfig) -> String {
502 if config.human_readable || config.si {
503 "Size".to_string()
504 } else if config.portability {
505 "1024-blocks".to_string()
506 } else if config.block_size == 1024 {
507 "1K-blocks".to_string()
508 } else if config.block_size == 1024 * 1024 {
509 "1M-blocks".to_string()
510 } else {
511 format!("{}-blocks", config.block_size)
512 }
513}
514
515fn build_row(info: &FsInfo, config: &DfConfig) -> Vec<String> {
517 if let Some(ref fields) = config.output_fields {
518 return fields
519 .iter()
520 .map(|f| match f.as_str() {
521 "source" => info.source.clone(),
522 "fstype" => info.fstype.clone(),
523 "itotal" => format!("{}", info.itotal),
524 "iused" => format!("{}", info.iused),
525 "iavail" => format!("{}", info.iavail),
526 "ipcent" => format_percent(info.iuse_percent),
527 "size" => format_size(info.total, config),
528 "used" => format_size(info.used, config),
529 "avail" => format_size(info.available, config),
530 "pcent" => format_percent(info.use_percent),
531 "file" => info.target.clone(),
532 "target" => info.target.clone(),
533 _ => String::new(),
534 })
535 .collect();
536 }
537
538 if config.inodes {
539 vec![
540 info.source.clone(),
541 format!("{}", info.itotal),
542 format!("{}", info.iused),
543 format!("{}", info.iavail),
544 format_percent(info.iuse_percent),
545 info.target.clone(),
546 ]
547 } else if config.print_type {
548 vec![
549 info.source.clone(),
550 info.fstype.clone(),
551 format_size(info.total, config),
552 format_size(info.used, config),
553 format_size(info.available, config),
554 format_percent(info.use_percent),
555 info.target.clone(),
556 ]
557 } else {
558 vec![
559 info.source.clone(),
560 format_size(info.total, config),
561 format_size(info.used, config),
562 format_size(info.available, config),
563 format_percent(info.use_percent),
564 info.target.clone(),
565 ]
566 }
567}
568
569fn build_header_row(config: &DfConfig) -> Vec<String> {
571 if let Some(ref fields) = config.output_fields {
572 return fields
573 .iter()
574 .map(|f| match f.as_str() {
575 "source" => "Filesystem".to_string(),
576 "fstype" => "Type".to_string(),
577 "itotal" => "Inodes".to_string(),
578 "iused" => "IUsed".to_string(),
579 "iavail" => "IFree".to_string(),
580 "ipcent" => "IUse%".to_string(),
581 "size" => size_header(config),
582 "used" => "Used".to_string(),
583 "avail" => "Avail".to_string(),
584 "pcent" => "Use%".to_string(),
585 "file" => "File".to_string(),
586 "target" => "Mounted on".to_string(),
587 _ => f.clone(),
588 })
589 .collect();
590 }
591
592 let pct_header = if config.portability {
593 "Capacity"
594 } else if config.inodes {
595 "IUse%"
596 } else {
597 "Use%"
598 };
599
600 if config.inodes {
601 vec![
602 "Filesystem".to_string(),
603 "Inodes".to_string(),
604 "IUsed".to_string(),
605 "IFree".to_string(),
606 pct_header.to_string(),
607 "Mounted on".to_string(),
608 ]
609 } else if config.print_type {
610 let avail_header = if config.human_readable || config.si {
611 "Avail"
612 } else {
613 "Available"
614 };
615 vec![
616 "Filesystem".to_string(),
617 "Type".to_string(),
618 size_header(config),
619 "Used".to_string(),
620 avail_header.to_string(),
621 pct_header.to_string(),
622 "Mounted on".to_string(),
623 ]
624 } else {
625 let avail_header = if config.human_readable || config.si {
626 "Avail"
627 } else {
628 "Available"
629 };
630 vec![
631 "Filesystem".to_string(),
632 size_header(config),
633 "Used".to_string(),
634 avail_header.to_string(),
635 pct_header.to_string(),
636 "Mounted on".to_string(),
637 ]
638 }
639}
640
641fn build_total_row(filesystems: &[FsInfo], config: &DfConfig) -> Vec<String> {
643 let total_size: u64 = filesystems.iter().map(|f| f.total).sum();
644 let total_used: u64 = filesystems.iter().map(|f| f.used).sum();
645 let total_avail: u64 = filesystems.iter().map(|f| f.available).sum();
646 let total_itotal: u64 = filesystems.iter().map(|f| f.itotal).sum();
647 let total_iused: u64 = filesystems.iter().map(|f| f.iused).sum();
648 let total_iavail: u64 = filesystems.iter().map(|f| f.iavail).sum();
649
650 let use_pct = {
651 let denom = total_used + total_avail;
652 if denom == 0 {
653 0.0
654 } else {
655 (total_used as f64 / denom as f64) * 100.0
656 }
657 };
658 let iuse_pct = if total_itotal == 0 {
659 0.0
660 } else {
661 (total_iused as f64 / total_itotal as f64) * 100.0
662 };
663
664 if config.inodes {
665 vec![
666 "total".to_string(),
667 format!("{}", total_itotal),
668 format!("{}", total_iused),
669 format!("{}", total_iavail),
670 format_percent(iuse_pct),
671 "-".to_string(),
672 ]
673 } else if config.print_type {
674 vec![
675 "total".to_string(),
676 "-".to_string(),
677 format_size(total_size, config),
678 format_size(total_used, config),
679 format_size(total_avail, config),
680 format_percent(use_pct),
681 "-".to_string(),
682 ]
683 } else {
684 vec![
685 "total".to_string(),
686 format_size(total_size, config),
687 format_size(total_used, config),
688 format_size(total_avail, config),
689 format_percent(use_pct),
690 "-".to_string(),
691 ]
692 }
693}
694
695enum ColAlign {
697 Left,
698 Right,
699 None, }
701
702fn get_col_alignments(config: &DfConfig, num_cols: usize) -> Vec<ColAlign> {
704 if num_cols == 0 {
705 return vec![];
706 }
707 let mut aligns = Vec::with_capacity(num_cols);
708
709 const NUMERIC_OUTPUT_FIELDS: &[&str] = &[
711 "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
712 ];
713 if config.output_fields.is_some() {
714 aligns.push(ColAlign::Left);
717 for _ in 1..num_cols.saturating_sub(1) {
718 aligns.push(ColAlign::Right);
719 }
720 if num_cols > 1 {
721 let last_field = config
722 .output_fields
723 .as_ref()
724 .and_then(|f| f.last())
725 .map(|s| s.as_str())
726 .unwrap_or("");
727 if NUMERIC_OUTPUT_FIELDS.contains(&last_field) {
728 aligns.push(ColAlign::Right);
729 } else {
730 aligns.push(ColAlign::None);
731 }
732 }
733 } else if config.print_type {
734 aligns.push(ColAlign::Left);
736 aligns.push(ColAlign::Left);
737 for _ in 2..num_cols.saturating_sub(1) {
738 aligns.push(ColAlign::Right);
739 }
740 if num_cols > 2 {
741 aligns.push(ColAlign::None);
742 }
743 } else {
744 aligns.push(ColAlign::Left);
746 for _ in 1..num_cols.saturating_sub(1) {
747 aligns.push(ColAlign::Right);
748 }
749 if num_cols > 1 {
750 aligns.push(ColAlign::None);
751 }
752 }
753
754 aligns
755}
756
757fn print_table(
759 header: &[String],
760 rows: &[Vec<String>],
761 config: &DfConfig,
762 out: &mut impl Write,
763) -> io::Result<()> {
764 let num_cols = header.len();
765 if num_cols == 0 {
766 return Ok(());
767 }
768
769 let mut widths = vec![0usize; num_cols];
771 for (i, h) in header.iter().enumerate() {
772 widths[i] = widths[i].max(h.len());
773 }
774 for row in rows {
775 for (i, val) in row.iter().enumerate() {
776 if i < num_cols {
777 widths[i] = widths[i].max(val.len());
778 }
779 }
780 }
781
782 if config.output_fields.is_none() && !widths.is_empty() {
786 widths[0] = widths[0].max(14);
787 }
788
789 if config.output_fields.is_none() {
793 let start_col = if config.print_type { 2 } else { 1 };
796 for i in start_col..start_col + 3 {
798 if i < num_cols {
799 widths[i] = widths[i].max(5);
800 }
801 }
802 }
803
804 let aligns = get_col_alignments(config, num_cols);
805
806 print_row(header, &widths, &aligns, out)?;
808
809 for row in rows {
811 print_row(row, &widths, &aligns, out)?;
812 }
813
814 Ok(())
815}
816
817fn print_row(
819 row: &[String],
820 widths: &[usize],
821 aligns: &[ColAlign],
822 out: &mut impl Write,
823) -> io::Result<()> {
824 let num_cols = widths.len();
825 for (i, val) in row.iter().enumerate() {
826 if i < num_cols {
827 if i > 0 {
828 write!(out, " ")?;
829 }
830 let w = widths[i];
831 match aligns.get(i).unwrap_or(&ColAlign::Right) {
832 ColAlign::Left => write!(out, "{:<width$}", val, width = w)?,
833 ColAlign::Right => write!(out, "{:>width$}", val, width = w)?,
834 ColAlign::None => write!(out, "{}", val)?,
835 }
836 }
837 }
838 writeln!(out)?;
839 Ok(())
840}
841
842pub fn print_header(config: &DfConfig, out: &mut impl Write) -> io::Result<()> {
844 let header = build_header_row(config);
845 let widths: Vec<usize> = header.iter().map(|h| h.len()).collect();
847 let aligns = get_col_alignments(config, header.len());
848 print_row(&header, &widths, &aligns, out)
849}
850
851pub fn print_fs_line(info: &FsInfo, config: &DfConfig, out: &mut impl Write) -> io::Result<()> {
853 let header = build_header_row(config);
854 let row = build_row(info, config);
855 let num_cols = header.len();
857 let mut widths = vec![0usize; num_cols];
858 for (i, h) in header.iter().enumerate() {
859 widths[i] = widths[i].max(h.len());
860 }
861 for (i, v) in row.iter().enumerate() {
862 if i < num_cols {
863 widths[i] = widths[i].max(v.len());
864 }
865 }
866 let aligns = get_col_alignments(config, num_cols);
867 print_row(&row, &widths, &aligns, out)
868}
869
870pub fn print_total_line(
872 filesystems: &[FsInfo],
873 config: &DfConfig,
874 out: &mut impl Write,
875) -> io::Result<()> {
876 let row = build_total_row(filesystems, config);
877 let header = build_header_row(config);
878 let num_cols = header.len();
879 let mut widths = vec![0usize; num_cols];
880 for (i, h) in header.iter().enumerate() {
881 widths[i] = widths[i].max(h.len());
882 }
883 for (i, v) in row.iter().enumerate() {
884 if i < num_cols {
885 widths[i] = widths[i].max(v.len());
886 }
887 }
888 let aligns = get_col_alignments(config, num_cols);
889 print_row(&row, &widths, &aligns, out)
890}
891
892pub fn run_df(config: &DfConfig) -> i32 {
894 let stdout = io::stdout();
895 let mut out = io::BufWriter::new(stdout.lock());
896
897 let (filesystems, had_error) = get_filesystems(config);
898
899 let header = build_header_row(config);
900 let mut rows: Vec<Vec<String>> = Vec::new();
901 for info in &filesystems {
902 rows.push(build_row(info, config));
903 }
904
905 if config.total {
906 rows.push(build_total_row(&filesystems, config));
907 }
908
909 if let Err(e) = print_table(&header, &rows, config, &mut out) {
910 if e.kind() == io::ErrorKind::BrokenPipe {
911 return 0;
912 }
913 eprintln!("df: write error: {}", e);
914 return 1;
915 }
916
917 let _ = out.flush();
918 if had_error { 1 } else { 0 }
919}