mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-08 03:54:35 +00:00
An unedited object stream whose indirect /Length is edited in one clone was parsed with that clone's length and put in the object-stream map the clones share, so the other clones read its members within the edited length. A container load that read the clone's edits is now kept to that clone, like an edited stream. Test: an_edited_object_stream_length_leaves_other_clones_members_alone fails without the fix (the other clone reads None) and passes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
851 lines
41 KiB
Rust
851 lines
41 KiB
Rust
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<Vec<u8>> {
|
||
&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<String>) {
|
||
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<usize> {
|
||
let d = &self.data[..];
|
||
let mut ends: Vec<usize> = 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<u32> {
|
||
self.overlay.keys().copied().collect()
|
||
}
|
||
|
||
/// Where object `num` is stored in the file (ignoring unsaved edits).
|
||
pub fn xref_entry(&self, num: u32) -> Option<XrefEntry> {
|
||
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<ObjRef> {
|
||
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<Object> {
|
||
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<Object> {
|
||
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<bool> = 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<Dict> {
|
||
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<pdfcraft_filters::CcittParams> {
|
||
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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<ObjRef> {
|
||
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<Option<Arc<Vec<u8>>>, 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<Arc<Object>, 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<u32> = 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::<Option<(u32, usize)>>()))
|
||
});
|
||
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<Item = (ObjRef, Arc<Object>)> + 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<Item = (ObjRef, Result<Arc<Object>, 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<u32>) -> impl Iterator<Item = (ObjRef, Result<Arc<Object>, 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<u32>) -> impl Iterator<Item = (ObjRef, Result<Arc<Object>, 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<Object, CosError> {
|
||
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<Arc<ObjStm>, 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<Object>) {
|
||
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<Object>) -> 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<Item = (u32, u16, Option<Arc<Object>>)> + '_ {
|
||
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<u32> {
|
||
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<Vec<u8>>) -> Result<Self, CosError> {
|
||
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<Object>),
|
||
}
|
||
|
||
impl std::ops::Deref for Held<'_> {
|
||
type Target = Object;
|
||
|
||
fn deref(&self) -> &Object {
|
||
match self {
|
||
Held::Direct(o) => o,
|
||
Held::Resolved(o) => o,
|
||
}
|
||
}
|
||
}
|