mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 02:27:08 -06:00
Codecs: a hostile number is a format error, not an overflow
H.264 and HEVC quantiser offsets, Matroska block times, Opus granule timing and QuickTime PCM frame sizes are range-checked before the arithmetic.
This commit is contained in:
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~20 points and ~120–210 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: codec parsers reject overflowing numeric fields, #426; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON
|
||||
format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
|
||||
@@ -128,6 +128,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-13 | Codec parsers (#426): H.264 and HEVC quantiser offsets, Matroska block times, Opus granule timing and QuickTime PCM frame sizes are range-checked before the arithmetic, so a hostile number is a format error instead of an overflow |
|
||||
| 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless |
|
||||
| 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline |
|
||||
| 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting |
|
||||
@@ -150,6 +151,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | trivial | Progress log: codec parsers reject overflowing numeric fields (#426) |
|
||||
| 2026-10-13 | trivial | Progress log: wild text cases |
|
||||
| 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) |
|
||||
| 2026-10-12 | trivial | Progress log: lines in scaled groups (#233) |
|
||||
|
||||
@@ -559,6 +559,12 @@ pub struct Pps {
|
||||
pub scaling: ScalingMatrices,
|
||||
}
|
||||
|
||||
/// `26 + minus26`, after `minus26` is inside −(26 + QpBdOffset) ..= 25.
|
||||
fn qp_plus_26(minus26: i32, qp_bd: i32, name: &str) -> Result<i32> {
|
||||
ensure!((-(26 + qp_bd)..=25).contains(&minus26), "{name} out of range");
|
||||
Ok(26 + minus26)
|
||||
}
|
||||
|
||||
impl Pps {
|
||||
/// Parse a PPS. Needs the SPS table because the tail depends on chroma_format_idc.
|
||||
pub fn parse(rbsp: &[u8], spss: &[Option<Sps>]) -> Result<Pps> {
|
||||
@@ -610,8 +616,11 @@ impl Pps {
|
||||
ensure!(num_ref_idx_l0_default_active <= 32 && num_ref_idx_l1_default_active <= 32, "num_ref_idx_default_active out of range");
|
||||
let weighted_pred = r.read_flag()?;
|
||||
let weighted_bipred_idc = r.read_bits(2)?;
|
||||
let pic_init_qp = 26 + r.read_se()?;
|
||||
let pic_init_qs = 26 + r.read_se()?;
|
||||
// pic_init_qp_minus26 is −(26 + QpBdOffsetY) ..= 25. Check that before adding 26: a hostile
|
||||
// Exp-Golomb value overflows the addition in debug builds.
|
||||
let qp_bd = 6 * sps.bit_depth_luma.saturating_sub(8) as i32;
|
||||
let pic_init_qp = qp_plus_26(r.read_se()?, qp_bd, "pic_init_qp_minus26")?;
|
||||
let pic_init_qs = qp_plus_26(r.read_se()?, qp_bd, "pic_init_qs_minus26")?;
|
||||
let chroma_qp_index_offset = r.read_se()?;
|
||||
ensure!((-12..=12).contains(&chroma_qp_index_offset), "chroma_qp_index_offset out of range");
|
||||
let deblocking_filter_control_present = r.read_flag()?;
|
||||
@@ -751,4 +760,64 @@ mod tests {
|
||||
assert!(Sps::parse(&sps_with(10, Some([0x8000_0000, 0, 0, 0]))).is_err());
|
||||
assert!(Sps::parse(&sps_with(10, Some([1, 1, 1, 1]))).is_ok());
|
||||
}
|
||||
|
||||
fn pps_bits(qp_minus26: i32, qs_minus26: i32) -> Vec<u8> {
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ue(0);
|
||||
w.write_ue(0);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_ue(0);
|
||||
w.write_ue(0);
|
||||
w.write_ue(0);
|
||||
w.write_bit(false);
|
||||
w.write_bits(0, 2);
|
||||
w.write_se(qp_minus26);
|
||||
w.write_se(qs_minus26);
|
||||
w.write_se(0);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.rbsp_trailing();
|
||||
w.finish()
|
||||
}
|
||||
|
||||
fn sps_table() -> Vec<Option<Sps>> {
|
||||
vec![Sps::parse(&sps_with(10, None)).ok()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pic_init_qp_is_checked_before_adding_26() {
|
||||
let table = sps_table();
|
||||
assert_eq!(Pps::parse(&pps_bits(0, 0), &table).unwrap().pic_init_qp, 26);
|
||||
assert_eq!(Pps::parse(&pps_bits(25, 0), &table).unwrap().pic_init_qp, 51);
|
||||
assert_eq!(Pps::parse(&pps_bits(-26, 0), &table).unwrap().pic_init_qp, 0);
|
||||
assert!(Pps::parse(&pps_bits(26, 0), &table).is_err());
|
||||
assert!(Pps::parse(&pps_bits(-27, 0), &table).is_err());
|
||||
assert!(Pps::parse(&pps_bits(i32::MAX, 0), &table).is_err());
|
||||
assert!(Pps::parse(&pps_bits(0, i32::MAX), &table).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slice_qp_addition_does_not_overflow() {
|
||||
let table = sps_table();
|
||||
let pps = Pps::parse(&pps_bits(0, 0), &table).unwrap();
|
||||
let sps = table[0].clone().unwrap();
|
||||
let nal = crate::slice::NalHeader { nal_ref_idc: 0, nal_unit_type: crate::slice::nal_type::SLICE };
|
||||
let rbsp = |delta: i32| {
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ue(0);
|
||||
w.write_ue(2); // I
|
||||
w.write_ue(0);
|
||||
w.write_bits(0, sps.log2_max_frame_num);
|
||||
w.write_se(delta);
|
||||
w.rbsp_trailing();
|
||||
w.finish()
|
||||
};
|
||||
let parse = |delta: i32| crate::slice::SliceHeader::parse(&rbsp(delta), nal, |_| Ok((&pps, table[0].as_ref().unwrap())));
|
||||
let (sh, _, _) = parse(0).unwrap();
|
||||
assert_eq!(sh.qp(&pps), 26);
|
||||
assert!(parse(26).is_err());
|
||||
assert!(parse(i32::MAX).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ pub struct SliceHeader {
|
||||
|
||||
impl SliceHeader {
|
||||
pub fn qp(&self, pps: &Pps) -> i32 {
|
||||
pps.pic_init_qp + self.slice_qp_delta
|
||||
pps.pic_init_qp.saturating_add(self.slice_qp_delta)
|
||||
}
|
||||
pub fn has_mmco5(&self) -> bool {
|
||||
self.mmcos.iter().any(|m| m.op == 5)
|
||||
@@ -315,7 +315,7 @@ impl SliceHeader {
|
||||
ensure!(cabac_init_idc <= 2, "cabac_init_idc out of range");
|
||||
}
|
||||
let slice_qp_delta = r.read_se()?;
|
||||
let qp = pps.pic_init_qp + slice_qp_delta;
|
||||
let qp = pps.pic_init_qp.checked_add(slice_qp_delta).ok_or_else(|| crate::Error::Invalid("slice QP out of range".into()))?;
|
||||
ensure!((0..=51).contains(&qp), "slice QP {qp} out of range");
|
||||
let mut sp_for_switch = false;
|
||||
let mut slice_qs_delta = 0;
|
||||
|
||||
@@ -726,7 +726,11 @@ impl Pps {
|
||||
let num_ref_idx_l0_default = r.read_ue_plus(1)?;
|
||||
let num_ref_idx_l1_default = r.read_ue_plus(1)?;
|
||||
ensure!(num_ref_idx_l0_default <= 15 && num_ref_idx_l1_default <= 15, "num_ref_idx_default_active out of range");
|
||||
let init_qp = 26 + r.read_se()?;
|
||||
// init_qp_minus26 is −(26 + QpBdOffsetY) ..= 25. Bit depth is not known here (no SPS), so the
|
||||
// widest legal window is 16-bit (QpBdOffsetY = 48). Reject before adding 26.
|
||||
let init_qp_minus26 = r.read_se()?;
|
||||
ensure!((-74..=25).contains(&init_qp_minus26), "init_qp_minus26 out of range");
|
||||
let init_qp = 26 + init_qp_minus26;
|
||||
let constrained_intra_pred = r.read_flag()?;
|
||||
let transform_skip = r.read_flag()?;
|
||||
let cu_qp_delta_enabled = r.read_flag()?;
|
||||
@@ -999,4 +1003,48 @@ mod tests {
|
||||
assert!(StRps::parse(&mut BitReader::new(&b), 0, &[], 1).is_err(), "ue {ue:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
fn pps_bits(init_qp_minus26: i32) -> Vec<u8> {
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ue(0);
|
||||
w.write_ue(0);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bits(0, 3);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_ue(0);
|
||||
w.write_ue(0);
|
||||
w.write_se(init_qp_minus26);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_se(0);
|
||||
w.write_se(0);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.write_bit(true); // loop filter across slices
|
||||
w.write_bit(false); // deblocking
|
||||
w.write_bit(false); // scaling list
|
||||
w.write_bit(false);
|
||||
w.write_ue(0); // log2_parallel_merge_level_minus2
|
||||
w.write_bit(false);
|
||||
w.write_bit(false);
|
||||
w.rbsp_trailing();
|
||||
w.finish()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_qp_is_checked_before_adding_26() {
|
||||
assert_eq!(Pps::parse(&pps_bits(0)).unwrap().init_qp, 26);
|
||||
assert_eq!(Pps::parse(&pps_bits(25)).unwrap().init_qp, 51);
|
||||
assert_eq!(Pps::parse(&pps_bits(-74)).unwrap().init_qp, -48);
|
||||
assert!(Pps::parse(&pps_bits(26)).is_err());
|
||||
assert!(Pps::parse(&pps_bits(-75)).is_err());
|
||||
assert!(Pps::parse(&pps_bits(i32::MAX)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ impl SliceHeader {
|
||||
sh.max_num_merge_cand = 5 - five_minus;
|
||||
}
|
||||
sh.qp_delta = r.read_se()?;
|
||||
let qp = pps.init_qp + sh.qp_delta;
|
||||
let qp = pps.init_qp.checked_add(sh.qp_delta).ok_or_else(|| crate::Error::Invalid("SliceQpY out of range".into()))?;
|
||||
let qp_bd = 6 * (sps.bit_depth_luma as i32 - 8);
|
||||
ensure!((-qp_bd..=51).contains(&qp), "SliceQpY {qp} out of range");
|
||||
if pps.slice_chroma_qp_offsets_present {
|
||||
@@ -368,7 +368,8 @@ impl SliceHeader {
|
||||
let len = r.read_ue_plus(1)?;
|
||||
ensure!(len <= 32, "offset_len_minus1 out of range");
|
||||
for _ in 0..n {
|
||||
sh.entry_points.push(r.read_bits(len)? + 1);
|
||||
let off = r.read_bits(len)?.checked_add(1).ok_or_else(|| crate::Error::Invalid("entry_point_offset out of range".into()))?;
|
||||
sh.entry_points.push(off);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,7 +392,9 @@ fn parse_pwt(r: &mut BitReader, sh: &SliceHeader, sps: &Sps) -> Result<PredWeigh
|
||||
ensure!(t.luma_log2_denom <= 7, "luma_log2_weight_denom out of range");
|
||||
let chroma = sps.chroma_array_type() != 0;
|
||||
if chroma {
|
||||
let d = t.luma_log2_denom as i32 + r.read_se()?;
|
||||
let delta = r.read_se()?;
|
||||
ensure!((-7..=7).contains(&delta), "delta_chroma_log2_weight_denom out of range");
|
||||
let d = t.luma_log2_denom as i32 + delta;
|
||||
ensure!((0..=7).contains(&d), "ChromaLog2WeightDenom out of range");
|
||||
t.chroma_log2_denom = d as u32;
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ impl<'a> SliceDecoder<'a> {
|
||||
let sh = &job.sh;
|
||||
let (sps, pps) = (&*job.sps, &*job.pps);
|
||||
ensure!(sh.data_offset < job.rbsp.len(), "empty slice segment data");
|
||||
let slice_qp = pps.init_qp + sh.qp_delta;
|
||||
let slice_qp = pps.init_qp.checked_add(sh.qp_delta).ok_or_else(|| crate::Error::Invalid("SliceQpY out of range".into()))?;
|
||||
let init_type = match sh.slice_type {
|
||||
SliceType::I => 0,
|
||||
SliceType::P => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Sample entries (`stsd` children) and codec configuration records: parsing and serialization.
|
||||
|
||||
use crate::bytes::{BoxBuf, Cur, FourCc, boxes};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::error::{Error, Result, invalid};
|
||||
use deckcraft_bitstream::BitReader;
|
||||
|
||||
/// One sample description (an entry of `stsd`).
|
||||
@@ -606,8 +606,9 @@ pub struct PcmConfig {
|
||||
|
||||
impl PcmConfig {
|
||||
/// Bytes per interleaved audio frame (all channels).
|
||||
pub fn bytes_per_frame(&self) -> u32 {
|
||||
(self.bits as u32).div_ceil(8) * self.channels.max(1)
|
||||
/// `None` when `channels` times the bytes per sample does not fit in `u32`.
|
||||
pub fn bytes_per_frame(&self) -> Option<u32> {
|
||||
u32::from(self.bits).div_ceil(8).checked_mul(self.channels.max(1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,6 +889,9 @@ pub(crate) fn parse_sample_entry(format: FourCc, payload: &[u8], handler: FourCc
|
||||
let _size_of_struct = c.u32()?;
|
||||
a.sample_rate = c.f64()?;
|
||||
a.channels = c.u32()?;
|
||||
if a.channels > u32::from(u16::MAX) {
|
||||
return invalid("channel count out of range");
|
||||
}
|
||||
let _always = c.u32()?;
|
||||
a.const_bits_per_channel = c.u32()?;
|
||||
a.lpcm_flags = c.u32()?;
|
||||
@@ -931,7 +935,7 @@ pub(crate) fn parse_sample_entry(format: FourCc, payload: &[u8], handler: FourCc
|
||||
},
|
||||
b"ac-3" => CodecConfig::Ac3 { dac3: get(b"dac3").unwrap_or(&[]).to_vec() },
|
||||
b"ec-3" => CodecConfig::Eac3 { dec3: get(b"dec3").unwrap_or(&[]).to_vec() },
|
||||
b"lpcm" | b"sowt" | b"twos" | b"in24" | b"in32" | b"fl32" | b"fl64" | b"raw " | b"NONE" => CodecConfig::Pcm(qt_pcm(f, &a)),
|
||||
b"lpcm" | b"sowt" | b"twos" | b"in24" | b"in32" | b"fl32" | b"fl64" | b"raw " | b"NONE" => CodecConfig::Pcm(qt_pcm(f, &a)?),
|
||||
b"ipcm" | b"fpcm" => {
|
||||
let mut pcm = PcmConfig {
|
||||
bits: a.sample_size,
|
||||
@@ -948,6 +952,9 @@ pub(crate) fn parse_sample_entry(format: FourCc, payload: &[u8], handler: FourCc
|
||||
pcm.big_endian = flags & 1 == 0;
|
||||
pcm.bits = c.u8()? as u16;
|
||||
}
|
||||
if pcm.bytes_per_frame().is_none() {
|
||||
return invalid("PCM frame size overflows");
|
||||
}
|
||||
CodecConfig::Pcm(pcm)
|
||||
}
|
||||
_ => unknown(),
|
||||
@@ -957,11 +964,11 @@ pub(crate) fn parse_sample_entry(format: FourCc, payload: &[u8], handler: FourCc
|
||||
Ok(SampleEntry { format, data_reference_index, codec: unknown(), video: None, audio: None, bitrate: None })
|
||||
}
|
||||
|
||||
fn qt_pcm(f: &[u8; 4], a: &AudioParams) -> PcmConfig {
|
||||
fn qt_pcm(f: &[u8; 4], a: &AudioParams) -> Result<PcmConfig> {
|
||||
let mut p = PcmConfig { channels: a.channels, sample_rate: a.sample_rate, signed: true, ..Default::default() };
|
||||
match f {
|
||||
b"lpcm" => {
|
||||
p.bits = a.const_bits_per_channel as u16;
|
||||
p.bits = u16::try_from(a.const_bits_per_channel).map_err(|_| Error::Invalid("const_bits_per_channel out of range".into()))?;
|
||||
p.float = a.lpcm_flags & 1 != 0;
|
||||
p.big_endian = a.lpcm_flags & 2 != 0;
|
||||
p.signed = a.lpcm_flags & 4 != 0 || p.float;
|
||||
@@ -992,7 +999,10 @@ fn qt_pcm(f: &[u8; 4], a: &AudioParams) -> PcmConfig {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
p
|
||||
if p.bytes_per_frame().is_none() {
|
||||
return invalid("PCM frame size overflows");
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn parse_btrt(p: &[u8]) -> Result<BitRate> {
|
||||
@@ -1360,7 +1370,7 @@ impl SampleEntry {
|
||||
b.u32(p.bits as u32);
|
||||
let flags = (p.float as u32) | ((p.big_endian as u32) << 1) | (((p.signed && !p.float) as u32) << 2) | 8;
|
||||
b.u32(flags);
|
||||
b.u32(p.bytes_per_frame());
|
||||
b.u32(p.bytes_per_frame().unwrap_or(0));
|
||||
b.u32(1);
|
||||
} else {
|
||||
b.u16(version);
|
||||
@@ -1536,6 +1546,40 @@ mod tests {
|
||||
assert_eq!(roundtrip(&e, true, b"soun"), e);
|
||||
}
|
||||
|
||||
fn qt_v2_lpcm(channels: u32, bits: u32) -> Vec<u8> {
|
||||
let mut p = vec![0; 6];
|
||||
let u16b = |p: &mut Vec<u8>, v: u16| p.extend(v.to_be_bytes());
|
||||
let u32b = |p: &mut Vec<u8>, v: u32| p.extend(v.to_be_bytes());
|
||||
u16b(&mut p, 1);
|
||||
u16b(&mut p, 2);
|
||||
u16b(&mut p, 0);
|
||||
u32b(&mut p, 0);
|
||||
u16b(&mut p, 2);
|
||||
u16b(&mut p, 16);
|
||||
u16b(&mut p, 0);
|
||||
u16b(&mut p, 0);
|
||||
u32b(&mut p, 65536);
|
||||
u32b(&mut p, 72);
|
||||
p.extend(48000.0f64.to_be_bytes());
|
||||
u32b(&mut p, channels);
|
||||
u32b(&mut p, 0);
|
||||
u32b(&mut p, bits);
|
||||
u32b(&mut p, 0);
|
||||
u32b(&mut p, 0);
|
||||
u32b(&mut p, 1);
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcm_frame_size_rejects_a_hostile_channel_count() {
|
||||
assert_eq!(PcmConfig { bits: 16, channels: 2, sample_rate: 48000.0, ..Default::default() }.bytes_per_frame(), Some(4));
|
||||
assert!(PcmConfig { bits: 16, channels: u32::MAX, ..Default::default() }.bytes_per_frame().is_none());
|
||||
assert!(parse_sample_entry(FourCc(*b"lpcm"), &qt_v2_lpcm(u32::MAX, 16), FourCc(*b"soun"), true).is_err());
|
||||
assert!(parse_sample_entry(FourCc(*b"lpcm"), &qt_v2_lpcm(2, u32::MAX), FourCc(*b"soun"), true).is_err());
|
||||
let ok = parse_sample_entry(FourCc(*b"lpcm"), &qt_v2_lpcm(2, 16), FourCc(*b"soun"), true).unwrap();
|
||||
assert!(matches!(ok.codec, CodecConfig::Pcm(p) if p.bytes_per_frame() == Some(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcm_entries() {
|
||||
for (bits, float, be, qt) in [
|
||||
|
||||
@@ -699,13 +699,14 @@ fn parse_stbl(stbl: &[u8], t: &mut Track, is_qt: bool, file_len: u64) -> Result<
|
||||
if let Some(p) = pcm {
|
||||
t.pcm_chunked = true;
|
||||
let a = t.audio().cloned().unwrap_or_default();
|
||||
let frame_bytes = |frames: u64| -> u64 {
|
||||
let bpf = p.bytes_per_frame().ok_or_else(|| Error::Invalid("PCM frame size overflows".into()))?;
|
||||
let frame_bytes = |frames: u64| -> Option<u64> {
|
||||
if a.qt_version == 1 && a.bytes_per_frame > 0 && a.samples_per_packet > 0 {
|
||||
frames / a.samples_per_packet as u64 * a.bytes_per_frame as u64
|
||||
frames.checked_div(u64::from(a.samples_per_packet))?.checked_mul(u64::from(a.bytes_per_frame))
|
||||
} else if const_size > 1 {
|
||||
frames * const_size as u64
|
||||
frames.checked_mul(u64::from(const_size))
|
||||
} else {
|
||||
frames * p.bytes_per_frame() as u64
|
||||
frames.checked_mul(u64::from(bpf))
|
||||
}
|
||||
};
|
||||
if chunks.len() > MAX_SAMPLES {
|
||||
@@ -721,7 +722,7 @@ fn parse_stbl(stbl: &[u8], t: &mut Track, is_qt: bool, file_len: u64) -> Result<
|
||||
continue;
|
||||
}
|
||||
let dur = stts_c.take(spc as u64);
|
||||
let size = frame_bytes(spc as u64).min(u32::MAX as u64) as u32;
|
||||
let size = frame_bytes(spc as u64).ok_or_else(|| Error::Invalid("PCM sample size overflows".into()))?.min(u64::from(u32::MAX)) as u32;
|
||||
samples.push(Sample {
|
||||
offset: off,
|
||||
size,
|
||||
|
||||
@@ -498,7 +498,10 @@ fn validate_track(cfg: &TrackConfig) -> Result<Option<u32>> {
|
||||
if (p.sample_rate - cfg.timescale as f64).abs() > 0.5 {
|
||||
return Err(Error::Mux("PCM track timescale must equal the sample rate".into()));
|
||||
}
|
||||
return Ok(Some(p.bytes_per_frame()));
|
||||
let Some(n) = p.bytes_per_frame() else {
|
||||
return Err(Error::Mux("PCM frame size overflows".into()));
|
||||
};
|
||||
return Ok(Some(n));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -354,6 +354,15 @@ fn parse_block<S: ByteSource + ?Sized>(
|
||||
Ok(Some((track, rel, flags, frames)))
|
||||
}
|
||||
|
||||
/// Cluster timestamps are unsigned and must fit in `i64` before a signed block offset is added.
|
||||
fn cluster_timestamp(raw: u64) -> Result<i64> {
|
||||
i64::try_from(raw).map_err(|_| Error::Invalid("cluster timestamp out of range".into()))
|
||||
}
|
||||
|
||||
fn block_timestamp(cluster_ts: i64, rel: i16) -> Result<i64> {
|
||||
cluster_ts.checked_add(i64::from(rel)).ok_or_else(|| Error::Invalid("block timestamp overflow".into()))
|
||||
}
|
||||
|
||||
/// Parse one SimpleBlock or BlockGroup element.
|
||||
fn parse_block_elem<S: ByteSource + ?Sized>(
|
||||
io: &mut Io<'_, S>,
|
||||
@@ -369,7 +378,7 @@ fn parse_block_elem<S: ByteSource + ?Sized>(
|
||||
Ok(Some(BlockRef {
|
||||
track,
|
||||
block_offset: h.pos,
|
||||
ts: cluster_ts + rel as i64,
|
||||
ts: block_timestamp(cluster_ts, rel)?,
|
||||
frames,
|
||||
block_duration: None,
|
||||
keyframe: flags & 0x80 != 0,
|
||||
@@ -398,7 +407,7 @@ fn parse_block_elem<S: ByteSource + ?Sized>(
|
||||
Ok(Some(BlockRef {
|
||||
track,
|
||||
block_offset: h.pos,
|
||||
ts: cluster_ts + rel as i64,
|
||||
ts: block_timestamp(cluster_ts, rel)?,
|
||||
frames,
|
||||
block_duration: dur,
|
||||
keyframe: !has_ref,
|
||||
@@ -492,7 +501,7 @@ fn scan_cluster<S: ByteSource + ?Sized>(
|
||||
}
|
||||
let ce = c.end(lim);
|
||||
match c.id {
|
||||
TIMESTAMP => ts = ebml::uint(io.peek(c.data_start(), (ce - c.data_start()) as usize)?) as i64,
|
||||
TIMESTAMP => ts = cluster_timestamp(ebml::uint(io.peek(c.data_start(), (ce - c.data_start()) as usize)?))?,
|
||||
SIMPLE_BLOCK | BLOCK_GROUP => {
|
||||
if let Some(b) = parse_block_elem(io, &c, ce, ts, &lookup)? {
|
||||
blocks.push(b);
|
||||
@@ -1104,7 +1113,7 @@ impl<S: ByteSource> Demuxer<S> {
|
||||
self.pos = he;
|
||||
match h.id {
|
||||
TIMESTAMP => {
|
||||
let v = ebml::uint(io.peek(h.data_start(), (he - h.data_start()) as usize)?) as i64;
|
||||
let v = cluster_timestamp(ebml::uint(io.peek(h.data_start(), (he - h.data_start()) as usize)?))?;
|
||||
self.cluster = Some(InCluster { ts: v, end: c.end });
|
||||
}
|
||||
SIMPLE_BLOCK | BLOCK_GROUP => {
|
||||
|
||||
@@ -213,3 +213,42 @@ fn synthetic_file() {
|
||||
assert_eq!(d.next_packet().unwrap().unwrap().pts, 1120);
|
||||
assert!(d.file().indexed);
|
||||
}
|
||||
|
||||
fn one_block(ts: u64, rel: i16) -> Vec<u8> {
|
||||
let mut f = Vec::new();
|
||||
let mut h = Vec::new();
|
||||
el_str(&mut h, DOC_TYPE, "matroska");
|
||||
el(&mut f, EBML, &h);
|
||||
write_id(&mut f, SEGMENT);
|
||||
write_unknown_size(&mut f, 8);
|
||||
let mut info = Vec::new();
|
||||
el_uint(&mut info, TIMESTAMP_SCALE, 1_000_000);
|
||||
el(&mut f, INFO, &info);
|
||||
let mut v = Vec::new();
|
||||
el_uint(&mut v, TRACK_NUMBER, 1);
|
||||
el_uint(&mut v, TRACK_UID, 1);
|
||||
el_uint(&mut v, TRACK_TYPE, 1);
|
||||
el_str(&mut v, CODEC_ID, "V_UNCOMPRESSED");
|
||||
let mut tr = Vec::new();
|
||||
el(&mut tr, TRACK_ENTRY, &v);
|
||||
el(&mut f, TRACKS, &tr);
|
||||
write_id(&mut f, CLUSTER);
|
||||
write_unknown_size(&mut f, 8);
|
||||
el_uint(&mut f, TIMESTAMP, ts);
|
||||
el(&mut f, SIMPLE_BLOCK, &block(1, rel, 0x80, &[0]));
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_timestamp_addition_does_not_overflow() {
|
||||
let bytes = one_block(1000, 5);
|
||||
let d = Demuxer::from_slice(&bytes).unwrap();
|
||||
let p: Vec<Packet> = d.map(|p| p.unwrap()).collect();
|
||||
assert_eq!(p[0].pts, 1005);
|
||||
let edge = one_block(i64::MAX as u64, 0);
|
||||
let d = Demuxer::from_slice(&edge).unwrap();
|
||||
let p: Vec<Packet> = d.map(|p| p.unwrap()).collect();
|
||||
assert_eq!(p[0].pts, i64::MAX);
|
||||
assert!(Demuxer::from_slice(&one_block(i64::MAX as u64, 1)).is_err());
|
||||
assert!(Demuxer::from_slice(&one_block(u64::MAX, 0)).is_err());
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ fn decode_ogg_opus(bytes: &Bytes) -> Option<Result<Pcm>> {
|
||||
let mut dec = deckcraft_opus::Decoder::new(head).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
dec.set_trim_pre_skip(true);
|
||||
let channels = dec.channels() as u16;
|
||||
let timing = deckcraft_ogg::OpusTiming::of(stream, dec.pre_skip() as u32);
|
||||
let timing = deckcraft_ogg::OpusTiming::of(stream, dec.pre_skip() as u32).map_err(|e| MediaError::Corrupt(e.to_string()))?;
|
||||
let mut out = Vec::new();
|
||||
for i in 0..stream.packets.len() {
|
||||
let Ok(p) = file.read_packet(src, s, i) else { continue };
|
||||
|
||||
@@ -170,7 +170,7 @@ fn probe_ogg(bytes: &Bytes) -> Option<MediaInfo> {
|
||||
let s = f.stream_of(deckcraft_ogg::Codec::Opus)?;
|
||||
let st = f.streams.get(s)?;
|
||||
let head = deckcraft_opus::OpusHead::parse(st.headers.first()?).ok()?;
|
||||
let timing = deckcraft_ogg::OpusTiming::of(st, head.pre_skip as u32);
|
||||
let timing = deckcraft_ogg::OpusTiming::of(st, head.pre_skip as u32).ok()?;
|
||||
Some(MediaInfo {
|
||||
container: "ogg",
|
||||
duration_ms: ms(timing.total as f64 / 48_000.0),
|
||||
|
||||
+30
-10
@@ -414,9 +414,20 @@ pub struct OpusTiming {
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
fn timing_add(a: i64, b: i64) -> Result<i64> {
|
||||
a.checked_add(b).ok_or_else(|| Error::Invalid("opus timing overflow".into()))
|
||||
}
|
||||
|
||||
fn timing_sub(a: i64, b: i64) -> Result<i64> {
|
||||
a.checked_sub(b).ok_or_else(|| Error::Invalid("opus timing overflow".into()))
|
||||
}
|
||||
|
||||
impl OpusTiming {
|
||||
/// Timing of the Opus stream `s` (`pre_skip` from its `OpusHead`).
|
||||
pub fn of(stream: &Stream, pre_skip: u32) -> OpusTiming {
|
||||
///
|
||||
/// A granule or a run of packet durations that does not fit in `i64` is an error. Ordinary
|
||||
/// files, including a truncated tail and end trimming, are unchanged.
|
||||
pub fn of(stream: &Stream, pre_skip: u32) -> Result<OpusTiming> {
|
||||
let durations: Vec<u32> = stream.packets.iter().map(|p| opus_packet_samples(p.prefix, p.size).unwrap_or(0)).collect();
|
||||
let n = durations.len();
|
||||
let mut ends: Vec<Option<i64>> = vec![None; n];
|
||||
@@ -433,14 +444,17 @@ impl OpusTiming {
|
||||
// trailing packets without a granule (truncated file): continue from the cursor
|
||||
let mut c = cursor.unwrap_or(0);
|
||||
for k in i..n {
|
||||
c += durations[k] as i64;
|
||||
c = timing_add(c, i64::from(durations[k]))?;
|
||||
ends[k] = Some(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
let Some(g) = stream.packets[j].granule else { break };
|
||||
let sum: i64 = durations[i..=j].iter().map(|&d| d as i64).sum();
|
||||
let forward = cursor.map(|c| c + sum);
|
||||
let sum = durations[i..=j].iter().try_fold(0i64, |a, &d| timing_add(a, i64::from(d)))?;
|
||||
let forward = match cursor {
|
||||
Some(c) => Some(timing_add(c, sum)?),
|
||||
None => None,
|
||||
};
|
||||
let end = match forward {
|
||||
// end trimming on the last page (RFC 7845 §4.5): keep the forward positions
|
||||
Some(f) if stream.packets[j].eos && g < f => {
|
||||
@@ -454,18 +468,24 @@ impl OpusTiming {
|
||||
let mut e = end;
|
||||
for k in (i..=j).rev() {
|
||||
ends[k] = Some(e);
|
||||
e -= durations[k] as i64;
|
||||
e = timing_sub(e, i64::from(durations[k]))?;
|
||||
}
|
||||
cursor = Some(end);
|
||||
i = j + 1;
|
||||
}
|
||||
let raw_starts: Vec<i64> = ends.iter().zip(&durations).map(|(e, &d)| e.unwrap_or(0) - d as i64).collect();
|
||||
let mut raw_starts = Vec::with_capacity(n);
|
||||
for (e, &d) in ends.iter().zip(&durations) {
|
||||
raw_starts.push(timing_sub(e.unwrap_or(0), i64::from(d))?);
|
||||
}
|
||||
let base = raw_starts.first().copied().unwrap_or(0).max(0);
|
||||
let shift = base + pre_skip as i64;
|
||||
let starts: Vec<i64> = raw_starts.iter().map(|s| s - shift).collect();
|
||||
let shift = timing_add(base, i64::from(pre_skip))?;
|
||||
let mut starts = Vec::with_capacity(n);
|
||||
for s in &raw_starts {
|
||||
starts.push(timing_sub(*s, shift)?);
|
||||
}
|
||||
let end_all = trimmed_end.or(cursor).unwrap_or(0);
|
||||
let total = (end_all - shift).max(0);
|
||||
OpusTiming { pre_skip, starts, durations, total }
|
||||
let total = timing_sub(end_all, shift)?.max(0);
|
||||
Ok(OpusTiming { pre_skip, starts, durations, total })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-4
@@ -99,7 +99,7 @@ fn opus_pages_packets_and_timing() {
|
||||
assert_eq!(st.packets.len(), 10);
|
||||
assert!(st.saw_eos);
|
||||
assert_eq!(o.read_packet(&f, s, 4).unwrap(), pkt(4, 44));
|
||||
let t = OpusTiming::of(st, 312);
|
||||
let t = OpusTiming::of(st, 312).unwrap();
|
||||
assert_eq!(t.durations, vec![960; 10]);
|
||||
assert_eq!(t.starts[0], -312);
|
||||
assert_eq!(t.starts[9], 9 * 960 - 312);
|
||||
@@ -129,7 +129,7 @@ fn packets_spanning_pages() {
|
||||
assert_eq!(st.packets[0].parts.len(), 2);
|
||||
assert_eq!(o.read_packet(&f, 0, 0).unwrap(), big);
|
||||
assert_eq!(o.read_packet(&f, 0, 1).unwrap(), small);
|
||||
let t = OpusTiming::of(st, 0);
|
||||
let t = OpusTiming::of(st, 0).unwrap();
|
||||
assert_eq!(t.starts, vec![0, 960]);
|
||||
assert_eq!(t.total, 1920);
|
||||
}
|
||||
@@ -144,13 +144,13 @@ fn corrupt_pages_are_skipped_and_truncation_is_safe() {
|
||||
let o = open(&g).unwrap();
|
||||
assert!(o.warnings.iter().any(|w| w.contains("CRC")), "{:?}", o.warnings);
|
||||
assert_eq!(o.streams[0].packets.len(), 9);
|
||||
let t = OpusTiming::of(&o.streams[0], 312);
|
||||
let t = OpusTiming::of(&o.streams[0], 312).unwrap();
|
||||
// the granules after the gap put the later packets at their true positions
|
||||
assert_eq!(*t.starts.last().unwrap(), 11 * 960 - 312);
|
||||
for cut in 0..f.len() {
|
||||
if let Ok(o) = open(&f[..cut].to_vec()) {
|
||||
for st in &o.streams {
|
||||
let t = OpusTiming::of(st, 312);
|
||||
let t = OpusTiming::of(st, 312).unwrap();
|
||||
assert_eq!(t.starts.len(), st.packets.len());
|
||||
}
|
||||
}
|
||||
@@ -179,3 +179,20 @@ fn opus_toc_durations() {
|
||||
assert_eq!(opus_packet_samples([0xF8 | 3, 7], 10), None); // 140 ms: invalid
|
||||
assert_eq!(opus_packet_samples([0xF8, 0], 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opus_timing_rejects_a_granule_that_overflows() {
|
||||
let packet = |granule: i64| Stream {
|
||||
serial: 1,
|
||||
codec: Codec::Opus,
|
||||
headers: vec![],
|
||||
packets: vec![Packet { parts: vec![], size: 10, granule: Some(granule), page: 0, eos: true, prefix: [0x00, 0] }],
|
||||
pages: 1,
|
||||
last_granule: Some(granule),
|
||||
saw_eos: true,
|
||||
};
|
||||
let ok = OpusTiming::of(&packet(480), 0).unwrap();
|
||||
assert_eq!(ok.starts, vec![0]);
|
||||
assert_eq!(ok.total, 480);
|
||||
assert!(OpusTiming::of(&packet(i64::MIN), 0).is_err());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user