Files
pdfcraft/crates/cos/src/document/open.rs
T
Grzegorz MartensandClaude Opus 5.5 5480dbe163 M2.6: shared JBIG2 globals are read once a document; Copy Image is tested on JBIG2
From CodeRabbit's review of the stack:
- Every JBIG2 image read its /JBIG2Globals afresh, within 32 MiB each but with no
  bound across an export or a redaction. A globals stream of the file, not edited since
  it was read and with its /DecodeParms direct, now has its filters undone once a
  document and is kept for the images that share it (in every clone, which share the
  file), up to 64 MiB in all. An edited one, one added in memory, or one with
  /DecodeParms by reference (another object, which can change under it) is read each
  time as before. This saves the filters only: the decoder reads the segments afresh for
  every image, so a real symbol dictionary still costs its decoding once an image.
- view.copy-image dropped "JBIG2 inside other filters" from its missing list on the
  strength of Export's test; Copy Image now has its own: a JBIG2 picture behind
  ASCIIHexDecode is found under the pointer and copied as its pixels and as a PNG, and
  one whose page information is in its globals is read through them.

A JBIG2 image's /Width and /Height are read as given directly; one by reference is an
error rather than an image of no rows.

A /JBIG2Globals reference to no object still reads as no globals: it is a reference
to null (ISO 32000-2 §7.3.10), which an entry may hold for being absent (§7.3.7), and
the renderer draws the page so. An image that needed those globals then mostly fails
to decode; in a probe of twelve pictures that decoded with them, eleven were refused
and one decoded to other pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 00:06:56 +01:00

346 lines
16 KiB
Rust

