From 4c1d368d0503456f9557fd5f3b94a81b55ccdc9e Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 22 Apr 2026 08:02:23 +0100 Subject: [PATCH] fix(A.3 #25): guard sample_index_for_seconds against NaN and infinity Review feedback (MINOR): the original <= 0.0 clamp caught negatives and zero but not non-finite inputs. Rust's saturating float-to-int cast turns f64::INFINITY into u64::MAX, which would park the capture buffer origin beyond any reachable sample index and trim the whole buffer forever if a future end_secs source ever produces infinity (clock glitch, overflow upstream, corrupted timestamp in a pass). Adds is_finite() check. NaN, +infinity, -infinity, and zero all return 0, which downstream trim_buffer_to_commit_point treats as no-op. Test covers all three non-finite cases. --- .../src/streaming/buffer_trim.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/transcription/src/streaming/buffer_trim.rs b/crates/transcription/src/streaming/buffer_trim.rs index 82e9f05..e7bda4a 100644 --- a/crates/transcription/src/streaming/buffer_trim.rs +++ b/crates/transcription/src/streaming/buffer_trim.rs @@ -14,8 +14,14 @@ /// Absolute sample index at the end of the given session-relative /// seconds mark, rounded to the nearest sample. `end_secs` typically /// comes from `LocalAgreement::last_committed_end_secs()`. +/// +/// Guards against non-finite inputs: NaN and ±infinity both return 0 +/// ("nothing committed yet"). Without this, Rust's saturating +/// float-to-int cast turns `f64::INFINITY` into `u64::MAX`, which +/// would park the capture buffer origin at an index beyond any +/// reachable sample and trim the entire buffer forever. pub fn sample_index_for_seconds(end_secs: f64, sample_rate: u32) -> u64 { - if end_secs <= 0.0 { + if !end_secs.is_finite() || end_secs <= 0.0 { return 0; } (end_secs * sample_rate as f64).round() as u64 @@ -65,6 +71,17 @@ mod tests { assert_eq!(sample_index_for_seconds(-1.0, 16_000), 0); } + #[test] + fn sample_index_for_seconds_rejects_nan_and_infinity() { + // Defensive against non-finite inputs: without the is_finite() + // check, Rust's saturating float-to-int cast makes +infinity + // become u64::MAX, which would park the buffer origin beyond + // reach and trim the whole buffer forever. + assert_eq!(sample_index_for_seconds(f64::NAN, 16_000), 0); + assert_eq!(sample_index_for_seconds(f64::INFINITY, 16_000), 0); + assert_eq!(sample_index_for_seconds(f64::NEG_INFINITY, 16_000), 0); + } + #[test] fn sample_index_for_seconds_rounds_nearest() { // 0.5 s at 16 kHz = 8000 samples exactly.