mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 01:41:18 +00:00
M7: a font's stream behind several filters reads as it did without an allowance
A DecodeBudget bounds what a stream's filter stages make together, and the first commit passed it each stream's own limit, so a /CIDToGIDMap of 65,536 CIDs behind ASCIIHex and Flate (131,072 bytes, with the hex stage's output on top) went unread, where a decode without an allowance bounds each stage alone and read it. Every Metrics::from_dict caller saw the font as missing those glyphs. The 16 MiB limits on CMaps and programs had the same edge. Each stage may now make up to the stream's own limit, as before, and the allowance pays for all of them; what the stream decodes to stays within its limit. Found by the Consumers review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
9bc24aadff
commit
ade9877370
2 files changed
+46
-2
No files matched your search
@@ -316,9 +316,16 @@ impl<'a> Reads<'a> {
|
||||
|
||||
/// `stream` decoded within `max` and the allowance; `None` when it can't be, noting when the
|
||||
/// allowance is why. A stream past `max` is unread as before allowances, not refused.
|
||||
///
|
||||
/// `max` bounds what the stream decodes to. Its filter stages are paid for together, up to
|
||||
/// `max` for each, the most a decode without an allowance lets them make: a map of 65,536 CIDs
|
||||
/// behind ASCIIHex and Flate makes all of `MAX_CID_TO_GID` in its last stage, with the hex
|
||||
/// stage's output on top.
|
||||
fn decode(&mut self, stream: &Stream, max: usize) -> Option<Vec<u8>> {
|
||||
match self.budget.decode_within(stream, max) {
|
||||
Ok(data) => Some(data),
|
||||
let stages = stream.filters().len().max(1);
|
||||
match self.budget.decode_within(stream, max.saturating_mul(stages)) {
|
||||
Ok(data) if data.len() <= max => Some(data),
|
||||
Ok(_) => None,
|
||||
Err(_) => {
|
||||
self.refused |= self.budget.remaining() == 0;
|
||||
None
|
||||
|
||||
@@ -297,6 +297,43 @@ fn a_cmap_past_its_own_limit_is_not_past_the_allowance() {
|
||||
assert_eq!(usize::MAX / 2 - budget.remaining(), sizes[..3].iter().sum::<usize>() + MAX_CMAP_BYTES);
|
||||
}
|
||||
|
||||
/// A `/CIDToGIDMap` as long as a map can be reads the same behind no filter, one or two: each
|
||||
/// stage may make as much as the map's limit, so the hex stage's output on top of it doesn't
|
||||
/// leave it unread. A map past the limit is unread behind any of them.
|
||||
#[test]
|
||||
fn a_full_cid_map_reads_behind_any_number_of_filters() {
|
||||
let read = |entries: usize, filters: usize| {
|
||||
let mut doc = Document::new_empty();
|
||||
let (mut f, _) = subset_type0(&mut doc, b"");
|
||||
let map: Vec<u8> = (0..entries).flat_map(|cid| u16::try_from(cid % 2000).unwrap().to_be_bytes()).collect();
|
||||
let flate = Stream::flate(Dict::new(), &map);
|
||||
let stream = match filters {
|
||||
0 => Stream::from_raw(Dict::new(), map),
|
||||
1 => flate,
|
||||
_ => {
|
||||
let hex: String = flate.raw.iter().map(|b| format!("{b:02X}")).chain([">".to_string()]).collect();
|
||||
let mut dict = Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::Array(vec![Object::name("ASCIIHexDecode"), Object::name("FlateDecode")]));
|
||||
Stream::from_raw(dict, hex.into_bytes())
|
||||
}
|
||||
};
|
||||
let map = Object::Ref(doc.add(Object::Stream(stream)));
|
||||
let Some(Object::Array(mut descendants)) = f.get(b"DescendantFonts").cloned() else { panic!("descendant") };
|
||||
if let Some(Object::Dict(d)) = descendants.get_mut(0) {
|
||||
d.set(b"CIDToGIDMap".to_vec(), map);
|
||||
}
|
||||
f.set(b"DescendantFonts".to_vec(), Object::Array(descendants));
|
||||
Metrics::from_dict(&doc, &f).outlined_cids
|
||||
};
|
||||
let full = read(1 << 16, 1);
|
||||
assert!(!full.is_empty());
|
||||
assert_eq!(read(1 << 16, 0), full);
|
||||
assert_eq!(read(1 << 16, 2), full);
|
||||
for filters in 0..=2 {
|
||||
assert!(read((1 << 16) + 1, filters).is_empty(), "{filters} filters");
|
||||
}
|
||||
}
|
||||
|
||||
/// Fonts that each name one large ToUnicode share the allowance they are read within: it pays for
|
||||
/// as many readings as it holds, and the rest are left unread, where each reading used to decode
|
||||
/// it afresh.
|
||||
|
||||
Reference in new issue
Block a user