mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 01:57:12 -06:00
fix(render): bound xref streams, JBIG2 referred-to counts, saved graphics states and /W2 runs (#4350)
* fix(render): bound xref streams, JBIG2 referred-to counts, saved graphics states and /W2 runs Input bounds and crash fixes in the vendored hayro crates, ported from the snapshot craft-libs carries (storytold/craft-libs#176, #239, #241): - hayro-syntax: an xref stream subsection must be backed by its decoded data. All-zero /W widths and a count longer than the data are refused before any entry is inserted; a tiny stream with a large /Size or /Index count inserted that many entries. - hayro-syntax: a subsection running past object number u32::MAX is refused; start + i overflowed. - hayro-syntax: the xref stream type field is read big-endian at its full /W[0] width, and /W[0] over 8 bytes is refused like /W[1]. - hayro-jbig2: the long-form referred-to segment count is capped at u16::MAX before its retention bytes are read; the 29-bit count went straight to Vec::with_capacity (about 2 GiB) and aborted the process. - hayro-interpret: at most 4096 saved graphics states holding at most 64 MiB together. A q past either limit is counted but not copied, and its Q still removes the clips added since. - hayro-interpret: a /W2 run whose first CID is past MAX_CID stops there, as /W does; a first CID of 4294967295 discarded the whole /W2 table. Each patch is marked in the source, has a regression test in its crate, and is listed in vendor/README.md. * fix(render): a binary usecmap counts against the CMap nesting limit; /W2 checks the CID first - hayro-cmap: a binary CMap's usecmap passed its own depth to the nested parse, so a self-referencing binary CMap recursed until the stack overflowed. It now counts like a text usecmap (ledger hayro-cmap 2). - hayro-interpret: the /W2 first-CID check runs before the run's metrics are read, so an incomplete run past MAX_CID no longer discards the rest of the table (ledger 52). * fix(render): refuse xref-stream object numbers past i32::MAX ObjectIdentifier stores an i32; a number between i32::MAX and u32::MAX was cast to a negative object number (ledger hayro-syntax 26).
This commit is contained in:
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+4
-1
@@ -109,10 +109,13 @@ pub(crate) fn parse<'a>(
|
||||
SEGMENT_USECMAP => {
|
||||
let base_data = get_cmap(CMapName::from_bytes(payload))?;
|
||||
|
||||
// PdfCraft patch: a binary `usecmap` counts against the nesting limit as a text
|
||||
// one does; passing the same depth let a self-referencing CMap recurse until the
|
||||
// stack overflowed.
|
||||
base = Some(Box::new(parse::parse_inner(
|
||||
base_data,
|
||||
get_cmap.clone(),
|
||||
depth,
|
||||
depth + 1,
|
||||
)?));
|
||||
}
|
||||
SEGMENT_WMODE => {
|
||||
|
||||
Vendored
+11
@@ -737,6 +737,17 @@ endcodespacerange
|
||||
CMap::parse(&data, |_| None).unwrap()
|
||||
}
|
||||
|
||||
/// PdfCraft patch test: a binary CMap whose `usecmap` names itself stops at the nesting limit.
|
||||
#[cfg(feature = "embed-cmaps")]
|
||||
#[test]
|
||||
fn self_referencing_binary_usecmap_stops_at_the_nesting_limit() {
|
||||
// Header (magic, version, file length 19), then one usecmap segment (length 9) naming "Self".
|
||||
const SELF_REF: &[u8] = &[
|
||||
b'b', b'c', b'm', b'a', b'p', 1, 0, 0, 0, 19, 0x09, 0, 0, 0, 9, b'S', b'e', b'l', b'f',
|
||||
];
|
||||
assert!(CMap::parse(SELF_REF, |_| Some(SELF_REF)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_parsing() {
|
||||
let data = br#"
|
||||
|
||||
+151
-2
@@ -169,6 +169,30 @@ pub struct Context<'a> {
|
||||
pub(crate) xref: &'a XRef,
|
||||
pub(crate) ocg_state: OcgState,
|
||||
nesting_depth: u32,
|
||||
/// PdfCraft patch: estimated bytes held by saved states, and for each `q` not saved because
|
||||
/// a limit was reached, the clip count to return to at its `Q`.
|
||||
saved_bytes: usize,
|
||||
skipped_saves: Vec<usize>,
|
||||
}
|
||||
|
||||
/// PdfCraft patch: most graphics states saved at once. Every `q` copies the current state, dash
|
||||
/// array and clip list included, so without a limit short operators multiply one large state.
|
||||
const MAX_SAVED_STATES: usize = 4096;
|
||||
/// PdfCraft patch: most bytes all saved states may hold together.
|
||||
const MAX_SAVED_STATE_BYTES: usize = 64 << 20;
|
||||
|
||||
/// Roughly what a saved copy of `state` holds.
|
||||
fn state_bytes(state: &State<'_>) -> usize {
|
||||
size_of::<State<'_>>()
|
||||
.saturating_add(
|
||||
state
|
||||
.graphics_state
|
||||
.stroke_props
|
||||
.dash_array
|
||||
.len()
|
||||
.saturating_mul(size_of::<f32>()),
|
||||
)
|
||||
.saturating_add(state.clips.len().saturating_mul(size_of::<ClipType>()))
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
@@ -223,15 +247,33 @@ impl<'a> Context<'a> {
|
||||
interpreter_cache: cache.clone(),
|
||||
ocg_state,
|
||||
nesting_depth,
|
||||
saved_bytes: 0,
|
||||
skipped_saves: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn save_state(&mut self) {
|
||||
let Some(cur) = self.states.last().cloned() else {
|
||||
let Some(cur) = self.states.last() else {
|
||||
warn!("attempted to save state without existing state");
|
||||
return;
|
||||
};
|
||||
|
||||
// PdfCraft patch: past either limit the `q` is recorded but nothing is copied. Its `Q`
|
||||
// still removes the clips added since; other changes made in between are kept.
|
||||
let bytes = state_bytes(cur);
|
||||
if !self.skipped_saves.is_empty()
|
||||
|| self.states.len() >= MAX_SAVED_STATES
|
||||
|| self.saved_bytes.saturating_add(bytes) > MAX_SAVED_STATE_BYTES
|
||||
{
|
||||
if self.skipped_saves.is_empty() {
|
||||
warn!("too many saved graphics states; further saves are not kept");
|
||||
}
|
||||
self.skipped_saves.push(cur.clips.len());
|
||||
return;
|
||||
}
|
||||
|
||||
let cur = cur.clone();
|
||||
self.saved_bytes = self.saved_bytes.saturating_add(bytes);
|
||||
self.states.push(cur);
|
||||
}
|
||||
|
||||
@@ -308,6 +350,14 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn restore_state(&mut self, device: &mut impl Device<'a>) {
|
||||
// PdfCraft patch: a save that was not kept is the most recent one.
|
||||
if let Some(target_clips) = self.skipped_saves.pop() {
|
||||
while self.get().clips.len() > target_clips {
|
||||
self.pop_clip_path(device);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(target_clips) = self
|
||||
.states
|
||||
.get(self.states.len().saturating_sub(2))
|
||||
@@ -324,6 +374,10 @@ impl<'a> Context<'a> {
|
||||
// The first state should never be popped.
|
||||
if self.states.len() > 1 {
|
||||
self.states.pop();
|
||||
// PdfCraft patch: the state now on top is the copy that was charged when saved.
|
||||
if let Some(top) = self.states.last() {
|
||||
self.saved_bytes = self.saved_bytes.saturating_sub(state_bytes(top));
|
||||
}
|
||||
}
|
||||
|
||||
device.set_soft_mask(
|
||||
@@ -395,7 +449,8 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn num_states(&self) -> usize {
|
||||
self.states.len()
|
||||
// PdfCraft patch: saves not kept still count, so callers restore back past them.
|
||||
self.states.len().saturating_add(self.skipped_saves.len())
|
||||
}
|
||||
|
||||
pub(crate) fn nesting_depth(&self) -> u32 {
|
||||
@@ -517,3 +572,97 @@ pub(crate) fn path_as_rect(path: &BezPath) -> Option<Rect> {
|
||||
|
||||
Some(path.fast_bounding_box())
|
||||
}
|
||||
|
||||
/// PdfCraft patch tests (ported from craft-libs): the graphics-state stack is bounded by count
|
||||
/// and by bytes, and every `q` still pairs with its `Q`.
|
||||
#[cfg(test)]
|
||||
mod saved_state_tests {
|
||||
use super::*;
|
||||
use crate::device::DummyDevice;
|
||||
use hayro_syntax::Pdf;
|
||||
|
||||
fn pdf() -> Pdf {
|
||||
let objects = [
|
||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
"<< /Type /Pages /Count 0 /Kids [] >>",
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, body) in objects.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(b"xref\n0 3\n0000000000 65535 f \n");
|
||||
for offset in offsets {
|
||||
out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(
|
||||
format!("trailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
|
||||
);
|
||||
Pdf::new(out).expect("synthetic pdf")
|
||||
}
|
||||
|
||||
fn triangle() -> BezPath {
|
||||
let mut path = BezPath::new();
|
||||
path.move_to((10.0, 10.0));
|
||||
path.line_to((90.0, 10.0));
|
||||
path.line_to((50.0, 90.0));
|
||||
path.close_path();
|
||||
path
|
||||
}
|
||||
|
||||
/// Saves `saves` more times, checks a clip added after the last save goes at its `Q`, and unwinds.
|
||||
fn save_and_unwind(ctx: &mut Context<'_>, saves: usize) {
|
||||
let before = ctx.num_states();
|
||||
for _ in 0..saves {
|
||||
ctx.save_state();
|
||||
}
|
||||
assert!(ctx.states.len() <= MAX_SAVED_STATES);
|
||||
assert!(ctx.saved_bytes <= MAX_SAVED_STATE_BYTES);
|
||||
assert_eq!(ctx.num_states(), before + saves, "every q still counts");
|
||||
|
||||
let mut device = DummyDevice;
|
||||
let clips = ctx.get().clips.len();
|
||||
ctx.push_clip_path(triangle(), FillRule::NonZero, &mut device);
|
||||
assert_eq!(ctx.get().clips.len(), clips + 1);
|
||||
ctx.restore_state(&mut device);
|
||||
assert_eq!(ctx.get().clips.len(), clips);
|
||||
|
||||
while ctx.num_states() > 1 {
|
||||
ctx.restore_state(&mut device);
|
||||
}
|
||||
assert_eq!(ctx.states.len(), 1);
|
||||
assert!(ctx.skipped_saves.is_empty());
|
||||
assert_eq!(ctx.saved_bytes, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_saves_stay_bounded_and_balanced() {
|
||||
let pdf = pdf();
|
||||
let cache = InterpreterCache::new();
|
||||
let bbox = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let settings = InterpreterSettings::default();
|
||||
let mut ctx = Context::new(Affine::IDENTITY, bbox, &cache, pdf.xref(), settings);
|
||||
|
||||
// A small state reaches the count limit.
|
||||
save_and_unwind(&mut ctx, 20_000);
|
||||
|
||||
// A large one (a 64 Ki-entry dash array, 256 KiB a copy) reaches the byte limit first.
|
||||
ctx.get_mut().graphics_state.stroke_props.dash_array = (0..1 << 16).map(|_| 2.0).collect();
|
||||
ctx.save_state();
|
||||
assert_eq!(ctx.states.len(), 2);
|
||||
while ctx.skipped_saves.is_empty() {
|
||||
ctx.save_state();
|
||||
}
|
||||
assert!(ctx.states.len() < MAX_SAVED_STATES);
|
||||
save_and_unwind(&mut ctx, 1000);
|
||||
|
||||
// Once unwound, a kept save restores what changed after it.
|
||||
let mut device = DummyDevice;
|
||||
ctx.save_state();
|
||||
ctx.pre_concat_affine(Affine::scale(2.0));
|
||||
ctx.restore_state(&mut device);
|
||||
assert_eq!(ctx.get().ctm, Affine::IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -618,6 +618,12 @@ fn read_widths2(arr: &Array<'_>) -> Option<HashMap<u32, [f32; 3]>> {
|
||||
let mut iter = range.iter::<f32>();
|
||||
|
||||
while let Some(w) = iter.next() {
|
||||
// PdfCraft patch: as in `read_widths`, checked before the run's metrics are
|
||||
// read. Without it, a run starting at 4294967295 overflowed `first` and
|
||||
// discarded the whole /W2 table.
|
||||
if first > MAX_CID {
|
||||
break;
|
||||
}
|
||||
let v1 = iter.next()?;
|
||||
let v2 = iter.next()?;
|
||||
map.insert(first, [w, v1, v2]);
|
||||
@@ -669,3 +675,24 @@ fn read_encoding(object: &Object<'_>, cmap_resolver: &CMapResolverFn) -> Option<
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch test (ported from craft-libs): a /W2 run whose first CID is past `MAX_CID`
|
||||
/// is skipped and the rest of the table kept.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::read_widths2;
|
||||
use hayro_syntax::object::{Array, FromBytes};
|
||||
|
||||
#[test]
|
||||
fn w2_run_past_the_largest_cid_keeps_the_rest_of_the_table() {
|
||||
let arr =
|
||||
Array::from_bytes(b"[4294967295 [1000 500] 10 [900 450 860] 20 30 800 400 870]")
|
||||
.unwrap();
|
||||
let map = read_widths2(&arr).unwrap();
|
||||
|
||||
assert_eq!(map.get(&4_294_967_295), None);
|
||||
assert_eq!(map.get(&10), Some(&[900.0, 450.0, 860.0]));
|
||||
assert_eq!(map.get(&25), Some(&[800.0, 400.0, 870.0]));
|
||||
assert_eq!(map.len(), 12);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+42
@@ -8,6 +8,12 @@ use alloc::vec::Vec;
|
||||
use crate::error::{ParseError, Result, SegmentError, bail, err};
|
||||
use crate::reader::Reader;
|
||||
|
||||
/// PdfCraft patch: the most segments one segment may refer to. A long-form header can claim a
|
||||
/// 29-bit count, which `Vec::with_capacity` below would turn into a multi-gigabyte reservation
|
||||
/// before a single referred-to number is read; the allocation failure aborts the process. Real
|
||||
/// files refer to a handful of segments.
|
||||
pub(crate) const MAX_REFERRED_TO_SEGMENTS: u32 = u16::MAX as u32;
|
||||
|
||||
/// "The segment type is a number between 0 and 63, inclusive. Not all values
|
||||
/// are allowed." (7.3)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -183,6 +189,12 @@ pub(crate) fn parse_segment_header(reader: &mut Reader<'_>) -> Result<SegmentHea
|
||||
u32::from_be_bytes([count_and_retention & 0x1F, rest[0], rest[1], rest[2]])
|
||||
};
|
||||
|
||||
// PdfCraft patch: refuse an oversized count before reserving memory for it and before
|
||||
// skipping the retention-flag bytes it claims.
|
||||
if referred_to_count > MAX_REFERRED_TO_SEGMENTS {
|
||||
bail!(SegmentError::InvalidReferredCount);
|
||||
}
|
||||
|
||||
// Skip retention flag bytes in long form.
|
||||
// "The first one-byte field following the initial four-byte field is formatted
|
||||
// as follows: Bit 0: Retain bit for this segment. Bit 1-7: Retain bits for
|
||||
@@ -416,4 +428,34 @@ mod tests {
|
||||
|
||||
assert_eq!(header.data_length, Some(32));
|
||||
}
|
||||
|
||||
/// PdfCraft patch test (ported from craft-libs): a long-form referred-to count over
|
||||
/// `MAX_REFERRED_TO_SEGMENTS` is refused before its retention bytes are read.
|
||||
#[test]
|
||||
fn test_referred_to_count_is_capped() {
|
||||
use crate::error::DecodeError;
|
||||
|
||||
let header = |count: u32| {
|
||||
let mut data = vec![
|
||||
0x00, 0x01, 0x00, 0x00, // Segment number = 65536
|
||||
0x86, // Flags: type 6, page assoc 1 byte, deferred non-retain
|
||||
];
|
||||
// Long form: bits 29-31 are 7, bits 0-28 the count.
|
||||
data.extend_from_slice(&(0xE000_0000 | count).to_be_bytes());
|
||||
parse_segment_header(&mut Reader::new(&data)).unwrap_err()
|
||||
};
|
||||
|
||||
// The 29-bit maximum, 536,870,911, would reserve about 2 GiB.
|
||||
for count in [0x1FFF_FFFF, MAX_REFERRED_TO_SEGMENTS + 1] {
|
||||
assert!(matches!(
|
||||
header(count),
|
||||
DecodeError::Segment(SegmentError::InvalidReferredCount)
|
||||
));
|
||||
}
|
||||
// At the cap the header is read on and ends at its missing retention bytes.
|
||||
assert!(matches!(
|
||||
header(MAX_REFERRED_TO_SEGMENTS),
|
||||
DecodeError::Parse(ParseError::UnexpectedEof)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+97
-12
@@ -965,17 +965,13 @@ fn populate_from_xref_stream<'a>(
|
||||
|
||||
let [f1_len, f2_len, f3_len] = stream.dict().get::<[u8; 3]>(W)?;
|
||||
|
||||
if f2_len > size_of::<u64>() as u8 {
|
||||
error!("xref offset length is larger than the allowed limit");
|
||||
// PdfCraft patch: the type field is read at its full width, so it is bounded like the offset.
|
||||
if f1_len > size_of::<u64>() as u8 || f2_len > size_of::<u64>() as u8 {
|
||||
error!("xref field length is larger than the allowed limit");
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
// Do such files exist?
|
||||
if f1_len != 1 {
|
||||
warn!("first field in xref stream was longer than 1");
|
||||
}
|
||||
|
||||
let xref_data = stream.decoded().ok()?;
|
||||
// PdfCraft patch: (#2366) this section's entries are read first, for `prev_offset`.
|
||||
let mut own = XrefMap::default();
|
||||
@@ -1042,15 +1038,38 @@ fn xref_stream_subsection<'a>(
|
||||
f3_len: u8,
|
||||
insert_map: &mut XrefMap,
|
||||
) -> Option<()> {
|
||||
// PdfCraft patch: every entry must be backed by stream bytes. With all widths zero an entry
|
||||
// reads nothing, so a tiny stream could declare billions of entries; and a count past the
|
||||
// decoded data is refused before any of it is inserted.
|
||||
let width = f1_len as usize + f2_len as usize + f3_len as usize;
|
||||
if width == 0 {
|
||||
warn!("xref stream entries have no width");
|
||||
|
||||
return None;
|
||||
}
|
||||
let available = xref_reader.len().saturating_sub(xref_reader.offset());
|
||||
if (num_elements as usize)
|
||||
.checked_mul(width)
|
||||
.is_none_or(|need| need > available)
|
||||
{
|
||||
warn!("xref stream subsection declares more entries than its data holds");
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
for i in 0..num_elements {
|
||||
// A zero-width type field defaults to 1 (ISO 32000-2 §7.5.8.2, `/W`).
|
||||
let f_type = if f1_len == 0 {
|
||||
1
|
||||
} else {
|
||||
// We assume a length of 1.
|
||||
xref_reader.read_bytes(1)?[0]
|
||||
// PdfCraft patch: the type field is read big-endian at its full `/W[0]` width.
|
||||
let bytes = xref_reader.read_bytes(f1_len as usize)?;
|
||||
bytes.iter().fold(0u64, |acc, b| (acc << 8) | u64::from(*b))
|
||||
};
|
||||
|
||||
let obj_number = start + i;
|
||||
// PdfCraft patch: object numbers are stored as `i32`; a subsection running past
|
||||
// `i32::MAX` is refused instead of overflowing or turning negative.
|
||||
let obj_number = i32::try_from(start.checked_add(i)?).ok()?;
|
||||
|
||||
match f_type {
|
||||
// We don't care about free objects.
|
||||
@@ -1073,7 +1092,7 @@ fn xref_stream_subsection<'a>(
|
||||
};
|
||||
|
||||
insert_map.insert(
|
||||
ObjectIdentifier::new(obj_number as i32, gen_number as i32),
|
||||
ObjectIdentifier::new(obj_number, gen_number as i32),
|
||||
EntryType::Normal(offset as usize),
|
||||
);
|
||||
}
|
||||
@@ -1091,7 +1110,7 @@ fn xref_stream_subsection<'a>(
|
||||
};
|
||||
|
||||
insert_map.insert(
|
||||
ObjectIdentifier::new(obj_number as i32, gen_number),
|
||||
ObjectIdentifier::new(obj_number, gen_number),
|
||||
EntryType::ObjStream(obj_stream_number, index),
|
||||
);
|
||||
}
|
||||
@@ -1248,6 +1267,72 @@ fn parse_metadata(info_dict: &Dict<'_>) -> Metadata {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// 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.
|
||||
#[test]
|
||||
fn xref_stream_entries_need_backing_bytes() {
|
||||
let mut map = XrefMap::default();
|
||||
// All widths zero: an entry reads nothing, so none may be declared.
|
||||
let mut empty = Reader::new(&[]);
|
||||
assert!(xref_stream_subsection(&mut empty, 0, u32::MAX, 0, 0, 0, &mut map).is_none());
|
||||
assert!(map.is_empty());
|
||||
// Three entries of width 3 need nine bytes; eight are refused before any insert.
|
||||
let data = [1, 0, 9, 1, 0, 9, 1, 0];
|
||||
let mut short = Reader::new(&data);
|
||||
assert!(xref_stream_subsection(&mut short, 0, 3, 1, 1, 1, &mut map).is_none());
|
||||
assert!(map.is_empty());
|
||||
let mut whole = Reader::new(&data[..6]);
|
||||
assert!(xref_stream_subsection(&mut whole, 0, 2, 1, 1, 1, &mut map).is_some());
|
||||
assert_eq!(map.len(), 2);
|
||||
// A subsection running past object number i32::MAX is refused.
|
||||
for start in [i32::MAX as u32, u32::MAX] {
|
||||
let mut past_end = XrefMap::default();
|
||||
let mut wrap = Reader::new(&data[..6]);
|
||||
assert!(xref_stream_subsection(&mut wrap, start, 2, 1, 1, 1, &mut past_end).is_none());
|
||||
assert!(past_end.keys().all(|id| id.obj_number >= 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xref_stream_type_field_uses_the_full_w0_width() {
|
||||
// /W [2 1 1]: two-byte type 0x0001 (normal), offset 7, gen 0.
|
||||
let mut map = XrefMap::default();
|
||||
let mut reader = Reader::new(&[0u8, 1, 7, 0]);
|
||||
assert!(xref_stream_subsection(&mut reader, 5, 1, 2, 1, 1, &mut map).is_some());
|
||||
assert!(matches!(
|
||||
map.get(&ObjectIdentifier::new(5, 0)),
|
||||
Some(EntryType::Normal(7))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xref_stream_loader_rejects_oversized_w0() {
|
||||
// One normal entry (offset 7, gen 0) whose type field is `type_len` bytes wide, so only
|
||||
// the width of /W[0] differs between the two streams.
|
||||
let load = |type_len: usize| {
|
||||
let mut entry = vec![0u8; type_len];
|
||||
entry[type_len - 1] = 1;
|
||||
entry.extend_from_slice(&[7, 0]);
|
||||
let mut data = format!(
|
||||
"1 0 obj\n<< /Type /XRef /Size 6 /Index [5 1] /W [{type_len} 1 1] /Length {} >>\nstream\n",
|
||||
entry.len()
|
||||
)
|
||||
.into_bytes();
|
||||
data.extend_from_slice(&entry);
|
||||
data.extend_from_slice(b"\nendstream\nendobj\n");
|
||||
let mut map = XrefMap::default();
|
||||
let got = populate_from_xref_stream(
|
||||
&data,
|
||||
&mut Reader::new(&data),
|
||||
&mut map,
|
||||
&mut BTreeSet::new(),
|
||||
);
|
||||
got.is_some()
|
||||
};
|
||||
assert!(load(8));
|
||||
assert!(!load(9));
|
||||
}
|
||||
|
||||
// Contributor-original, MIT OR Apache-2.0. Matches the bounded, uncompressed
|
||||
// synthetic regression used to reproduce the cold-publication race. A second
|
||||
// stream optionally checks that racing registration never aliases two ids.
|
||||
|
||||
Reference in New Issue
Block a user