From 0bc95a25e467d13ce3bf0c033622b28ddad108aa Mon Sep 17 00:00:00 2001 From: "dobby-yivi-agent[bot]" <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:49:06 +0000 Subject: [PATCH] fix(email): avoid 'NaN B' file size for zero-byte uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #37. format_file_size(0) computed log10(0) = -inf, which cast to i32 saturates to i32::MIN, then 1024.powi(i32::MIN) is 0, so the division produces NaN and the email showed "NaN B". Extremely large sizes (past TB) would also index out of bounds into UNITS. Short-circuit size == 0 to "0 B" and clamp the computed index to the last unit so u64::MAX stays in-range. Adds 7 unit tests covering 0, sub- KB, KB/MB/GB/TB round numbers, and u64::MAX. This does not address the root cause reported in #37 — whatever code path produces size == 0 when S3 storage is in use — but it turns the visible NaN into a benign "0 B" so recipients at least see something sensible if the upstream issue recurs. --- src/email.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/email.rs b/src/email.rs index b97e71b..16a372b 100644 --- a/src/email.rs +++ b/src/email.rs @@ -84,11 +84,15 @@ struct EmailTemplate<'a> { fn format_file_size(size: u64) -> String { const UNITS: [&str; 5] = ["B", "kB", "MB", "GB", "TB"]; - let i = ((size as f64).log10() / (1024_f64).log10()).floor(); + if size == 0 { + return "0 B".to_owned(); + } + let i = ((size as f64).log10() / (1024_f64).log10()).floor() as usize; + let i = i.min(UNITS.len() - 1); format!( "{:.1} {}", (size as f64 / (1024_f64).powi(i as i32)), - UNITS[i as usize] + UNITS[i] ) } @@ -243,3 +247,48 @@ pub async fn send_email( Ok("Email successfully sent".to_owned()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_file_size_zero() { + assert_eq!(format_file_size(0), "0 B"); + } + + #[test] + fn format_file_size_bytes() { + assert_eq!(format_file_size(1), "1.0 B"); + assert_eq!(format_file_size(1023), "1023.0 B"); + } + + #[test] + fn format_file_size_kibibytes() { + assert_eq!(format_file_size(1024), "1.0 kB"); + assert_eq!(format_file_size(1536), "1.5 kB"); + } + + #[test] + fn format_file_size_mebibytes() { + assert_eq!(format_file_size(1024 * 1024), "1.0 MB"); + } + + #[test] + fn format_file_size_gibibytes() { + assert_eq!(format_file_size(1024 * 1024 * 1024), "1.0 GB"); + } + + #[test] + fn format_file_size_tebibytes() { + assert_eq!(format_file_size(1024_u64.pow(4)), "1.0 TB"); + } + + #[test] + fn format_file_size_clamps_above_tb() { + // u64 max is ~16 EB, far beyond TB — previously UNITS[i] would panic. + // The clamp keeps us at TB and produces a sensible large-TB number. + let result = format_file_size(u64::MAX); + assert!(result.ends_with(" TB"), "got {}", result); + } +}