From 988d678eec0bcd08ee92cc00f10a427e5058cab6 Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:50:38 +0200 Subject: [PATCH] Fix overflow in extreme timecode frame counts Widen the magnitude and drop-frame label arithmetic to i128 so i64::MIN stays exact and extreme drop-frame counts cannot overflow. Add round-trip regressions for all frame rates and both drop-frame variants. --- crates/time/src/timecode.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/time/src/timecode.rs b/crates/time/src/timecode.rs index f8f6041b..1adf3901 100644 --- a/crates/time/src/timecode.rs +++ b/crates/time/src/timecode.rs @@ -133,8 +133,10 @@ impl Timecode { /// Label for an absolute frame count. pub fn from_frames(frames: i64, rate: FrameRate) -> Timecode { let negative = frames < 0; - let mut f = frames.saturating_abs(); - let base = rate.nominal().max(1); + // Drop-frame labels can exceed i64::MAX even when the frame count fits. Widen before + // taking the magnitude so i64::MIN is represented exactly too. + let mut f = i128::from(frames).abs(); + let base = i128::from(rate.nominal().max(1)); if rate.is_drop() { // Drop 2 (or 4) labels each minute except every tenth. let drop = if base >= 60 { 4 } else { 2 }; @@ -147,7 +149,13 @@ impl Timecode { } let frames_l = f % base; let total_secs = f / base; - Timecode { negative, hours: total_secs / 3600, minutes: (total_secs / 60) % 60, seconds: total_secs % 60, frames: frames_l } + Timecode { + negative, + hours: i64::try_from(total_secs / 3600).unwrap_or(i64::MAX), + minutes: i64::try_from((total_secs / 60) % 60).unwrap_or(0), + seconds: i64::try_from(total_secs % 60).unwrap_or(0), + frames: i64::try_from(frames_l).unwrap_or(0), + } } /// Absolute frame count of this label. @@ -247,6 +255,28 @@ mod tests { assert_eq!(tc.to_samples(FrameRate::Fps30, sr), 178_710_400); } + #[test] + fn extreme_frame_counts_round_trip_at_every_rate() { + for rate in FrameRate::ALL { + for frames in [i64::MIN, i64::MIN + 1, -1, 0, 1, i64::MAX - 1, i64::MAX] { + let tc = Timecode::from_frames(frames, rate); + assert_eq!(tc.to_frames(rate), frames, "{rate:?}: {frames}"); + assert_eq!(tc.negative, frames < 0); + assert!((0..60).contains(&tc.minutes)); + assert!((0..60).contains(&tc.seconds)); + assert!((0..rate.nominal()).contains(&tc.frames)); + assert_eq!(Timecode::parse(&tc.format(rate)).unwrap(), tc); + } + } + } + + #[test] + fn extreme_drop_frame_labels_do_not_overflow() { + for rate in [FrameRate::Fps29_97Drop, FrameRate::Fps59_94Drop] { + assert_eq!(Timecode::from_frames(i64::MAX, rate).to_frames(rate), i64::MAX); + } + } + #[test] fn parse_variants() { let tc = Timecode::parse("01:02:03:04").unwrap();