diff --git a/crates/media/src/ogg/mod.rs b/crates/media/src/ogg/mod.rs index 6311335f5..fefca4c9c 100644 --- a/crates/media/src/ogg/mod.rs +++ b/crates/media/src/ogg/mod.rs @@ -708,6 +708,11 @@ fn find_capture(src: &(impl ByteSource + ?Sized), from: u64, len: u64, recovery: /// The first `max` bytes of a packet's parts. fn read_parts(src: &(impl ByteSource + ?Sized), parts: &[(u64, u32)], max: usize) -> Result> { + // Ranges come from a scan of some source; check them against this one before sizing a buffer. + let len = src.len(); + if parts.iter().any(|&(o, n)| o.checked_add(u64::from(n)).is_none_or(|end| end > len)) { + return Err(Error::Invalid("packet range lies outside the source".into())); + } let mut v = Vec::with_capacity(parts.iter().map(|p| p.1 as usize).sum::().min(max)); for &(o, n) in parts { let a = v.len(); @@ -797,6 +802,8 @@ impl OpusTiming { c = c.saturating_add(i64::from(durations[k])); ends[k] = Some(c); } + // the total counts these packets too + cursor = Some(c); break; } let Some(g) = stream.packets[j].granule else { break }; diff --git a/crates/media/src/ogg/tests.rs b/crates/media/src/ogg/tests.rs index bc47bfc01..4842c504c 100644 --- a/crates/media/src/ogg/tests.rs +++ b/crates/media/src/ogg/tests.rs @@ -696,3 +696,34 @@ fn crc_update_continues_across_any_split() { assert_eq!(crc32_update(crc32_bitwise(0, a), b), whole, "cut {cut} (reference head)"); } } + +#[test] +fn read_packet_checks_its_ranges_against_the_source_before_allocating() { + let f = opus_file(10, 312, 500); + let o = open(&f[..]).unwrap(); + let s = o.stream_of(Codec::Opus).unwrap(); + // The last packet lies past the end of a source cut short: an error, not a read of missing bytes. + let short = &f[..f.len() / 2]; + assert!(matches!(o.read_packet(short, s, 9), Err(Error::Invalid(_)))); +} + +#[test] +fn opus_timing_includes_packets_after_the_last_known_granule() { + let f = opus_file(6, 312, 0); + let o = open(&f[..]).unwrap(); + let mut stream = o.streams[0].clone(); + // Missing granules on complete-packet pages are non-conforming. The recovery fallback still + // assigns their packet positions; the total must include the same samples. + stream.packets.last_mut().unwrap().granule = None; + let timing = OpusTiming::of(&stream, 312); + assert_eq!(timing.starts, vec![-312, 648, 1608, 2568, 3528, 4488]); + assert_eq!(timing.total, 6 * 960 - 312); + + // With no usable granule, every complete packet still contributes its TOC duration. + for packet in &mut stream.packets { + packet.granule = None; + } + let timing = OpusTiming::of(&stream, 312); + assert_eq!(timing.starts, vec![-312, 648, 1608, 2568, 3528, 4488]); + assert_eq!(timing.total, 6 * 960 - 312); +}