M2: a reference to a reference is what it names

`IndirectObject<f32>` read an object holding `6 0 R` as the number 6, so `/Decode [11 0 R 0]`, with
11 naming 6 and 6 holding 1, read as 6, and `/W` gave glyphs such widths. As anything but a number,
such an object was lost.

`XRef::get_with` is how every reference is read, to an object stored on its own or in an object
stream, so it now follows such objects in a loop, up to 32 references as `Document::resolve` does in
#5548. A loop takes no stack however long the chain, and a cycle runs out of references and reads as
a missing object, as a reference to nothing does: the image with such a `/Decode` isn't drawn, as
with `/Decode [99 0 R 0]`.

An object stream isn't reached that way: it has to be a stream stored as that object, and following
a reference to one stored inside another let a 1 KB file run out of stack. `XRef::objects` leaves
such objects out, or a page came out once more for each one holding a reference to it, and a comment
inside a reference counts as white space, as it does everywhere else.

Reading an object stream's index now counts against the 64 nested loads, and reads its dictionary
alone. That is older than this branch, which only made it slightly worse: a chain of object streams,
each with a member of the next for its `/N`, `/First` or `/Length`, recursed with no limit, holding a
copy of the chain at each, and aborted on 750 KB with an 8 MiB stack, or 250 KB with 2 MiB, in a test
build. Parsing the stream read `/Length` as well as `/N`, two ways to the next stream's index, and a
failed index isn't remembered, so the work doubled at each stream: 20 streams in a ring took 20 s on
`main`. ISO 32000-2 7.5.7 keeps an object stream's `/Length` out of object streams, so no legal file
has a `/Length` chain. Nothing forbids `/N` or `/First` by reference, though no writer I know of makes
such a chain. Past the limit it reads as missing unless a stream within 64 of it has been read.

