Merge pull request #4612 from VoiceLessQ/fix/4366-name-content-in-an-array-object

PDF import: a page's content listed in an array object is named as content (refs #4366)
This commit is contained in:
BFlat
2026-10-30 00:41:02 +07:00
committed by GitHub
2 changed files with 57 additions and 7 deletions
+48 -5
View File
@@ -3850,8 +3850,8 @@ fn pdf_a_huge_form_drawn_over_and_over_is_refused_in_bounded_time() {
#[test]
fn a_stream_past_the_cap_is_named() {
let image = "/Type /XObject /Subtype /Image /Width 10 /Height 10 /ColorSpace /DeviceRGB /BitsPerComponent 8";
let page = |entries: &str| {
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R /Resources << /XObject << /Im 5 0 R >> >> {entries} >>")
let page = |contents: &str, entries: &str| {
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents {contents} /Resources << /XObject << /Im 5 0 R >> >> {entries} >>")
.into_bytes()
};
let plain = |content: &str| format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len() + 1).into_bytes();
@@ -3864,17 +3864,27 @@ fn a_stream_past_the_cap_is_named() {
let files = [
(
"an image",
pdf(page(""), vec![plain("q 10 0 0 10 0 0 cm /Im Do Q"), packed_as_is(image, &spaces)]),
pdf(page("4 0 R", ""), vec![plain("q 10 0 0 10 0 0 cm /Im Do Q"), packed_as_is(image, &spaces)]),
"an image of 10 × 10 px in it (object 5) would decode to more than 256 MB",
),
(
"a page's content",
pdf(page(""), vec![packed_as_is("", &spaces), packed(image, "\0\0\0")]),
pdf(page("4 0 R", ""), vec![packed_as_is("", &spaces), packed(image, "\0\0\0")]),
"a page's content in it (object 4) would decode to more than 256 MB",
),
(
"a page's content in an array",
pdf(page("[4 0 R]", ""), vec![packed_as_is("", &spaces), packed(image, "\0\0\0")]),
"a page's content in it (object 4) would decode to more than 256 MB",
),
(
"a page's content in an array of its own",
pdf(page("6 0 R", ""), vec![packed_as_is("", &spaces), packed(image, "\0\0\0"), b"[4 0 R]".to_vec()]),
"a page's content in it (object 4) would decode to more than 256 MB",
),
(
"the editing data",
pdf(page(editing), vec![plain("0 0 5 5 re f"), packed(image, "\0\0\0"), packed_as_is("", &spaces)]),
pdf(page("4 0 R", editing), vec![plain("0 0 5 5 re f"), packed(image, "\0\0\0"), packed_as_is("", &spaces)]),
"its editing data (object 6) would decode to more than 256 MB",
),
];
@@ -3888,6 +3898,39 @@ fn a_stream_past_the_cap_is_named() {
});
}
// Pages that share one `Contents` array, the array an object of its own: its stream is named as
// a page's content, and a refused stream elsewhere is still found past the pages (#4612 review).
#[test]
fn pages_sharing_a_contents_array_name_its_stream() {
let image = "/Type /XObject /Subtype /Image /Width 10 /Height 10 /ColorSpace /DeviceRGB /BitsPerComponent 8";
let plain = |content: &str| format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len() + 1).into_bytes();
let spaces = spaces_past_the_cap();
// 1 catalog, 2 pages, 3 the shared array, 4 its stream, 5 to 7 the pages, 8 an image they draw.
let pdf = |content: Vec<u8>, image: Vec<u8>| {
let page = b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 3 0 R /Resources << /XObject << /Im 8 0 R >> >> >>";
let head = [
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [5 0 R 6 0 R 7 0 R] /Count 3 >>".to_vec(),
b"[4 0 R]".to_vec(),
content,
];
pdf_of(&head.into_iter().chain([page.to_vec(), page.to_vec(), page.to_vec(), image]).collect::<Vec<_>>())
};
let files = [
(pdf(packed_as_is("", &spaces), packed(image, "\0\0\0")), "a page's content in it (object 4) would decode to more than 256 MB"),
(
pdf(plain("q 10 0 0 10 0 0 cm /Im Do Q"), packed_as_is(image, &spaces)),
"an image of 10 × 10 px in it (object 8) would decode to more than 256 MB",
),
];
on_a_small_stack(move || {
for (bytes, why) in &files {
let e = vectorcraft_engine::cmd::fileio::load("shared.pdf", bytes).err().map(|e| e.to_string());
assert!(e.as_deref().is_some_and(|e| e.contains(why)), "{why}: {e:?}");
}
});
}
#[test]
fn pdf_art_that_draws_itself() {
let own = "<< /XObject << /F 5 0 R >> /Pattern << /P 5 0 R >> /Font << /F1 5 0 R >> /ExtGState << /G << /SMask << /S /Alpha /G 5 0 R >> >> >> >>";
+9 -2
View File
@@ -1,7 +1,7 @@
//! The pages of a PDF to import: opening with a password, the pages picked, the box each page
//! is cropped to ([`CropTo`]) and [`info`].
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use craft_pdf::syntax::object::{Array, Dict, MaybeRef, Name, Object, ObjectIdentifier, Stream, dict_or_stream};
use craft_pdf::syntax::page::Page;
@@ -193,9 +193,16 @@ fn stream_kind(pdf: &Pdf, id: ObjectIdentifier, dict: &Dict<'_>) -> String {
}
let is = |m: Option<MaybeRef<Object<'_>>>| matches!(m, Some(MaybeRef::Ref(r)) if ObjectIdentifier::from(r) == id);
let pages = pdf.pages();
// `Contents` is a stream or an array of them, the array in the page or an object of its own.
// Each object is read once however many pages share it, so a file can't make this quadratic.
let mut read = HashSet::new();
let content = pages.iter().any(|page| match page.raw().get_raw::<Object<'_>>(b"Contents") {
Some(MaybeRef::NotRef(Object::Array(a))) => items(&a).any(|m| is(Some(m))),
other => is(other),
Some(MaybeRef::Ref(r)) => {
let at = ObjectIdentifier::from(r);
read.insert(at) && page.raw().get::<Array<'_>>(b"Contents").map_or(at == id, |a| items(&a).any(|m| is(Some(m))))
}
_ => false,
});
if content {
return "a page's content in it".to_string();