Skip to main content

uu_df/
table.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5// spell-checker:ignore tmpfs Pcent Itotal Iused Iavail Ipcent nosuid nodev
6//! The filesystem usage data table.
7//!
8//! A table ([`Table`]) comprises a header row ([`Header`]) and a
9//! collection of data rows ([`Row`]), one per filesystem.
10use unicode_width::UnicodeWidthStr;
11
12use crate::blocks::{SuffixType, to_magnitude_and_suffix};
13use crate::columns::{Alignment, Column};
14use crate::filesystem::Filesystem;
15use crate::{BlockSize, Options};
16use uucore::fsext::{FsUsage, MountInfo};
17use uucore::translate;
18
19use std::ffi::OsString;
20use std::iter;
21use std::ops::{Add, AddAssign};
22
23/// A row in the filesystem usage data table.
24///
25/// A row comprises several pieces of information, including the
26/// filesystem device, the mountpoint, the number of bytes used, etc.
27pub(crate) struct Row {
28    /// The filename given on the command-line, if given.
29    file: Option<OsString>,
30
31    /// Name of the device on which the filesystem lives.
32    fs_device: String,
33
34    /// Type of filesystem (for example, `"ext4"`, `"tmpfs"`, etc.).
35    fs_type: String,
36
37    /// Path at which the filesystem is mounted.
38    fs_mount: OsString,
39
40    /// Total number of bytes in the filesystem regardless of whether they are used.
41    bytes: BytesCell,
42
43    /// Number of used bytes.
44    bytes_used: BytesCell,
45
46    /// Number of available bytes.
47    bytes_avail: BytesCell,
48
49    /// Percentage of bytes that are used, given as a float between 0 and 1.
50    ///
51    /// If the filesystem has zero bytes, then this is `None`.
52    bytes_usage: Option<f64>,
53
54    /// Percentage of bytes that are available, given as a float between 0 and 1.
55    ///
56    /// These are the bytes that are available to non-privileged processes.
57    ///
58    /// If the filesystem has zero bytes, then this is `None`.
59    #[cfg(target_os = "macos")]
60    bytes_capacity: Option<f64>,
61
62    /// Total number of inodes in the filesystem.
63    inodes: u128,
64
65    /// Number of used inodes.
66    inodes_used: u128,
67
68    /// Number of free inodes.
69    inodes_free: u128,
70
71    /// Percentage of inodes that are used, given as a float between 0 and 1.
72    ///
73    /// If the filesystem has zero bytes, then this is `None`.
74    inodes_usage: Option<f64>,
75}
76
77impl Row {
78    pub(crate) fn new(source: &str) -> Self {
79        Self {
80            file: None,
81            fs_device: source.into(),
82            fs_type: "-".into(),
83            fs_mount: "-".into(),
84            bytes: BytesCell::default(),
85            bytes_used: BytesCell::default(),
86            bytes_avail: BytesCell::default(),
87            bytes_usage: None,
88            #[cfg(target_os = "macos")]
89            bytes_capacity: None,
90            inodes: 0,
91            inodes_used: 0,
92            inodes_free: 0,
93            inodes_usage: None,
94        }
95    }
96}
97
98impl AddAssign for Row {
99    /// Sum the numeric values of two rows.
100    ///
101    /// The `Row::fs_device` field is set to `"total"` and the
102    /// remaining `String` fields are set to `"-"`.
103    fn add_assign(&mut self, rhs: Self) {
104        let bytes = self.bytes + rhs.bytes;
105        let bytes_used = self.bytes_used + rhs.bytes_used;
106        let bytes_avail = self.bytes_avail + rhs.bytes_avail;
107        let inodes = self.inodes + rhs.inodes;
108        let inodes_used = self.inodes_used + rhs.inodes_used;
109        *self = Self {
110            file: None,
111            fs_device: translate!("df-total"),
112            fs_type: "-".into(),
113            fs_mount: "-".into(),
114            bytes,
115            bytes_used,
116            bytes_avail,
117            bytes_usage: if bytes.bytes == 0 {
118                None
119            } else {
120                // We use "(bytes_used + bytes_avail)" instead of "bytes" because on some filesystems (e.g.
121                // ext4) "bytes" also includes reserved blocks we ignore for the usage calculation.
122                // https://www.gnu.org/software/coreutils/faq/coreutils-faq.html#df-Size-and-Used-and-Available-do-not-add-up
123                Some(bytes_used.bytes as f64 / (bytes_used.bytes + bytes_avail.bytes) as f64)
124            },
125            // TODO Figure out how to compute this.
126            #[cfg(target_os = "macos")]
127            bytes_capacity: None,
128            inodes,
129            inodes_used,
130            inodes_free: self.inodes_free + rhs.inodes_free,
131            inodes_usage: if inodes == 0 {
132                None
133            } else {
134                Some(inodes_used as f64 / inodes as f64)
135            },
136        }
137    }
138}
139
140impl Row {
141    fn from_filesystem(fs: Filesystem, row_block_size: &BlockSize) -> Self {
142        let MountInfo {
143            dev_name,
144            fs_type,
145            mount_dir,
146            ..
147        } = fs.mount_info;
148        let FsUsage {
149            blocksize,
150            blocks,
151            bfree,
152            bavail,
153            files,
154            ffree,
155            ..
156        } = fs.usage;
157
158        // On Windows WSL, files can be less than ffree. Protect such cases via saturating_sub.
159        let bused = blocks.saturating_sub(bfree);
160        let fused = files.saturating_sub(ffree);
161        Self {
162            file: fs.file,
163            fs_device: dev_name,
164            fs_type,
165            fs_mount: mount_dir,
166            bytes: BytesCell::new(blocks * blocksize, row_block_size),
167            bytes_used: BytesCell::new(bused * blocksize, row_block_size),
168            bytes_avail: BytesCell::new(bavail * blocksize, row_block_size),
169            bytes_usage: if blocks == 0 {
170                None
171            } else {
172                // We use "(bused + bavail)" instead of "blocks" because on some filesystems (e.g.
173                // ext4) "blocks" also includes reserved blocks we ignore for the usage calculation.
174                // https://www.gnu.org/software/coreutils/faq/coreutils-faq.html#df-Size-and-Used-and-Available-do-not-add-up
175                Some(bused as f64 / (bused + bavail) as f64)
176            },
177            #[cfg(target_os = "macos")]
178            bytes_capacity: if bavail == 0 {
179                None
180            } else {
181                Some(bavail as f64 / ((bused + bavail) as f64))
182            },
183            inodes: files as u128,
184            inodes_used: fused as u128,
185            inodes_free: ffree as u128,
186            inodes_usage: if files == 0 {
187                None
188            } else {
189                Some(fused as f64 / files as f64)
190            },
191        }
192    }
193}
194
195#[derive(Debug, Copy, Clone)]
196struct BytesCell {
197    bytes: u64,
198    scaled: u64,
199}
200
201/// A bytes column in the filesystem usage data table.
202///
203/// This is used to keep track of the scaled values to properly compute
204/// the total values.
205impl Default for BytesCell {
206    fn default() -> Self {
207        Self {
208            bytes: 0,
209            scaled: 0,
210        }
211    }
212}
213
214impl BytesCell {
215    fn new(bytes: u64, block_size: &BlockSize) -> Self {
216        Self {
217            bytes,
218            scaled: {
219                let BlockSize::Bytes(d) = block_size;
220                (bytes as f64 / *d as f64).ceil() as u64
221            },
222        }
223    }
224}
225
226impl Add for BytesCell {
227    type Output = Self;
228
229    fn add(self, rhs: Self) -> Self {
230        Self {
231            bytes: self.bytes + rhs.bytes,
232            scaled: self.scaled + rhs.scaled,
233        }
234    }
235}
236
237/// A `Cell` in the table. We store raw `bytes` as the data (e.g. directory name
238/// may be non-Unicode). We also record the printed `width` for alignment purpose,
239/// as it is easier to compute on the original string.
240struct Cell {
241    bytes: Vec<u8>,
242    width: usize,
243}
244
245impl Cell {
246    /// Create a cell, knowing that s contains only 1-length chars
247    fn from_ascii_string<T: AsRef<str>>(s: T) -> Self {
248        let s = s.as_ref();
249        Self {
250            bytes: s.as_bytes().into(),
251            width: s.len(),
252        }
253    }
254
255    /// Create a cell from an unknown origin string that may contain
256    /// wide characters.
257    fn from_string<T: AsRef<str>>(s: T) -> Self {
258        let s = s.as_ref();
259        Self {
260            bytes: s.as_bytes().into(),
261            width: UnicodeWidthStr::width(s),
262        }
263    }
264
265    /// Create a cell from an `OsString`
266    fn from_os_string(os: &OsString) -> Self {
267        Self {
268            bytes: uucore::os_str_as_bytes(os).unwrap().to_vec(),
269            width: UnicodeWidthStr::width(os.to_string_lossy().as_ref()),
270        }
271    }
272}
273
274/// A formatter for [`Row`].
275///
276/// The `options` control how the information in the row gets formatted.
277pub(crate) struct RowFormatter<'a> {
278    /// The data in this row.
279    row: &'a Row,
280
281    /// Options that control how to format the data.
282    options: &'a Options,
283    // TODO We don't need all of the command-line options here. Some
284    // of the command-line options indicate which rows to include or
285    // exclude. Other command-line options indicate which columns to
286    // include or exclude. Still other options indicate how to format
287    // numbers. We could split the options up into those groups to
288    // reduce the coupling between this `table.rs` module and the main
289    // `df.rs` module.
290    /// Whether to use the special rules for displaying the total row.
291    is_total_row: bool,
292}
293
294impl<'a> RowFormatter<'a> {
295    /// Instantiate this struct.
296    pub(crate) fn new(row: &'a Row, options: &'a Options, is_total_row: bool) -> Self {
297        Self {
298            row,
299            options,
300            is_total_row,
301        }
302    }
303
304    /// Get a string giving the scaled version of the input number.
305    ///
306    /// The scaling factor is defined in the `options` field.
307    fn scaled_bytes(&self, bytes_column: &BytesCell) -> Cell {
308        let size = bytes_column.scaled;
309        let s = if let Some(h) = self.options.human_readable {
310            let size = if self.is_total_row {
311                let BlockSize::Bytes(d) = self.options.block_size;
312                d * size
313            } else {
314                bytes_column.bytes
315            };
316            to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h), true)
317        } else {
318            size.to_string()
319        };
320        Cell::from_ascii_string(s)
321    }
322
323    /// Get a string giving the scaled version of the input number.
324    ///
325    /// The scaling factor is defined in the `options` field.
326    fn scaled_inodes(&self, size: u128) -> Cell {
327        let s = if let Some(h) = self.options.human_readable {
328            to_magnitude_and_suffix(size, SuffixType::HumanReadable(h), true)
329        } else {
330            size.to_string()
331        };
332        Cell::from_ascii_string(s)
333    }
334
335    /// Convert a float between 0 and 1 into a percentage string.
336    ///
337    /// If `None`, return the string `"-"` instead.
338    fn percentage(fraction: Option<f64>) -> Cell {
339        let s = match fraction {
340            None => "-".to_string(),
341            Some(x) => format!("{:.0}%", (100.0 * x).ceil()),
342        };
343        Cell::from_ascii_string(s)
344    }
345
346    /// Returns formatted row data.
347    fn get_cells(&self) -> Vec<Cell> {
348        let mut cells = Vec::new();
349
350        for column in &self.options.columns {
351            let cell = match column {
352                Column::Source => {
353                    if self.is_total_row {
354                        Cell::from_string(translate!("df-total"))
355                    } else {
356                        Cell::from_string(&self.row.fs_device)
357                    }
358                }
359                Column::Size => self.scaled_bytes(&self.row.bytes),
360                Column::Used => self.scaled_bytes(&self.row.bytes_used),
361                Column::Avail => self.scaled_bytes(&self.row.bytes_avail),
362                Column::Pcent => Self::percentage(self.row.bytes_usage),
363
364                Column::Target => {
365                    if self.is_total_row && !self.options.columns.contains(&Column::Source) {
366                        Cell::from_string(translate!("df-total"))
367                    } else {
368                        Cell::from_os_string(&self.row.fs_mount)
369                    }
370                }
371                Column::Itotal => self.scaled_inodes(self.row.inodes),
372                Column::Iused => self.scaled_inodes(self.row.inodes_used),
373                Column::Iavail => self.scaled_inodes(self.row.inodes_free),
374                Column::Ipcent => Self::percentage(self.row.inodes_usage),
375                Column::File => self
376                    .row
377                    .file
378                    .as_ref()
379                    .map_or(Cell::from_ascii_string("-"), Cell::from_os_string),
380
381                Column::Fstype => Cell::from_string(&self.row.fs_type),
382                #[cfg(target_os = "macos")]
383                Column::Capacity => Self::percentage(self.row.bytes_capacity),
384            };
385
386            cells.push(cell);
387        }
388
389        cells
390    }
391}
392
393/// A `HeaderMode` defines what header labels should be shown.
394#[derive(Default)]
395pub(crate) enum HeaderMode {
396    #[default]
397    Default,
398    // the user used -h or -H
399    HumanReadable,
400    // the user used -P
401    PosixPortability,
402    // the user used --output
403    Output,
404}
405
406/// The data of the header row.
407struct Header {}
408
409impl Header {
410    /// Return the headers for the specified columns.
411    ///
412    /// The `options` control which column headers are returned.
413    fn get_headers(options: &Options) -> Vec<String> {
414        let mut headers = Vec::new();
415
416        for column in &options.columns {
417            let header = match column {
418                Column::Source => translate!("df-header-filesystem"),
419                Column::Size => match options.header_mode {
420                    HeaderMode::HumanReadable => translate!("df-header-size"),
421                    HeaderMode::PosixPortability => {
422                        format!(
423                            "{}{}",
424                            options.block_size.as_u64(),
425                            translate!("df-blocks-suffix")
426                        )
427                    }
428                    _ => format!(
429                        "{}{}",
430                        options.block_size.to_header(),
431                        translate!("df-blocks-suffix")
432                    ),
433                },
434                Column::Used => translate!("df-header-used"),
435                Column::Avail => match options.header_mode {
436                    HeaderMode::HumanReadable | HeaderMode::Output => {
437                        translate!("df-header-avail")
438                    }
439                    _ => translate!("df-header-available"),
440                },
441                Column::Pcent => match options.header_mode {
442                    HeaderMode::PosixPortability => translate!("df-header-capacity"),
443                    _ => translate!("df-header-use-percent"),
444                },
445                Column::Target => translate!("df-header-mounted-on"),
446                Column::Itotal => translate!("df-header-inodes"),
447                Column::Iused => translate!("df-header-iused"),
448                Column::Iavail => translate!("df-header-iavail"),
449                Column::Ipcent => translate!("df-header-iuse-percent"),
450                Column::File => translate!("df-header-file"),
451                Column::Fstype => translate!("df-header-type"),
452                #[cfg(target_os = "macos")]
453                Column::Capacity => translate!("df-header-capacity"),
454            };
455
456            headers.push(header);
457        }
458
459        headers
460    }
461}
462
463/// The output table.
464pub(crate) struct Table {
465    alignments: Vec<Alignment>,
466    rows: Vec<Vec<Cell>>,
467    widths: Vec<usize>,
468}
469
470impl Table {
471    pub(crate) fn new(options: &Options, filesystems: Vec<Filesystem>) -> Self {
472        let headers = Header::get_headers(options);
473        let mut widths: Vec<_> = options
474            .columns
475            .iter()
476            .enumerate()
477            .map(|(i, col)| col.min_width().max(headers[i].len()))
478            .collect();
479
480        let mut rows = vec![headers.iter().map(Cell::from_string).collect()];
481
482        // The running total of filesystem sizes and usage.
483        //
484        // This accumulator is computed in case we need to display the
485        // total counts in the last row of the table.
486        let mut total = Row::new(&translate!("df-total"));
487
488        for filesystem in filesystems {
489            // If the filesystem is not empty, or if the options require
490            // showing all filesystems, then print the data as a row in
491            // the output table.
492            if options.show_all_fs || filesystem.usage.blocks > 0 {
493                let row = Row::from_filesystem(filesystem, &options.block_size);
494                let fmt = RowFormatter::new(&row, options, false);
495                let values = fmt.get_cells();
496                if options.show_total {
497                    total += row;
498                }
499
500                rows.push(values);
501            }
502        }
503
504        if options.show_total {
505            let total_row = RowFormatter::new(&total, options, true);
506            rows.push(total_row.get_cells());
507        }
508
509        // extend the column widths (in chars) for long values in rows
510        // do it here, after total row was added to the list of rows
511        for row in &rows {
512            for (i, value) in row.iter().enumerate() {
513                if value.width > widths[i] {
514                    widths[i] = value.width;
515                }
516            }
517        }
518
519        Self {
520            rows,
521            widths,
522            alignments: Self::get_alignments(&options.columns),
523        }
524    }
525
526    fn get_alignments(columns: &Vec<Column>) -> Vec<Alignment> {
527        let mut alignments = Vec::new();
528
529        for column in columns {
530            alignments.push(column.alignment());
531        }
532
533        alignments
534    }
535
536    pub(crate) fn write_to(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
537        for row in &self.rows {
538            let mut col_iter = row.iter().enumerate().peekable();
539            while let Some((i, elem)) = col_iter.next() {
540                let is_last_col = col_iter.peek().is_none();
541
542                let pad_width = self.widths[i].saturating_sub(elem.width);
543                match self.alignments.get(i) {
544                    Some(Alignment::Left) => {
545                        writer.write_all(&elem.bytes)?;
546                        // no trailing spaces in last column
547                        if !is_last_col {
548                            writer
549                                .write_all(&iter::repeat_n(b' ', pad_width).collect::<Vec<_>>())?;
550                        }
551                    }
552                    Some(Alignment::Right) => {
553                        writer.write_all(&iter::repeat_n(b' ', pad_width).collect::<Vec<_>>())?;
554                        writer.write_all(&elem.bytes)?;
555                    }
556                    None => break,
557                }
558
559                if !is_last_col {
560                    // column separator
561                    writer.write_all(b" ")?;
562                }
563            }
564
565            writeln!(writer)?;
566        }
567
568        Ok(())
569    }
570}
571
572#[cfg(test)]
573mod tests {
574
575    use std::vec;
576    use uucore::locale::setup_localization;
577
578    use crate::blocks::HumanReadable;
579    use crate::columns::Column;
580    use crate::table::{BytesCell, Cell, Header, HeaderMode, Row, RowFormatter, Table};
581    use crate::{BlockSize, Options};
582
583    fn init() {
584        unsafe {
585            std::env::set_var("LANG", "C");
586        }
587        let _ = setup_localization("df");
588    }
589
590    const COLUMNS_WITH_FS_TYPE: [Column; 7] = [
591        Column::Source,
592        Column::Fstype,
593        Column::Size,
594        Column::Used,
595        Column::Avail,
596        Column::Pcent,
597        Column::Target,
598    ];
599    const COLUMNS_WITH_INODES: [Column; 6] = [
600        Column::Source,
601        Column::Itotal,
602        Column::Iused,
603        Column::Iavail,
604        Column::Ipcent,
605        Column::Target,
606    ];
607
608    impl Default for Row {
609        fn default() -> Self {
610            Self {
611                file: Some("/path/to/file".into()),
612                fs_device: "my_device".to_string(),
613                fs_type: "my_type".to_string(),
614                fs_mount: "my_mount".into(),
615
616                bytes: BytesCell::new(100, &BlockSize::Bytes(1)),
617                bytes_used: BytesCell::new(25, &BlockSize::Bytes(1)),
618                bytes_avail: BytesCell::new(75, &BlockSize::Bytes(1)),
619                bytes_usage: Some(0.25),
620
621                #[cfg(target_os = "macos")]
622                bytes_capacity: Some(0.5),
623
624                inodes: 10,
625                inodes_used: 2,
626                inodes_free: 8,
627                inodes_usage: Some(0.2),
628            }
629        }
630    }
631
632    #[test]
633    fn test_default_header() {
634        init();
635        let options = Options::default();
636
637        assert_eq!(
638            Header::get_headers(&options),
639            vec!(
640                "Filesystem",
641                "1K-blocks",
642                "Used",
643                "Available",
644                "Use%",
645                "Mounted on"
646            )
647        );
648    }
649
650    #[test]
651    fn test_header_with_fs_type() {
652        init();
653        let options = Options {
654            columns: COLUMNS_WITH_FS_TYPE.to_vec(),
655            ..Default::default()
656        };
657        assert_eq!(
658            Header::get_headers(&options),
659            vec!(
660                "Filesystem",
661                "Type",
662                "1K-blocks",
663                "Used",
664                "Available",
665                "Use%",
666                "Mounted on"
667            )
668        );
669    }
670
671    #[test]
672    fn test_header_with_inodes() {
673        init();
674        let options = Options {
675            columns: COLUMNS_WITH_INODES.to_vec(),
676            ..Default::default()
677        };
678        assert_eq!(
679            Header::get_headers(&options),
680            vec!(
681                "Filesystem",
682                "Inodes",
683                "IUsed",
684                "IFree",
685                "IUse%",
686                "Mounted on"
687            )
688        );
689    }
690
691    #[test]
692    fn test_header_with_block_size_1024() {
693        init();
694        let options = Options {
695            block_size: BlockSize::Bytes(3 * 1024),
696            ..Default::default()
697        };
698        assert_eq!(
699            Header::get_headers(&options),
700            vec!(
701                "Filesystem",
702                "3K-blocks",
703                "Used",
704                "Available",
705                "Use%",
706                "Mounted on"
707            )
708        );
709    }
710
711    #[test]
712    fn test_human_readable_header() {
713        init();
714        let options = Options {
715            header_mode: HeaderMode::HumanReadable,
716            ..Default::default()
717        };
718        assert_eq!(
719            Header::get_headers(&options),
720            vec!("Filesystem", "Size", "Used", "Avail", "Use%", "Mounted on")
721        );
722    }
723
724    #[test]
725    fn test_posix_portability_header() {
726        init();
727        let options = Options {
728            header_mode: HeaderMode::PosixPortability,
729            ..Default::default()
730        };
731        assert_eq!(
732            Header::get_headers(&options),
733            vec!(
734                "Filesystem",
735                "1024-blocks",
736                "Used",
737                "Available",
738                "Capacity",
739                "Mounted on"
740            )
741        );
742    }
743
744    #[test]
745    fn test_output_header() {
746        init();
747        let options = Options {
748            header_mode: HeaderMode::Output,
749            ..Default::default()
750        };
751        assert_eq!(
752            Header::get_headers(&options),
753            vec!(
754                "Filesystem",
755                "1K-blocks",
756                "Used",
757                "Avail",
758                "Use%",
759                "Mounted on"
760            )
761        );
762    }
763
764    fn compare_cell_content(cells: Vec<Cell>, expected: Vec<&str>) -> bool {
765        cells
766            .into_iter()
767            .zip(expected)
768            .all(|(c, s)| c.bytes == s.as_bytes())
769    }
770
771    #[test]
772    fn test_row_formatter() {
773        init();
774        let options = Options {
775            block_size: BlockSize::Bytes(1),
776            ..Default::default()
777        };
778        let row = Row {
779            fs_device: "my_device".to_string(),
780            fs_mount: "my_mount".into(),
781
782            bytes: BytesCell::new(100, &BlockSize::Bytes(1)),
783            bytes_used: BytesCell::new(25, &BlockSize::Bytes(1)),
784            bytes_avail: BytesCell::new(75, &BlockSize::Bytes(1)),
785            bytes_usage: Some(0.25),
786
787            ..Default::default()
788        };
789        let fmt = RowFormatter::new(&row, &options, false);
790        assert!(compare_cell_content(
791            fmt.get_cells(),
792            vec!("my_device", "100", "25", "75", "25%", "my_mount")
793        ));
794    }
795
796    #[test]
797    fn test_row_formatter_with_fs_type() {
798        init();
799        let options = Options {
800            columns: COLUMNS_WITH_FS_TYPE.to_vec(),
801            block_size: BlockSize::Bytes(1),
802            ..Default::default()
803        };
804        let row = Row {
805            fs_device: "my_device".to_string(),
806            fs_type: "my_type".to_string(),
807            fs_mount: "my_mount".into(),
808
809            bytes: BytesCell::new(100, &BlockSize::Bytes(1)),
810            bytes_used: BytesCell::new(25, &BlockSize::Bytes(1)),
811            bytes_avail: BytesCell::new(75, &BlockSize::Bytes(1)),
812            bytes_usage: Some(0.25),
813
814            ..Default::default()
815        };
816        let fmt = RowFormatter::new(&row, &options, false);
817        assert!(compare_cell_content(
818            fmt.get_cells(),
819            vec!("my_device", "my_type", "100", "25", "75", "25%", "my_mount")
820        ));
821    }
822
823    #[test]
824    fn test_row_formatter_with_inodes() {
825        init();
826        let options = Options {
827            columns: COLUMNS_WITH_INODES.to_vec(),
828            block_size: BlockSize::Bytes(1),
829            ..Default::default()
830        };
831        let row = Row {
832            fs_device: "my_device".to_string(),
833            fs_mount: "my_mount".into(),
834
835            inodes: 10,
836            inodes_used: 2,
837            inodes_free: 8,
838            inodes_usage: Some(0.2),
839
840            ..Default::default()
841        };
842        let fmt = RowFormatter::new(&row, &options, false);
843        assert!(compare_cell_content(
844            fmt.get_cells(),
845            vec!("my_device", "10", "2", "8", "20%", "my_mount")
846        ));
847    }
848
849    #[test]
850    fn test_row_formatter_with_bytes_and_inodes() {
851        init();
852        let options = Options {
853            columns: vec![Column::Size, Column::Itotal],
854            block_size: BlockSize::Bytes(100),
855            ..Default::default()
856        };
857        let row = Row {
858            bytes: BytesCell::new(100, &BlockSize::Bytes(100)),
859            inodes: 10,
860            ..Default::default()
861        };
862        let fmt = RowFormatter::new(&row, &options, false);
863        assert!(compare_cell_content(fmt.get_cells(), vec!("1", "10")));
864    }
865
866    #[test]
867    fn test_row_formatter_with_human_readable_si() {
868        init();
869        let options = Options {
870            human_readable: Some(HumanReadable::Decimal),
871            columns: COLUMNS_WITH_FS_TYPE.to_vec(),
872            ..Default::default()
873        };
874        let row = Row {
875            fs_device: "my_device".to_string(),
876            fs_type: "my_type".to_string(),
877            fs_mount: "my_mount".into(),
878
879            bytes: BytesCell::new(40000, &BlockSize::default()),
880            bytes_used: BytesCell::new(1000, &BlockSize::default()),
881            bytes_avail: BytesCell::new(39000, &BlockSize::default()),
882            bytes_usage: Some(0.025),
883
884            ..Default::default()
885        };
886        let fmt = RowFormatter::new(&row, &options, false);
887        assert!(compare_cell_content(
888            fmt.get_cells(),
889            vec!(
890                "my_device",
891                "my_type",
892                "40k",
893                "1.0k",
894                "39k",
895                "3%",
896                "my_mount"
897            )
898        ));
899    }
900
901    #[test]
902    fn test_row_formatter_with_human_readable_binary() {
903        init();
904        let options = Options {
905            human_readable: Some(HumanReadable::Binary),
906            columns: COLUMNS_WITH_FS_TYPE.to_vec(),
907            ..Default::default()
908        };
909        let row = Row {
910            fs_device: "my_device".to_string(),
911            fs_type: "my_type".to_string(),
912            fs_mount: "my_mount".into(),
913
914            bytes: BytesCell::new(4096, &BlockSize::default()),
915            bytes_used: BytesCell::new(1024, &BlockSize::default()),
916            bytes_avail: BytesCell::new(3072, &BlockSize::default()),
917            bytes_usage: Some(0.25),
918
919            ..Default::default()
920        };
921        let fmt = RowFormatter::new(&row, &options, false);
922        assert!(compare_cell_content(
923            fmt.get_cells(),
924            vec!(
925                "my_device",
926                "my_type",
927                "4.0K",
928                "1.0K",
929                "3.0K",
930                "25%",
931                "my_mount"
932            )
933        ));
934    }
935
936    #[test]
937    fn test_row_formatter_with_round_up_usage() {
938        init();
939        let options = Options {
940            columns: vec![Column::Pcent],
941            ..Default::default()
942        };
943        let row = Row {
944            bytes_usage: Some(0.251),
945            ..Default::default()
946        };
947        let fmt = RowFormatter::new(&row, &options, false);
948        assert!(compare_cell_content(fmt.get_cells(), vec!("26%")));
949    }
950
951    #[test]
952    fn test_row_formatter_with_round_up_byte_values() {
953        init();
954        fn get_formatted_values(bytes: u64, bytes_used: u64, bytes_avail: u64) -> Vec<Cell> {
955            let options = Options {
956                block_size: BlockSize::Bytes(1000),
957                columns: vec![Column::Size, Column::Used, Column::Avail],
958                ..Default::default()
959            };
960
961            let row = Row {
962                bytes: BytesCell::new(bytes, &BlockSize::Bytes(1000)),
963                bytes_used: BytesCell::new(bytes_used, &BlockSize::Bytes(1000)),
964                bytes_avail: BytesCell::new(bytes_avail, &BlockSize::Bytes(1000)),
965                ..Default::default()
966            };
967            RowFormatter::new(&row, &options, false).get_cells()
968        }
969
970        assert!(compare_cell_content(
971            get_formatted_values(100, 100, 0),
972            vec!("1", "1", "0")
973        ));
974        assert!(compare_cell_content(
975            get_formatted_values(100, 99, 1),
976            vec!("1", "1", "1")
977        ));
978        assert!(compare_cell_content(
979            get_formatted_values(1000, 1000, 0),
980            vec!("1", "1", "0")
981        ));
982        assert!(compare_cell_content(
983            get_formatted_values(1001, 1000, 1),
984            vec!("2", "1", "1")
985        ));
986    }
987
988    #[test]
989    fn test_row_converter_with_invalid_numbers() {
990        init();
991        // copy from wsl linux
992        let d = crate::Filesystem {
993            file: None,
994            mount_info: crate::MountInfo {
995                dev_id: "28".to_string(),
996                dev_name: "none".to_string(),
997                fs_type: "9p".to_string(),
998                mount_dir: "/usr/lib/wsl/drivers".into(),
999                mount_option: "ro,nosuid,nodev,noatime".to_string(),
1000                mount_root: "/".into(),
1001                remote: false,
1002                dummy: false,
1003            },
1004            usage: crate::table::FsUsage {
1005                blocksize: 4096,
1006                blocks: 244_029_695,
1007                bfree: 125_085_030,
1008                bavail: 125_085_030,
1009                bavail_top_bit_set: false,
1010                files: 999,
1011                ffree: 1_000_000,
1012            },
1013        };
1014
1015        let row = Row::from_filesystem(d, &BlockSize::default());
1016
1017        assert_eq!(row.inodes_used, 0);
1018    }
1019
1020    #[test]
1021    fn test_table_column_width_computation_include_total_row() {
1022        init();
1023        let d1 = crate::Filesystem {
1024            file: None,
1025            mount_info: crate::MountInfo {
1026                dev_id: "28".to_string(),
1027                dev_name: "none".to_string(),
1028                fs_type: "9p".to_string(),
1029                mount_dir: "/usr/lib/wsl/drivers".into(),
1030                mount_option: "ro,nosuid,nodev,noatime".to_string(),
1031                mount_root: "/".into(),
1032                remote: false,
1033                dummy: false,
1034            },
1035            usage: crate::table::FsUsage {
1036                blocksize: 4096,
1037                blocks: 244_029_695,
1038                bfree: 125_085_030,
1039                bavail: 125_085_030,
1040                bavail_top_bit_set: false,
1041                files: 99_999_999_999,
1042                ffree: 999_999,
1043            },
1044        };
1045
1046        let filesystems = vec![d1.clone(), d1];
1047
1048        let mut options = Options {
1049            show_total: true,
1050            columns: vec![
1051                Column::Source,
1052                Column::Itotal,
1053                Column::Iused,
1054                Column::Iavail,
1055            ],
1056            ..Default::default()
1057        };
1058
1059        let table_w_total = Table::new(&options, filesystems.clone());
1060        let mut data_w_total: Vec<u8> = vec![];
1061        table_w_total
1062            .write_to(&mut data_w_total)
1063            .expect("Write error.");
1064        assert_eq!(
1065            String::from_utf8_lossy(&data_w_total),
1066            "Filesystem           Inodes        IUsed   IFree\n\
1067             none            99999999999  99999000000  999999\n\
1068             none            99999999999  99999000000  999999\n\
1069             total          199999999998 199998000000 1999998\n"
1070        );
1071
1072        options.show_total = false;
1073
1074        let table_w_o_total = Table::new(&options, filesystems);
1075        let mut data_w_o_total: Vec<u8> = vec![];
1076        table_w_o_total
1077            .write_to(&mut data_w_o_total)
1078            .expect("Write error.");
1079        assert_eq!(
1080            String::from_utf8_lossy(&data_w_o_total),
1081            "Filesystem          Inodes       IUsed  IFree\n\
1082             none           99999999999 99999000000 999999\n\
1083             none           99999999999 99999000000 999999\n"
1084        );
1085    }
1086
1087    #[cfg(unix)]
1088    #[test]
1089    fn test_table_column_width_non_unicode() {
1090        init();
1091        let bad_unicode_os_str = uucore::os_str_from_bytes(b"/usr/lib/w\xf3l/drivers")
1092            .expect("Only unix platforms can test non-unicode names")
1093            .to_os_string();
1094        let d1 = crate::Filesystem {
1095            file: None,
1096            mount_info: crate::MountInfo {
1097                dev_id: "28".to_string(),
1098                dev_name: "none".to_string(),
1099                fs_type: "9p".to_string(),
1100                mount_dir: bad_unicode_os_str,
1101                mount_option: "ro,nosuid,nodev,noatime".to_string(),
1102                mount_root: "/".into(),
1103                remote: false,
1104                dummy: false,
1105            },
1106            usage: crate::table::FsUsage {
1107                blocksize: 4096,
1108                blocks: 244_029_695,
1109                bfree: 125_085_030,
1110                bavail: 125_085_030,
1111                bavail_top_bit_set: false,
1112                files: 99_999_999_999,
1113                ffree: 999_999,
1114            },
1115        };
1116
1117        let filesystems = vec![d1];
1118
1119        let options = Options {
1120            show_total: false,
1121            columns: vec![Column::Source, Column::Target, Column::Itotal],
1122            ..Default::default()
1123        };
1124
1125        let table = Table::new(&options, filesystems.clone());
1126        let mut data: Vec<u8> = vec![];
1127        table.write_to(&mut data).expect("Write error.");
1128        assert_eq!(
1129            data,
1130            b"Filesystem     Mounted on                Inodes\n\
1131              none           /usr/lib/w\xf3l/drivers 99999999999\n",
1132            "Comparison failed, lossy data for reference:\n{}\n",
1133            String::from_utf8_lossy(&data)
1134        );
1135    }
1136
1137    #[test]
1138    fn test_row_accumulation_u64_overflow() {
1139        init();
1140        let total = u64::MAX as u128;
1141        let used1 = 3000u128;
1142        let used2 = 50000u128;
1143
1144        let mut row1 = Row {
1145            inodes: total,
1146            inodes_used: used1,
1147            inodes_free: total - used1,
1148            ..Default::default()
1149        };
1150
1151        let row2 = Row {
1152            inodes: total,
1153            inodes_used: used2,
1154            inodes_free: total - used2,
1155            ..Default::default()
1156        };
1157
1158        row1 += row2;
1159
1160        assert_eq!(row1.inodes, total * 2);
1161        assert_eq!(row1.inodes_used, used1 + used2);
1162        assert_eq!(row1.inodes_free, total * 2 - used1 - used2);
1163    }
1164}