From 9723ff3dd836c0f35db5bf93c2de5770b95a5a1b Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:37:22 +0800 Subject: [PATCH] fix(ui): a size one byte under a unit no longer reads 1024.0K 1 MiB - 1 byte -> "1024.0K" in the SFTP listing -> "1024.0 KiB" while installing a remote server Both loops asked whether the value had reached the next unit, then printed it rounded to one decimal. 1023.999 has not reached 1024, and prints as `1024.0` -- a number that disagrees with the unit beside it, next to a `1.0M` one byte along. The comparison is now against what will be shown rather than what is held, at the same one decimal the format uses. The exact boundaries are unchanged, and `1023.9K` still prints as itself: the carry only moves a value that would have displayed as `1024.0`. The two existing tests grew the boundary case rather than gaining siblings, since each already walks its own formatter through the units. `update.rs`'s `human_bytes` is left alone -- it prints megabytes and never advances a unit, so it has no boundary to get wrong. That the three of them format differently at all is deliberate: a size column is tight, a progress line is not. --- src/ui/remote_connect.rs | 8 +++++++- src/ui/sftp.rs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/ui/remote_connect.rs b/src/ui/remote_connect.rs index 98356583..11b1d5c6 100644 --- a/src/ui/remote_connect.rs +++ b/src/ui/remote_connect.rs @@ -540,7 +540,9 @@ pub fn human_bytes(n: u64) -> String { let units = ["KiB", "MiB", "GiB"]; let mut value = n / KIB; for (i, unit) in units.iter().enumerate() { - if value < KIB || i == units.len() - 1 { + // Against the shown value, not the held one: 1023.999 KiB prints as + // `1024.0 KiB`, a number that disagrees with its unit. + if (value * 10.0).round() < 10_240.0 || i == units.len() - 1 { return format!("{value:.1} {unit}"); } value /= KIB; @@ -905,6 +907,10 @@ mod tests { assert_eq!(human_bytes(1024), "1.0 KiB"); assert_eq!(human_bytes(1_572_864), "1.5 MiB"); assert_eq!(human_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB"); + + // Rounding must not leave the number and the unit disagreeing. + assert_eq!(human_bytes(1_048_575), "1.0 MiB", "1 MiB - 1"); + assert_eq!(human_bytes(1_073_741_823), "1.0 GiB", "1 GiB - 1"); } #[test] diff --git a/src/ui/sftp.rs b/src/ui/sftp.rs index a08b5684..01bd08a0 100644 --- a/src/ui/sftp.rs +++ b/src/ui/sftp.rs @@ -283,7 +283,11 @@ fn human_size(bytes: u64) -> String { const UNITS: [&str; 5] = ["B", "K", "M", "G", "T"]; let mut value = bytes as f64; let mut unit = 0; - while value >= 1024.0 && unit < UNITS.len() - 1 { + // The comparison is against what will be *shown*, not what is held. + // One byte under a mebibyte is 1023.999 KiB, which prints as `1024.0K` — + // a number and a unit that disagree. Rounding to the same one decimal the + // format uses moves it up with the value. + while unit < UNITS.len() - 1 && (value * 10.0).round() >= 10_240.0 { value /= 1024.0; unit += 1; } @@ -1895,6 +1899,7 @@ impl Tty7App { #[cfg(test)] mod tests { + use super::*; fn upload(job_id: u64, state: SftpJobState) -> SftpJobProgress { @@ -2139,6 +2144,12 @@ mod tests { assert_eq!(human_size(1024), "1.0K"); assert_eq!(human_size(1536), "1.5K"); assert_eq!(human_size(1024 * 1024), "1.0M"); + + // A byte under the next unit rounds to `1024.0` at one decimal, and a + // size column showing `1024.0K` beside `1.0M` reads as a mistake. + assert_eq!(human_size(1_048_575), "1.0M", "1 MiB - 1"); + assert_eq!(human_size(1_073_741_823), "1.0G", "1 GiB - 1"); + assert_eq!(human_size(1_048_524), "1023.9K", "just below the carry"); } #[test]