mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 21:17:55 -06:00
345 lines
16 KiB
Rust
345 lines
16 KiB
Rust
use super::reader::{find, generated_id};
|
|
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(),
|
|
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(),
|
|
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 mut id = generated_id(¶ms.seed);
|
|
id.truncate(16);
|
|
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,
|
|
}
|
|
}
|
|
}
|