use super::reader::find;
use super::state::Credentials;
use super::xref::XrefIndex;
use super::{Arc, BTreeMap, CosError, Dict, Document, ObjRef, Object};
impl Document {
/// A new document with an empty page tree (for split / extract / combine). It has no
/// original bytes, so saving always writes a full file.
pub fn new_empty() -> Self {
let mut doc = Document {
data: Arc::new(Vec::new()),
endstreams: Arc::default(),
source_identity: Arc::new(()),
entries: Arc::new(XrefIndex::empty()),
trailer: Dict::new(),
revisions: Arc::new(Vec::new()),
xref_oddities: Arc::default(),
located: Arc::default(),
repair_log: Arc::new(Vec::new()),
cache: Arc::default(),
objstms: Arc::default(),
jbig2_globals: Arc::default(),
stream_limit: crate::object::MAX_DECODED,
overlay: BTreeMap::new(),
next_num: 1,
header_offset: 0,
version: "1.7".into(),
security: None,
encrypt_num: None,
encryption_changed: false,
full_save: false,
out_security: None,
out_encrypt_num: None,
};
let mut pages = Dict::new();
pages.set(b"Type".to_vec(), Object::name("Pages"));
pages.set(b"Kids".to_vec(), Object::Array(Vec::new()));
pages.set(b"Count".to_vec(), Object::Int(0));
let pages = doc.add(pages);
let mut catalog = Dict::new();
catalog.set(b"Type".to_vec(), Object::name("Catalog"));
catalog.set(b"Pages".to_vec(), Object::Ref(pages));
let root = doc.add(catalog);
doc.trailer.set(b"Root".to_vec(), Object::Ref(root));
doc
}
/// Parse a document. Damaged cross-reference data is reconstructed; see `repair_log`.
/// Encrypted documents open only if their user password is empty; see `open_with_password`.
pub fn open(data: Arc<Vec<u8>>) -> Result<Self, CosError> {
Self::open_with_password(data, None)
}
/// Parse a document, authenticating encrypted ones with `password` (owner or user; `None`
/// tries the empty password). Fails with `NeedsPassword` / `WrongPassword` as appropriate.
pub fn open_with_password(data: Arc<Vec<u8>>, password: Option<&str>) -> Result<Self, CosError> {
Self::open_with_stream_limit(data, password, crate::object::MAX_DECODED)
}
/// Open with a tighter decompression bound for object and cross-reference streams.
/// Inspection can use this without changing the editing reader's default limit. Such a
/// document is read-only: objects in a larger object stream read as missing, so the
/// writers refuse to save it ([`CosError::ReadOnlyLimit`]).
pub fn open_with_stream_limit(data: Arc<Vec<u8>>, password: Option<&str>, stream_limit: usize) -> Result<Self, CosError> {
Self::open_as(data, Credentials::Password(password), stream_limit)
}
/// Open `data`, an earlier revision of this file (the bytes a signature covers) or another
/// save of it, with this document's credentials: an encrypted one must have the same
/// `/Encrypt` dictionary and `/ID` as this document, whose file key is then reused, so no
/// password is needed or kept; unencrypted data opens as [`open`](Self::open) does. Data
/// encrypted differently fails with `NeedsPassword`.
pub fn open_with_same_credentials(&self, data: Arc<Vec<u8>>) -> Result<Self, CosError> {
let credentials = match &self.security {
Some(handler) => Credentials::Handler { handler, id0: self.first_id() },
None => Credentials::Password(None),
};
Self::open_as(data, credentials, self.stream_limit)
}
fn open_as(data: Arc<Vec<u8>>, credentials: Credentials<'_>, stream_limit: usize) -> Result<Self, CosError> {
// Viewers accept files whose header is missing or damaged as long as the body looks
// like PDF; so do we (a note goes to the repair log).
let header = find(&data, b"%PDF-", 0, 1024);
let headerless = header.is_none();
if headerless && find(&data, b" obj", 0, 4096).is_none() {
return Err(CosError::NotPdf);
}
let header_offset = header.unwrap_or(0);
let version = match header {
Some(h) => String::from_utf8_lossy(&data[(h + 5).min(data.len())..(h + 8).min(data.len())]).trim().to_string(),
None => "1.4".to_string(),
};
let mut doc = Document {
data,
endstreams: Arc::default(),
source_identity: Arc::new(()),
entries: Arc::new(XrefIndex::empty()),
trailer: Dict::new(),
revisions: Arc::new(Vec::new()),
xref_oddities: Arc::default(),
located: Arc::default(),
repair_log: Arc::new(Vec::new()),
cache: Arc::default(),
objstms: Arc::default(),
jbig2_globals: Arc::default(),
stream_limit: stream_limit.min(crate::object::MAX_DECODED),
overlay: BTreeMap::new(),
next_num: 1,
header_offset,
version,
security: None,
encrypt_num: None,
encryption_changed: false,
full_save: false,
out_security: None,
out_encrypt_num: None,
};
let mut log = Vec::new();
if headerless {
log.push("the %PDF- header is missing; reading the file as PDF 1.4".into());
}
let parsed = doc.read_xref_chain(&mut log);
let mut authenticated = false;
let ok = match parsed {
Ok((entries, trailer, revisions, oddities)) => {
doc.entries = Arc::new(entries);
doc.trailer = trailer;
doc.revisions = Arc::new(revisions);
doc.xref_oddities = Arc::new(oddities);
doc.fill_xref_gaps(&mut log);
// Authenticate before checking the catalog: it may sit in an encrypted object
// stream, which reads as garbage until the key is known.
if let Some(enc) = doc.trailer.get(b"Encrypt").cloned() {
doc.authenticate(&enc, &credentials)?;
authenticated = true;
}
doc.root_is_catalog()
}
Err(e) => {
log.push(format!("cross-reference data unreadable ({e}); rebuilding from object headers"));
false
}
};
if !ok {
if !doc.entries.is_empty() {
log.push("cross-reference data does not lead to a valid catalog; rebuilding from object headers".into());
}
doc.reconstruct(&mut log)?;
}
doc.repair_log = Arc::new(log);
// Reconstruction may have found a different trailer: (re)authenticate against it.
if (!ok || !authenticated)
&& let Some(enc) = doc.trailer.get(b"Encrypt").cloned()
{
doc.authenticate(&enc, &credentials)?;
}
if Arc::make_mut(&mut doc.entries).remove_exhausted() {
Arc::make_mut(&mut doc.repair_log).push("skipped exhausted xref object number 4294967295".into());
}
let max = doc.entries.max_num();
let size = u32::try_from(doc.trailer.int(b"Size").unwrap_or(0).max(0)).unwrap_or(u32::MAX);
if size == u32::MAX {
Arc::make_mut(&mut doc.repair_log).push("exhausted trailer /Size; new objects use unused object numbers".into());
}
doc.next_num = max.saturating_add(1).max(size).max(1);
Ok(doc)
}
fn authenticate(&mut self, enc: &Object, credentials: &Credentials<'_>) -> Result<(), CosError> {
// Authentication can change how the same input is decoded.
self.source_identity = Arc::new(());
let (num, dict) = match enc {
Object::Ref(r) => (Some(r.num), self.get(*r).as_dict().cloned()),
Object::Dict(d) => (None, Some(d.clone())),
_ => (None, None),
};
let Some(dict) = dict else {
// A dangling /Encrypt: viewers treat the file as unencrypted, and so do we.
let mut log = self.repair_log.as_ref().clone();
log.push("the trailer names an /Encrypt dictionary that does not exist; reading the file as unencrypted".into());
self.repair_log = Arc::new(log);
self.trailer.remove(b"Encrypt");
return Ok(());
};
let params = crate::security::encrypt_dict(self, &dict);
let id0 = self.first_id();
let handler = match credentials {
Credentials::Password(password) => Arc::new(pdfcraft_crypt::SecurityHandler::open(params, &id0, *password).map_err(|e| match e {
pdfcraft_crypt::CryptError::WrongPassword if password.is_none() => CosError::NeedsPassword,
pdfcraft_crypt::CryptError::WrongPassword => CosError::WrongPassword,
other => CosError::Security(other.to_string()),
})?),
// The same encryption as the document whose key this is: that key opens it.
Credentials::Handler { handler, id0: known } if *handler.dict() == params && *known == id0 => Arc::clone(handler),
Credentials::Handler { .. } => return Err(CosError::NeedsPassword),
};
self.security = Some(handler);
self.encrypt_num = num;
// Objects read while locating the catalog were read before decryption was possible.
self.cache = Arc::default();
self.objstms = Arc::default();
Ok(())
}
/// The first file identifier, part of the encryption key (empty when there is none).
fn first_id(&self) -> Vec<u8> {
match self.trailer.get(b"ID") {
Some(Object::Array(a)) => a.first().and_then(|s| s.as_string()).map(|s| s.bytes.clone()).unwrap_or_default(),
_ => Vec::new(),
}
}
/// Lift the restrictions of a document opened with its user password by entering the
/// owner (permissions) password (§7.6.4). Fails with `WrongPassword` for any other
/// password. Does nothing when the document is unencrypted or already opened as owner.
/// Both passwords unlock the same file key, so nothing read so far is decoded again.
pub fn unlock(&mut self, password: &str) -> Result<(), CosError> {
let Some(current) = self.security.as_deref() else { return Ok(()) };
if current.auth() == pdfcraft_crypt::Auth::Owner {
return Ok(());
}
let owner = current.authenticate_owner(&self.first_id(), password).map_err(|e| match e {
pdfcraft_crypt::CryptError::WrongPassword => CosError::WrongPassword,
other => CosError::Security(other.to_string()),
})?;
self.security = Some(Arc::new(owner));
Ok(())
}
/// The security handler, when the document is encrypted.
pub fn security(&self) -> Option<&pdfcraft_crypt::SecurityHandler> {
self.security.as_deref()
}
/// What the opening password allows (`None` for unencrypted documents: everything).
pub fn permissions(&self) -> Option<pdfcraft_crypt::Permissions> {
self.security().map(|s| s.permissions())
}
/// Make the next save a full rewrite: earlier revisions (which still hold what an edit
/// removed) must not stay in the file. Redaction requires this.
pub fn require_full_save(&mut self) {
self.full_save = true;
}
/// The next save must rewrite the whole file (see [`Document::require_full_save`]).
pub fn full_save_required(&self) -> bool {
self.full_save
}
/// Security was set or removed since the document was opened (the next save applies it).
pub fn encryption_changed(&self) -> bool {
self.encryption_changed
}
/// The handler used to write: the new one after `set_encryption` / `remove_encryption`,
/// otherwise the one the document was opened with.
pub(crate) fn output_security(&self) -> (Option<&pdfcraft_crypt::SecurityHandler>, Option<u32>) {
if self.encryption_changed { (self.out_security.as_deref(), self.out_encrypt_num) } else { (self.security.as_deref(), self.encrypt_num) }
}
/// The security the next save writes: protection applied with `set_encryption`, none after
/// `remove_encryption`, otherwise the security the document was opened with.
pub fn output_handler(&self) -> Option<&pdfcraft_crypt::SecurityHandler> {
self.output_security().0
}
/// Protect the document with a password (§7.6.4). Takes effect on the next save, which is
/// always a full rewrite. Returns the handler (authenticated as owner).
pub fn set_encryption(&mut self, params: &pdfcraft_crypt::NewEncryption) -> Result<(), CosError> {
// The file identifier is part of the key; make sure it exists and keep it.
let id0 = match self.trailer.get(b"ID") {
Some(Object::Array(a)) if a.len() == 2 => a[0].as_string().map(|s| s.bytes.clone()).unwrap_or_default(),
_ => {
let id = pdfcraft_crypt::file_id(&params.seed);
let s = Object::String(crate::PdfString { bytes: id.clone(), hex: true });
self.trailer.set(b"ID".to_vec(), Object::Array(vec![s.clone(), s]));
id
}
};
let h = pdfcraft_crypt::create(params, &id0).map_err(|e| CosError::Security(e.to_string()))?;
let d = h.dict();
let s = |b: &[u8]| Object::String(crate::PdfString { bytes: b.to_vec(), hex: true });
let mut e = Dict::new();
e.set(b"Filter".to_vec(), Object::name("Standard"));
e.set(b"V".to_vec(), Object::Int(d.v));
e.set(b"R".to_vec(), Object::Int(d.r));
e.set(b"Length".to_vec(), Object::Int(d.length_bits));
e.set(b"O".to_vec(), s(&d.o));
e.set(b"U".to_vec(), s(&d.u));
e.set(b"P".to_vec(), Object::Int(i64::from(d.p)));
if d.v >= 4 {
let mut cf = Dict::new();
let mut std_cf = Dict::new();
std_cf.set(b"Type".to_vec(), Object::name("CryptFilter"));
let (cfm, len) = if d.v >= 5 { ("AESV3", 32) } else { ("AESV2", 16) };
std_cf.set(b"CFM".to_vec(), Object::name(cfm));
std_cf.set(b"Length".to_vec(), Object::Int(len));
std_cf.set(b"AuthEvent".to_vec(), Object::name("DocOpen"));
cf.set(b"StdCF".to_vec(), Object::Dict(std_cf));
e.set(b"CF".to_vec(), Object::Dict(cf));
e.set(b"StmF".to_vec(), Object::name("StdCF"));
e.set(b"StrF".to_vec(), Object::name("StdCF"));
if !d.encrypt_metadata {
e.set(b"EncryptMetadata".to_vec(), Object::Bool(false));
}
}
if d.v >= 5 {
e.set(b"OE".to_vec(), s(&d.oe));
e.set(b"UE".to_vec(), s(&d.ue));
e.set(b"Perms".to_vec(), s(&d.perms));
}
if let Some(Object::Ref(old)) = self.trailer.get(b"Encrypt").cloned() {
self.free(old);
}
let r = self.add(e);
self.trailer.set(b"Encrypt".to_vec(), Object::Ref(r));
// Objects are still read with the original handler; saves use the new one.
self.out_security = Some(Arc::new(h));
self.out_encrypt_num = Some(r.num);
self.encryption_changed = true;
Ok(())
}
/// Remove password protection (requires the document to be open — any authenticated
/// password; callers should require the owner password, §7.6.4). Applied by a full save.
pub fn remove_encryption(&mut self) {
if let Some(Object::Ref(r)) = self.trailer.get(b"Encrypt").cloned() {
self.free(r);
}
self.trailer.remove(b"Encrypt");
self.out_security = None;
self.out_encrypt_num = None;
self.encryption_changed = true;
}
/// Decrypt a freshly parsed indirect object if the document is encrypted.
pub(super) fn decrypted(&self, id: ObjRef, o: Object) -> Object {
match &self.security {
Some(h) if Some(id.num) != self.encrypt_num => crate::security::transform(h, &o, id.num, id.generation, crate::security::Pass::Decrypt),
_ => o,
}
}
}