From 45be5718b99c248efa1b0e0f1807da84231166d6 Mon Sep 17 00:00:00 2001 From: Abimael Martell <1450169+abimaelmartell@users.noreply.github.com> Date: Thu, 13 Aug 2026 08:47:44 -0700 Subject: [PATCH] fix(extractor): document bfrange remap truncation and assert visit count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 65,536 cap counts overwrites so repeated ranges cannot keep expanding. The test now checks the assignment count, not just HashMap size (u16 keys are always ≤ 65,536). Co-authored-by: Cursor --- src/tounicode.rs | 64 +++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/src/tounicode.rs b/src/tounicode.rs index 33cbbfb..799fe45 100644 --- a/src/tounicode.rs +++ b/src/tounicode.rs @@ -540,28 +540,14 @@ impl ToUnicodeCMap { /// Remap a CMap that references pre-subsetting GIDs to sequential post-subsetting GIDs. /// Collects all source CIDs, sorts them, and reassigns to 1, 2, 3, ... + /// + /// Range expansion stops after `MAX_CID_W_EXPANSION` CID visits, counting + /// overwrites, so repeated full-width `bfrange`s cannot re-expand the + /// 16-bit domain. Later overlapping ranges that would have introduced new + /// CIDs after that many visits are truncated. pub fn remap_to_sequential(&self) -> ToUnicodeCMap { let mut cid_to_unicode: HashMap = HashMap::new(); - let mut assigned = 0usize; - - // Expand ranges first. Count every CID considered, including - // overwrites, so repeating a full-width bfrange cannot re-expand the - // 16-bit domain. - 'ranges: for &(start, end, base) in &self.ranges { - if start > end { - continue; - } - for cid in start..=end { - if assigned >= MAX_CID_W_EXPANSION { - break 'ranges; - } - assigned += 1; - let unicode_cp = base + (cid - start) as u32; - if let Some(ch) = char::from_u32(unicode_cp) { - cid_to_unicode.insert(cid, ch.to_string()); - } - } - } + expand_bfranges_for_remap(&self.ranges, &mut cid_to_unicode, MAX_CID_W_EXPANSION); // char_map entries override range entries for (&cid, unicode) in &self.char_map { @@ -586,6 +572,33 @@ impl ToUnicodeCMap { } } +/// Expand `bfrange` entries into individual CID→Unicode inserts. +/// Returns how many CIDs were visited. Counts overwrites so a repeated +/// full-width range cannot keep working after `max_assignments`. +fn expand_bfranges_for_remap( + ranges: &[(u16, u16, u32)], + cid_to_unicode: &mut HashMap, + max_assignments: usize, +) -> usize { + let mut assigned = 0usize; + 'ranges: for &(start, end, base) in ranges { + if start > end { + continue; + } + for cid in start..=end { + if assigned >= max_assignments { + break 'ranges; + } + assigned += 1; + let unicode_cp = base + (cid - start) as u32; + if let Some(ch) = char::from_u32(unicode_cp) { + cid_to_unicode.insert(cid, ch.to_string()); + } + } + } + assigned +} + /// Parse a hex string to u16 fn parse_hex_u16(hex: &str) -> Option { u16::from_str_radix(hex.trim(), 16).ok() @@ -2936,8 +2949,14 @@ endbfrange #[test] fn remap_to_sequential_repeated_full_bfranges_stay_bounded() { - // 5,000 copies of `<0003> <0041>` must not re-expand the - // 16-bit domain on every declaration during subset remap. + // 5,000 copies of `<0003> ` must stop after 65,536 CID visits, + // not 5,000 × ~65,533 expansions. + let ranges = vec![(3u16, 65535u16, 0x41u32); 5_000]; + let mut map = std::collections::HashMap::new(); + let assigned = expand_bfranges_for_remap(&ranges, &mut map, MAX_CID_W_EXPANSION); + assert_eq!(assigned, MAX_CID_W_EXPANSION); + assert!(map.len() <= MAX_CID_W_EXPANSION); + let mut body = String::new(); let mut remaining = 5_000usize; while remaining > 0 { @@ -2952,7 +2971,6 @@ endbfrange let data = format!("1 begincodespacerange\n<0000> \nendcodespacerange\n{body}"); let cmap = ToUnicodeCMap::parse(data.as_bytes()).unwrap(); let remapped = cmap.remap_to_sequential(); - assert!(remapped.char_map.len() <= MAX_CID_W_EXPANSION); assert_eq!(remapped.lookup(1), Some("A".to_string())); }