`/W` and `/W2` with their list by reference are tested too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz Martens
2026-11-01 15:28:31 +01:00
co-authored by Claude Opus 5.5
parent 33d7a51301
commit 4303d49eea
6 changed files with 384 additions and 22 deletions
+64 -3
View File
@@ -1093,13 +1093,74 @@ fn a_decode_array_with_references_in_it_is_read() {
for decode in ["[6 0 R 0 1 0 1 0]", "[1 0 6 0 R 0 1 0]", "[1 0 1 0 6 0 R 0]", "[6 0 R 7 0 R 6 0 R 7 0 R 6 0 R 7 0 R]"] {
assert_eq!(render("1", decode, "/DeviceRGB", "204060>"), rgb, "{decode}");
}
// A reference that only leads back to itself holds no number. The read ends, and the image is
// drawn over the red under it.
// A reference that only leads back to itself holds no number, and reads as one that names
// nothing does.
let missing = render("1", "[99 0 R 0]", "/DeviceGray", "40>");
for decode in ["[8 0 R 0]", "[1 8 0 R]", "[9 0 R 0]", "[1 9 0 R]", "[8 0 R 9 0 R]"] {
assert_ne!(render("1", decode, "/DeviceGray", "40>"), [255, 0, 0, 255], "{decode}");
assert_eq!(render("1", decode, "/DeviceGray", "40>"), missing, "{decode}");
}
}
/// An object that holds only a reference stands for what that names, stored on its own or in an
/// object stream. Read as a number it was the reference's own object number: here 8 names 7, 7
/// names 6 and 6 holds 1, and that read as 7, which drew white. A chain past the limit, and so a
/// cycle, reads as a missing object does.
#[test]
fn a_reference_to_a_reference_is_what_it_names() {
use super::*;
let file = |decode: &str, objects: &str| -> Vec<u8> {
let content = "1 0 0 rg 0 0 200 200 re f q 200 0 0 200 0 0 cm /Im Do Q";
format!(
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /XObject << /Im 5 0 R >> >> /Contents 4 0 R >> endobj\n\
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
5 0 obj << /Type /XObject /Subtype /Image /Width 1 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceGray /Decode {decode} /Filter /ASCIIHexDecode /Length 3 >> stream\n40>\nendstream endobj\n\
{objects}\n\
trailer << /Root 1 0 R >>\n%%EOF",
content.len()
)
.into_bytes()
};
let draw = |bytes: Vec<u8>| -> [u8; 4] {
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
page: 0,
kind: RequestKind::Pixels,
tile: None,
scale: 1.0,
tag: 0,
preview: false,
});
assert!(page.error.is_none(), "{:?}", page.error);
let start = (100 * page.width as usize + 100) * 4;
let mut pixel = [0; 4];
pixel.copy_from_slice(page.rgba.get(start..start + 4).expect("centre pixel"));
pixel
};
let grey = draw(file("[1 0]", ""));
assert_eq!(grey, [191, 191, 191, 255]);
let chain = "6 0 obj 1 endobj 7 0 obj 6 0 R endobj 8 0 obj 7 0 R endobj";
assert_eq!(draw(file("[8 0 R 0]", chain)), grey, "stored on their own");
let doc = pdfcraft_cos::Document::open(Arc::new(file("[8 0 R 0]", chain))).unwrap();
let packed = pdfcraft_cos::write_full(&doc, &pdfcraft_cos::SaveOptions::default()).unwrap();
assert!(packed.windows(7).any(|w| w == b"/ObjStm"), "the save packs them");
assert_eq!(draw(packed), grey, "stored in an object stream");
assert_eq!(draw(file("9 0 R", "9 0 obj 10 0 R endobj 10 0 obj [1 0] endobj")), grey, "not only numbers");
let commented = "6 0 obj 1 endobj 7 0 obj 6 %c\n0 R endobj 8 0 obj 7 0 %c\nR endobj";
assert_eq!(draw(file("[8 %c\n0 R 0]", commented)), grey, "a comment counts as white space");
let links = |n: usize| -> String {
let links: String = (0..n).map(|i| format!("{} 0 obj {} 0 R endobj\n", 10 + i, 11 + i)).collect();
format!("{links}{} 0 obj 1 endobj", 10 + n)
};
let missing = draw(file("[99 0 R 0]", ""));
assert_ne!(missing, grey);
assert_eq!(draw(file("[10 0 R 0]", &links(32))), grey, "32 references are followed");
assert_eq!(draw(file("[10 0 R 0]", &links(33))), missing, "33 aren't");
assert_eq!(draw(file("[6 0 R 0]", "6 0 obj 6 0 R endobj")), missing, "nor is a cycle");
}
/// A function's `/Encode`, `/Decode`, `/Domain` and `/Range` are read a pair at a time too, so they
/// lost a reference at the start of a pair the same way, and the array with it. Each form is compared
/// with the direct one, which differs from the identity so the array is shown to count.
+1 -1
View File
File diff suppressed because one or more lines are too long
+12 -3
View File
@@ -713,6 +713,7 @@ mod width_tests {
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >> endobj\n\
4 0 obj 20 endobj\n5 0 obj {array} endobj\n6 0 obj 22 endobj\n\
7 0 obj 7 0 R endobj\n8 0 obj 9 0 R endobj\n9 0 obj 8 0 R endobj\n\
10 0 obj 4 0 R endobj\n11 0 obj [500 600] endobj\n12 0 obj [-1000 500 880] endobj\n\
trailer << /Root 1 0 R >>\n%%EOF\n"
);
Pdf::new(pdf.into_bytes()).unwrap()
@@ -752,12 +753,20 @@ mod width_tests {
assert_eq!(vertical("[20 22 -900 400 800]"), by_range);
assert_eq!(vertical("[4 0 R 22 -900 400 800]"), by_range);
// A reference that only leads back to itself holds no number. The reads end.
// A group's list can be given by reference too, and a reference to a reference is what it
// names.
let by_list = vec![(20, 500.0), (21, 600.0)];
assert_eq!(widths("[20 11 0 R]"), by_list);
assert_eq!(widths("[10 0 R [500 600]]"), by_list);
assert_eq!(widths("[10 0 R 11 0 R]"), by_list);
assert_eq!(vertical("[20 12 0 R]"), vec![(20, [-1000.0, 500.0, 880.0])]);
// A reference that only leads back to itself holds no number, so those glyphs keep `/DW`.
for array in ["[7 0 R [500 600]]", "[8 0 R [500 600]]", "[20 7 0 R 700]", "[7 0 R 8 0 R 700]", "[20 [7 0 R 600]]"] {
widths(array);
assert!(widths(array).is_empty(), "{array}");
}
for array in ["[7 0 R [-1000 500 880]]", "[8 0 R 22 -900 400 800]", "[20 [7 0 R 500 880]]"] {
vertical(array);
assert!(vertical(array).is_empty(), "{array}");
}
}
}
+9 -3
View File
@@ -1,5 +1,4 @@
use crate::object::ObjectIdentifier;
use crate::object::Stream;
use crate::reader::ReaderContext;
use crate::sync::HashMap;
use crate::sync::{Arc, Mutex, MutexExt};
@@ -92,6 +91,12 @@ impl Data {
&self.data
}
/// PdfCraft patch: (hayro-syntax (33)) enter the gate for what an object stream's index depends
/// on, once its data is read and [`Self::get_with`] has let go of it.
pub(crate) fn enter_loading(&self, id: ObjectIdentifier) -> Option<LoadGuard<'_>> {
self.loading.enter(id)
}
/// Get access to the data of a decoded object stream.
pub(crate) fn get_with(&self, id: ObjectIdentifier, ctx: &ReaderContext<'_>) -> Option<&[u8]> {
// PdfCraft patch: (#307, see vendor/README.md hayro-syntax (10)) register exactly once:
@@ -110,7 +115,8 @@ impl Data {
let _loading = self.loading.enter(id)?;
self.decoded
.get_or_init(idx, || {
let stream = ctx.xref().get_with::<Stream<'_>>(id, ctx)?;
// PdfCraft patch: (hayro-syntax (33)) only ever an object stream.
let stream = ctx.xref().object_stream(id, ctx)?;
stream.decoded().ok().map(Cow::into_owned)
})?
.as_deref()
@@ -244,7 +250,7 @@ mod tests {
}
}
struct LoadGuard<'a>(&'a LoadGate);
pub(crate) struct LoadGuard<'a>(&'a LoadGate);
impl Drop for LoadGuard<'_> {
fn drop(&mut self) {
+5 -4
View File
@@ -39,10 +39,11 @@ impl From<ObjectIdentifier> for ObjRef {
impl Skippable for ObjRef {
fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> {
// PdfCraft patch: (hayro-syntax (33)) a comment counts as white space, here too.
r.skip::<i32>(false)?;
r.skip_white_spaces();
r.skip_white_spaces_and_comments();
r.skip::<i32>(false)?;
r.skip_white_spaces();
r.skip_white_spaces_and_comments();
r.forward_tag(b"R")?;
Some(())
@@ -52,9 +53,9 @@ impl Skippable for ObjRef {
impl Readable<'_> for ObjRef {
fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option<Self> {
let obj_ref = r.read_without_context::<i32>()?;
r.skip_white_spaces();
r.skip_white_spaces_and_comments();
let gen_num = r.read_without_context::<i32>()?;
r.skip_white_spaces();
r.skip_white_spaces_and_comments();
r.forward_tag(b"R")?;
Some(Self::new(obj_ref, gen_num))
+293 -8
View File
@@ -12,7 +12,7 @@ use crate::object::dict::keys::{
};
use crate::object::dict::{DictCache, probe_dict};
use crate::object::indirect::IndirectObject;
use crate::object::{Array, MaybeRef};
use crate::object::{Array, MaybeRef, ObjRef};
use crate::object::{DateTime, Dict};
use crate::object::{Object, ObjectLike};
use crate::pdf::PdfVersion;
@@ -31,6 +31,17 @@ use core::ops::Deref;
pub(crate) const XREF_ENTRY_LEN: usize = 20;
/// PdfCraft patch: (hayro-syntax (33)) how many references a read follows through objects that hold
/// nothing else. Past it, as any cycle goes, the object reads as a missing one.
const MAX_REFERENCE_HOPS: usize = 32;
/// PdfCraft patch: (hayro-syntax (33)) what [`XRef::fetch`] found at an object's place.
enum Fetched<T> {
Object(T),
/// All the object holds is a reference to this one.
Reference(ObjectIdentifier),
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum XRefError {
Unknown,
@@ -492,7 +503,11 @@ impl XRef {
iter::from_fn(move || {
for next in iter.by_ref() {
if let Some(obj) = self.get_with(next.0, &ReaderContext::new(self, false)) {
// PdfCraft patch: (hayro-syntax (33)) an object that only refers to another
// isn't one of its own: followed, that one came out once more for each.
if let Some(Fetched::Object(obj)) =
self.fetch(next.0, &ReaderContext::new(self, false))
{
return Some(obj);
} else {
// Skip invalid objects.
@@ -563,12 +578,48 @@ impl XRef {
}
/// Return the object with the given identifier.
///
/// PdfCraft patch: (hayro-syntax (33)) an object that holds only a reference stands for what
/// that names. Read as a number it was the reference's own object number, and as anything else
/// it was lost. This is a loop, so a long chain takes no stack.
#[allow(private_bounds)]
pub(crate) fn get_with<'a, T>(
&'a self,
id: ObjectIdentifier,
ctx: &ReaderContext<'a>,
) -> Option<T>
where
T: ObjectLike<'a>,
{
let mut id = id;
for _ in 0..=MAX_REFERENCE_HOPS {
match self.fetch::<T>(id, ctx)? {
Fetched::Object(object) => return Some(object),
Fetched::Reference(next) => id = next,
}
}
warn!("references lead on past the limit, or in a cycle: {id:?}");
None
}
/// PdfCraft patch: (hayro-syntax (33)) the object stream `id`. It has to be a stream stored as
/// that object (ISO 32000-2 7.5.7), so this follows no reference: the check of where it is stored
/// is of that object, and one reached through a reference could be a stream stored inside
/// another.
pub(crate) fn object_stream<'a>(
&'a self,
id: ObjectIdentifier,
ctx: &ReaderContext<'a>,
) -> Option<Stream<'a>> {
match self.fetch::<Stream<'a>>(id, ctx)? {
Fetched::Object(stream) => Some(stream),
Fetched::Reference(_) => None,
}
}
/// What is stored at the object with the given identifier.
fn fetch<'a, T>(&'a self, id: ObjectIdentifier, ctx: &ReaderContext<'a>) -> Option<Fetched<T>>
where
T: ObjectLike<'a>,
{
@@ -597,9 +648,13 @@ impl XRef {
ctx.set_in_object_stream(false);
r.jump(offset);
if let Some(next) = held_reference(r.clone(), id) {
return Some(Fetched::Reference(next));
}
if let Some(object) = r.read_with_context::<IndirectObject<T>>(&ctx) {
if object.id() == &id {
return Some(object.get());
return Some(Fetched::Object(object.get()));
}
} else {
// There is a valid object at the offset, it's just not of the type the caller
@@ -622,7 +677,7 @@ impl XRef {
self.repair();
// Now try reading again.
self.get_with::<T>(id, &ctx)
self.fetch::<T>(id, &ctx)
}
}
EntryType::ObjStream(obj_stram_gen_num, index) => {
@@ -657,11 +712,20 @@ impl XRef {
// A stream whose pairs don't parse stays unreadable, as it was.
Some(offsets) => offsets?,
None => {
let stream = self.get_with::<Stream<'_>>(obj_stream_id, &ctx)?;
// PdfCraft patch: (hayro-syntax (33)) this read can reach another object
// stream, whose `/N` or `/First` can be a member of the next: the gate's
// limit holds here too, or a chain of them ran out of stack. It reads the
// dictionary, which is all it needs: parsing the stream reads `/Length`
// too, a second way to the next stream's index, which doubled at each.
let _loading = repr.data.enter_loading(obj_stream_id)?;
let Fetched::Object(dict) = self.fetch::<Dict<'_>>(obj_stream_id, &ctx)?
else {
return None;
};
// `/N` and `/First` may be references that can't be followed while
// another stream loads: reading them fails without being remembered.
let num_objects = stream.dict().get::<usize>(N)?;
let first_offset = stream.dict().get::<usize>(FIRST)?;
let num_objects = dict.get::<usize>(N)?;
let first_offset = dict.get::<usize>(FIRST)?;
let parsed =
read_object_stream_index(num_objects, first_offset, data).map(Arc::new);
self.with_object_stream_indexes(|indexes| {
@@ -673,7 +737,12 @@ impl XRef {
.unwrap_or(parsed)?
}
};
ObjectStream::with_offsets(data, &ctx, offsets).get(index)
let stream = ObjectStream::with_offsets(data, &ctx, offsets);
if let Some(next) = stream.reference(index) {
return Some(Fetched::Reference(next));
}
stream.get(index).map(Fetched::Object)
}
}
}
@@ -1232,6 +1301,28 @@ impl<'a> ObjectStream<'a> {
r.read_with_context::<T>(&self.ctx)
}
/// PdfCraft patch: (hayro-syntax (33)) what the object at `index` refers to, when a reference is
/// all it holds.
fn reference(&self, index: u32) -> Option<ObjectIdentifier> {
let offset = self.offsets.get(index as usize)?.1;
let mut r = Reader::new(self.data);
r.jump(offset);
r.skip_white_spaces_and_comments();
Some(r.read_without_context::<ObjRef>()?.into())
}
}
/// PdfCraft patch: (hayro-syntax (33)) what the indirect object `id` at `r` refers to, when a
/// reference is all it holds.
fn held_reference(mut r: Reader<'_>, id: ObjectIdentifier) -> Option<ObjectIdentifier> {
if r.read_without_context::<ObjectIdentifier>()? != id {
return None;
}
r.skip_white_spaces_and_comments();
Some(r.read_without_context::<ObjRef>()?.into())
}
fn parse_metadata(info_dict: &Dict<'_>) -> Metadata {
@@ -1266,6 +1357,200 @@ fn parse_metadata(info_dict: &Dict<'_>) -> Metadata {
#[cfg(test)]
mod tests {
use super::*;
use crate::Pdf;
/// A file with an xref stream: `stored` are objects on their own, as they would follow
/// `n 0 obj`, and `packed` says of each object in an object stream which stream and place.
fn with_xref_stream(stored: &[(u32, Vec<u8>)], packed: &[(u32, u32, u16)]) -> Vec<u8> {
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = alloc::collections::BTreeMap::new();
for (num, body) in stored {
offsets.insert(*num, out.len());
out.extend_from_slice(alloc::format!("{num} 0 obj ").as_bytes());
out.extend_from_slice(body);
out.extend_from_slice(b"\nendobj\n");
}
let last = stored.iter().map(|o| o.0).chain(packed.iter().map(|o| o.0)).max().unwrap_or(0);
let (xref, size) = (last + 1, last + 2);
let at = out.len();
offsets.insert(xref, at);
let packed: alloc::collections::BTreeMap<u32, (u32, u16)> = packed.iter().map(|o| (o.0, (o.1, o.2))).collect();
let mut rows = Vec::new();
for num in 0..size {
if let Some(offset) = offsets.get(&num) {
rows.push(1);
rows.extend_from_slice(&(*offset as u32).to_be_bytes());
rows.extend_from_slice(&[0, 0]);
} else if let Some((stream, index)) = packed.get(&num) {
rows.push(2);
rows.extend_from_slice(&stream.to_be_bytes());
rows.extend_from_slice(&index.to_be_bytes());
} else {
rows.extend_from_slice(&[0; 7]);
}
}
out.extend_from_slice(
alloc::format!(
"{xref} 0 obj << /Type /XRef /Size {size} /W [1 4 2] /Root 1 0 R /Length {} >> stream\n",
rows.len()
)
.as_bytes(),
);
out.extend_from_slice(&rows);
out.extend_from_slice(alloc::format!("\nendstream endobj\nstartxref\n{at}\n%%EOF\n").as_bytes());
out
}
/// `/Type /ObjStm` around `members`, each stored at its offset, and `entries` of its own.
fn object_stream_of(members: &[(u32, &[u8])], entries: &str) -> Vec<u8> {
let (mut head, mut body) = (alloc::string::String::new(), Vec::new());
for (num, member) in members {
head.push_str(&alloc::format!("{num} {} ", body.len()));
body.extend_from_slice(member);
body.push(b'\n');
}
head.push('\n');
let dict = alloc::format!(
"<< /Type /ObjStm /N {} /First {} /Length {} {entries} >> stream\n",
members.len(),
head.len(),
head.len() + body.len()
);
let mut out = dict.into_bytes();
out.extend_from_slice(head.as_bytes());
out.extend_from_slice(&body);
out.extend_from_slice(b"\nendstream");
out
}
fn page_objects() -> Vec<(u32, Vec<u8>)> {
vec![
(1, b"<< /Type /Catalog /Pages 2 0 R >>".to_vec()),
(2, b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec()),
(3, b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>".to_vec()),
]
}
/// An object stream is a stream stored as that object, however many references lead to one.
/// Object 10 holds a reference to 11, a stream stored inside object stream 12, and 31 claims to
/// be in 10, with 11's `/N`, `/First` or `/Length` being 31: reading it recursed until the
/// stack ran out. It reads as missing, and so does a member of such a stream.
#[test]
fn an_object_stream_isnt_reached_through_a_reference() {
for key in ["N", "First", "Length"] {
let inner = alloc::format!("<< /Type /ObjStm /{key} 31 0 R >> stream\n31 0 1\n\nendstream");
let mut stored = page_objects();
stored.push((10, b"11 0 R".to_vec()));
stored.push((12, object_stream_of(&[(11, inner.as_bytes())], "")));
let pdf = Pdf::new(with_xref_stream(&stored, &[(11, 12, 0), (31, 10, 0)])).unwrap();
assert_eq!(pdf.xref().get::<i32>(ObjectIdentifier::new(31, 0)), None, "{key}");
}
let inner = b"<< /Type /ObjStm /N 1 /First 5 /Length 8 >> stream\n20 0 7\n\nendstream";
let mut stored = page_objects();
stored.push((10, b"11 0 R".to_vec()));
stored.push((12, object_stream_of(&[(11, inner)], "")));
let pdf = Pdf::new(with_xref_stream(&stored, &[(11, 12, 0), (20, 10, 0)])).unwrap();
assert_eq!(pdf.xref().get::<i32>(ObjectIdentifier::new(20, 0)), None);
}
/// `count` object streams stored as themselves, the `keys` of each a member of the next, or
/// with `ring` of the first after the last. Reading the first member reads them all, and it is
/// `count + 100` to tell.
fn chain_of_object_streams(count: u32, keys: &[&str], ring: bool) -> Vec<u8> {
let mut stored = page_objects();
let mut packed = Vec::new();
for i in 0..count {
let (stream, member) = (100 + 2 * i, 101 + 2 * i);
let value = if i == 0 { count + 100 } else { 1 };
let data = alloc::format!("{member} 0 \n{value}\n");
let (first, length) = (alloc::format!("{}", alloc::format!("{member} 0 \n").len()), alloc::format!("{}", data.len()));
let next = match (i + 1 == count, ring) {
(true, true) => "101 0 R".into(),
(true, false) => "1".into(),
_ => alloc::format!("{} 0 R", member + 2),
};
let entry = |key, own: &'_ str| if keys.contains(&key) { next.clone() } else { own.into() };
let (n, first, length): (alloc::string::String, alloc::string::String, alloc::string::String) =
(entry("N", "1"), entry("First", &first), entry("Length", &length));
let body = alloc::format!("<< /Type /ObjStm /N {n} /First {first} /Length {length} >> stream\n{data}\nendstream");
stored.push((stream, body.into_bytes()));
packed.push((member, stream, 0));
}
with_xref_stream(&stored, &packed)
}
/// A chain of object streams, each with a member of the next for its `/N`, `/First` or
/// `/Length`, had no limit: one read recursed through all of them, holding a copy of the chain
/// at each, and 50,000 ran out of an 8 MiB stack. It stops at the gate's limit, on half the stack
/// of a render worker. Past that `/N` and `/First` can't be read, so the member reads as missing,
/// and a `/Length` that can't be read falls back on `endstream`, so it reads. A short chain reads.
#[test]
fn a_chain_of_object_streams_ends_at_the_limit_of_nested_loads() {
for key in ["N", "First", "Length"] {
let pdf = chain_of_object_streams(50_000, &[key], false);
std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(move || {
let pdf = Pdf::new(pdf).unwrap();
let read = pdf.xref().get::<i32>(ObjectIdentifier::new(101, 0));
assert_eq!(read, (key == "Length").then_some(50_100), "{key}");
})
.unwrap()
.join()
.unwrap();
}
let pdf = Pdf::new(chain_of_object_streams(10, &["N"], false)).unwrap();
assert_eq!(pdf.xref().get::<i32>(ObjectIdentifier::new(101, 0)), Some(110));
}
/// `/Length` and `/N` or `/First` both by reference were two ways to the next stream's index,
/// and an index that fails isn't remembered, so the work doubled at each stream: 65 streams, one
/// past the limit, in 8 KB, didn't finish, and other threads waited at the gate. The index reads
/// the dictionary alone now. Without that this doesn't end, so the time allowed can't fail it.
#[test]
fn a_chain_of_object_streams_takes_no_more_work_for_two_ways_on() {
for (count, ring) in [(65, false), (20, true), (5_000, false)] {
for keys in [&["Length", "N"][..], &["Length", "First"]] {
let pdf = chain_of_object_streams(count, keys, ring);
let (done, finished) = std::sync::mpsc::channel();
std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(move || {
let pdf = Pdf::new(pdf).unwrap();
let _ = pdf.xref().get::<i32>(ObjectIdentifier::new(101, 0));
let _ = done.send(());
})
.unwrap();
assert!(finished.recv_timeout(std::time::Duration::from_secs(120)).is_ok(), "{keys:?} of {count}");
}
}
}
/// Object 5, a member of a real object stream, holds a reference to object 6 stored on its own,
/// and 7, stored on its own, to 8, a member: each reads as what it names.
#[test]
fn a_reference_held_in_either_storage_is_followed() {
let mut stored = page_objects();
stored.push((6, b"42".to_vec()));
stored.push((7, b"8 0 R".to_vec()));
stored.push((9, object_stream_of(&[(5, b"6 0 R"), (8, b"43")], "")));
let pdf = Pdf::new(with_xref_stream(&stored, &[(5, 9, 0), (8, 9, 1)])).unwrap();
assert_eq!(pdf.xref().get::<i32>(ObjectIdentifier::new(5, 0)), Some(42));
assert_eq!(pdf.xref().get::<i32>(ObjectIdentifier::new(7, 0)), Some(43));
}
/// With no page tree, every page object is a page, and one that another object only refers to
/// is one page, not two.
#[test]
fn a_page_a_reference_is_held_to_is_found_once() {
let mut stored = page_objects();
stored[0].1 = b"<< /Type /Catalog /Pages 99 0 R >>".to_vec();
stored[2].1 = b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Contents 4 0 R >>".to_vec();
stored.push((4, b"<< /Length 0 >> stream\n\nendstream".to_vec()));
stored.push((12, b"3 0 R".to_vec()));
let pdf = Pdf::new(with_xref_stream(&stored, &[])).unwrap();
assert_eq!(pdf.pages().len(), 1);
}
// PdfCraft patch tests (ported from craft-libs): xref stream entries are bounded by the
// decoded data, and the type field is read at its full width.