From 6f5f36136817b16a6162e910e2db39a8d2d00eef Mon Sep 17 00:00:00 2001 From: FredApps Date: Sun, 8 Nov 2026 14:39:32 +0100 Subject: [PATCH] Refuse encoding unverified glyphs after a font decode allowance refusal --- crates/fonts/src/pdf.rs | 8 +++++++- crates/fonts/src/pdf/tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 7972856188..2627e47330 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -979,8 +979,14 @@ impl Metrics { } /// The bytes that show `text` in this font, or `None` if some character has no code or - /// no glyph in it (the caller then substitutes another font). + /// no glyph in it, or the metrics were read past their allowance (the caller then substitutes + /// another font). Empty text needs no verified glyph and always encodes. pub fn encode(&self, text: &str) -> Option> { + // A refused read cannot establish the mappings or glyphs needed to write new text. + // Empty text needs neither and remains usable for erasing existing text. + if self.past_allowance && !text.is_empty() { + return None; + } // The lowest code showing each wanted character. Explicit entries and the encoding are // checked one by one; each range is asked for the wanted characters it can show. let wanted: HashSet = text.chars().collect(); diff --git a/crates/fonts/src/pdf/tests.rs b/crates/fonts/src/pdf/tests.rs index a334ecba6a..f9cd7ec761 100644 --- a/crates/fonts/src/pdf/tests.rs +++ b/crates/fonts/src/pdf/tests.rs @@ -1173,3 +1173,26 @@ fn low_codes_resolve_to_the_covering_range() { assert_eq!(covered, [5, 6, 7, 100, 101]); assert_eq!((u.covering(0xFFFF).is_some(), u.covering(0x1_0001).is_some(), u.covering(0x1_0002).is_none()), (true, true, true)); } + +#[test] +fn refused_subset_reads_do_not_encode_unverified_glyphs() { + let mut doc = Document::new_empty(); + let (mut f, sizes) = subset_type0(&mut doc, b""); + f.set(b"Encoding".to_vec(), Object::name("Identity-H")); + f.set(b"ToUnicode".to_vec(), Object::name("Identity-H")); + if let Some(Object::Array(descendants)) = f.get_mut(b"DescendantFonts") + && let Some(Object::Dict(descendant)) = descendants.first_mut() + { + descendant.set(b"DW".to_vec(), Object::Int(1000)); + } + let whole = Metrics::from_dict(&doc, &f); + assert!(!whole.past_allowance && !whole.outlined_cids.is_empty()); + assert!(whole.encode("\u{ffff}").is_none()); + // Refuse the program, then the CID map after reading the complete program. + for allowance in [0, sizes[1]] { + let partial = Metrics::from_dict_within(&doc, &f, &mut DecodeBudget::new(allowance, 0)); + assert!(partial.past_allowance); + assert_eq!(partial.encode(""), Some(Vec::new())); + assert!(partial.encode("\u{ffff}").is_none(), "allowance {allowance} must not turn /DW into proof of a glyph"); + } +}