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();