diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index c0ae9e8765..b960909bfb 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -115,7 +115,20 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg let Some(object) = descriptor.get(key) else { continue }; let object = doc.resolve(object); let Object::Stream(stream) = object.as_ref() else { continue }; - let Ok(mut bytes) = budget.decode_within(&doc.with_direct_parms(stream), MAX_SFNT) else { continue }; + // As in `Reads::decode`: once the allowance is spent the decode refuses a program with + // data, so it is passed over before its parameters are resolved, work the allowance doesn't + // count; an empty one is no program either way. + if budget.remaining() == 0 { + continue; + } + let stream = doc.with_direct_parms(stream); + // As in `Reads::decode`: the stages share the limit once for each of them, which any one + // may use, and the program itself must be within it. + let stages = stream.filters().len().max(1); + let Ok(mut bytes) = budget.decode_within(&stream, MAX_SFNT.saturating_mul(stages)) else { continue }; + if bytes.len() > MAX_SFNT { + continue; + } let Ok(face) = skrifa::FontRef::from_index(&bytes, 0) else { continue }; if face.metrics(skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default()).units_per_em == 0 { continue; @@ -328,6 +341,12 @@ impl<'a> Reads<'a> { /// round. A stream past `max` is unread, not refused, when what is left can pay for its stages; /// when it can't, the stream spends it and counts as refused. fn decode(&mut self, stream: &Stream, max: usize) -> Option> { + // Once the allowance is spent the decode refuses the stream; refused here, before the + // parameters are resolved, which is work the allowance doesn't count. + if self.budget.remaining() == 0 && !stream.raw.is_empty() { + self.refused = true; + return None; + } let stream = self.doc.with_direct_parms(stream); let stages = stream.filters().len().max(1); match self.budget.decode_within(&stream, max.saturating_mul(stages)) { diff --git a/crates/fonts/src/pdf/tests.rs b/crates/fonts/src/pdf/tests.rs index e770cd60c2..301d90be77 100644 --- a/crates/fonts/src/pdf/tests.rs +++ b/crates/fonts/src/pdf/tests.rs @@ -282,6 +282,20 @@ fn a_fonts_streams_are_paid_for_from_its_allowance() { assert_eq!(cost - budget.remaining(), sizes[0] + sizes[3]); } +/// With nothing left, an empty stream still reads, as the allowance reads one, and is no reason to +/// call the font read past it; a stream with data is left unread, and is. +#[test] +fn an_empty_stream_reads_with_nothing_left() { + let past = |data: &[u8]| { + let mut doc = Document::new_empty(); + let unicode = Object::Ref(doc.add(Object::Stream(Stream::from_raw(Dict::new(), data.to_vec())))); + let f = font(&mut doc, vec![("Subtype", Object::name("TrueType")), ("ToUnicode", unicode)]); + Metrics::from_dict_within(&doc, &f, &mut DecodeBudget::new(0, 0)).past_allowance + }; + assert!(!past(b"")); + assert!(past(b"1 beginbfchar <41> <0058> endbfchar")); +} + /// A stream behind one filter past its own limit, with more left than its limit, is left unread as /// it always was, paying for the limit, and is no reason to call the font read past the allowance. #[test] @@ -334,6 +348,50 @@ fn a_full_cid_map_reads_behind_one_or_two_filters() { } } +/// The editor's program is held to its limit as the metrics reads' streams are: a 12 MiB program +/// whose Flate data is about 8 MiB reads behind ASCIIHex and Flate as behind Flate alone, though +/// the two stages make about 20 MiB together. A program of the limit reads behind either, and one a +/// byte past it is unread behind either. +#[test] +fn an_editors_program_reads_behind_ascii_hex_and_flate_within_its_limit() { + let program = |len: usize, noise: usize| { + let mut program = include_bytes!("../../../../assets/fonts/Inter-Regular.ttf").to_vec(); + // Bytes Flate can't shrink, then zeros. + let mut x = 1u32; + program.extend((0..noise).map(|_| { + x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345); + x.to_be_bytes()[0] + })); + program.resize(len, 0); + program + }; + let read = |program: &[u8], hex: bool| { + let mut doc = Document::new_empty(); + let flate = Stream::flate(Dict::new(), program); + let stream = if hex { + let digits = b"0123456789ABCDEF"; + let mut data: Vec = flate.raw.iter().flat_map(|b| [digits[usize::from(b >> 4)], digits[usize::from(b & 15)]]).collect(); + data.push(b'>'); + let mut dict = Dict::new(); + dict.set(b"Filter".to_vec(), Object::Array(vec![Object::name("ASCIIHexDecode"), Object::name("FlateDecode")])); + Stream::from_raw(dict, data) + } else { + flate + }; + let mut descriptor = Dict::new(); + descriptor.set(b"FontFile2".to_vec(), Object::Ref(doc.add(Object::Stream(stream)))); + let simple = font(&mut doc, vec![("FontDescriptor", Object::Dict(descriptor))]); + embedded_sfnt(&doc, &simple) + }; + let near = program(12 << 20, 8 << 20); + let full = program(MAX_SFNT, 0); + for hex in [false, true] { + assert_eq!(read(&near, hex).as_deref(), Some(near.as_slice()), "behind ASCIIHex too: {hex}"); + assert_eq!(read(&full, hex).as_deref(), Some(full.as_slice()), "behind ASCIIHex too: {hex}"); + assert!(read(&program(MAX_SFNT + 1, 0), hex).is_none(), "behind ASCIIHex too: {hex}"); + } +} + /// `data` in rows of `columns` bytes behind Flate and a PNG predictor, each row tagged Up (the /// difference from the row above), so read without the predictor it is garbled; with its /// `/DecodeParms` given by reference or direct.