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 file: String,
65 pub total: u64,
66 pub used: u64,
67 pub available: u64,
68 pub use_percent: f64,
69 pub itotal: u64,
70 pub iused: u64,
71 pub iavail: u64,
72 pub iuse_percent: f64,
73}
74
75const REMOTE_FS_TYPES: &[&str] = &[
77 "nfs",
78 "nfs4",
79 "cifs",
80 "smbfs",
81 "ncpfs",
82 "afs",
83 "coda",
84 "ftpfs",
85 "mfs",
86 "sshfs",
87 "fuse.sshfs",
88 "ncp",
89 "9p",
90];
91
92const PSEUDO_FS_TYPES: &[&str] = &[
94 "sysfs",
95 "proc",
96 "devtmpfs",
97 "devpts",
98 "securityfs",
99 "cgroup",
100 "cgroup2",
101 "pstore",
102 "efivarfs",
103 "bpf",
104 "autofs",
105 "mqueue",
106 "hugetlbfs",
107 "debugfs",
108 "tracefs",
109 "fusectl",
110 "configfs",
111 "ramfs",
112 "binfmt_misc",
113 "rpc_pipefs",
114 "nsfs",
115 "overlay",
116 "squashfs",
117];
118
119pub fn read_mounts() -> Vec<MountEntry> {
125 let content = std::fs::read_to_string("/proc/mounts")
126 .or_else(|_| std::fs::read_to_string("/etc/mtab"))
127 .unwrap_or_default();
128 content
129 .lines()
130 .filter_map(|line| {
131 let parts: Vec<&str> = line.split_whitespace().collect();
132 if parts.len() >= 4 {
133 Some(MountEntry {
134 source: unescape_octal(parts[0]),
135 target: unescape_octal(parts[1]),
136 fstype: parts[2].to_string(),
137 })
138 } else {
139 None
140 }
141 })
142 .collect()
143}
144
145fn unescape_octal(s: &str) -> String {
147 let mut result = String::with_capacity(s.len());
148 let bytes = s.as_bytes();
149 let mut i = 0;
150 while i < bytes.len() {
151 if bytes[i] == b'\\' && i + 3 < bytes.len() {
152 let d1 = bytes[i + 1];
153 let d2 = bytes[i + 2];
154 let d3 = bytes[i + 3];
155 if (b'0'..=b'3').contains(&d1)
156 && (b'0'..=b'7').contains(&d2)
157 && (b'0'..=b'7').contains(&d3)
158 {
159 let val = (d1 - b'0') * 64 + (d2 - b'0') * 8 + (d3 - b'0');
160 result.push(val as char);
161 i += 4;
162 continue;
163 }
164 }
165 result.push(bytes[i] as char);
166 i += 1;
167 }
168 result
169}
170
171#[cfg(unix)]
177fn statvfs_info(mount: &MountEntry) -> Option<FsInfo> {
178 use std::ffi::CString;
179
180 let path = CString::new(mount.target.as_bytes()).ok()?;
181 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
182 let ret = unsafe { libc::statvfs(path.as_ptr(), &mut stat) };
183 if ret != 0 {
184 return None;
185 }
186
187 #[cfg(target_os = "linux")]
188 let block_size = stat.f_frsize as u64;
189 #[cfg(not(target_os = "linux"))]
190 let block_size = stat.f_bsize as u64;
191 let total = stat.f_blocks as u64 * block_size;
192 let free = stat.f_bfree as u64 * block_size;
193 let available = stat.f_bavail as u64 * block_size;
194 let used = total.saturating_sub(free);
195
196 let use_percent = if total == 0 {
199 -1.0
200 } else {
201 let denom = used + available;
203 if denom == 0 {
204 0.0
205 } else {
206 (used as f64 / denom as f64) * 100.0
207 }
208 };
209
210 let itotal = stat.f_files as u64;
211 let ifree = stat.f_ffree as u64;
212 let iused = itotal.saturating_sub(ifree);
213 let iuse_percent = if itotal == 0 {
214 -1.0
215 } else {
216 (iused as f64 / itotal as f64) * 100.0
217 };
218
219 Some(FsInfo {
220 source: mount.source.clone(),
221 fstype: mount.fstype.clone(),
222 target: mount.target.clone(),
223 file: mount.target.clone(),
224 total,
225 used,
226 available,
227 use_percent,
228 itotal,
229 iused,
230 iavail: ifree,
231 iuse_percent,
232 })
233}
234
235#[cfg(not(unix))]
236fn statvfs_info(_mount: &MountEntry) -> Option<FsInfo> {
237 None
238}
239
240fn find_mount_for_file<'a>(path: &str, mounts: &'a [MountEntry]) -> Option<&'a MountEntry> {
247 let canonical = std::fs::canonicalize(path).ok()?;
248 let canonical_str = canonical.to_string_lossy();
249 let mut best: Option<&MountEntry> = None;
250 let mut best_len = 0;
251 for mount in mounts {
252 let target = &mount.target;
253 if canonical_str.starts_with(target.as_str())
254 && (canonical_str.len() == target.len()
255 || target == "/"
256 || canonical_str.as_bytes().get(target.len()) == Some(&b'/'))
257 {
258 if target.len() > best_len {
259 best_len = target.len();
260 best = Some(mount);
261 }
262 }
263 }
264 best
265}
266
267fn is_remote(fstype: &str) -> bool {
273 REMOTE_FS_TYPES.contains(&fstype)
274}
275
276fn is_pseudo(fstype: &str) -> bool {
278 PSEUDO_FS_TYPES.contains(&fstype)
279}
280
281pub fn get_filesystems(config: &DfConfig) -> (Vec<FsInfo>, bool) {
284 let mounts = read_mounts();
285 let mut had_error = false;
286
287 if !config.files.is_empty() {
289 let mut result = Vec::new();
290 for file in &config.files {
292 match find_mount_for_file(file, &mounts) {
293 Some(mount) => {
294 if let Some(mut info) = statvfs_info(mount) {
295 info.file = file.clone();
296 result.push(info);
297 }
298 }
299 None => {
300 eprintln!("df: {}: No such file or directory", file);
301 had_error = true;
302 }
303 }
304 }
305 return (result, had_error);
306 }
307
308 let mut result = Vec::new();
309 let mut seen_sources = HashSet::new();
310
311 for mount in &mounts {
312 if !config.type_filter.is_empty() && !config.type_filter.contains(&mount.fstype) {
314 continue;
315 }
316
317 if config.exclude_type.contains(&mount.fstype) {
319 continue;
320 }
321
322 if config.local_only && is_remote(&mount.fstype) {
324 continue;
325 }
326
327 if !config.all && is_pseudo(&mount.fstype) {
329 continue;
330 }
331
332 if !config.all {
334 if mount.source == "none" || mount.source == "tmpfs" || mount.source == "devtmpfs" {
335 } else if !seen_sources.insert(mount.source.clone()) {
337 continue;
338 }
339 }
340
341 if let Some(info) = statvfs_info(mount) {
342 if !config.all && info.total == 0 && config.type_filter.is_empty() {
344 continue;
345 }
346 result.push(info);
347 }
348 }
349
350 (result, had_error)
351}
352
353pub fn human_readable_1024(bytes: u64) -> String {
360 const UNITS: &[&str] = &["", "K", "M", "G", "T", "P", "E"];
361 if bytes == 0 {
362 return "0".to_string();
363 }
364 let mut value = bytes as f64;
365 for unit in UNITS {
366 if value < 1024.0 {
367 if value < 10.0 && !unit.is_empty() {
368 let rounded = (value * 10.0).ceil() / 10.0;
370 if rounded >= 10.0 {
371 return format!("{:.0}{}", rounded.ceil(), unit);
372 }
373 return format!("{:.1}{}", rounded, unit);
374 }
375 return format!("{:.0}{}", value.ceil(), unit);
376 }
377 value /= 1024.0;
378 }
379 format!("{:.0}E", value.ceil())
380}
381
382pub fn human_readable_1000(bytes: u64) -> String {
385 const UNITS: &[&str] = &["", "k", "M", "G", "T", "P", "E"];
386 if bytes == 0 {
387 return "0".to_string();
388 }
389 let mut value = bytes as f64;
390 for unit in UNITS {
391 if value < 1000.0 {
392 if value < 10.0 && !unit.is_empty() {
393 let rounded = (value * 10.0).ceil() / 10.0;
394 if rounded >= 10.0 {
395 return format!("{:.0}{}", rounded.ceil(), unit);
396 }
397 return format!("{:.1}{}", rounded, unit);
398 }
399 return format!("{:.0}{}", value.ceil(), unit);
400 }
401 value /= 1000.0;
402 }
403 format!("{:.0}E", value.ceil())
404}
405
406pub fn format_size(bytes: u64, config: &DfConfig) -> String {
408 if config.human_readable {
409 human_readable_1024(bytes)
410 } else if config.si {
411 human_readable_1000(bytes)
412 } else {
413 format!("{}", (bytes + config.block_size - 1) / config.block_size)
415 }
416}
417
418fn format_percent(pct: f64) -> String {
421 if pct < 0.0 {
422 return "-".to_string();
423 }
424 if pct == 0.0 {
425 return "0%".to_string();
426 }
427 let rounded = pct.ceil() as u64;
429 format!("{}%", rounded)
430}
431
432pub fn parse_block_size(s: &str) -> Result<u64, String> {
438 let s = s.trim();
439 if s.is_empty() {
440 return Err("invalid block size".to_string());
441 }
442
443 let s = s.strip_prefix('\'').unwrap_or(s);
445
446 let (num_str, suffix) = if s
447 .as_bytes()
448 .last()
449 .map_or(false, |b| b.is_ascii_alphabetic())
450 {
451 let last = s.len() - 1;
452 (&s[..last], &s[last..])
453 } else {
454 (s, "")
455 };
456
457 let num: u64 = if num_str.is_empty() {
458 1
459 } else {
460 num_str
461 .parse()
462 .map_err(|_| format!("invalid block size: '{}'", s))?
463 };
464
465 let multiplier = match suffix.to_uppercase().as_str() {
466 "" => 1u64,
467 "K" => 1024,
468 "M" => 1024 * 1024,
469 "G" => 1024 * 1024 * 1024,
470 "T" => 1024 * 1024 * 1024 * 1024,
471 "P" => 1024u64 * 1024 * 1024 * 1024 * 1024,
472 "E" => 1024u64 * 1024 * 1024 * 1024 * 1024 * 1024,
473 _ => return Err(format!("invalid suffix in block size: '{}'", s)),
474 };
475
476 Ok(num * multiplier)
477}
478
479pub const VALID_OUTPUT_FIELDS: &[&str] = &[
485 "source", "fstype", "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
486 "file", "target",
487];
488
489pub fn parse_output_fields(s: &str) -> Result<Vec<String>, String> {
491 let fields: Vec<String> = s.split(',').map(|f| f.trim().to_lowercase()).collect();
492 let mut seen = std::collections::HashSet::new();
493 for field in &fields {
494 if !VALID_OUTPUT_FIELDS.contains(&field.as_str()) {
495 return Err(format!("df: '{}': not a valid field for --output", field));
496 }
497 if !seen.insert(field.as_str()) {
498 return Err(format!(
499 "option --output: field '{}' used more than once",
500 field
501 ));
502 }
503 }
504 Ok(fields)
505}
506
507fn size_header(config: &DfConfig) -> String {
513 if config.human_readable || config.si {
514 "Size".to_string()
515 } else if config.portability {
516 "1024-blocks".to_string()
517 } else if config.block_size == 1024 {
518 "1K-blocks".to_string()
519 } else if config.block_size == 1024 * 1024 {
520 "1M-blocks".to_string()
521 } else {
522 format!("{}-blocks", config.block_size)
523 }
524}
525
526pub(crate) fn build_row(info: &FsInfo, config: &DfConfig) -> Vec<String> {
528 if let Some(ref fields) = config.output_fields {
529 return fields
530 .iter()
531 .map(|f| match f.as_str() {
532 "source" => info.source.clone(),
533 "fstype" => info.fstype.clone(),
534 "itotal" => format!("{}", info.itotal),
535 "iused" => format!("{}", info.iused),
536 "iavail" => format!("{}", info.iavail),
537 "ipcent" => format_percent(info.iuse_percent),
538 "size" => format_size(info.total, config),
539 "used" => format_size(info.used, config),
540 "avail" => format_size(info.available, config),
541 "pcent" => format_percent(info.use_percent),
542 "file" => info.file.clone(),
543 "target" => info.target.clone(),
544 _ => String::new(),
545 })
546 .collect();
547 }
548
549 if config.inodes {
550 vec![
551 info.source.clone(),
552 format!("{}", info.itotal),
553 format!("{}", info.iused),
554 format!("{}", info.iavail),
555 format_percent(info.iuse_percent),
556 info.target.clone(),
557 ]
558 } else if config.print_type {
559 vec![
560 info.source.clone(),
561 info.fstype.clone(),
562 format_size(info.total, config),
563 format_size(info.used, config),
564 format_size(info.available, config),
565 format_percent(info.use_percent),
566 info.target.clone(),
567 ]
568 } else {
569 vec![
570 info.source.clone(),
571 format_size(info.total, config),
572 format_size(info.used, config),
573 format_size(info.available, config),
574 format_percent(info.use_percent),
575 info.target.clone(),
576 ]
577 }
578}
579
580pub(crate) fn build_header_row(config: &DfConfig) -> Vec<String> {
582 if let Some(ref fields) = config.output_fields {
583 return fields
584 .iter()
585 .map(|f| match f.as_str() {
586 "source" => "Filesystem".to_string(),
587 "fstype" => "Type".to_string(),
588 "itotal" => "Inodes".to_string(),
589 "iused" => "IUsed".to_string(),
590 "iavail" => "IFree".to_string(),
591 "ipcent" => "IUse%".to_string(),
592 "size" => size_header(config),
593 "used" => "Used".to_string(),
594 "avail" => "Avail".to_string(),
595 "pcent" => "Use%".to_string(),
596 "file" => "File".to_string(),
597 "target" => "Mounted on".to_string(),
598 _ => f.clone(),
599 })
600 .collect();
601 }
602
603 let pct_header = if config.portability {
604 "Capacity"
605 } else if config.inodes {
606 "IUse%"
607 } else {
608 "Use%"
609 };
610
611 if config.inodes {
612 vec![
613 "Filesystem".to_string(),
614 "Inodes".to_string(),
615 "IUsed".to_string(),
616 "IFree".to_string(),
617 pct_header.to_string(),
618 "Mounted on".to_string(),
619 ]
620 } else if config.print_type {
621 let avail_header = if config.human_readable || config.si {
622 "Avail"
623 } else {
624 "Available"
625 };
626 vec![
627 "Filesystem".to_string(),
628 "Type".to_string(),
629 size_header(config),
630 "Used".to_string(),
631 avail_header.to_string(),
632 pct_header.to_string(),
633 "Mounted on".to_string(),
634 ]
635 } else {
636 let avail_header = if config.human_readable || config.si {
637 "Avail"
638 } else {
639 "Available"
640 };
641 vec![
642 "Filesystem".to_string(),
643 size_header(config),
644 "Used".to_string(),
645 avail_header.to_string(),
646 pct_header.to_string(),
647 "Mounted on".to_string(),
648 ]
649 }
650}
651
652fn build_total_row(filesystems: &[FsInfo], config: &DfConfig) -> Vec<String> {
654 let total_size: u64 = filesystems.iter().map(|f| f.total).sum();
655 let total_used: u64 = filesystems.iter().map(|f| f.used).sum();
656 let total_avail: u64 = filesystems.iter().map(|f| f.available).sum();
657 let total_itotal: u64 = filesystems.iter().map(|f| f.itotal).sum();
658 let total_iused: u64 = filesystems.iter().map(|f| f.iused).sum();
659 let total_iavail: u64 = filesystems.iter().map(|f| f.iavail).sum();
660
661 let use_pct = {
662 let denom = total_used + total_avail;
663 if denom == 0 {
664 0.0
665 } else {
666 (total_used as f64 / denom as f64) * 100.0
667 }
668 };
669 let iuse_pct = if total_itotal == 0 {
670 0.0
671 } else {
672 (total_iused as f64 / total_itotal as f64) * 100.0
673 };
674
675 if config.inodes {
676 vec![
677 "total".to_string(),
678 format!("{}", total_itotal),
679 format!("{}", total_iused),
680 format!("{}", total_iavail),
681 format_percent(iuse_pct),
682 "-".to_string(),
683 ]
684 } else if config.print_type {
685 vec![
686 "total".to_string(),
687 "-".to_string(),
688 format_size(total_size, config),
689 format_size(total_used, config),
690 format_size(total_avail, config),
691 format_percent(use_pct),
692 "-".to_string(),
693 ]
694 } else {
695 vec![
696 "total".to_string(),
697 format_size(total_size, config),
698 format_size(total_used, config),
699 format_size(total_avail, config),
700 format_percent(use_pct),
701 "-".to_string(),
702 ]
703 }
704}
705
706enum ColAlign {
708 Left,
709 Right,
710 None, }
712
713fn get_col_alignments(config: &DfConfig, num_cols: usize) -> Vec<ColAlign> {
715 if num_cols == 0 {
716 return vec![];
717 }
718 let mut aligns = Vec::with_capacity(num_cols);
719
720 const NUMERIC_OUTPUT_FIELDS: &[&str] = &[
722 "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
723 ];
724 if config.output_fields.is_some() {
725 aligns.push(ColAlign::Left);
728 for _ in 1..num_cols.saturating_sub(1) {
729 aligns.push(ColAlign::Right);
730 }
731 if num_cols > 1 {
732 let last_field = config
733 .output_fields
734 .as_ref()
735 .and_then(|f| f.last())
736 .map(|s| s.as_str())
737 .unwrap_or("");
738 if NUMERIC_OUTPUT_FIELDS.contains(&last_field) {
739 aligns.push(ColAlign::Right);
740 } else {
741 aligns.push(ColAlign::None);
742 }
743 }
744 } else if config.print_type {
745 aligns.push(ColAlign::Left);
747 aligns.push(ColAlign::Left);
748 for _ in 2..num_cols.saturating_sub(1) {
749 aligns.push(ColAlign::Right);
750 }
751 if num_cols > 2 {
752 aligns.push(ColAlign::None);
753 }
754 } else {
755 aligns.push(ColAlign::Left);
757 for _ in 1..num_cols.saturating_sub(1) {
758 aligns.push(ColAlign::Right);
759 }
760 if num_cols > 1 {
761 aligns.push(ColAlign::None);
762 }
763 }
764
765 aligns
766}
767
768fn compute_widths(header: &[String], rows: &[Vec<String>], config: &DfConfig) -> Vec<usize> {
770 let num_cols = header.len();
771 let mut widths = vec![0usize; num_cols];
772 for (i, h) in header.iter().enumerate() {
773 widths[i] = widths[i].max(h.len());
774 }
775 for row in rows {
776 for (i, val) in row.iter().enumerate() {
777 if i < num_cols {
778 widths[i] = widths[i].max(val.len());
779 }
780 }
781 }
782
783 if let Some(ref fields) = config.output_fields {
787 for (i, field) in fields.iter().enumerate() {
788 if i >= num_cols {
789 break;
790 }
791 match field.as_str() {
792 "source" => widths[i] = widths[i].max(14),
793 "size" | "used" | "avail" | "itotal" | "iused" | "iavail" => {
794 widths[i] = widths[i].max(5);
795 }
796 _ => {}
797 }
798 }
799 } else if !widths.is_empty() {
800 widths[0] = widths[0].max(14);
801 let start_col = if config.print_type { 2 } else { 1 };
802 for i in start_col..start_col + 3 {
803 if i < num_cols {
804 widths[i] = widths[i].max(5);
805 }
806 }
807 }
808
809 widths
810}
811
812pub(crate) fn print_table(
814 header: &[String],
815 rows: &[Vec<String>],
816 config: &DfConfig,
817 out: &mut impl Write,
818) -> io::Result<()> {
819 let num_cols = header.len();
820 if num_cols == 0 {
821 return Ok(());
822 }
823
824 let widths = compute_widths(header, rows, config);
825 let aligns = get_col_alignments(config, num_cols);
826
827 print_row(header, &widths, &aligns, out)?;
828 for row in rows {
829 print_row(row, &widths, &aligns, out)?;
830 }
831
832 Ok(())
833}
834
835fn print_row(
837 row: &[String],
838 widths: &[usize],
839 aligns: &[ColAlign],
840 out: &mut impl Write,
841) -> io::Result<()> {
842 let num_cols = widths.len();
843 for (i, val) in row.iter().enumerate() {
844 if i < num_cols {
845 if i > 0 {
846 write!(out, " ")?;
847 }
848 let w = widths[i];
849 match aligns.get(i).unwrap_or(&ColAlign::Right) {
850 ColAlign::Left => write!(out, "{:<width$}", val, width = w)?,
851 ColAlign::Right => write!(out, "{:>width$}", val, width = w)?,
852 ColAlign::None => write!(out, "{}", val)?,
853 }
854 }
855 }
856 writeln!(out)?;
857 Ok(())
858}
859
860#[cfg(test)]
861#[allow(dead_code)]
862pub(crate) fn print_header(config: &DfConfig, out: &mut impl Write) -> io::Result<()> {
863 let header = build_header_row(config);
864 let widths = compute_widths(&header, &[], config);
865 let aligns = get_col_alignments(config, header.len());
866 print_row(&header, &widths, &aligns, out)
867}
868
869#[cfg(test)]
870#[allow(dead_code)]
871pub(crate) fn print_fs_line(
872 info: &FsInfo,
873 config: &DfConfig,
874 out: &mut impl Write,
875) -> io::Result<()> {
876 let header = build_header_row(config);
877 let row = build_row(info, config);
878 let rows = [row];
879 let widths = compute_widths(&header, &rows, config);
880 let aligns = get_col_alignments(config, header.len());
881 print_row(&rows[0], &widths, &aligns, out)
882}
883
884#[cfg(test)]
885#[allow(dead_code)]
886pub(crate) fn print_total_line(
887 filesystems: &[FsInfo],
888 config: &DfConfig,
889 out: &mut impl Write,
890) -> io::Result<()> {
891 let header = build_header_row(config);
892 let row = build_total_row(filesystems, config);
893 let rows = [row];
894 let widths = compute_widths(&header, &rows, config);
895 let aligns = get_col_alignments(config, header.len());
896 print_row(&rows[0], &widths, &aligns, out)
897}
898
899pub fn run_df(config: &DfConfig) -> i32 {
901 let stdout = io::stdout();
902 let mut out = io::BufWriter::new(stdout.lock());
903
904 let (filesystems, had_error) = get_filesystems(config);
905
906 let header = build_header_row(config);
907 let mut rows: Vec<Vec<String>> = Vec::new();
908 for info in &filesystems {
909 rows.push(build_row(info, config));
910 }
911
912 if config.total {
913 rows.push(build_total_row(&filesystems, config));
914 }
915
916 if let Err(e) = print_table(&header, &rows, config, &mut out) {
917 if e.kind() == io::ErrorKind::BrokenPipe {
918 return 0;
919 }
920 eprintln!("df: write error: {}", e);
921 return 1;
922 }
923
924 let _ = out.flush();
925 if had_error { 1 } else { 0 }
926}