use super::reader::find; use super::state::{ LENGTHS, LoadGuard, Loaded, ObjStm, ObjectReader, ReadState, Slot, SourceIdentity, loaded_bytes, note_overlay_read, objstm_member, overlay_reads, }; use super::xref::{Revision, XrefEntry}; use super::{ Arc, CosError, Cow, Dict, Document, Lexer, MAX_DECODED, MAX_JBIG2_GLOBALS, ObjRef, Object, Stream, is_whitespace, parse_indirect_shared, }; /// The most decoded `/JBIG2Globals` a document keeps for its images to share (see /// `Document::jbig2_globals`): two streams at [`MAX_JBIG2_GLOBALS`]. const JBIG2_GLOBALS_HELD: usize = 2 * MAX_JBIG2_GLOBALS; impl Document { pub fn bytes(&self) -> &Arc> { &self.data } /// Identity of the original bytes, xref and authenticated decode context. Edits and /// clones preserve it; reopening (including after any save) produces a new identity. pub fn source_identity(&self) -> SourceIdentity { SourceIdentity(Arc::downgrade(&self.source_identity)) } /// What the document reads as now (see [`ReadState`]): unlike [`Self::source_identity`], it /// changes with every edit, so a value derived from the document can be cached against it. pub fn read_state(&self) -> ReadState { ReadState { source: self.source_identity(), trailer: self.trailer.clone(), overlay: self.overlay.clone() } } pub fn version(&self) -> &str { &self.version } /// Set the PDF header version (`"1.4"`, `"1.7"`, `"2.0"`, …) written by a full save. pub fn set_version(&mut self, version: impl Into) { self.version = version.into(); } pub fn trailer(&self) -> &Dict { &self.trailer } pub fn trailer_mut(&mut self) -> &mut Dict { &mut self.trailer } pub fn revisions(&self) -> &[Revision] { &self.revisions } /// Position of the `%PDF-` header in the file: 0 unless bytes come before it. pub fn header_offset(&self) -> usize { self.header_offset } /// There is no cross-reference chain to append to: the index was rebuilt from object /// headers because the file's own was unusable (see [`Self::repair_log`]), or the document /// was built from scratch ([`Self::new_empty`]). A save writes such a document in full, even /// when nothing was edited: for a repaired file that is what puts the repair on disk. pub fn reconstructed(&self) -> bool { self.revisions.is_empty() } /// Where each revision of the file ends (oldest first): the byte after the `%%EOF` (and its /// end-of-line) that closes its cross-reference section. `data[..end]` is that revision as it /// was saved. A linearized file's first-page section belongs to the revision it precedes, so /// ends only ever increase. Empty for a reconstructed file. pub fn revision_ends(&self) -> Vec { let d = &self.data[..]; let mut ends: Vec = Vec::with_capacity(self.revisions.len()); for r in self.revisions.iter() { let from = (r.xref_offset as usize).saturating_add(self.header_offset).min(d.len()); let Some(at) = d[from..].windows(5).position(|w| w == b"%%EOF") else { continue }; let mut end = from + at + 5; while end < d.len() && matches!(d[end], b'\r' | b'\n') && end - (from + at + 5) < 2 { end += 1; } match ends.last_mut() { Some(last) if end <= *last => {} _ => ends.push(end), } } ends } pub fn repair_log(&self) -> &[String] { &self.repair_log } /// Add a line to [`Self::repair_log`]: a leniency applied while reading that the user should /// know about (fidelity: never silent). pub fn note_repair(&mut self, line: String) { let mut log = self.repair_log.as_ref().clone(); log.push(line); self.repair_log = Arc::new(log); } /// `true` when there are unsaved edits. Removing an `/Encrypt` dictionary written inline in /// the trailer (as MuPDF writes it) changes no object, and is an edit all the same. pub fn is_modified(&self) -> bool { !self.overlay.is_empty() || self.encryption_changed } /// Object numbers changed since the document was opened (or last saved). pub fn modified_objects(&self) -> Vec { self.overlay.keys().copied().collect() } /// Where object `num` is stored in the file (ignoring unsaved edits). pub fn xref_entry(&self, num: u32) -> Option { self.entries.lookup(num, &self.data) } /// Cross-reference entries this reader did not take at face value, as (byte offset of the /// section, object number): an in-use entry at offset 0, a second and different entry for /// one object in the same section, a reserved cross-reference stream entry type, or a /// hybrid /XRefStm entry that disagrees with the table. The reader sets such an entry aside /// and keeps what it already had for the object. Another reader, the page renderer among /// them, may follow it instead, so the two can then show different objects for one number. /// Code that compares revisions (signature validation) must not call such an object /// unchanged. pub fn xref_oddities(&self) -> &[(u64, u32)] { &self.xref_oddities } /// Whether the cross-reference entry of `num` locates that object itself: its `num G obj` /// header at exactly the recorded offset, or the member with that number at the recorded /// position of its object stream. When it does not, [`Self::get`] still finds the object by /// searching, but another reader may resolve the number differently. Ignores unsaved edits. pub fn xref_entry_is_exact(&self, num: u32) -> bool { match self.xref_entry(num) { None | Some(XrefEntry::Free { .. }) => true, Some(XrefEntry::InFile { offset, .. }) => { let at = |off: usize| matches!(parse_indirect_shared(&self.data, off, &|_| None, &self.endstreams), Ok((id, _)) if id.num == num); // Offsets may count from a header that does not start the file. usize::try_from(offset).is_ok_and(|off| at(off) || (self.header_offset != 0 && at(off.saturating_add(self.header_offset)))) } Some(XrefEntry::InStream { stream, index }) => self .objstm(stream) .is_ok_and(|stm| usize::try_from(index).ok().and_then(|i| stm.index.get(i)).is_some_and(|m| m.is_some_and(|(n, _)| n == num))), } } /// Object `num` has an unsaved edit. pub fn is_edited(&self, num: u32) -> bool { self.overlay.contains_key(&num) } pub fn root(&self) -> Option { self.trailer.reference(b"Root") } // ── object access ─────────────────────────────────────────────────────────────────────── /// Fetch an object by reference. Missing objects are `Null` (ISO 32000-2 §7.3.10). pub fn get(&self, r: ObjRef) -> Arc { self.try_get(r.num).unwrap_or_else(|_| Arc::new(Object::Null)) } /// Follow a reference if `o` is one; otherwise return `o` itself. pub fn resolve(&self, o: &Object) -> Arc { match o { Object::Ref(r) => self.get(*r), other => Arc::new(other.clone()), } } /// `s` with the `/DecodeParms` its filters use made direct, so [`Stream::decoded`] reads them: /// an indirect dictionary resolved, and of an array (itself perhaps indirect), the entries at /// the filters' places (`/Crypt` counts, ISO 32000-2 §7.4.1) resolved, the rest dropped. Of /// each dictionary only the entries the decoders read are kept, numbers and flags given by /// reference (`/Predictor 7 0 R`) resolved; `/JBIG2Globals` stays a reference. A `/Crypt` /// entry, which no decoder reads ([`Stream::filters`] leaves it out), is null. `s` itself /// when none of those is indirect, or when the other filters are more than any decoder takes /// ([`pdfcraft_filters::MAX_CHAIN`]). Only small values are copied, so a file can't have a /// large object copied once for each key and filter. pub fn with_direct_parms<'s>(&self, s: &'s Stream) -> Cow<'s, Stream> { let Some(p) = s.dict.get(b"DecodeParms") else { return Cow::Borrowed(s) }; let crypt = |f: &Object| matches!(f.as_name(), Some(b"Crypt")); let filters: Vec = match s.dict.get(b"Filter") { Some(Object::Array(a)) => a.iter().map(crypt).collect(), Some(f) => vec![crypt(f)], None => Vec::new(), }; if filters.iter().filter(|c| !**c).count() > pdfcraft_filters::MAX_CHAIN { return Cow::Borrowed(s); } let mut changed = matches!(p, Object::Ref(_)); // Its numbers and flags kept, resolved; anything else is no filter's parameters (null). let one = |e: &Object, changed: &mut bool| -> Object { *changed |= matches!(e, Object::Ref(_)); let Object::Dict(d) = &*self.held(e) else { return Object::Null }; let mut made = Dict::new(); for key in DECODE_PARMS_KEYS { let Some(v) = d.get(key) else { continue }; *changed |= matches!(v, Object::Ref(_)); let v = self.held(v); if matches!(*v, Object::Int(_) | Object::Real(_) | Object::Bool(_)) { made.set(key.to_vec(), (*v).clone()); } } if let Some(g @ Object::Ref(_)) = d.get(b"JBIG2Globals") { made.set(b"JBIG2Globals".to_vec(), g.clone()); } Object::Dict(made) }; let made = match &*self.held(p) { Object::Array(a) => { Object::Array(a.iter().zip(&filters).map(|(e, crypt)| if *crypt { Object::Null } else { one(e, &mut changed) }).collect()) } _ => one(p, &mut changed), }; if !changed { return Cow::Borrowed(s); } let mut s = s.clone(); s.dict.set(b"DecodeParms".to_vec(), made); Cow::Owned(s) } /// `o`, or the object it refers to, without copying either. fn held<'o>(&self, o: &'o Object) -> Held<'o> { match o { Object::Ref(_) => Held::Resolved(self.resolve(o)), o => Held::Direct(o), } } /// Resolve and return a dictionary (a stream's dictionary counts). pub fn dict(&self, o: &Object) -> Option { self.resolve(o).as_dict().cloned() } /// The CCITT parameters of image `s` when its last filter is `CCITTFaxDecode`: its /// `/DecodeParms` (resolved; the dictionary of a lone filter, or its entry in an array), with the /// defaults of ISO 32000-2 Table 11 for what that leaves out. `None` for another image, and for /// one whose `/Filter` isn't a name or an array of names. pub fn fax_params(&self, s: &Stream) -> Option { let chain = s.readable_filters().ok()?; if !matches!(chain.last(), Some((pdfcraft_filters::Filter::CcittFax, _))) { return None; } let parms = self.last_filter_parms(s); let int = |key: &[u8], default: i64| parms.get(key).and_then(|v| self.resolve(v).as_int()).unwrap_or(default); let flag = |key: &[u8], default: bool| match parms.get(key).map(|v| self.resolve(v)).as_deref() { Some(Object::Bool(b)) => *b, _ => default, }; let d = pdfcraft_filters::CcittParams::default(); Some(pdfcraft_filters::CcittParams { k: int(b"K", d.k), columns: int(b"Columns", d.columns), rows: int(b"Rows", d.rows), end_of_line: flag(b"EndOfLine", d.end_of_line), encoded_byte_align: flag(b"EncodedByteAlign", d.encoded_byte_align), end_of_block: flag(b"EndOfBlock", d.end_of_block), black_is_1: flag(b"BlackIs1", d.black_is_1), }) } /// The `/DecodeParms` dictionary of stream `s`'s last filter, resolved: that of a lone filter, /// or its entry in an array. Empty when there is none. fn last_filter_parms(&self, s: &Stream) -> Dict { // Its place in /Filter, /Crypt included, pairs it with its /DecodeParms entry (§7.4.1): the // last filter but a /Crypt, which `filters` leaves out wherever it stands. let index = match s.dict.get(b"Filter") { Some(Object::Array(a)) => a.iter().rposition(|f| f.as_name() != Some(b"Crypt".as_slice())).unwrap_or(0), _ => 0, }; match s.dict.get(b"DecodeParms").map(|p| self.resolve(p)).as_deref() { Some(Object::Array(a)) => a.get(index).and_then(|p| self.dict(p)), Some(p) if index == 0 => p.as_dict().cloned(), _ => None, } .unwrap_or_default() } /// [`Document::decoded_image_spending`] with no limit on the work, for tests of what an image /// decodes to: everything else decodes images through an [`ImageBudget`](crate::ImageBudget). #[cfg(test)] pub(crate) fn decoded_image(&self, s: &Stream) -> Result, CosError> { self.decoded_image_within(s, MAX_DECODED) } /// [`Document::decoded_image`] within `max` bytes. #[cfg(test)] pub(crate) fn decoded_image_within(&self, s: &Stream, max: usize) -> Result, CosError> { let mut unbounded = u64::MAX; self.decoded_image_spending(s, max, &mut unbounded) } /// The samples of image `s`, decoded within `max` bytes ([`MAX_DECODED`] at most), spending from /// `work`: see [`ImageBudget::decode`](crate::ImageBudget::decode), the way in from outside this /// crate, which says what this decodes and how. pub(crate) fn decoded_image_spending(&self, s: &Stream, max: usize, work: &mut u64) -> Result, CosError> { let max = max.min(MAX_DECODED); let s = &*self.with_direct_parms(s); let filter = |e: pdfcraft_filters::FilterError| CosError::Filter(e.to_string()); let chain = s.readable_filters().map_err(filter)?; // The filters before the image codec, such as ASCII85Decode, are undone first. let undo = |before: &[(pdfcraft_filters::Filter, pdfcraft_filters::Params)], work: &mut u64| match before { [] => Ok(Cow::Borrowed(&s.raw[..])), _ => pdfcraft_filters::decode_tolerant_within(before, &s.raw, max, work).map(Cow::Owned).map_err(filter), }; // `/Width` and `/Height` are read as given directly, as every caller reads them first: one // given by reference has the image refused there, before it gets here. let height = s.dict.int(b"Height").and_then(|h| u32::try_from(h).ok()); if let Some(((pdfcraft_filters::Filter::Jbig2, _), before)) = chain.split_last() { let (Some(columns), Some(height)) = (s.dict.int(b"Width").and_then(|w| u32::try_from(w).ok()), height) else { return Err(CosError::Filter("a JBIG2 image's /Width or /Height isn't a size".into())); }; // Nothing to read for what can't be decoded: the images of a file share its globals. let globals = if *work == 0 { None } else { self.jbig2_globals(s, work)? }; let data = if *work == 0 { Cow::Borrowed(&[][..]) } else { undo(before, work)? }; return pdfcraft_filters::decode_jbig2_within(&data, globals.as_deref().map(Vec::as_slice), columns, height, max, work).map_err(filter); } let Some(p) = self.fax_params(s) else { return s.decoded_spending(max, work) }; // As many rows as the image has: a /Rows below /Height would cut the picture short, and // rows past /Height aren't drawn. let p = pdfcraft_filters::CcittParams { rows: 0, ..p }; if s.dict.int(b"Width") != Some(p.columns) { return Err(CosError::Filter("a fax image's /Columns aren't its /Width".into())); } let stride = usize::try_from(p.columns).unwrap_or(usize::MAX).div_ceil(8).max(1); let rows = u32::try_from(max / stride).unwrap_or(u32::MAX).min(height.unwrap_or(0)); let data = undo(chain.split_last().map_or(&[][..], |(_, before)| before), work)?; pdfcraft_filters::decode_ccitt_within(&data, &p, rows, work).map_err(filter) } /// The `/JBIG2Globals` stream a JBIG2 image `s` names by reference in its last filter's /// `/DecodeParms`: `None` for a stream whose last filter isn't `JBIG2Decode`, or one without /// globals. Redaction finds with it the other images that share a redacted image's globals. pub fn jbig2_globals_ref(&self, s: &Stream) -> Option { let chain = s.readable_filters().ok()?; if !matches!(chain.last(), Some((pdfcraft_filters::Filter::Jbig2, _))) { return None; } self.last_filter_parms(s).get(b"JBIG2Globals").and_then(Object::as_ref) } /// What the `/JBIG2Globals` stream of JBIG2 image `s` (in its last filter's `/DecodeParms`) /// decodes to, within [`MAX_JBIG2_GLOBALS`]; `None` without one (a reference to no object is /// one to null, ISO 32000-2 §7.3.10, which is an absent entry, §7.3.7). The image is then read /// without them, as the renderer draws it, so an image that needed them mostly fails to decode /// and can decode to other pixels. A stream that is there and can't be read is an error, rather /// than the image decoded without it. /// /// The images of a scan share one globals stream, so a stream of the file, not edited since /// it was read (the same in every clone) and with its `/DecodeParms` direct (by reference they /// are another object, which can change while this one doesn't), has its filters undone once a /// document and is kept, up to [`JBIG2_GLOBALS_HELD`] in all; others are read each time. That /// saves the filters only: the segments it holds are decoded again for every image. Undoing /// the filters spends from `work`, so globals read for every image pay every time. fn jbig2_globals(&self, s: &Stream, work: &mut u64) -> Result>>, CosError> { let Some(entry) = self.last_filter_parms(s).get(b"JBIG2Globals").cloned() else { return Ok(None) }; let held = match entry { Object::Ref(r) if !self.overlay.contains_key(&r.num) => Some(r.num), _ => None, }; if let Some(hit) = held.and_then(|num| self.jbig2_globals.lock().ok()?.get(&num).cloned()) { return Ok(Some(hit)); } let Object::Stream(g) = &*self.resolve(&entry) else { return Ok(None) }; let readable = self.with_direct_parms(g); let held = held.filter(|_| matches!(readable, Cow::Borrowed(_))); let mut data = readable.decoded_spending(MAX_JBIG2_GLOBALS, work).map_err(|e| { let detail = match e { CosError::Filter(detail) => detail, e => e.to_string(), }; CosError::Filter(format!("the JBIG2 globals can't be read: {detail}")) })?; // What is kept counts by its length, so that's all it may hold on to. data.shrink_to_fit(); let data = Arc::new(data); // A poisoned lock only means nothing is kept. if let (Some(num), Ok(mut kept)) = (held, self.jbig2_globals.lock()) { let total = kept.values().map(|g| g.len()).fold(data.len(), usize::saturating_add); if total <= JBIG2_GLOBALS_HELD { kept.insert(num, data.clone()); } } Ok(Some(data)) } pub fn try_get(&self, num: u32) -> Result, CosError> { if let Some(slot) = self.overlay.get(&num) { note_overlay_read(); return Ok(match slot { Slot::Set(_, o) => o.clone(), Slot::Freed(_) => Arc::new(Object::Null), }); } if let Some(loaded) = self.cache.lock().map_err(|_| CosError::Poisoned)?.get(&num) { return loaded.clone(); } // Loading can re-enter (indirect stream `/Length`, object streams); damaged files make // cycles such as `6 0 obj << /Length 6 0 R >>`. Bound the nesting on every path. let _guard = LoadGuard::enter(num).ok_or_else(|| CosError::Syntax { offset: 0, detail: format!("reference cycle while loading object {num}") })?; let edits_read = overlay_reads(); let loaded = self.load(num, 0).map(Arc::new); // A failure is kept like an object, or every read of a damaged object searches the whole // file again. A poisoned lock says nothing about the object. Clones share the cache, so // a load that read this document's edits (an edited object stream or `/Length`) is not kept. if overlay_reads() == edits_read && !matches!(loaded, Err(CosError::Poisoned)) { // The editing cache is bounded like the scan reader's: re-parsing a file-backed view // is cheap, and returned objects outlive eviction. self.cache.lock().map_err(|_| CosError::Poisoned)?.insert(num, loaded.clone()); self.evict_editor_cache_if_over_budget(num, &loaded)?; } loaded } /// Bounds for the editing cache: roomier than the scan reader's (128 objects / 8 MB) /// so interactive editing does not thrash, but still capped so touching a large /// document cannot pin its whole object graph. Only owned (decrypted/copied) bytes /// count; file-backed views re-parse for free. /// /// Worst case per document: input bytes + up to `EDITOR_CACHE_OBJECTS` × /// `EDITOR_LARGE_OBJECT` (1024 × 256 KB = 256 MB) of small objects between full /// accounting passes + `EDITOR_OBJSTM_BYTES` (32 MB) + unsaved `overlay` edits. /// The count cap always holds; byte accounting between passes is approximate. /// Clones share these caches, so one clone's eviction drops another's hot set; /// that only costs re-parsing, never correctness. Eviction keeps just the newcomer: /// file-backed views re-parse cheaply and editor working sets stay far below 1024. pub(super) const EDITOR_CACHE_OBJECTS: usize = 1024; pub(super) const EDITOR_CACHE_BYTES: usize = 32 * 1024 * 1024; /// Object streams decode to megabytes each, so fewer are kept than plain objects. const EDITOR_OBJSTM_OBJECTS: usize = 256; const EDITOR_OBJSTM_BYTES: usize = 32 * 1024 * 1024; /// Newcomers above this force a full accounting pass even below the count cap: /// `EDITOR_CACHE_BYTES / 128`, so 128 large newcomers always trigger a pass before the /// byte budget can double. const EDITOR_LARGE_OBJECT: usize = 256 * 1024; fn evict_editor_cache_if_over_budget(&self, fresh_num: u32, fresh: &Loaded) -> Result<(), CosError> { // Fast path: the full fold is O(cache) per insert, so skip it for a small newcomer // below the count cap. `len` is copied out first; nothing here holds the lock. let len = self.cache.lock().map_err(|_| CosError::Poisoned)?.len(); if len <= Self::EDITOR_CACHE_OBJECTS && loaded_bytes(fresh, &self.data) <= Self::EDITOR_LARGE_OBJECT { return Ok(()); } // Walk a snapshot off-lock; re-lock only to evict. let live: Vec<(u32, Loaded)> = self.cache.lock().map_err(|_| CosError::Poisoned)?.iter().map(|(n, o)| (*n, o.clone())).collect(); let data = &self.data; let owned: usize = live.iter().fold(0usize, |n, (_, o)| n.saturating_add(loaded_bytes(o, data))); if live.len() <= Self::EDITOR_CACHE_OBJECTS && owned <= Self::EDITOR_CACHE_BYTES { return Ok(()); } // Re-check under the final lock: another thread may have inserted or evicted since // the snapshot. Keep its newcomers, and skip the eviction if back within budget. let snapshot: std::collections::HashSet = live.into_iter().map(|(n, _)| n).collect(); let mut cache = self.cache.lock().map_err(|_| CosError::Poisoned)?; if cache.len() <= Self::EDITOR_CACHE_OBJECTS { let data = &self.data; let owned: usize = cache.values().fold(0usize, |n, o| n.saturating_add(loaded_bytes(o, data))); if owned <= Self::EDITOR_CACHE_BYTES { return Ok(()); } } cache.retain(|n, _| *n == fresh_num || !snapshot.contains(n)); Ok(()) } fn evict_editor_streams_if_over_budget(&self, current: u32) -> Result<(), CosError> { // The caller inserts `current` first, so retaining it keeps a live entry. let over = { let streams = self.objstms.lock().map_err(|_| CosError::Poisoned)?; let bytes = streams.values().fold(0usize, |bytes, stream| { bytes .saturating_add(stream.data.capacity()) .saturating_add(stream.index.capacity().saturating_mul(std::mem::size_of::>())) }); bytes > Self::EDITOR_OBJSTM_BYTES || streams.len() > Self::EDITOR_OBJSTM_OBJECTS }; if over { self.objstms.lock().map_err(|_| CosError::Poisoned)?.retain(|n, _| *n == current); } Ok(()) } /// Visit every current object without filling this document's editing cache. /// /// Full-document searches (for example, standalone timestamps) need to inspect objects /// that may never be used again. Keep their parse caches private and release them in /// batches. Returned objects own their data and remain valid after the next batch. pub fn scan_objects(&self) -> impl Iterator)> + use<> { let numbers = self.object_numbers(); let mut reader = self.object_reader(); numbers.into_iter().map(move |num| { let reference = ObjRef::new(num, reader.document.generation(num)); (reference, reader.get(reference)) }) } /// Like [`Self::scan_objects`], preserving read errors for callers that must distinguish /// incomplete discovery from an actual null object. Caches remain private and bounded. pub fn scan_objects_checked(&self) -> impl Iterator, CosError>)> + use<> { self.scan_objects_subset(self.object_numbers()) } /// Inspect selected current objects with the same bounded cache as a whole-file scan. /// Missing/freed numbers yield `Null`; malformed objects return their read error. pub fn scan_objects_subset(&self, numbers: Vec) -> impl Iterator, CosError>)> + use<> { let mut reader = self.object_reader(); numbers.into_iter().map(move |num| { let reference = ObjRef::new(num, reader.document.generation(num)); (reference, reader.try_get(reference)) }) } /// Inspect selected objects in the immutable source, ignoring all unsaved edits. This /// allows a consumer to check an edit's dependencies without retaining an old document. pub fn scan_original_objects_subset(&self, numbers: Vec) -> impl Iterator, CosError>)> + use<> { let mut reader = self.object_reader(); reader.document.overlay.clear(); numbers.into_iter().map(move |num| { let reference = ObjRef::new(num, reader.document.generation(num)); (reference, reader.try_get(reference)) }) } /// Opened with a tighter stream limit than editing uses ([`Document::open_with_stream_limit`]). pub(crate) fn stream_limited(&self) -> bool { self.stream_limit < crate::object::MAX_DECODED } pub(crate) fn object_reader(&self) -> ObjectReader { let mut document = self.clone(); document.cache = Arc::default(); document.objstms = Arc::default(); ObjectReader { document, decoded_count: 0 } } fn load(&self, num: u32, depth: u32) -> Result { if depth > 16 { return Err(CosError::Syntax { offset: 0, detail: "reference cycle while loading".into() }); } match self.entries.lookup(num, &self.data) { None | Some(XrefEntry::Free { .. }) => Ok(Object::Null), Some(XrefEntry::InFile { offset, .. }) => { let resolve = |r: ObjRef| { if self.overlay.contains_key(&r.num) { note_overlay_read(); } if let Some(known) = LENGTHS.with(|m| m.borrow().get(&r.num).copied()) { return known; } let length = self.try_get(r.num).ok().and_then(|o| o.as_int()); LENGTHS.with(|m| m.borrow_mut().insert(r.num, length)); length }; let off = offset as usize; let at = |off: usize| match parse_indirect_shared(&self.data, off, &resolve, &self.endstreams) { Ok((id, o)) if id.num == num => Some(self.decrypted(id, o)), _ => None, }; // Offsets relative to a shifted header, or simply wrong: try both fixes. at(off) .or_else(|| if self.header_offset == 0 { None } else { at(off.saturating_add(self.header_offset)) }) .or_else(|| self.located.get(&num).and_then(|(o, _)| usize::try_from(*o).ok()).and_then(at)) .or_else(|| self.scan_for(num).map(|(id, o)| self.decrypted(id, o))) .ok_or(CosError::MissingObject(num)) } Some(XrefEntry::InStream { stream, index }) => { let stm = self.objstm(stream)?; // The member at the entry's position, else wherever the stream lists `num`. let at = usize::try_from(index).ok().and_then(|i| stm.index.get(i)).copied().flatten().filter(|(n, _)| *n == num); let (_, off) = at.or_else(|| stm.index.iter().flatten().find(|(n, _)| *n == num).copied()).ok_or(CosError::MissingObject(num))?; Lexer::new(&stm.data, off).object() } } } pub(super) fn objstm(&self, num: u32) -> Result, CosError> { // An edited or freed stream is this document's alone: clones share the parsed streams. let edited = self.overlay.contains_key(&num); if !edited && let Some(s) = self.objstms.lock().map_err(|_| CosError::Poisoned)?.get(&num) { return Ok(s.clone()); } // A file entry that cannot place the stream (one that says it is itself compressed // inside another stream, as in #2367, or points nowhere) is tried by the stream's own // header, as a plain object with a wrong offset is. An edited or freed stream, in the // overlay or in a saved revision's table, is what that says it is: the file's copy // must not come back. let edits_read = overlay_reads(); let loaded = match self.try_get(num) { Ok(o) if matches!(*o, Object::Stream(_)) => o, _ if self.overlay.contains_key(&num) || matches!(self.xref_entry(num), Some(XrefEntry::Free { .. })) => { return Err(CosError::MissingObject(num)); } _ => Arc::new(self.scan_for(num).map(|(id, o)| self.decrypted(id, o)).ok_or(CosError::MissingObject(num))?), }; // A stream whose load read this document's edits (its `/Length`) is as much its own as an edited one. let own = edited || overlay_reads() != edits_read; let Object::Stream(s) = &*loaded else { return Err(CosError::MissingObject(num)) }; let n = s.dict.int(b"N").unwrap_or(0).clamp(0, 1_000_000) as usize; let first = usize::try_from(s.dict.int(b"First").unwrap_or(0).max(0)).ok(); let data = s.decoded_within(self.stream_limit)?; let mut lx = Lexer::new(&data, 0); let mut index = Vec::with_capacity(n); for _ in 0..n { let (Some(Object::Int(onum)), Some(Object::Int(off))) = (lx.object().ok(), lx.object().ok()) else { break }; index.push(objstm_member(onum, off, first, data.len())); } let stm = Arc::new(ObjStm { index, data: Arc::new(data) }); if own { return Ok(stm); } // Same bound as the object cache: decoded object streams can be megabytes each. // Insert first so the retained `num` is actually in the map. self.objstms.lock().map_err(|_| CosError::Poisoned)?.insert(num, stm.clone()); self.evict_editor_streams_if_over_budget(num)?; Ok(stm) } /// Last-resort lookup: scan the file for `num G obj`. fn scan_for(&self, num: u32) -> Option<(ObjRef, Object)> { #[cfg(test)] super::OBJECT_SEARCHES.with(|c| c.set(c.get() + 1)); let needle = format!("{num} "); let data = &self.data; let mut found = None; let mut i = 0; while let Some(p) = find(data, needle.as_bytes(), i, data.len()) { i = p + 1; if p > 0 && !is_whitespace(data[p - 1]) { continue; } if let Ok((id, o)) = parse_indirect_shared(data, p, &|_| None, &self.endstreams) && id.num == num { found = Some((id, o)); // keep the last (newest) definition } } found } // ── editing ───────────────────────────────────────────────────────────────────────────── /// Stream data still pointing into another document's file would keep that whole file /// alive for as long as this edit exists: give it a buffer of its own (just its bytes). fn adopt(&self, obj: Object) -> Object { match obj { Object::Stream(mut s) if s.raw.borrows_other_than(&self.data) => { s.raw = s.raw.detached(); Object::Stream(s) } other => other, } } /// Replace an object (keeping its generation). pub fn set(&mut self, r: ObjRef, obj: impl Into) { let obj = self.adopt(obj.into()); self.overlay.insert(r.num, Slot::Set(r.generation, Arc::new(obj))); if r.num >= self.next_num { self.next_num = r.num.saturating_add(1); } } /// Add a new indirect object and return its reference. pub fn add(&mut self, obj: impl Into) -> ObjRef { let r = ObjRef::new(self.allocate_num(), 0); let obj = self.adopt(obj.into()); self.overlay.insert(r.num, Slot::Set(0, Arc::new(obj))); r } /// Find a never-used number, retaining the high-water mark for incremental /Size. /// Exhausted hints fall back to gaps without replacing source or overlay objects. fn allocate_num(&mut self) -> u32 { let mut num = if self.next_num == u32::MAX { 1 } else { self.next_num.max(1) }; while self.entries.lookup(num, &self.data).is_some() || self.overlay.contains_key(&num) { num = if num == u32::MAX - 1 { 1 } else { num + 1 }; } self.next_num = self.next_num.max(num.saturating_add(1)); num } /// Delete an object (its number becomes free with the next generation). pub fn free(&mut self, r: ObjRef) { self.overlay.insert(r.num, Slot::Freed(r.generation.saturating_add(1))); } /// Free what was added since the file was read and nothing refers to any more: the objects /// [`SaveOptions::drop_unlinked`](crate::SaveOptions::drop_unlinked) leaves out of an /// incremental save, freed as it writes them (the generation stays). For a writer about to /// add an object that nothing refers to either, which that option would leave out too. pub fn free_unlinked_additions(&mut self) { for num in crate::writer::unlinked_additions(self) { let generation = self.generation(num); self.overlay.insert(num, Slot::Freed(generation)); } } /// Record `current`'s object graph as unsaved edits of this document. /// /// Used when the working snapshot was restored from an earlier revision and the next /// incremental save must still append to this file (the last saved revision). Objects /// that already match are left untouched. Reads go through private readers so neither /// document's editing cache is filled. pub fn overlay_changes_from(&mut self, current: &Document) -> Result<(), CosError> { let saved_nums = self.object_numbers(); let current_nums = current.object_numbers(); let mut freed = Vec::new(); let mut changed = Vec::new(); { let mut saved_reader = self.object_reader(); let mut current_reader = current.object_reader(); for &num in &saved_nums { if current_nums.binary_search(&num).is_err() { freed.push(num); } } for &num in ¤t_nums { let generation = current.generation(num); let same = saved_nums.binary_search(&num).is_ok() && self.generation(num) == generation && saved_reader.try_get(ObjRef::new(num, 0))? == current_reader.try_get(ObjRef::new(num, 0))?; if !same { changed.push(num); } } } for num in freed { let generation = self.generation(num); self.free(ObjRef::new(num, generation)); } let mut current_reader = current.object_reader(); for num in changed { let generation = current.generation(num); let object = current_reader.try_get(ObjRef::new(num, 0))?; self.set(ObjRef::new(num, generation), (*object).clone()); } *self.trailer_mut() = current.trailer().clone(); Ok(()) } /// Edit a dictionary (or stream dictionary) object in place. pub fn update_dict(&mut self, r: ObjRef, f: impl FnOnce(&mut Dict)) -> Result<(), CosError> { let mut obj = (*self.get(r)).clone(); let d = obj.as_dict_mut().ok_or(CosError::NotADictionary(r))?; f(d); self.set(r, obj); Ok(()) } /// Current generation of an object number. pub fn generation(&self, num: u32) -> u16 { match self.overlay.get(&num) { Some(Slot::Set(g, _)) => *g, Some(Slot::Freed(g)) => *g, None => match self.entries.lookup(num, &self.data) { Some(XrefEntry::InFile { generation, .. }) => generation, Some(XrefEntry::Free { next_generation }) => next_generation, _ => 0, }, } } pub(crate) fn overlay_entries(&self) -> impl Iterator>)> + '_ { self.overlay.iter().map(|(n, s)| match s { Slot::Set(g, o) => (*n, *g, Some(o.clone())), Slot::Freed(g) => (*n, *g, None), }) } pub(crate) fn next_num(&self) -> u32 { self.next_num } /// All object numbers known (file and overlay), excluding freed ones. pub fn object_numbers(&self) -> Vec { let source_numbers = self.entries.object_numbers(&self.data); let mut numbers = Vec::with_capacity(source_numbers.len().max(self.overlay.len())); let mut source = source_numbers.into_iter().peekable(); for (&num, slot) in &self.overlay { while source.peek().is_some_and(|n| *n < num) { if let Some(n) = source.next() { numbers.push(n); } } if source.peek().is_some_and(|n| *n == num) { source.next(); } if matches!(slot, Slot::Set(..)) { numbers.push(num); } } numbers.extend(source); numbers } /// Rebase this document on bytes written by the writer (after a save): edits become part of /// the file and the overlay is cleared, so the next incremental save appends only new edits. pub fn reopen_after_save(&self, bytes: Arc>) -> Result { Document::open(bytes) } // ── cross-reference parsing ───────────────────────────────────────────────────────────── pub(super) fn root_is_catalog(&self) -> bool { let Some(r) = self.root() else { return false }; matches!(self.try_get(r.num).as_deref(), Ok(Object::Dict(d)) if d.name(b"Type") == Some(b"Catalog") || d.contains(b"Pages")) } } /// The `/DecodeParms` entries the decoders read (ISO 32000-2 Tables 8, 11 and 13: the Flate and /// LZW predictors, CCITT, DCT's colour transform): numbers and flags a file may give by /// reference, which [`Dict::int`] doesn't follow. const DECODE_PARMS_KEYS: [&[u8]; 13] = [ b"Predictor", b"Colors", b"BitsPerComponent", b"Columns", b"EarlyChange", b"K", b"Rows", b"EndOfLine", b"EncodedByteAlign", b"EndOfBlock", b"BlackIs1", b"DamagedRowsBeforeError", b"ColorTransform", ]; /// An object read in place, or the object a reference names as the document holds it: either way /// nothing is copied ([`Document::held`]). enum Held<'o> { Direct(&'o Object), Resolved(Arc), } impl std::ops::Deref for Held<'_> { type Target = Object; fn deref(&self) -> &Object { match self { Held::Direct(o) => o, Held::Resolved(o) => o, } } }