Compare commits

..
6 changed files with 48 additions and 35 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ pub fn decode(bytes: &Bytes) -> Result<Pcm> {
Some(r) => r,
None => contained(|| decode_symphonia(bytes, "mkv")),
},
Container::Mp4 if !crate::mp4guard::tables_fit(bytes) => Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())),
_ if !crate::mp4guard::probe_fits(bytes) => Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())),
c => contained(|| decode_symphonia(bytes, c.name())),
}
}
+40
View File
@@ -12,6 +12,20 @@ pub(crate) fn tables_fit(b: &[u8]) -> bool {
boxes_fit(b, b.len() as u64, 0)
}
/// Bytes symphonia's probe scans for a format marker before giving up.
const PROBE_SEARCH_LIMIT: usize = 1024 * 1024;
/// Whether symphonia can be given `b`, whatever the sniffer made of it: its probe takes the first
/// `ftyp` within its search window as the start of an MP4 (the size field 4 bytes before it), so
/// the table check must start where that parser will, not at offset 0.
pub(crate) fn probe_fits(b: &[u8]) -> bool {
let head = &b[..b.len().min(PROBE_SEARCH_LIMIT + 16)];
match head.windows(4).position(|w| w == b"ftyp") {
Some(p) => tables_fit(&b[p.saturating_sub(4)..]),
None => true,
}
}
fn be32(b: &[u8], at: usize) -> Option<u64> {
b.get(at..at.checked_add(4)?).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as u64)
}
@@ -74,3 +88,29 @@ fn table_fits(kind: &[u8], body: &[u8], file_len: u64) -> bool {
};
be32(body, 4).is_some_and(|count| count <= body.len().saturating_sub(8) as u64 / entry)
}
#[cfg(test)]
mod tests {
use super::*;
/// An MP4 whose `stsc` count asks for far more entries than the file holds.
fn mp4_with_huge_stsc() -> Vec<u8> {
let stsc = [&24u32.to_be_bytes()[..], b"stsc", &[0, 0, 0, 0], &u32::MAX.to_be_bytes(), &[0; 8]].concat();
let stbl = [&(stsc.len() as u32 + 8).to_be_bytes()[..], b"stbl", &stsc].concat();
let moov = [&(stbl.len() as u32 + 8).to_be_bytes()[..], b"moov", &stbl].concat();
[&16u32.to_be_bytes()[..], b"ftyp", b"isom", &[0; 4], &moov].concat()
}
#[test]
fn a_prefixed_mp4_is_checked_where_symphonia_starts_it() {
let mp4 = mp4_with_huge_stsc();
assert!(!tables_fit(&mp4));
assert!(!probe_fits(&mp4));
let prefixed = [&b"junk before the file "[..], &mp4].concat();
assert_eq!(crate::Container::sniff(&prefixed), crate::Container::Unknown);
assert!(tables_fit(&prefixed), "offset 0 sees no MP4 boxes");
assert!(!probe_fits(&prefixed));
let far = [&vec![0u8; PROBE_SEARCH_LIMIT + 16][..], &mp4].concat();
assert!(probe_fits(&far), "past the probe window symphonia never reads it as an MP4");
}
}
+1 -1
View File
@@ -71,7 +71,7 @@ pub fn probe_headers(bytes: &Bytes) -> Result<MediaInfo> {
};
if info.audio.is_none() && info.video.is_none() || info.audio.as_ref().is_some_and(|a| a.sample_rate == 0) {
// Plain audio files (and anything our demuxers didn't describe): symphonia's probe.
if c == Container::Mp4 && !crate::mp4guard::tables_fit(bytes) {
if !crate::mp4guard::probe_fits(bytes) {
return Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into()));
}
let (codec, rate, channels, frames) = std::panic::catch_unwind(|| crate::audio::probe_symphonia(bytes, c.name()))
-5
View File
@@ -46,9 +46,6 @@ pub struct Imp<'a> {
pub tag_bytes: usize,
/// The tag budget ran out and the warning was given.
pub tags_capped: bool,
/// Whether a part has relationships of its own, by normalized part name: looked up once
/// however many embedded objects point at the part.
pub has_rels: HashMap<String, bool>,
}
impl Imp<'_> {
@@ -449,7 +446,6 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
tags_by_part: HashMap::new(),
tag_bytes: 0,
tags_capped: false,
has_rels: HashMap::new(),
};
let part = Part::new(&pkg, &theme_path, &doc);
let theme = theme::read_theme(&mut imp, &part, &doc.root);
@@ -491,7 +487,6 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
tags_by_part: HashMap::new(),
tag_bytes: 0,
tags_capped: false,
has_rels: HashMap::new(),
};
// Slide ids first so hyperlinks anywhere can point at slides.
let slide_ids = register_slides(&mut imp, &docs, &slide_entries, &mut lost_slides);
+5 -26
View File
@@ -627,22 +627,12 @@ fn push_text_runs(para: &mut Paragraph, props: RunProps, text: &str) {
/// Appends an Office Math (a14:m) run: its plain text and its OMML.
fn push_math_run(para: &mut Paragraph, part: &Part, e: &El) {
if para.runs.len() < MAX_PARAGRAPH_RUNS {
// The text of the outermost `t` elements: a `t` nested in another is already part of
// the outer one's text, and taking it again would copy it once per nesting level.
fn outer_t(e: &El, depth: usize, text: &mut String) {
if depth > crate::xml::MAX_DEPTH {
return;
}
for c in e.elements() {
if c.is("t") {
text.push_str(&c.text());
} else {
outer_t(c, depth + 1, text);
}
}
}
let mut text = String::new();
outer_t(e, 0, &mut text);
let mut ts = vec![];
e.find_all("t", &mut ts);
for t in ts {
text.push_str(&t.text());
}
let omml = e.elements().map(|c| part.keep_inner(c)).collect::<String>();
para.runs.push(Run { text, props: RunProps::default(), kind: RunKind::Math { omml } });
}
@@ -687,17 +677,6 @@ mod tests {
use super::*;
use crate::xml::parse;
#[test]
fn nested_math_text_is_taken_once() {
// 200 `m:t` nested in each other around one word: the word, not 200 copies of it.
let x = format!(r#"<a14:m xmlns:a14="a" xmlns:m="m"><m:oMath>{}word{}</m:oMath></a14:m>"#, "<m:t>".repeat(200), "</m:t>".repeat(200));
let d = parse(x.as_bytes()).unwrap();
let part = Part { name: "ppt/slides/slide1.xml".into(), rels: Default::default(), ns: vec![], inner_ns: vec![] };
let mut para = Paragraph::default();
push_math_run(&mut para, &part, &d.root);
assert_eq!(para.runs[0].text, "word");
}
#[test]
fn colors_and_transforms() {
let d = parse(
+1 -2
View File
@@ -475,8 +475,7 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S
.into_iter()
.map(|id| {
let rel = part.rels.get(&id).filter(|r| !r.external)?;
let pkg = imp.pkg;
if *imp.has_rels.entry(crate::opc::norm(&rel.target)).or_insert_with(|| !pkg.rels(&rel.target).items.is_empty()) {
if !imp.pkg.rels(&rel.target).items.is_empty() {
return None;
}
let (kind, target) = (rel.ty.clone(), rel.target.clone());