mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
M11: 6.1.13 reads the trailer, joined page contents and deep values, within one budget
From the review of #6202: - the trailer is scanned with the indirect objects; - a page's /Contents streams are read joined, as one content stream: operands may end one stream and their operator start the next (ISO 32000-1 7.8.2); - values are followed as deep as the parser nests them (128); - content is decoded within one 256 MiB allowance for the document; - a page box whose size overflows to infinity is reported; - the test covers a long name used as a content operand. 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
7c83ff748e
commit
e21c600cdb
3 files changed
+118
-28
No files matched your search
@@ -521,7 +521,7 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
}
|
||||
check_objects(doc, level, &objs, &page_of, &mut issue);
|
||||
// 6.1.13: the implementation limits of ISO 32000-1 Annex C.
|
||||
let over = limits::exceeded(&objs, &referenced_content(doc, &objs));
|
||||
let over = limits::exceeded(doc, &objs, &referenced_content(doc, &objs));
|
||||
if over.int {
|
||||
issue("6.1.13", "An integer is outside the 32-bit range", None, None, false);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
//! and the page size limits after it): integers that fit in 32 bits, strings of at most 32767
|
||||
//! bytes, names of at most 127 bytes, and page boundaries between 3 and 14400 units wide and high.
|
||||
//!
|
||||
//! The values are read from the indirect objects and from the content streams, as parsed: a name
|
||||
//! written with `#xx` escapes counts its decoded bytes. Character identifiers above 65535 (in a
|
||||
//! CMap) and the real-number range are not checked here.
|
||||
//! The values are read from the trailer, the indirect objects and the content streams, as parsed:
|
||||
//! 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.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object};
|
||||
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;
|
||||
@@ -20,8 +23,11 @@ const MAX_NAME: usize = 127;
|
||||
/// The smallest and largest page boundary, in default user space units.
|
||||
const MIN_PAGE: f64 = 3.0;
|
||||
const MAX_PAGE: f64 = 14400.0;
|
||||
/// How deep a direct object is followed (arrays and dictionaries inside one another).
|
||||
const MAX_DEPTH: u8 = 32;
|
||||
/// How deep a direct object is followed (arrays and dictionaries inside one another): as deep as
|
||||
/// the parser nests them.
|
||||
const MAX_DEPTH: u8 = 128;
|
||||
/// The decoded content, across all the document's content streams, the scan reads.
|
||||
const MAX_CONTENT_BYTES: usize = 256 << 20;
|
||||
/// The page boundaries, of which only the media and crop boxes are inherited.
|
||||
const BOXES: [&[u8]; 5] = [b"MediaBox", b"CropBox", b"BleedBox", b"TrimBox", b"ArtBox"];
|
||||
|
||||
@@ -62,6 +68,21 @@ impl Exceeded {
|
||||
}
|
||||
}
|
||||
|
||||
/// The operands of one content stream, inline images' dictionaries included.
|
||||
fn content(&mut self, data: &[u8]) {
|
||||
for op in pdfcraft_content::ops(data) {
|
||||
if self.all() {
|
||||
return;
|
||||
}
|
||||
for v in &op.operands {
|
||||
self.value(v, 0);
|
||||
}
|
||||
if let Some((image, _)) = &op.inline {
|
||||
self.dict(image, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dict(&mut self, d: &Dict, depth: u8) {
|
||||
for (k, v) in d.iter() {
|
||||
self.name(k);
|
||||
@@ -70,36 +91,64 @@ impl Exceeded {
|
||||
}
|
||||
}
|
||||
|
||||
/// The limits that the indirect objects `objs`, and the content streams among them (`content`:
|
||||
/// the ones only a reference says hold content, besides form XObjects and appearance streams), go
|
||||
/// past.
|
||||
pub(crate) fn exceeded(objs: &[(ObjRef, std::sync::Arc<Object>)], content: &HashSet<ObjRef>) -> Exceeded {
|
||||
/// The limits that the trailer, the indirect objects `objs` and the content streams go past.
|
||||
/// `content` holds the content streams only a reference identifies (a page's `/Contents`, Type 3
|
||||
/// glyph procedures), besides form XObjects and appearance streams, which say so themselves.
|
||||
pub(crate) fn exceeded(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)], content: &HashSet<ObjRef>) -> Exceeded {
|
||||
let mut found = Exceeded::default();
|
||||
for (r, o) in objs {
|
||||
found.dict(doc.trailer(), 0);
|
||||
for (_, o) in objs {
|
||||
if found.all() {
|
||||
break;
|
||||
return found;
|
||||
}
|
||||
found.value(o, 0);
|
||||
if let Object::Stream(s) = &**o
|
||||
&& (content.contains(r) || crate::is_content(&s.dict))
|
||||
&& let Ok(data) = s.decoded()
|
||||
{
|
||||
for op in pdfcraft_content::ops(&data) {
|
||||
if found.all() {
|
||||
break;
|
||||
}
|
||||
for v in &op.operands {
|
||||
found.value(v, 0);
|
||||
}
|
||||
if let Some((image, _)) = &op.inline {
|
||||
found.dict(image, 0);
|
||||
}
|
||||
}
|
||||
let mut budget = DecodeBudget::new(MAX_CONTENT_BYTES, 0);
|
||||
let mut read: HashSet<ObjRef> = HashSet::new();
|
||||
for page in pdfcraft_model::pages(doc) {
|
||||
if found.all() || budget.remaining() == 0 {
|
||||
return found;
|
||||
}
|
||||
let refs = contents(doc, &page.dict);
|
||||
let mut joined = Vec::new();
|
||||
for r in &refs {
|
||||
if let Object::Stream(s) = &*doc.get(*r)
|
||||
&& let Ok(data) = budget.decode(s)
|
||||
{
|
||||
joined.extend_from_slice(&data);
|
||||
joined.push(b' ');
|
||||
}
|
||||
}
|
||||
read.extend(refs);
|
||||
found.content(&joined);
|
||||
}
|
||||
for (r, o) in objs {
|
||||
if found.all() || budget.remaining() == 0 {
|
||||
break;
|
||||
}
|
||||
if let Object::Stream(s) = &**o
|
||||
&& !read.contains(r)
|
||||
&& (content.contains(r) || crate::is_content(&s.dict))
|
||||
&& let Ok(data) = budget.decode(s)
|
||||
{
|
||||
found.content(&data);
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// A page's `/Contents` streams, in order.
|
||||
fn contents(doc: &Document, page: &Dict) -> Vec<ObjRef> {
|
||||
match page.get(b"Contents") {
|
||||
Some(Object::Ref(r)) => match &*doc.get(*r) {
|
||||
Object::Array(a) => a.iter().filter_map(Object::as_ref).collect(),
|
||||
_ => vec![*r],
|
||||
},
|
||||
Some(Object::Array(a)) => a.iter().filter_map(Object::as_ref).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether one of a page's boundaries (with the media and crop boxes it inherits) is narrower or
|
||||
/// lower than 3 units, or wider or higher than 14400.
|
||||
pub(crate) fn page_box_out_of_range(doc: &Document, page: &Dict) -> bool {
|
||||
@@ -108,6 +157,7 @@ pub(crate) fn page_box_out_of_range(doc: &Document, page: &Dict) -> bool {
|
||||
let v: Vec<f64> = b.as_array().map(|a| a.iter().filter_map(|x| doc.resolve(x).as_f64()).collect()).unwrap_or_default();
|
||||
let [x0, y0, x1, y1] = v.as_slice() else { return false };
|
||||
let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
|
||||
[w, h].iter().any(|s| s.is_finite() && !(MIN_PAGE..=MAX_PAGE).contains(s))
|
||||
// A size past `f64` (corners near ±1e308) is as much too large as any other.
|
||||
[w, h].iter().any(|s| !(MIN_PAGE..=MAX_PAGE).contains(s))
|
||||
})
|
||||
}
|
||||
@@ -521,6 +521,7 @@ fn implementation_limits_are_reported() {
|
||||
let name = "n".repeat(128);
|
||||
assert_eq!(limits(&doc(&format!("/V /{name}"), "", ok, "")), ["A name is longer than 127 bytes"]);
|
||||
assert_eq!(limits(&doc(&format!("/{name} 1"), "", ok, "")), ["A name is longer than 127 bytes"]);
|
||||
assert_eq!(limits(&doc("", &format!("/{name} Do"), ok, "")), ["A name is longer than 127 bytes"]);
|
||||
// 200 bytes as written, 66 once the escapes are read.
|
||||
let escaped = format!("A{}", "#41".repeat(65) + &"B".repeat(4));
|
||||
assert!(limits(&doc(&format!("/V /{escaped}"), "", ok, "")).is_empty());
|
||||
@@ -528,3 +529,42 @@ fn implementation_limits_are_reported() {
|
||||
assert_eq!(limits(&doc("", "", ok, "/CropBox [0 0 14401 200]")), ["Page 1 has a boundary smaller than 3 or larger than 14400 units"]);
|
||||
assert!(limits(&doc("", "", "[0 0 2 2]", "/MediaBox [0 0 3 14400]")).is_empty(), "the page's own box overrides its parent's");
|
||||
}
|
||||
|
||||
/// 6.1.13 also reads the trailer, a page's `/Contents` streams joined (operands may end one
|
||||
/// stream and their operator start the next, ISO 32000-1 7.8.2), values nested as deep as the
|
||||
/// parser allows, and a page box whose size overflows to infinity.
|
||||
#[test]
|
||||
fn implementation_limits_reach_the_trailer_split_content_deep_values_and_huge_boxes() {
|
||||
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 = |data: &str| {
|
||||
format!(
|
||||
"<< /Length {} >>
|
||||
stream
|
||||
{data}
|
||||
endstream",
|
||||
data.len()
|
||||
)
|
||||
};
|
||||
let page = |contents: &str, catalog_extra: &str, media: &str, trailer: &str| {
|
||||
let (first, second) = contents.split_once('|').unwrap_or((contents, ""));
|
||||
build(
|
||||
&[
|
||||
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
|
||||
&format!("<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox {media} >>"),
|
||||
"<< /Type /Page /Parent 2 0 R /Contents [4 0 R 5 0 R] >>",
|
||||
&stream(first),
|
||||
&stream(second),
|
||||
],
|
||||
trailer,
|
||||
)
|
||||
};
|
||||
let ok = "[0 0 200 200]";
|
||||
assert!(limits(&page("|", "", ok, "")).is_empty());
|
||||
assert_eq!(limits(&page("|", "", ok, "/Custom 2147483648")), ["An integer is outside the 32-bit range"]);
|
||||
let long = "x".repeat(32768);
|
||||
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"]);
|
||||
}
|
||||
Reference in new issue
Block a user