From eaab79c60a16e5b6287bc060811b1788cc85153e Mon Sep 17 00:00:00 2001 From: Ulrich Hornung Date: Thu, 14 Mar 2024 14:25:58 +0100 Subject: [PATCH] switch from u64 to Optional to handle fs with large inodes nr --- src/uu/df/src/table.rs | 196 +++++++++++++++++++++++++++++++++++------ 1 file changed, 167 insertions(+), 29 deletions(-) diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs index f6e09420482..d5f4d9140de 100644 --- a/src/uu/df/src/table.rs +++ b/src/uu/df/src/table.rs @@ -18,10 +18,14 @@ use uucore::fsext::{FsUsage, MountInfo}; use std::fmt; use std::ops::AddAssign; +type InodesIntT = u128; +type MaybeInodesT = Option; + /// A row in the filesystem usage data table. /// /// A row comprises several pieces of information, including the /// filesystem device, the mountpoint, the number of bytes used, etc. +#[derive(Clone)] pub(crate) struct Row { /// The filename given on the command-line, if given. file: Option, @@ -58,13 +62,13 @@ pub(crate) struct Row { bytes_capacity: Option, /// Total number of inodes in the filesystem. - inodes: u64, + inodes: MaybeInodesT, /// Number of used inodes. - inodes_used: u64, + inodes_used: MaybeInodesT, /// Number of free inodes. - inodes_free: u64, + inodes_free: MaybeInodesT, /// Percentage of inodes that are used, given as a float between 0 and 1. /// @@ -85,14 +89,36 @@ impl Row { bytes_usage: None, #[cfg(target_os = "macos")] bytes_capacity: None, - inodes: 0, - inodes_used: 0, - inodes_free: 0, + inodes: Some(0), + inodes_used: Some(0), + inodes_free: Some(0), inodes_usage: None, } } } +fn checked_accumulation_op(accumulator: &MaybeInodesT, summand: &MaybeInodesT) -> MaybeInodesT { + let a = (*accumulator)?; + if let Some(s) = *summand { + if s > InodesIntT::MAX / 2 { + eprintln!("invalid inodes number ({s}) - filesystem will be ignored"); + *accumulator + } else { + a.checked_add(s) + } + } else { + *accumulator + } +} + +fn calc_inode_usage(inodes: MaybeInodesT, inodes_used: MaybeInodesT) -> Option { + if inodes? == 0 { + None + } else { + Some(inodes_used? as f64 / inodes? as f64) + } +} + impl AddAssign for Row { /// Sum the numeric values of two rows. /// @@ -102,8 +128,8 @@ impl AddAssign for Row { let bytes = self.bytes + rhs.bytes; let bytes_used = self.bytes_used + rhs.bytes_used; let bytes_avail = self.bytes_avail + rhs.bytes_avail; - let inodes = self.inodes + rhs.inodes; - let inodes_used = self.inodes_used + rhs.inodes_used; + let inodes = checked_accumulation_op(&self.inodes, &rhs.inodes); + let inodes_used = checked_accumulation_op(&self.inodes_used, &rhs.inodes_used); *self = Self { file: None, fs_device: "total".into(), @@ -125,12 +151,8 @@ impl AddAssign for Row { bytes_capacity: None, inodes, inodes_used, - inodes_free: self.inodes_free + rhs.inodes_free, - inodes_usage: if inodes == 0 { - None - } else { - Some(inodes_used as f64 / inodes as f64) - }, + inodes_free: checked_accumulation_op(&self.inodes_free, &rhs.inodes_free), + inodes_usage: calc_inode_usage(inodes, inodes_used), } } } @@ -178,9 +200,9 @@ impl From for Row { } else { Some(bavail as f64 / ((bused + bavail) as f64)) }, - inodes: files, - inodes_used: fused, - inodes_free: ffree, + inodes: Some(files as InodesIntT), + inodes_used: Some(fused as InodesIntT), + inodes_free: Some(ffree as InodesIntT), inodes_usage: if files == 0 { None } else { @@ -235,11 +257,15 @@ impl<'a> RowFormatter<'a> { /// Get a string giving the scaled version of the input number. /// /// The scaling factor is defined in the `options` field. - fn scaled_inodes(&self, size: u64) -> String { - if let Some(h) = self.options.human_readable { - to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h)) + fn scaled_inodes(&self, maybe_size: MaybeInodesT) -> String { + if let Some(size) = maybe_size { + if let Some(h) = self.options.human_readable { + to_magnitude_and_suffix(size, SuffixType::HumanReadable(h)) + } else { + size.to_string() + } } else { - size.to_string() + "int_overflow".into() } } @@ -505,9 +531,9 @@ mod tests { #[cfg(target_os = "macos")] bytes_capacity: Some(0.5), - inodes: 10, - inodes_used: 2, - inodes_free: 8, + inodes: Some(10), + inodes_used: Some(2), + inodes_free: Some(8), inodes_usage: Some(0.2), } } @@ -698,9 +724,9 @@ mod tests { fs_device: "my_device".to_string(), fs_mount: "my_mount".to_string(), - inodes: 10, - inodes_used: 2, - inodes_free: 8, + inodes: Some(10), + inodes_used: Some(2), + inodes_free: Some(8), inodes_usage: Some(0.2), ..Default::default() @@ -721,7 +747,7 @@ mod tests { }; let row = Row { bytes: 100, - inodes: 10, + inodes: Some(10), ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); @@ -846,6 +872,118 @@ mod tests { let row = Row::from(d); - assert_eq!(row.inodes_used, 0); + assert_eq!(row.inodes_used, Some(0)); + } + + #[test] + fn test_row_accumulation_u64_overflow() { + let total = u64::MAX as super::InodesIntT; + let used1 = 3000 as super::InodesIntT; + let used2 = 50000 as super::InodesIntT; + + let mut row1 = Row { + inodes: Some(total), + inodes_used: Some(used1), + inodes_free: Some(total - used1), + ..Default::default() + }; + + let row2 = Row { + inodes: Some(total), + inodes_used: Some(used2), + inodes_free: Some(total - used2), + ..Default::default() + }; + + row1 += row2; + + assert_eq!(row1.inodes, Some(total * 2)); + assert_eq!(row1.inodes_used, Some(used1 + used2)); + assert_eq!(row1.inodes_free, Some(total * 2 - used1 - used2)); + } + + #[test] + fn test_row_accumulation_close_to_u128_overflow() { + let total = u128::MAX as super::InodesIntT / 2 - 1; + let used1 = total - 50000; + let used2 = total - 100000; + + let mut row1 = Row { + inodes: Some(total), + inodes_used: Some(used1), + inodes_free: Some(total - used1), + ..Default::default() + }; + + let row2 = Row { + inodes: Some(total), + inodes_used: Some(used2), + inodes_free: Some(total - used2), + ..Default::default() + }; + + row1 += row2; + + assert_eq!(row1.inodes, Some(total * 2)); + assert_eq!(row1.inodes_used, Some(used1 + used2)); + assert_eq!(row1.inodes_free, Some(total * 2 - used1 - used2)); + } + + #[test] + fn test_row_accumulation_and_usage_close_over_u128_overflow() { + let total = u128::MAX as super::InodesIntT / 2 - 1; + let used1 = total / 2; + let free1 = total - used1; + let used2 = total / 2 - 10; + let free2 = total - used2; + + let mut row1 = Row { + inodes: Some(total), + inodes_used: Some(used1), + inodes_free: Some(free1), + ..Default::default() + }; + + let row2 = Row { + inodes: Some(total), + inodes_used: Some(used2), + inodes_free: Some(free2), + ..Default::default() + }; + + row1 += row2.clone(); + + assert_eq!(row1.inodes, Some(total * 2)); + assert_eq!(row1.inodes_used, Some(used1 + used2)); + assert_eq!(row1.inodes_free, Some(free1 + free2)); + assert_eq!(row1.inodes_usage, Some(0.5)); + + row1 += row2.clone(); + + assert_eq!(row1.inodes, None); // total * 3 + assert_eq!(row1.inodes_used, Some(used1 + used2 * 2)); + assert_eq!(row1.inodes_free, Some(free1 + free2 * 2)); + assert_eq!(row1.inodes_usage, None); + + row1 += row2.clone(); + + assert_eq!(row1.inodes, None); // total * 4 + assert_eq!(row1.inodes_used, Some(used1 + used2 * 3)); // used * 4 + assert_eq!(row1.inodes_free, None); // free * 4 + assert_eq!(row1.inodes_usage, None); + + row1 += row2.clone(); + + assert_eq!(row1.inodes, None); // total * 5 + assert_eq!(row1.inodes_used, None); // used * 5 + assert_eq!(row1.inodes_free, None); // free * 5 + assert_eq!(row1.inodes_usage, None); + + row1 += row2.clone(); + + assert_eq!(row1.inodes, None); // total * 6 + assert_eq!(row1.inodes_used, None); // used * 6 + assert_eq!(row1.inodes_free, None); // free * 6 + assert_eq!(row1.inodes_usage, None); } }