Skip to main content

coreutils_rs/df/
core.rs

1use std::collections::HashSet;
2use std::io::{self, Write};
3
4// ──────────────────────────────────────────────────
5// Configuration
6// ──────────────────────────────────────────────────
7
8/// Configuration for the df command.
9pub 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
47// ──────────────────────────────────────────────────
48// Mount entry and filesystem info
49// ──────────────────────────────────────────────────
50
51/// A parsed mount entry from /proc/mounts.
52pub struct MountEntry {
53    pub source: String,
54    pub target: String,
55    pub fstype: String,
56}
57
58/// Filesystem information after calling statvfs.
59pub 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
73// Remote filesystem types that should be excluded with --local.
74const 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
90// Pseudo filesystem types filtered out unless --all is given.
91const 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
117// ──────────────────────────────────────────────────
118// Reading mount entries
119// ──────────────────────────────────────────────────
120
121/// Read mount entries from /proc/mounts (falls back to /etc/mtab).
122pub 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
143/// Unescape octal sequences like \040 (space) in mount paths.
144fn 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// ──────────────────────────────────────────────────
170// Calling statvfs
171// ──────────────────────────────────────────────────
172
173/// Call statvfs(2) on a path and return filesystem info.
174#[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        // GNU df calculates use% as: used / (used + available) * 100
198        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
235// ──────────────────────────────────────────────────
236// Finding filesystem for a specific file
237// ──────────────────────────────────────────────────
238
239/// Find the mount entry for a given file path by finding the longest
240/// matching mount target prefix.
241fn 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
262// ──────────────────────────────────────────────────
263// Getting filesystem info
264// ──────────────────────────────────────────────────
265
266/// Determine whether a filesystem type is remote.
267fn is_remote(fstype: &str) -> bool {
268    REMOTE_FS_TYPES.contains(&fstype)
269}
270
271/// Determine whether a filesystem type is pseudo.
272fn is_pseudo(fstype: &str) -> bool {
273    PSEUDO_FS_TYPES.contains(&fstype)
274}
275
276/// Get filesystem info for all relevant mount points.
277/// Returns (filesystems, had_error) where had_error is true if any file was not found.
278pub fn get_filesystems(config: &DfConfig) -> (Vec<FsInfo>, bool) {
279    let mounts = read_mounts();
280    let mut had_error = false;
281
282    // If specific files are given, find their mount points.
283    if !config.files.is_empty() {
284        let mut result = Vec::new();
285        // GNU df does NOT deduplicate when specific files are given.
286        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        // Filter by type.
307        if !config.type_filter.is_empty() && !config.type_filter.contains(&mount.fstype) {
308            continue;
309        }
310
311        // Exclude by type.
312        if config.exclude_type.contains(&mount.fstype) {
313            continue;
314        }
315
316        // Skip remote filesystems if --local.
317        if config.local_only && is_remote(&mount.fstype) {
318            continue;
319        }
320
321        // Skip pseudo filesystems unless --all.
322        if !config.all && is_pseudo(&mount.fstype) {
323            continue;
324        }
325
326        // Skip duplicate sources unless --all (keep last mount for a given device).
327        if !config.all {
328            if mount.source == "none" || mount.source == "tmpfs" || mount.source == "devtmpfs" {
329                // Allow these through; filter by fstype instead of source.
330            } else if !seen_sources.insert(mount.source.clone()) {
331                continue;
332            }
333        }
334
335        if let Some(info) = statvfs_info(mount) {
336            // Without --all, skip filesystems with 0 total blocks (pseudo/virtual).
337            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
347// ──────────────────────────────────────────────────
348// Size formatting
349// ──────────────────────────────────────────────────
350
351/// Format a byte count in human-readable form using powers of 1024.
352pub 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
370/// Format a byte count in human-readable form using powers of 1000.
371pub 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
389/// Format a size value according to the config.
390pub 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
400/// Format a percentage for display.
401fn format_percent(pct: f64) -> String {
402    if pct == 0.0 {
403        return "0%".to_string();
404    }
405    // GNU df rounds up: ceil the percentage.
406    let rounded = pct.ceil() as u64;
407    format!("{}%", rounded)
408}
409
410// ──────────────────────────────────────────────────
411// Block size parsing
412// ──────────────────────────────────────────────────
413
414/// Parse a block size string like "1K", "1M", "1G", etc.
415pub 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    // Check for leading apostrophe (thousands grouping) - just strip it.
422    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
457// ──────────────────────────────────────────────────
458// Valid output field names
459// ──────────────────────────────────────────────────
460
461/// Valid field names for --output.
462pub const VALID_OUTPUT_FIELDS: &[&str] = &[
463    "source", "fstype", "itotal", "iused", "iavail", "ipcent", "size", "used", "avail", "pcent",
464    "file", "target",
465];
466
467/// Parse the --output field list.
468pub 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
478// ──────────────────────────────────────────────────
479// Output formatting (GNU-compatible auto-sized columns)
480// ──────────────────────────────────────────────────
481
482/// Determine the size column header.
483fn 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
497/// Build a row of string values for a filesystem entry.
498fn 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
551/// Build the header row.
552fn 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
623/// Build a total row.
624fn 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
677/// Column alignment type.
678enum ColAlign {
679    Left,
680    Right,
681    None, // last column, no padding
682}
683
684/// Get column alignment for the standard df output.
685fn 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        // For --output, first column is left-aligned, rest right-aligned, last is no-pad.
693        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        // Filesystem(left) Type(left) Size(right) Used(right) Avail(right) Use%(right) Mounted(none)
702        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        // Filesystem(left) numeric(right)... last(none)
712        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
724/// Print all rows with auto-sized columns, matching GNU df output format.
725fn 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    // Compute column widths from header and all data rows.
737    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 header.
752    print_row(header, &widths, &aligns, out)?;
753
754    // Print data rows.
755    for row in rows {
756        print_row(row, &widths, &aligns, out)?;
757    }
758
759    Ok(())
760}
761
762/// Print a single row with the given column widths and alignments.
763fn 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
787/// Print the df output header (for backward compat with tests).
788pub fn print_header(config: &DfConfig, out: &mut impl Write) -> io::Result<()> {
789    let header = build_header_row(config);
790    // Use minimal widths from just the header itself.
791    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
796/// Print a single filesystem info line (for backward compat with tests).
797pub 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    // Compute widths from both header and this single row.
801    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
815/// Print the total line (for backward compat with tests).
816pub 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
837/// Run the df command and write output.
838pub 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}