mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 20:43:12 +00:00
M11: 6.1.13 also reports reals past ±3.403e38 and CMaps mapping to CIDs above 65535
Finishes the clause. Table C.1 of ISO 32000-1 Annex C bounds real values at about ±3.403 × 10^38 and character identifiers at 65535. A real past that range (an integer token too long even for a real reads as one) is reported, and so is an embedded CMap of a Type 0 font, or one it uses through /UseCMap, whose begincidrange entry reaches past CID 65535 or whose begincidchar entry names one (ISO 32000-1 9.7.5.4). A CMap named rather than embedded is a predefined one, within the limit. The CMaps are decoded within one 16 MiB allowance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
1 parent
31e67cbfec
commit
8d948e1dd0
3 files changed
+113
-5
No files matched your search
@@ -117,6 +117,8 @@ pub const WORDINGS: &[&str] = &[
|
||||
// The document as a whole.
|
||||
"The document is encrypted",
|
||||
"An integer is outside the 32-bit range",
|
||||
"A real number is outside ±3.403 × 10³⁸",
|
||||
"A CMap maps a character code to a CID above 65535",
|
||||
"A string is longer than 32767 bytes",
|
||||
"A name is longer than 127 bytes",
|
||||
"Page {} has a boundary smaller than 3 or larger than 14400 units",
|
||||
@@ -525,9 +527,15 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
if over.int {
|
||||
issue("6.1.13", "An integer is outside the 32-bit range", None, None, false);
|
||||
}
|
||||
if over.real {
|
||||
issue("6.1.13", "A real number is outside ±3.403 × 10³⁸", None, None, false);
|
||||
}
|
||||
if over.string {
|
||||
issue("6.1.13", "A string is longer than 32767 bytes", None, None, false);
|
||||
}
|
||||
if limits::cid_over_limit(doc, &objs) {
|
||||
issue("6.1.13", "A CMap maps a character code to a CID above 65535", None, None, false);
|
||||
}
|
||||
if over.name {
|
||||
issue("6.1.13", "A name is longer than 127 bytes", None, None, false);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//! a name written with `#xx` escapes counts its decoded bytes. A page's `/Contents` streams are read
|
||||
//! joined, as one content stream (an operator's operands may end one stream and the operator start
|
||||
//! the next, ISO 32000-1 7.8.2). Content is decoded within one allowance for the whole document.
|
||||
//! Character identifiers above 65535 (in a CMap) and the real-number range are not checked here,
|
||||
//! nor an integer token too long for 64 bits, which the parser reads as a real.
|
||||
//! Real numbers beyond ±3.403 × 10³⁸ are reported, and so is an embedded CMap of a Type 0 font
|
||||
//! (or one it uses through `/UseCMap`) that maps a code to a CID above 65535. An integer token too
|
||||
//! long even for a real reads as one past that range, and is reported as a real.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -16,6 +17,14 @@ use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object};
|
||||
/// Table C.1: the smallest and largest integer values.
|
||||
const MIN_INT: i64 = i32::MIN as i64;
|
||||
const MAX_INT: i64 = i32::MAX as i64;
|
||||
/// Table C.1: the largest real value (approximate).
|
||||
const MAX_REAL: f64 = 3.403e38;
|
||||
/// Table C.1: the largest character identifier.
|
||||
const MAX_CID: u64 = 65535;
|
||||
/// The decoded CMap data, across all the document's embedded CMaps, the scan reads.
|
||||
const MAX_CMAP_BYTES: usize = 16 << 20;
|
||||
/// How many CMaps a `/UseCMap` chain is followed through.
|
||||
const MAX_CMAP_CHAIN: usize = 8;
|
||||
/// Table C.1: the longest string, in bytes.
|
||||
const MAX_STRING: usize = 32767;
|
||||
/// Table C.1: the longest name, in bytes.
|
||||
@@ -35,13 +44,14 @@ const BOXES: [&[u8]; 5] = [b"MediaBox", b"CropBox", b"BleedBox", b"TrimBox", b"A
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct Exceeded {
|
||||
pub(crate) int: bool,
|
||||
pub(crate) real: bool,
|
||||
pub(crate) string: bool,
|
||||
pub(crate) name: bool,
|
||||
}
|
||||
|
||||
impl Exceeded {
|
||||
fn all(self) -> bool {
|
||||
self.int && self.string && self.name
|
||||
self.int && self.real && self.string && self.name
|
||||
}
|
||||
|
||||
fn name(&mut self, n: &[u8]) {
|
||||
@@ -55,6 +65,7 @@ impl Exceeded {
|
||||
}
|
||||
match o {
|
||||
Object::Int(i) => self.int |= !(MIN_INT..=MAX_INT).contains(i),
|
||||
Object::Real(r) => self.real |= !r.is_finite() || r.abs() > MAX_REAL,
|
||||
Object::String(s) => self.string |= s.bytes.len() > MAX_STRING,
|
||||
Object::Name(n) => self.name(n),
|
||||
Object::Array(a) => {
|
||||
@@ -64,7 +75,7 @@ impl Exceeded {
|
||||
}
|
||||
Object::Dict(d) => self.dict(d, depth),
|
||||
Object::Stream(s) => self.dict(&s.dict, depth),
|
||||
Object::Null | Object::Bool(_) | Object::Real(_) | Object::Ref(_) => {}
|
||||
Object::Null | Object::Bool(_) | Object::Ref(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,3 +172,52 @@ pub(crate) fn page_box_out_of_range(doc: &Document, page: &Dict) -> bool {
|
||||
[w, h].iter().any(|s| !(MIN_PAGE..=MAX_PAGE).contains(s))
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether an embedded CMap of a Type 0 font among `objs`, or one it uses through `/UseCMap`, maps
|
||||
/// a code to a CID above 65535: a `begincidrange` entry `<low> <high> cid` reaches
|
||||
/// `cid + high − low`, a `begincidchar` entry `<code> cid` reaches `cid` (ISO 32000-1 9.7.5.4). A
|
||||
/// CMap named rather than embedded is a predefined one, within the limit. The CMaps are decoded
|
||||
/// within one allowance for the document.
|
||||
pub(crate) fn cid_over_limit(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)]) -> bool {
|
||||
let mut budget = DecodeBudget::new(MAX_CMAP_BYTES, 0);
|
||||
let mut read: HashSet<ObjRef> = HashSet::new();
|
||||
for (_, o) in objs {
|
||||
let Some(d) = o.as_dict() else { continue };
|
||||
if d.name(b"Subtype") != Some(b"Type0") {
|
||||
continue;
|
||||
}
|
||||
let mut next = d.get(b"Encoding").and_then(Object::as_ref);
|
||||
for _ in 0..MAX_CMAP_CHAIN {
|
||||
let Some(r) = next.filter(|r| read.insert(*r)) else { break };
|
||||
if budget.remaining() == 0 {
|
||||
return false;
|
||||
}
|
||||
let Object::Stream(cmap) = &*doc.get(r) else { break };
|
||||
if budget.decode(cmap).is_ok_and(|data| maps_past_limit(&data)) {
|
||||
return true;
|
||||
}
|
||||
next = cmap.dict.get(b"UseCMap").and_then(Object::as_ref);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether the CMap program `data` maps a code to a CID above [`MAX_CID`]. The entries of a
|
||||
/// `begincidrange` … `endcidrange` block are the operands of its `endcidrange`, read as content is.
|
||||
fn maps_past_limit(data: &[u8]) -> bool {
|
||||
let value = |o: &Object| -> Option<u64> {
|
||||
match o {
|
||||
Object::String(s) if s.bytes.len() <= 4 => Some(s.bytes.iter().fold(0u64, |v, b| (v << 8) | u64::from(*b))),
|
||||
Object::Int(i) => u64::try_from(*i).ok(),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
pdfcraft_content::ops(data).any(|op| match op.op.as_slice() {
|
||||
b"endcidrange" => op.operands.as_chunks::<3>().0.iter().any(|[low, high, cid]| match (value(low), value(high), value(cid)) {
|
||||
(Some(low), Some(high), Some(cid)) => cid.saturating_add(high.saturating_sub(low)) > MAX_CID,
|
||||
_ => false,
|
||||
}),
|
||||
b"endcidchar" => op.operands.as_chunks::<2>().0.iter().any(|[_, cid]| value(cid).is_some_and(|cid| cid > MAX_CID)),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
@@ -566,5 +566,45 @@ endstream",
|
||||
assert_eq!(limits(&page(&format!("BT ({long})|Tj ET"), "", ok, "")), ["A string is longer than 32767 bytes"]);
|
||||
let deep = format!("/Deep {}2147483648{}", "[".repeat(40), "]".repeat(40));
|
||||
assert_eq!(limits(&page("|", &deep, ok, "")), ["An integer is outside the 32-bit range"]);
|
||||
assert_eq!(limits(&page("|", "", "[-1e308 0 1e308 200]", "")), ["Page 1 has a boundary smaller than 3 or larger than 14400 units"]);
|
||||
// Corners near ±1e308 are reals past the limit too, and the width overflows to infinity.
|
||||
assert_eq!(
|
||||
limits(&page("|", "", "[-1e308 0 1e308 200]", "")),
|
||||
["A real number is outside ±3.403 × 10³⁸", "Page 1 has a boundary smaller than 3 or larger than 14400 units"]
|
||||
);
|
||||
}
|
||||
|
||||
/// 6.1.13's two remaining limits of ISO 32000-1 Annex C: a real number beyond ±3.403 × 10³⁸, and
|
||||
/// a CID above 65535 that an embedded CMap of a Type 0 font maps to (a `begincidrange` reaching
|
||||
/// past it, a `begincidchar` naming one, or a CMap it uses through `/UseCMap`).
|
||||
#[test]
|
||||
fn implementation_limits_cover_reals_and_cids() {
|
||||
let limits =
|
||||
|doc: &Document| -> Vec<String> { verify(doc, Level::A2b).into_iter().filter(|i| i.clause == "6.1.13").map(|i| i.message).collect() };
|
||||
let stream = |dict: &str, data: &str| format!("<< {dict} /Length {} >>\nstream\n{data}\nendstream", data.len());
|
||||
let doc = |catalog_extra: &str, cmap: &str, used: &str| {
|
||||
let first = stream("/Type /CMap /CMapName /Test-A /UseCMap 6 0 R", cmap);
|
||||
let second = stream("/Type /CMap /CMapName /Test-B", used);
|
||||
build(
|
||||
&[
|
||||
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
|
||||
"<< /Type /Page /Parent 2 0 R /Resources << /Font << /F 4 0 R >> >> >>",
|
||||
"<< /Type /Font /Subtype /Type0 /BaseFont /Test /Encoding 5 0 R /DescendantFonts [] >>",
|
||||
&first,
|
||||
&second,
|
||||
],
|
||||
"",
|
||||
)
|
||||
};
|
||||
let range = |low: &str, high: &str, cid: u32| format!("1 begincidrange <{low}> <{high}> {cid} endcidrange");
|
||||
let near = format!("34{}.0", "0".repeat(37));
|
||||
let far = format!("4{}.0", "0".repeat(38));
|
||||
assert!(limits(&doc(&format!("/R {near} /S -{near}"), &range("0000", "00ff", 65280), "")).is_empty(), "both at their limits");
|
||||
assert_eq!(limits(&doc(&format!("/R {far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸"]);
|
||||
assert_eq!(limits(&doc(&format!("/R -{far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸"]);
|
||||
let cid = ["A CMap maps a character code to a CID above 65535"];
|
||||
assert_eq!(limits(&doc("", &range("3f00", "3fff", 65536), "")), cid);
|
||||
assert_eq!(limits(&doc("", &range("0000", "0100", 65280), "")), cid, "the range reaches 65536");
|
||||
assert_eq!(limits(&doc("", "1 begincidchar <01> 70000 endcidchar", "")), cid);
|
||||
assert_eq!(limits(&doc("", &range("0000", "00ff", 0), &range("3f00", "3fff", 65536))), cid, "through /UseCMap");
|
||||
}
|
||||
Reference in new issue
Block a user