diff --git a/crates/pdf/src/font.rs b/crates/pdf/src/font.rs index 296878c3..2c27838a 100644 --- a/crates/pdf/src/font.rs +++ b/crates/pdf/src/font.rs @@ -263,8 +263,8 @@ fn parse_cmap(data: &[u8]) -> CMap { } Obj::Array(list) => { for (k, s) in list.iter().enumerate() { - if let Obj::Str(s) = s { - m.unicode.insert(lo + k as u32, utf16(s)); + if let (Obj::Str(s), Some(c)) = (s, lo.checked_add(k as u32)) { + m.unicode.insert(c, utf16(s)); } } } @@ -401,13 +401,15 @@ impl Font { match w.get(i + 1).map(|x| file.resolve(x)) { Some(Obj::Array(list)) => { for (k, v) in list.iter().enumerate() { - f.cid_widths.insert(first + k as u32, file.resolve(v).num().unwrap_or(0.0) / 1000.0); + if let Some(cid) = first.checked_add(k as u32) { + f.cid_widths.insert(cid, file.resolve(v).num().unwrap_or(0.0) / 1000.0); + } } i += 2; } Some(Obj::Num(last)) => { let wv = w.get(i + 2).and_then(|x| file.resolve(x).num()).unwrap_or(0.0) / 1000.0; - for c in first..=(*last as u32).min(first + 65535) { + for c in first..=(*last as u32).min(first.saturating_add(65535)) { f.cid_widths.insert(c, wv); } i += 3; @@ -432,13 +434,15 @@ impl Font { Some(Obj::Array(list)) => { let v: Vec = list.iter().filter_map(|x| file.resolve(x).num()).collect(); for (k, t) in v.as_chunks::<3>().0.iter().enumerate() { - f.cid_vmetrics.insert(first + k as u32, (t[0] / 1000.0, t[1] / 1000.0, t[2] / 1000.0)); + if let Some(cid) = first.checked_add(k as u32) { + f.cid_vmetrics.insert(cid, (t[0] / 1000.0, t[1] / 1000.0, t[2] / 1000.0)); + } } i += 2; } Some(Obj::Num(last)) => { let (Some(w1), Some(vx), Some(vy)) = (num(i + 2), num(i + 3), num(i + 4)) else { break }; - for c in first..=(last.max(0.0) as u32).min(first + 65535) { + for c in first..=(last.max(0.0) as u32).min(first.saturating_add(65535)) { f.cid_vmetrics.insert(c, (w1 / 1000.0, vx / 1000.0, vy / 1000.0)); } i += 5; @@ -609,7 +613,7 @@ impl Font { if self.cid_ranges.is_empty() { return code; } - self.cid_ranges.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map(|(lo, _, c)| c + (code - lo)).unwrap_or(0) + self.cid_ranges.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).and_then(|(lo, _, c)| c.checked_add(code - lo)).unwrap_or(0) } /// Vertical metrics of a code in text space (before the font size): the vertical diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index f2eff046..b15996a1 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -1090,3 +1090,47 @@ fn retained_layers_preserve_artwork_beyond_the_bounds_depth_limit() { assert_eq!(cropped.pixel_size(), (120, 80), "unknown bounds keep the full page"); assert_eq!(offset, [0.0, 0.0]); } + +#[test] +fn cid_width_entries_at_the_top_of_the_cid_range_do_not_overflow() { + // A first CID of u32::MAX (larger values saturate to it) used to overflow `first + k` and `first + 65535`: + // a panic in debug builds; in release builds the list wrapped round to CID 0 and the range was dropped. + let load = |entry: &str| { + let type0 = "<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H /DescendantFonts [13 0 R] >>"; + let desc = format!("<< /Type /Font /Subtype /CIDFontType2 {entry} >>"); + let file = object::File::parse(&page_pdf("", "", vec![(10, type0.into(), None), (13, desc, None)])).unwrap(); + let d = file.objects.get(&(10, 0)).and_then(object::Obj::dict).unwrap(); + font::Font::load(&file, d, &mut vec![]) + }; + let max = u32::MAX; + // c [w1 w2 ...] + let f = load("/W [4294967295 [500 600 700]]"); + assert_eq!((f.width(max), f.width(0)), (0.5, 1.0)); + // cfirst clast w + assert_eq!(load("/W [4294967295 4294967295 400]").width(max), 0.4); + // c [w1y vx vy ...] + let f = load("/W2 [4294967295 [1 2 3 4 5 6]]"); + assert_eq!((f.vmetrics(max), f.vmetrics(0)), ((0.001, 0.002, 0.003), (-1.0, 0.5, 0.88))); + // cfirst clast w1y vx vy + assert_eq!(load("/W2 [4294967295 4294967295 1 2 3]").vmetrics(max), (0.001, 0.002, 0.003)); +} + +#[test] +fn cmap_entries_at_the_top_of_the_code_range_do_not_overflow() { + // A bfrange array starting at code 0xFFFFFFFF and a cidrange mapping onto CID u32::MAX used to overflow + // `lo + k` and `c + (code - lo)`: a panic in debug builds, a wrap round to code / CID 0 in release builds. + let to_unicode = "beginbfrange [<0041> <0042>] endbfrange"; + let encoding = "begincodespacerange <0000> endcodespacerange begincidrange <0000> 4294967295 endcidrange"; + let objs: Objs = vec![ + (10, "<< /Type /Font /Subtype /Type0 /Encoding 15 0 R /ToUnicode 14 0 R /DescendantFonts [13 0 R] >>".into(), None), + (13, "<< /Type /Font /Subtype /CIDFontType2 /W [0 [700 800] 4294967295 [500]] >>".into(), None), + (14, "<< >>".into(), Some(to_unicode.as_bytes().to_vec())), + (15, "<< /Type /CMap >>".into(), Some(encoding.as_bytes().to_vec())), + ]; + let file = object::File::parse(&page_pdf("", "", objs)).unwrap(); + let d = file.objects.get(&(10, 0)).and_then(object::Obj::dict).unwrap(); + let f = font::Font::load(&file, d, &mut vec![]); + assert_eq!((f.unicode(0xFFFF_FFFF), f.unicode(0)), ("A".to_string(), String::new())); + // Code 0 is CID u32::MAX; code 2 would be past it, so it falls back to CID 0 like an unmapped code. + assert_eq!((f.width(0), f.width(2)), (0.5, 0.7)); +}