mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 10:46:28 +00:00
Merge main into fix/jbig2-dictionary-any-route
Brings in #5548 (crypt filters by reference): - writer.rs: write_full keeps this branch's walk(), which reachable() shares, and walk() now also follows the references of a direct /Encrypt, as #5548 made write_full do; collect_refs is pub(crate) for #5548's encryption_members; - document.rs re-exports both DECODE_PARMS_KEYS and Held; - document/tests.rs keeps both sides' new tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ mod state;
|
||||
mod xref;
|
||||
|
||||
pub use objects::DECODE_PARMS_KEYS;
|
||||
pub(crate) use objects::Held;
|
||||
#[cfg(test)]
|
||||
pub(crate) use reader::find;
|
||||
#[cfg(test)]
|
||||
@@ -63,6 +64,10 @@ pub struct Document {
|
||||
security: Option<Arc<pdfcraft_crypt::SecurityHandler>>,
|
||||
/// Object number of the `/Encrypt` dictionary (never encrypted itself).
|
||||
encrypt_num: Option<u32>,
|
||||
/// What that dictionary and `/ID` refer to, which the key comes from, and so is as stored.
|
||||
encrypt_members: Arc<HashSet<u32>>,
|
||||
/// Object numbers a scan of the file didn't find: the file doesn't change, so it never will.
|
||||
unfound: Arc<Mutex<HashSet<u32>>>,
|
||||
/// Encryption was added, changed or removed since opening: only a full save can apply it.
|
||||
encryption_changed: bool,
|
||||
/// Set by edits that must not leave earlier revisions in the file (redaction).
|
||||
|
||||
@@ -156,12 +156,16 @@ impl Document {
|
||||
self.try_get(r.num).unwrap_or_else(|_| Arc::new(Object::Null))
|
||||
}
|
||||
|
||||
/// Follow a reference if `o` is one; otherwise return `o` itself.
|
||||
/// Follow a reference if `o` is one; otherwise return `o` itself. An object can be nothing but
|
||||
/// a reference to another, which is followed too, up to [`MAX_REFERENCE_HOPS`] of them.
|
||||
pub fn resolve(&self, o: &Object) -> Arc<Object> {
|
||||
match o {
|
||||
Object::Ref(r) => self.get(*r),
|
||||
other => Arc::new(other.clone()),
|
||||
let Object::Ref(r) = o else { return Arc::new(o.clone()) };
|
||||
let mut held = self.get(*r);
|
||||
for _ in 0..MAX_REFERENCE_HOPS {
|
||||
let Object::Ref(r) = &*held else { break };
|
||||
held = self.get(*r);
|
||||
}
|
||||
held
|
||||
}
|
||||
|
||||
/// `s` with the `/DecodeParms` its filters use made direct, so [`Stream::decoded`] reads them:
|
||||
@@ -218,7 +222,7 @@ impl Document {
|
||||
}
|
||||
|
||||
/// `o`, or the object it refers to, without copying either.
|
||||
fn held<'o>(&self, o: &'o Object) -> Held<'o> {
|
||||
pub(crate) fn held<'o>(&self, o: &'o Object) -> Held<'o> {
|
||||
match o {
|
||||
Object::Ref(_) => Held::Resolved(self.resolve(o)),
|
||||
o => Held::Direct(o),
|
||||
@@ -384,6 +388,7 @@ impl Document {
|
||||
// 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 _scope = crate::security::FollowScope::enter();
|
||||
let obj = self.load(num, 0)?;
|
||||
let obj = Arc::new(obj);
|
||||
// The editing cache is bounded like the scan reader's: re-parsing a file-backed view
|
||||
@@ -551,7 +556,11 @@ impl Document {
|
||||
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)?;
|
||||
// Its parameters can be given by reference, and can sit in another object stream. A scan of
|
||||
// the file's object streams comes here without a read around it.
|
||||
let _scope = crate::security::FollowScope::enter();
|
||||
let readable = crate::security::following(Cow::Borrowed(s), || self.with_direct_parms(s));
|
||||
let data = readable.decoded_within(self.stream_limit)?;
|
||||
let mut lx = Lexer::new(&data, 0);
|
||||
let mut index = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
@@ -566,8 +575,14 @@ impl Document {
|
||||
Ok(stm)
|
||||
}
|
||||
|
||||
/// Last-resort lookup: scan the file for `num G obj`.
|
||||
/// Last-resort lookup: scan the file for `num G obj`. What isn't there is remembered: each miss
|
||||
/// reads the whole file, and a file can refer to such an object from everywhere.
|
||||
fn scan_for(&self, num: u32) -> Option<(ObjRef, Object)> {
|
||||
if self.unfound.lock().is_ok_and(|unfound| unfound.contains(&num)) {
|
||||
return None;
|
||||
}
|
||||
#[cfg(test)]
|
||||
super::tests::FILE_SCANS.with(|scans| scans.set(scans.get() + 1));
|
||||
let needle = format!("{num} ");
|
||||
let data = &self.data;
|
||||
let mut found = None;
|
||||
@@ -583,6 +598,11 @@ impl Document {
|
||||
found = Some((id, o)); // keep the last (newest) definition
|
||||
}
|
||||
}
|
||||
if found.is_none()
|
||||
&& let Ok(mut unfound) = self.unfound.lock()
|
||||
{
|
||||
unfound.insert(num);
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
@@ -775,9 +795,13 @@ pub const DECODE_PARMS_KEYS: [&[u8]; 13] = [
|
||||
b"ColorTransform",
|
||||
];
|
||||
|
||||
/// How many references in a row [`Document::resolve`] follows: enough for any file, and it ends a
|
||||
/// cycle of them.
|
||||
const MAX_REFERENCE_HOPS: usize = 32;
|
||||
|
||||
/// 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> {
|
||||
pub(crate) enum Held<'o> {
|
||||
Direct(&'o Object),
|
||||
Resolved(Arc<Object>),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use super::reader::find;
|
||||
use super::state::Credentials;
|
||||
use super::xref::XrefIndex;
|
||||
use super::{Arc, BTreeMap, CosError, Dict, Document, ObjRef, Object};
|
||||
use super::{Arc, BTreeMap, CosError, Dict, Document, HashSet, ObjRef, Object};
|
||||
|
||||
/// More objects than `/Encrypt` and `/ID` refer to in any file, so that one which refers to the page
|
||||
/// tree, say, doesn't have a whole document read to open it.
|
||||
const MAX_ENCRYPT_MEMBERS: usize = 64;
|
||||
|
||||
impl Document {
|
||||
/// A new document with an empty page tree (for split / extract / combine). It has no
|
||||
@@ -27,6 +31,8 @@ impl Document {
|
||||
version: "1.7".into(),
|
||||
security: None,
|
||||
encrypt_num: None,
|
||||
encrypt_members: Arc::default(),
|
||||
unfound: Arc::default(),
|
||||
encryption_changed: false,
|
||||
full_save: false,
|
||||
out_security: None,
|
||||
@@ -111,6 +117,8 @@ impl Document {
|
||||
version,
|
||||
security: None,
|
||||
encrypt_num: None,
|
||||
encrypt_members: Arc::default(),
|
||||
unfound: Arc::default(),
|
||||
encryption_changed: false,
|
||||
full_save: false,
|
||||
out_security: None,
|
||||
@@ -185,6 +193,8 @@ impl Document {
|
||||
};
|
||||
let params = crate::security::encrypt_dict(self, &dict);
|
||||
let id0 = self.first_id();
|
||||
// Read here, while nothing is decrypted yet.
|
||||
let members = self.encryption_members(enc);
|
||||
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,
|
||||
@@ -197,6 +207,7 @@ impl Document {
|
||||
};
|
||||
self.security = Some(handler);
|
||||
self.encrypt_num = num;
|
||||
self.encrypt_members = Arc::new(members);
|
||||
// Objects read while locating the catalog were read before decryption was possible.
|
||||
self.cache = Arc::default();
|
||||
self.objstms = Arc::default();
|
||||
@@ -205,10 +216,42 @@ impl Document {
|
||||
|
||||
/// 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(),
|
||||
let id = self.trailer.get(b"ID").map(|id| self.resolve(id));
|
||||
let first = id.as_deref().and_then(Object::as_array).and_then(|a| a.first()).map(|s| self.resolve(s));
|
||||
first.as_deref().and_then(Object::as_string).map(|s| s.bytes.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The objects `/Encrypt`, given as `enc`, and `/ID` refer to, the ones they refer to, and so
|
||||
/// on, up to [`MAX_ENCRYPT_MEMBERS`]. Their strings aren't encrypted (ISO 32000-2 §7.6.2): the
|
||||
/// key is made from them, so they have to read as stored, and stay so. A number with no object
|
||||
/// behind it, and a stream, which those dictionaries never hold, isn't one: an object added
|
||||
/// under that number later, or a page's content a hostile `/Encrypt` names, is encrypted.
|
||||
fn encryption_members(&self, enc: &Object) -> HashSet<u32> {
|
||||
let mut pending = Vec::new();
|
||||
crate::writer::collect_refs(enc, &mut |r| pending.push(r));
|
||||
if let Some(id) = self.trailer.get(b"ID") {
|
||||
crate::writer::collect_refs(id, &mut |r| pending.push(r));
|
||||
}
|
||||
let mut members = HashSet::new();
|
||||
while let Some(r) = pending.pop() {
|
||||
if members.len() >= MAX_ENCRYPT_MEMBERS || members.contains(&r.num) {
|
||||
continue;
|
||||
}
|
||||
let held = self.get(r);
|
||||
if matches!(&*held, Object::Null | Object::Stream(_)) {
|
||||
continue;
|
||||
}
|
||||
members.insert(r.num);
|
||||
crate::writer::collect_refs(&held, &mut |x| pending.push(x));
|
||||
}
|
||||
members
|
||||
}
|
||||
|
||||
/// Whether object `num` is written as it is held, being one of [`Document::encryption_members`],
|
||||
/// unedited, and the save writing the same `/Encrypt`. What was edited or added is encrypted
|
||||
/// whatever its number.
|
||||
pub(crate) fn stored_as_is(&self, num: u32) -> bool {
|
||||
!self.encryption_changed && !self.is_edited(num) && self.encrypt_members.contains(&num)
|
||||
}
|
||||
|
||||
/// Lift the restrictions of a document opened with its user password by entering the
|
||||
@@ -270,8 +313,9 @@ impl Document {
|
||||
/// 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 = self.trailer.get(b"ID").map(|id| self.resolve(id));
|
||||
let id0 = match id.as_deref() {
|
||||
Some(Object::Array(a)) if a.len() == 2 => self.first_id(),
|
||||
_ => {
|
||||
let id = pdfcraft_crypt::file_id(¶ms.seed);
|
||||
let s = Object::String(crate::PdfString { bytes: id.clone(), hex: true });
|
||||
@@ -338,7 +382,9 @@ impl Document {
|
||||
/// 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),
|
||||
Some(h) if Some(id.num) != self.encrypt_num && !self.encrypt_members.contains(&id.num) => {
|
||||
crate::security::transform(self, h, &o, id.num, id.generation, crate::security::Pass::Decrypt)
|
||||
}
|
||||
_ => o,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,148 @@ fn what_is_reachable_is_what_a_full_save_keeps() {
|
||||
assert!(holds(7) && !holds(8));
|
||||
}
|
||||
|
||||
std::thread_local! {
|
||||
/// How many times `Document::scan_for` read the file on this thread.
|
||||
pub(super) static FILE_SCANS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// A scan that doesn't find an object isn't made again: each reads the whole file, and a file can
|
||||
/// refer to such an object from every object it has.
|
||||
#[test]
|
||||
fn what_a_scan_of_the_file_did_not_find_is_remembered() {
|
||||
let body = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [] /Count 0 >> endobj\n";
|
||||
let mut out = body.to_vec();
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(b"xref\n0 4\n0000000000 65535 f \n");
|
||||
out.extend_from_slice(format!("{:010} 00000 n \n", 9).as_bytes());
|
||||
out.extend_from_slice(format!("{:010} 00000 n \n", body.iter().position(|b| *b == b'\n').unwrap() + 49).as_bytes());
|
||||
out.extend_from_slice(b"0000000001 00000 n \n");
|
||||
out.extend_from_slice(format!("trailer << /Size 4 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes());
|
||||
let doc = Document::open(Arc::new(out)).unwrap();
|
||||
FILE_SCANS.with(|scans| scans.set(0));
|
||||
for _ in 0..5 {
|
||||
assert!(doc.try_get(3).is_err());
|
||||
}
|
||||
assert_eq!(FILE_SCANS.with(std::cell::Cell::get), 1);
|
||||
let clone = doc.clone();
|
||||
assert!(clone.try_get(3).is_err());
|
||||
assert_eq!(FILE_SCANS.with(std::cell::Cell::get), 1, "a clone has the same file");
|
||||
}
|
||||
|
||||
/// An object can be nothing but a reference to another. A chain of them is followed, and one that
|
||||
/// goes round ends in what isn't a dictionary or a name, without hanging.
|
||||
#[test]
|
||||
fn references_to_references_are_followed() {
|
||||
let mut doc = Document::new_empty();
|
||||
let name = doc.add(Object::name("Target"));
|
||||
let one = doc.add(Object::Ref(name));
|
||||
let two = doc.add(Object::Ref(one));
|
||||
assert_eq!(doc.resolve(&Object::Ref(two)).as_name(), Some(b"Target".as_slice()));
|
||||
let a = doc.add(Object::Null);
|
||||
let b = doc.add(Object::Ref(a));
|
||||
doc.set(a, Object::Ref(b));
|
||||
assert_eq!(doc.resolve(&Object::Ref(a)).as_name(), None);
|
||||
}
|
||||
|
||||
/// An object stream's `/DecodeParms` can be given by reference: without them a predictor isn't
|
||||
/// undone, so its members were lost.
|
||||
#[test]
|
||||
fn an_object_stream_with_parameters_by_reference_is_read() {
|
||||
let members = b"3 0 << /Hidden (yes) >>";
|
||||
// PNG Sub, one row.
|
||||
let mut rows = vec![1u8];
|
||||
let mut before = 0u8;
|
||||
for b in members {
|
||||
rows.push(b.wrapping_sub(before));
|
||||
before = *b;
|
||||
}
|
||||
let coded = pdfcraft_filters::encode_flate(&rows);
|
||||
let mut out = b"%PDF-1.5\n".to_vec();
|
||||
let mut offs = [0usize; 7];
|
||||
let mut obj = |out: &mut Vec<u8>, n: usize, body: &[u8]| {
|
||||
offs[n] = out.len();
|
||||
out.extend_from_slice(format!("{n} 0 obj\n").as_bytes());
|
||||
out.extend_from_slice(body);
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
};
|
||||
obj(&mut out, 1, b"<< /Type /Catalog /Pages 2 0 R >>");
|
||||
obj(&mut out, 2, b"<< /Type /Pages /Kids [] /Count 0 /Extra 3 0 R >>");
|
||||
let mut stm = format!("<< /Type /ObjStm /N 1 /First 4 /Filter /FlateDecode /DecodeParms 6 0 R /Length {} >>\nstream\n", coded.len()).into_bytes();
|
||||
stm.extend_from_slice(&coded);
|
||||
stm.extend_from_slice(b"\nendstream");
|
||||
obj(&mut out, 4, &stm);
|
||||
let data = [2u8, 0, 4, 0];
|
||||
let mut xs = format!("<< /Type /XRef /Size 7 /W [1 2 1] /Index [3 1] /Length {} >>\nstream\n", data.len()).into_bytes();
|
||||
xs.extend_from_slice(&data);
|
||||
xs.extend_from_slice(b"\nendstream");
|
||||
obj(&mut out, 5, &xs);
|
||||
obj(&mut out, 6, format!("<< /Predictor 11 /Columns {} >>", members.len()).as_bytes());
|
||||
let table = out.len();
|
||||
out.extend_from_slice(b"xref\n0 7\n0000000000 65535 f \n");
|
||||
for (n, off) in offs.iter().enumerate().skip(1) {
|
||||
let line = if n == 3 { "0000000000 65535 f \n".to_string() } else { format!("{off:010} 00000 n \n") };
|
||||
out.extend_from_slice(line.as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer << /Size 7 /Root 1 0 R /XRefStm {} >>\nstartxref\n{table}\n%%EOF\n", offs[5]).as_bytes());
|
||||
let doc = Document::open(Arc::new(out)).unwrap();
|
||||
let hidden = doc.get(ObjRef::new(3, 0));
|
||||
assert_eq!(hidden.as_dict().and_then(|d| d.get(b"Hidden")).and_then(Object::as_string).map(|s| s.bytes.clone()), Some(b"yes".to_vec()));
|
||||
}
|
||||
|
||||
/// Each entry of an `/Encrypt` dictionary, and of a crypt filter in it, reads the same by reference.
|
||||
/// Read as absent, `/StmF` or `/CFM` was `/Identity`, so a whole file's streams stayed ciphertext.
|
||||
#[test]
|
||||
fn encrypt_dictionary_entries_read_the_same_by_reference() {
|
||||
let string = |s: &str| Object::String(crate::PdfString::text(s));
|
||||
let mut filter = Dict::new();
|
||||
filter.set(b"CFM".to_vec(), Object::name("AESV2"));
|
||||
filter.set(b"Length".to_vec(), Object::Int(5));
|
||||
let cf = |filter: Dict| {
|
||||
let mut cf = Dict::new();
|
||||
cf.set(b"StdCF".to_vec(), Object::Dict(filter));
|
||||
cf.set(b"Other".to_vec(), Object::Dict(Dict::new()));
|
||||
Object::Dict(cf)
|
||||
};
|
||||
let mut d = Dict::new();
|
||||
let entries = [
|
||||
("Filter", Object::name("Standard")),
|
||||
("V", Object::Int(4)),
|
||||
("R", Object::Int(4)),
|
||||
("Length", Object::Int(128)),
|
||||
("P", Object::Int(-44)),
|
||||
("O", string("owner")),
|
||||
("U", string("user")),
|
||||
("EncryptMetadata", Object::Bool(false)),
|
||||
("CF", cf(filter.clone())),
|
||||
("StmF", Object::name("StdCF")),
|
||||
("StrF", Object::name("Other")),
|
||||
("EFF", Object::name("StdCF")),
|
||||
];
|
||||
for (key, value) in &entries {
|
||||
d.set(key.as_bytes().to_vec(), value.clone());
|
||||
}
|
||||
let mut doc = Document::new_empty();
|
||||
let direct = crate::security::encrypt_dict(&doc, &d);
|
||||
assert_eq!((direct.stm_f.as_slice(), direct.str_f.as_slice(), direct.ef_f.as_slice()), (&b"StdCF"[..], &b"Other"[..], &b"StdCF"[..]));
|
||||
assert_eq!(direct.length_bits, 40, "the filter's own length, in bytes");
|
||||
assert!(!direct.encrypt_metadata);
|
||||
assert_eq!(direct.crypt_filters.iter().filter(|(_, m)| *m == pdfcraft_crypt::Method::Aes128).count(), 1);
|
||||
for (key, value) in &entries {
|
||||
let mut one = d.clone();
|
||||
let r = doc.add(value.clone());
|
||||
one.set(key.as_bytes().to_vec(), Object::Ref(r));
|
||||
assert_eq!(crate::security::encrypt_dict(&doc, &one), direct, "/{key}");
|
||||
}
|
||||
for key in ["CFM", "Length"] {
|
||||
let mut by_reference = filter.clone();
|
||||
let r = doc.add(filter.get(key.as_bytes()).unwrap().clone());
|
||||
by_reference.set(key.as_bytes().to_vec(), Object::Ref(r));
|
||||
let mut one = d.clone();
|
||||
one.set(b"CF".to_vec(), cf(by_reference));
|
||||
assert_eq!(crate::security::encrypt_dict(&doc, &one), direct, "/{key} in the filter");
|
||||
}
|
||||
}
|
||||
|
||||
/// /DecodeParms given by reference, whole, as array entries or as the numbers inside, are read
|
||||
/// once made direct: here a Flate predictor, which the stream alone doesn't see. Only the entries the filters use are
|
||||
/// resolved, and nothing is for a chain longer than a decoder takes.
|
||||
|
||||
+214
-80
@@ -7,10 +7,69 @@
|
||||
//! false. Streams naming a crypt filter (`/Crypt` in `/Filter`) use that filter; embedded files
|
||||
//! use `/EFF`.
|
||||
|
||||
use pdfcraft_crypt::{EncryptDict, Method, SecurityHandler, StreamKind};
|
||||
use std::cell::Cell;
|
||||
|
||||
use pdfcraft_crypt::{EncryptDict, Method, SecurityHandler, StreamKind};
|
||||
use pdfcraft_filters::MAX_CHAIN;
|
||||
|
||||
use crate::document::Held;
|
||||
use crate::{Dict, Document, Object, PdfString, Stream};
|
||||
|
||||
/// How many references one read or write of an object may follow, with all that it loads on the
|
||||
/// way. How an object is treated can be given by reference, and what that loads can do the same: a
|
||||
/// file could make one read a tree of them, each object read again once the cache has let it go.
|
||||
/// An object takes a few of these, and two dozen if every part of it is given by reference; past
|
||||
/// this they read as absent. The parameters of an object stream, read together, count as one.
|
||||
const MAX_FOLLOWED: u32 = 32;
|
||||
|
||||
std::thread_local! {
|
||||
/// How many [`FollowScope`]s this thread is inside.
|
||||
static SCOPES: Cell<u32> = const { Cell::new(0) };
|
||||
/// The references followed in the outermost of them.
|
||||
static FOLLOWED: Cell<u32> = const { Cell::new(0) };
|
||||
/// The references refused in the outermost of them, past [`MAX_FOLLOWED`].
|
||||
static REFUSED: Cell<u32> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
/// One read or write of an object, with everything it loads on the way: what [`MAX_FOLLOWED`] counts.
|
||||
pub(crate) struct FollowScope;
|
||||
|
||||
impl FollowScope {
|
||||
pub(crate) fn enter() -> Self {
|
||||
if SCOPES.replace(SCOPES.get().saturating_add(1)) == 0 {
|
||||
FOLLOWED.set(0);
|
||||
REFUSED.set(0);
|
||||
}
|
||||
FollowScope
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FollowScope {
|
||||
fn drop(&mut self) {
|
||||
SCOPES.set(SCOPES.get().saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// What `f` gives, counted against [`MAX_FOLLOWED`], or `spent` past it. Called inside a
|
||||
/// [`FollowScope`], which is what starts the count again.
|
||||
pub(crate) fn following<T>(spent: T, f: impl FnOnce() -> T) -> T {
|
||||
if FOLLOWED.get() >= MAX_FOLLOWED {
|
||||
REFUSED.set(REFUSED.get().saturating_add(1));
|
||||
return spent;
|
||||
}
|
||||
FOLLOWED.set(FOLLOWED.get() + 1);
|
||||
f()
|
||||
}
|
||||
|
||||
/// `o`, or the object it refers to, for what decides how a stream or dictionary is treated. Past
|
||||
/// [`MAX_FOLLOWED`] a reference reads as absent.
|
||||
fn followed<'o>(doc: &Document, o: &'o Object) -> Held<'o> {
|
||||
match o {
|
||||
Object::Ref(_) => following(Held::Direct(&Object::Null), || doc.held(o)),
|
||||
o => Held::Direct(o),
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_of(doc: &Document, d: &Dict, key: &[u8]) -> Vec<u8> {
|
||||
d.get(key).map(|v| doc.resolve(v)).and_then(|v| v.as_string().map(|s| s.bytes.clone())).unwrap_or_default()
|
||||
}
|
||||
@@ -18,12 +77,12 @@ fn bytes_of(doc: &Document, d: &Dict, key: &[u8]) -> Vec<u8> {
|
||||
/// Read the handler parameters from an `/Encrypt` dictionary.
|
||||
pub(crate) fn encrypt_dict(doc: &Document, d: &Dict) -> EncryptDict {
|
||||
let int = |k: &[u8], default: i64| d.get(k).map(|v| doc.resolve(v)).and_then(|v| v.as_int()).unwrap_or(default);
|
||||
let name = |k: &[u8]| d.get(k).and_then(|v| v.as_name()).map(|n| n.to_vec()).unwrap_or_else(|| b"Identity".to_vec());
|
||||
let name = |d: &Dict, k: &[u8]| d.get(k).and_then(|v| doc.resolve(v).as_name().map(<[u8]>::to_vec));
|
||||
let mut crypt_filters = Vec::new();
|
||||
if let Some(cf) = d.get(b"CF").map(|c| doc.resolve(c)).and_then(|c| c.as_dict().cloned()) {
|
||||
for (k, v) in cf.iter() {
|
||||
let Some(f) = doc.resolve(v).as_dict().cloned() else { continue };
|
||||
let method = match f.name(b"CFM") {
|
||||
let method = match name(&f, b"CFM").as_deref() {
|
||||
Some(b"V2") => Method::Rc4,
|
||||
Some(b"AESV2") => Method::Aes128,
|
||||
Some(b"AESV3") => Method::Aes256,
|
||||
@@ -40,7 +99,8 @@ pub(crate) fn encrypt_dict(doc: &Document, d: &Dict) -> EncryptDict {
|
||||
.get(b"CF")
|
||||
.map(|c| doc.resolve(c))
|
||||
.and_then(|c| c.as_dict().and_then(|c| c.get(b"StdCF").map(|f| doc.resolve(f))))
|
||||
.and_then(|f| f.as_dict().and_then(|f| f.int(b"Length")))
|
||||
.and_then(|f| f.as_dict().and_then(|f| f.get(b"Length").map(|n| doc.resolve(n))))
|
||||
.and_then(|n| n.as_int())
|
||||
.filter(|n| *n > 0);
|
||||
let length_bits = match (v, cf_len) {
|
||||
(5.., _) => 256,
|
||||
@@ -50,7 +110,7 @@ pub(crate) fn encrypt_dict(doc: &Document, d: &Dict) -> EncryptDict {
|
||||
_ => int(b"Length", 40),
|
||||
};
|
||||
EncryptDict {
|
||||
filter: d.name(b"Filter").map(|n| n.to_vec()).unwrap_or_default(),
|
||||
filter: name(d, b"Filter").unwrap_or_default(),
|
||||
v,
|
||||
r: int(b"R", 0),
|
||||
length_bits,
|
||||
@@ -60,28 +120,46 @@ pub(crate) fn encrypt_dict(doc: &Document, d: &Dict) -> EncryptDict {
|
||||
ue: bytes_of(doc, d, b"UE"),
|
||||
perms: bytes_of(doc, d, b"Perms"),
|
||||
p: int(b"P", -1) as i32,
|
||||
encrypt_metadata: !matches!(d.get(b"EncryptMetadata"), Some(Object::Bool(false))),
|
||||
encrypt_metadata: !matches!(d.get(b"EncryptMetadata").map(|v| doc.resolve(v)).as_deref(), Some(Object::Bool(false))),
|
||||
crypt_filters,
|
||||
stm_f: name(b"StmF"),
|
||||
str_f: name(b"StrF"),
|
||||
ef_f: d.get(b"EFF").and_then(|v| v.as_name()).map(|n| n.to_vec()).unwrap_or_default(),
|
||||
stm_f: name(d, b"StmF").unwrap_or_else(|| b"Identity".to_vec()),
|
||||
str_f: name(d, b"StrF").unwrap_or_else(|| b"Identity".to_vec()),
|
||||
ef_f: name(d, b"EFF").unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The crypt filter a stream names in its `/Filter` chain (`/Crypt` + `/DecodeParms /Name`).
|
||||
fn named_crypt_filter(dict: &Dict) -> Option<Vec<u8>> {
|
||||
let names: Vec<&[u8]> = match dict.get(b"Filter") {
|
||||
Some(Object::Name(n)) => vec![n],
|
||||
Some(Object::Array(a)) => a.iter().filter_map(Object::as_name).collect(),
|
||||
/// Whether `f`, an entry of a `/Filter` chain or its only filter, is `/Crypt`.
|
||||
fn is_crypt(doc: &Document, f: &Object) -> bool {
|
||||
followed(doc, f).as_name() == Some(b"Crypt")
|
||||
}
|
||||
|
||||
/// The crypt filter a stream names in its `/Filter` chain (`/Crypt` + `/DecodeParms /Name`), each
|
||||
/// given directly or by reference, in an array or not. A chain longer than any decoder takes names
|
||||
/// none: nothing can read that stream anyway, and every stream of a file can share one long array.
|
||||
/// A `/Name` that reads as absent means `/Identity`, unless a reference on the way to it was refused
|
||||
/// (see [`MAX_FOLLOWED`]): that says nothing about the stream, so it names none, and the document's
|
||||
/// own filter is used.
|
||||
fn named_crypt_filter(doc: &Document, dict: &Dict) -> Option<Vec<u8>> {
|
||||
let refused = REFUSED.get();
|
||||
let filter = followed(doc, dict.get(b"Filter")?);
|
||||
let i = match &*filter {
|
||||
Object::Array(a) if a.len() <= MAX_CHAIN + 1 => a.iter().position(|f| is_crypt(doc, f))?,
|
||||
f if is_crypt(doc, f) => 0,
|
||||
_ => return None,
|
||||
};
|
||||
let i = names.iter().position(|n| *n == b"Crypt")?;
|
||||
let parms = match dict.get(b"DecodeParms") {
|
||||
Some(Object::Dict(d)) if i == 0 => Some(d.clone()),
|
||||
Some(Object::Array(a)) => a.get(i).and_then(|p| p.as_dict().cloned()),
|
||||
let parms = dict.get(b"DecodeParms").map(|p| followed(doc, p));
|
||||
let entry = match parms.as_deref() {
|
||||
Some(Object::Array(a)) => a.get(i).map(|p| followed(doc, p)),
|
||||
Some(p) if i == 0 => Some(Held::Direct(p)),
|
||||
_ => None,
|
||||
};
|
||||
Some(parms.and_then(|p| p.name(b"Name").map(|n| n.to_vec())).unwrap_or_else(|| b"Identity".to_vec()))
|
||||
let name = entry.as_deref().and_then(Object::as_dict).and_then(|p| p.get(b"Name")).map(|n| followed(doc, n));
|
||||
match name.as_deref().and_then(Object::as_name) {
|
||||
Some(name) => Some(name.to_vec()),
|
||||
// /Identity here would read ciphertext as data, and write the stream in the clear.
|
||||
None if REFUSED.get() > refused => None,
|
||||
None => Some(b"Identity".to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A signature dictionary (field signature, document timestamp or usage-rights signature),
|
||||
@@ -91,42 +169,67 @@ fn named_crypt_filter(dict: &Dict) -> Option<Vec<u8>> {
|
||||
/// `/Contents` alongside an array `/ByteRange` (`/Type` is optional). A signature dictionary
|
||||
/// has no `/Subtype`; a dictionary with one (an annotation, whose `/Contents` is the comment
|
||||
/// text) or with another `/Type` is never exempt, whatever else it carries.
|
||||
fn is_signature_dict(d: &Dict) -> bool {
|
||||
if d.get(b"Subtype").is_some() {
|
||||
/// With `doc` its entries are read through references; a dictionary nested in another object
|
||||
/// has none (see [`transform`]).
|
||||
fn is_signature_dict(doc: Option<&Document>, d: &Dict) -> bool {
|
||||
// All this decides is what happens to such a /Contents.
|
||||
if !matches!(d.get(b"Contents"), Some(Object::String(_))) {
|
||||
return false;
|
||||
}
|
||||
match d.get(b"Type") {
|
||||
Some(_) => matches!(d.name(b"Type"), Some(b"Sig" | b"DocTimeStamp")),
|
||||
None => matches!(d.get(b"Contents"), Some(Object::String(_))) && matches!(d.get(b"ByteRange"), Some(Object::Array(_))),
|
||||
// A null entry, or one that refers to nothing, is an absent one (ISO 32000-2 §7.3.7, §7.3.10).
|
||||
let entry = |key: &[u8]| {
|
||||
let value = d.get(key)?;
|
||||
let held = doc.map_or(Held::Direct(value), |doc| followed(doc, value));
|
||||
(!matches!(*held, Object::Null)).then_some(held)
|
||||
};
|
||||
if entry(b"Subtype").is_some() {
|
||||
return false;
|
||||
}
|
||||
match entry(b"Type") {
|
||||
Some(t) => matches!(t.as_name(), Some(b"Sig" | b"DocTimeStamp")),
|
||||
None => matches!(entry(b"ByteRange").as_deref(), Some(Object::Array(_))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the `/Crypt` entry from a stream's filter chain, with its decode parameters.
|
||||
fn drop_crypt_filter(dict: &mut Dict) {
|
||||
let i = match dict.get(b"Filter") {
|
||||
Some(Object::Array(a)) => a.iter().position(|n| n.as_name() == Some(b"Crypt")),
|
||||
Some(Object::Name(n)) if n.as_slice() == b"Crypt" => Some(0),
|
||||
_ => None,
|
||||
/// Longer than any filter's name. A name can be of any length, and a chain every stream shares can
|
||||
/// hold a long one.
|
||||
const MAX_FILTER_NAME: usize = 127;
|
||||
|
||||
/// The dictionary of `source` without the `/Crypt` entries of its filter chain and their decode
|
||||
/// parameters. The two come out direct when given by reference, which every stream of a file can
|
||||
/// share, so of what was shared only small values are copied: filter names, and the parameters
|
||||
/// decoders read ([`Document::with_direct_parms`]). The chain is short, as [`named_crypt_filter`]
|
||||
/// found a filter in it.
|
||||
fn drop_crypt_filter(doc: &Document, source: &Stream) -> Dict {
|
||||
let entry = |f: &Object| match f {
|
||||
f if is_crypt(doc, f) => Object::name("Crypt"),
|
||||
Object::Name(n) if n.len() <= MAX_FILTER_NAME => f.clone(),
|
||||
Object::Ref(_) => f.clone(),
|
||||
_ => Object::Null,
|
||||
};
|
||||
let Some(i) = i else { return };
|
||||
let mut filters = match dict.get(b"Filter") {
|
||||
Some(Object::Array(a)) => a.clone(),
|
||||
_ => Vec::new(),
|
||||
let filter = source.dict.get(b"Filter").map(|f| followed(doc, f));
|
||||
let chain: Vec<Object> = match filter.as_deref() {
|
||||
Some(Object::Array(a)) => a.iter().map(entry).collect(),
|
||||
Some(f) => vec![entry(f)],
|
||||
None => Vec::new(),
|
||||
};
|
||||
if i < filters.len() {
|
||||
filters.remove(i);
|
||||
}
|
||||
let crypt: Vec<bool> = chain.iter().map(|f| f.as_name() == Some(b"Crypt")).collect();
|
||||
let mut chained = Stream::from_raw(source.dict.clone(), Vec::new());
|
||||
chained.dict.set(b"Filter".to_vec(), Object::Array(chain.clone()));
|
||||
// A lone parameter dictionary belongs to the first filter (as `named_crypt_filter` reads it).
|
||||
match dict.get(b"DecodeParms").cloned() {
|
||||
Some(Object::Array(mut parms)) if i < parms.len() => {
|
||||
parms.remove(i);
|
||||
dict.set(b"DecodeParms".to_vec(), Object::Array(parms));
|
||||
}
|
||||
Some(Object::Dict(_)) if i == 0 => {
|
||||
let parms = match doc.with_direct_parms(&chained).dict.get(b"DecodeParms") {
|
||||
Some(Object::Array(p)) => Some(Object::Array(p.iter().zip(&crypt).filter(|(_, c)| !**c).map(|(p, _)| p.clone()).collect())),
|
||||
Some(Object::Dict(_)) if crypt.first() == Some(&true) => None,
|
||||
_ => source.dict.get(b"DecodeParms").cloned(),
|
||||
};
|
||||
let mut dict = source.dict.clone();
|
||||
match parms {
|
||||
Some(p) => dict.set(b"DecodeParms".to_vec(), p),
|
||||
None => {
|
||||
dict.remove(b"DecodeParms");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let filters: Vec<Object> = chain.into_iter().zip(&crypt).filter(|(_, c)| !**c).map(|(f, _)| f).collect();
|
||||
if filters.is_empty() {
|
||||
dict.remove(b"Filter");
|
||||
if matches!(dict.get(b"DecodeParms"), Some(Object::Array(p)) if p.is_empty()) {
|
||||
@@ -135,6 +238,7 @@ fn drop_crypt_filter(dict: &mut Dict) {
|
||||
} else {
|
||||
dict.set(b"Filter".to_vec(), Object::Array(filters));
|
||||
}
|
||||
dict
|
||||
}
|
||||
|
||||
/// What [`transform`] does to an object.
|
||||
@@ -152,51 +256,81 @@ pub(crate) enum Pass {
|
||||
|
||||
/// Decrypt or encrypt every string and stream in an indirect object, except a signature
|
||||
/// dictionary's `/Contents` (see [`is_signature_dict`]).
|
||||
pub(crate) fn transform(h: &SecurityHandler, o: &Object, num: u32, generation: u16, pass: Pass) -> Object {
|
||||
///
|
||||
/// `o` is as `doc` holds it: what a stream's dictionary gives by reference is read from `doc`, so
|
||||
/// its references have to be the document's, not a save's renumbered ones.
|
||||
pub(crate) fn transform(doc: &Document, h: &SecurityHandler, o: &Object, num: u32, generation: u16, pass: Pass) -> Object {
|
||||
let _scope = FollowScope::enter();
|
||||
let decrypt = pass == Pass::Decrypt;
|
||||
let strings = |s: &PdfString| {
|
||||
let bytes = if decrypt { h.decrypt_string(num, generation, &s.bytes) } else { h.encrypt_string(num, generation, &s.bytes) };
|
||||
PdfString { bytes, hex: s.hex || !decrypt }
|
||||
};
|
||||
walk(o, &strings, h, num, generation, pass)
|
||||
// References are followed here only, for the object itself, never for what is nested in it.
|
||||
// `walk` goes deep, and a load from down there adds that depth again for each object loaded,
|
||||
// which overflowed the stack on a 10 KB file. A stream can't be nested, and a dictionary that
|
||||
// is reads as it says directly.
|
||||
match o {
|
||||
Object::Stream(s) => stream(doc, s, &strings, h, num, generation, pass),
|
||||
Object::Dict(d) if is_signature_dict(Some(doc), d) => signature_dict(d, &strings),
|
||||
o => walk(o, &strings),
|
||||
}
|
||||
}
|
||||
|
||||
fn walk(o: &Object, strings: &dyn Fn(&PdfString) -> PdfString, h: &SecurityHandler, num: u32, generation: u16, pass: Pass) -> Object {
|
||||
let decrypt = pass == Pass::Decrypt;
|
||||
/// `d`, a signature dictionary, with the strings of everything but its `/Contents` transformed.
|
||||
fn signature_dict(d: &Dict, strings: &dyn Fn(&PdfString) -> PdfString) -> Object {
|
||||
Object::Dict(
|
||||
d.iter()
|
||||
.map(|(k, v)| match (k.as_slice(), v) {
|
||||
(b"Contents", Object::String(_)) => (k.clone(), v.clone()),
|
||||
_ => (k.clone(), walk(v, strings)),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn walk(o: &Object, strings: &dyn Fn(&PdfString) -> PdfString) -> Object {
|
||||
match o {
|
||||
Object::String(s) => Object::String(strings(s)),
|
||||
Object::Array(a) => Object::Array(a.iter().map(|x| walk(x, strings, h, num, generation, pass)).collect()),
|
||||
Object::Dict(d) if is_signature_dict(d) => Object::Dict(
|
||||
d.iter()
|
||||
.map(|(k, v)| match (k.as_slice(), v) {
|
||||
(b"Contents", Object::String(_)) => (k.clone(), v.clone()),
|
||||
_ => (k.clone(), walk(v, strings, h, num, generation, pass)),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Object::Dict(d) => Object::Dict(d.iter().map(|(k, v)| (k.clone(), walk(v, strings, h, num, generation, pass))).collect()),
|
||||
Object::Stream(s) => {
|
||||
let mut dict: Dict = s.dict.iter().map(|(k, v)| (k.clone(), walk(v, strings, h, num, generation, pass))).collect();
|
||||
let ty = s.dict.name(b"Type");
|
||||
// Cross-reference streams are never encrypted (§7.5.8), but only reading may trust the
|
||||
// label: a stream a producer calls /XRef stays plaintext if writing believes it, and the
|
||||
// writer builds its own cross-reference streams outside this transform.
|
||||
if (decrypt && ty == Some(b"XRef")) || (ty == Some(b"Metadata") && !h.encrypts_metadata()) {
|
||||
return Object::Stream(Stream { dict, raw: s.raw.clone() });
|
||||
}
|
||||
let mut named = named_crypt_filter(&s.dict);
|
||||
if pass == Pass::EncryptReplacing && named.as_deref().is_some_and(|n| !h.dict().crypt_filters.iter().any(|(f, _)| f == n)) {
|
||||
drop_crypt_filter(&mut dict);
|
||||
named = None;
|
||||
}
|
||||
let kind = match (&named, ty) {
|
||||
(Some(n), _) => StreamKind::Named(n),
|
||||
(None, Some(b"EmbeddedFile")) => StreamKind::EmbeddedFile,
|
||||
_ => StreamKind::Normal,
|
||||
};
|
||||
let raw = if decrypt { h.decrypt_stream(num, generation, &s.raw, kind) } else { h.encrypt_stream(num, generation, &s.raw, kind) };
|
||||
Object::Stream(Stream { dict, raw: raw.into() })
|
||||
}
|
||||
Object::Array(a) => Object::Array(a.iter().map(|x| walk(x, strings)).collect()),
|
||||
Object::Dict(d) if is_signature_dict(None, d) => signature_dict(d, strings),
|
||||
Object::Dict(d) => Object::Dict(d.iter().map(|(k, v)| (k.clone(), walk(v, strings))).collect()),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stream(
|
||||
doc: &Document,
|
||||
s: &Stream,
|
||||
strings: &dyn Fn(&PdfString) -> PdfString,
|
||||
h: &SecurityHandler,
|
||||
num: u32,
|
||||
generation: u16,
|
||||
pass: Pass,
|
||||
) -> Object {
|
||||
let decrypt = pass == Pass::Decrypt;
|
||||
let walked = |d: &Dict| -> Dict { d.iter().map(|(k, v)| (k.clone(), walk(v, strings))).collect() };
|
||||
let ty = s.dict.get(b"Type").map(|t| followed(doc, t));
|
||||
let ty = ty.as_deref().and_then(Object::as_name);
|
||||
// Cross-reference streams are never encrypted (§7.5.8), but only reading may trust the
|
||||
// label: a stream a producer calls /XRef stays plaintext if writing believes it, and the
|
||||
// writer builds its own cross-reference streams outside this transform.
|
||||
if (decrypt && ty == Some(b"XRef")) || (ty == Some(b"Metadata") && !h.encrypts_metadata()) {
|
||||
return Object::Stream(Stream { dict: walked(&s.dict), raw: s.raw.clone() });
|
||||
}
|
||||
let mut named = named_crypt_filter(doc, &s.dict);
|
||||
// Before the walk, which then encrypts what is in the dictionary this leaves.
|
||||
let foreign = pass == Pass::EncryptReplacing && named.as_deref().is_some_and(|n| !h.dict().crypt_filters.iter().any(|(f, _)| f == n));
|
||||
let dropped = foreign.then(|| drop_crypt_filter(doc, s));
|
||||
if dropped.is_some() {
|
||||
named = None;
|
||||
}
|
||||
let dict = walked(dropped.as_ref().unwrap_or(&s.dict));
|
||||
let kind = match (&named, ty) {
|
||||
(Some(n), _) => StreamKind::Named(n),
|
||||
(None, Some(b"EmbeddedFile")) => StreamKind::EmbeddedFile,
|
||||
_ => StreamKind::Normal,
|
||||
};
|
||||
let raw = if decrypt { h.decrypt_stream(num, generation, &s.raw, kind) } else { h.encrypt_stream(num, generation, &s.raw, kind) };
|
||||
Object::Stream(Stream { dict, raw: raw.into() })
|
||||
}
|
||||
|
||||
@@ -133,11 +133,12 @@ fn write_stream(s: &Stream, out: &mut Vec<u8>) {
|
||||
|
||||
/// The object as written: encrypted with its (output) number when the document is encrypted,
|
||||
/// except the `/Encrypt` dictionary itself (`source_num` is its number in the source document).
|
||||
/// `o` has the document's numbers in its references ([`crate::security::transform`]).
|
||||
fn prepared<'a>(doc: &Document, source_num: u32, num: u32, generation: u16, o: &'a Object) -> Cow<'a, Object> {
|
||||
match doc.output_security() {
|
||||
(Some(h), encrypt_num) if Some(source_num) != encrypt_num => {
|
||||
(Some(h), encrypt_num) if Some(source_num) != encrypt_num && !doc.stored_as_is(source_num) => {
|
||||
let pass = if doc.encryption_changed() { crate::security::Pass::EncryptReplacing } else { crate::security::Pass::Encrypt };
|
||||
Cow::Owned(crate::security::transform(h, o, num, generation, pass))
|
||||
Cow::Owned(crate::security::transform(doc, h, o, num, generation, pass))
|
||||
}
|
||||
_ => Cow::Borrowed(o),
|
||||
}
|
||||
@@ -288,7 +289,10 @@ pub fn write_full(doc: &Document, opts: &SaveOptions) -> Result<Vec<u8>, CosErro
|
||||
// The /Encrypt dictionary and everything it references (e.g. indirect /CF crypt-filter
|
||||
// dictionaries) are needed before anything can be decrypted: never inside object streams.
|
||||
let mut encryption_objects: std::collections::HashSet<ObjRef> = std::collections::HashSet::new();
|
||||
let mut pending: Vec<ObjRef> = trailer_in.reference(b"Encrypt").into_iter().collect();
|
||||
let mut pending: Vec<ObjRef> = Vec::new();
|
||||
if let Some(encrypt) = trailer_in.get(b"Encrypt") {
|
||||
collect_refs(encrypt, &mut |r| pending.push(r));
|
||||
}
|
||||
while let Some(r) = pending.pop() {
|
||||
if encryption_objects.insert(r) {
|
||||
collect_refs(&reader.get(r), &mut |x| pending.push(x));
|
||||
@@ -309,9 +313,10 @@ pub fn write_full(doc: &Document, opts: &SaveOptions) -> Result<Vec<u8>, CosErro
|
||||
if in_stream {
|
||||
packed.push((num, *r));
|
||||
} else {
|
||||
let o = renumber(&*reread(&mut reader, *r, *is_stream)?, &map);
|
||||
let o = reread(&mut reader, *r, *is_stream)?;
|
||||
let o = renumber(&prepared(&reader.document, r.num, num, 0, &o), &map);
|
||||
rows.insert(num, Row::InFile(out.len() as u64, 0));
|
||||
write_indirect(num, 0, &prepared(&reader.document, r.num, num, 0, &o), &mut out);
|
||||
write_indirect(num, 0, &o, &mut out);
|
||||
}
|
||||
}
|
||||
// Object streams take the numbers after the last object. Their contents are not encrypted
|
||||
@@ -341,11 +346,11 @@ pub fn write_full(doc: &Document, opts: &SaveOptions) -> Result<Vec<u8>, CosErro
|
||||
trailer.set(key.to_vec(), Object::Ref(ObjRef::new(*r, 0)));
|
||||
}
|
||||
}
|
||||
if let Some(Object::Dict(d)) = trailer_in.get(b"Encrypt") {
|
||||
trailer.set(b"Encrypt".to_vec(), Object::Dict(d.clone()));
|
||||
if let Some(encrypt @ Object::Dict(_)) = trailer_in.get(b"Encrypt") {
|
||||
trailer.set(b"Encrypt".to_vec(), renumber(encrypt, &map));
|
||||
}
|
||||
if let Some(id) = trailer_in.get(b"ID") {
|
||||
trailer.set(b"ID".to_vec(), id.clone());
|
||||
trailer.set(b"ID".to_vec(), direct_id(&reader.document, id));
|
||||
}
|
||||
for (key, value) in trailer_in.iter() {
|
||||
if !trailer_key_rewritten(key) {
|
||||
@@ -415,6 +420,22 @@ impl ObjectStreamBatch {
|
||||
}
|
||||
}
|
||||
|
||||
/// `/ID` for the trailer, direct, as it has to be with `/Encrypt`: nothing else in the file holds
|
||||
/// what it refers to now. Given by reference it is two entries at most, however many references a
|
||||
/// file gave, so that one large string isn't copied for each. An entry that isn't a string stays in
|
||||
/// its place as an empty one, which is how the key read it.
|
||||
fn direct_id(doc: &Document, id: &Object) -> Object {
|
||||
if matches!(id, Object::Array(a) if !a.iter().any(|s| matches!(s, Object::Ref(_)))) {
|
||||
return id.clone();
|
||||
}
|
||||
let entry = |s: &Object| match &*doc.resolve(s) {
|
||||
s @ Object::String(_) => s.clone(),
|
||||
_ => Object::String(crate::PdfString { bytes: Vec::new(), hex: true }),
|
||||
};
|
||||
let held = doc.resolve(id);
|
||||
Object::Array(held.as_array().into_iter().flatten().take(2).map(entry).collect())
|
||||
}
|
||||
|
||||
/// Trailer keys a full rewrite generates itself. Everything else is copied, with references
|
||||
/// renumbered, so a save does not drop metadata the reader accepted.
|
||||
fn trailer_key_rewritten(key: &[u8]) -> bool {
|
||||
@@ -471,6 +492,10 @@ fn walk(reader: &mut ObjectReader, trailer: &Dict) -> Vec<(ObjRef, bool)> {
|
||||
queue.push_back(r);
|
||||
}
|
||||
}
|
||||
// A direct /Encrypt can refer to objects, /CF or /StmF, say.
|
||||
if let Some(encrypt @ Object::Dict(_)) = trailer.get(b"Encrypt") {
|
||||
collect_refs(encrypt, &mut |r| queue.push_back(r));
|
||||
}
|
||||
// Unknown trailer entries are document data. Objects they alone reference must be written
|
||||
// too, or a full rewrite would keep the key and drop its value.
|
||||
for (key, value) in trailer.iter() {
|
||||
@@ -500,7 +525,7 @@ pub fn reachable(doc: &Document) -> HashSet<ObjRef> {
|
||||
walk(&mut reader, &trailer).into_iter().map(|(r, _)| r).collect()
|
||||
}
|
||||
|
||||
fn collect_refs(o: &Object, f: &mut impl FnMut(ObjRef)) {
|
||||
pub(crate) fn collect_refs(o: &Object, f: &mut impl FnMut(ObjRef)) {
|
||||
match o {
|
||||
Object::Ref(r) => f(*r),
|
||||
Object::Array(a) => a.iter().for_each(|x| collect_refs(x, f)),
|
||||
@@ -677,6 +702,15 @@ fn civil_from_days(days: i64) -> (i64, u32, u32) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// `/ID` by reference comes out direct, each entry where it was: the key is made from the first.
|
||||
#[test]
|
||||
fn a_file_identifier_keeps_its_entries_in_their_places() {
|
||||
let doc = Document::new_empty();
|
||||
let string = |bytes: &[u8]| Object::String(crate::PdfString { bytes: bytes.to_vec(), hex: true });
|
||||
let nowhere = Object::Ref(ObjRef::new(9_999, 0));
|
||||
assert_eq!(super::direct_id(&doc, &Object::Array(vec![nowhere, string(b"ab")])), Object::Array(vec![string(b""), string(b"ab")]));
|
||||
}
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -494,3 +494,826 @@ fn a_stream_labelled_xref_is_encrypted_when_saved() {
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
assert!(!bytes.windows(SECRET.len()).any(|w| w == SECRET.as_bytes()), "the labelled stream was written in the clear");
|
||||
}
|
||||
|
||||
/// The content stream's reference.
|
||||
fn contents_of(doc: &Document) -> ObjRef {
|
||||
let catalog = doc.get(doc.root().unwrap()).as_dict().cloned().unwrap();
|
||||
let pages = doc.get(catalog.reference(b"Pages").unwrap()).as_dict().cloned().unwrap();
|
||||
let page = doc.resolve(&pages.get(b"Kids").unwrap().as_array().unwrap()[0]).as_dict().cloned().unwrap();
|
||||
page.reference(b"Contents").unwrap()
|
||||
}
|
||||
|
||||
/// The content stream's data as loaded, whatever its filters say.
|
||||
fn raw_content(doc: &Document) -> String {
|
||||
match &*doc.get(contents_of(doc)) {
|
||||
Object::Stream(s) => String::from_utf8_lossy(&s.raw).trim().to_string(),
|
||||
_ => panic!("no content stream"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives the content stream the `/Filter` and `/DecodeParms` of `selector`.
|
||||
fn select(doc: &mut Document, (filter, parms): (Object, Object)) {
|
||||
let contents = contents_of(doc);
|
||||
let Object::Stream(mut s) = doc.get(contents).as_ref().clone() else { panic!("no content stream") };
|
||||
s.dict.set(b"Filter".to_vec(), filter);
|
||||
s.dict.set(b"DecodeParms".to_vec(), parms);
|
||||
doc.set(contents, Object::Stream(s));
|
||||
}
|
||||
|
||||
/// `/Crypt` naming `name`, in the `form`th spelling: all direct, then each part by reference.
|
||||
/// `None` past the last.
|
||||
fn selector(doc: &mut Document, form: usize, name: &str) -> Option<(Object, Object)> {
|
||||
let crypt = Object::name("Crypt");
|
||||
let parms = |n: Object| {
|
||||
let mut d = pdfcraft_cos::Dict::new();
|
||||
d.set(b"Name".to_vec(), n);
|
||||
Object::Dict(d)
|
||||
};
|
||||
let named = parms(Object::name(name));
|
||||
Some(match form {
|
||||
0 => (crypt, named),
|
||||
1 => (crypt, Object::Ref(doc.add(named))),
|
||||
2 => (Object::Array(vec![crypt]), Object::Array(vec![Object::Ref(doc.add(named))])),
|
||||
3 => (Object::Array(vec![crypt]), Object::Ref(doc.add(Object::Array(vec![named])))),
|
||||
4 => (crypt, parms(Object::Ref(doc.add(Object::name(name))))),
|
||||
5 => (Object::Ref(doc.add(crypt)), named),
|
||||
6 => (Object::Ref(doc.add(Object::Array(vec![crypt]))), Object::Array(vec![named])),
|
||||
7 => (Object::Array(vec![Object::Ref(doc.add(crypt))]), Object::Array(vec![named])),
|
||||
// Behind a filter given by reference: places in the two arrays pair up all the same.
|
||||
8 => (Object::Array(vec![Object::Ref(doc.add(Object::name("ASCIIHexDecode"))), crypt]), Object::Array(vec![Object::Null, named])),
|
||||
// The two entries in different forms, which is how a lone filter is usually spelled.
|
||||
9 => (Object::Array(vec![crypt]), named),
|
||||
10 => (crypt, Object::Array(vec![named])),
|
||||
// The filter twice, which names the first's.
|
||||
11 => (Object::Array(vec![crypt.clone(), crypt]), Object::Array(vec![named.clone(), named])),
|
||||
// A reference to a reference.
|
||||
12 => {
|
||||
let name = doc.add(Object::name(name));
|
||||
let hop = doc.add(Object::Ref(name));
|
||||
(crypt, parms(Object::Ref(hop)))
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn holds(bytes: &[u8], text: &str) -> bool {
|
||||
String::from_utf8_lossy(bytes).contains(text)
|
||||
}
|
||||
|
||||
/// An object `/Encrypt` names is written as stored only while it is as stored. A file whose
|
||||
/// `/Encrypt` names its `/Info` reads that dictionary as stored; once the title is edited, both
|
||||
/// saves encrypt it rather than writing the new title in the clear.
|
||||
#[test]
|
||||
fn an_edited_object_that_encrypt_names_is_encrypted() {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes128,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [9; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let info = doc.trailer().reference(b"Info").unwrap();
|
||||
let mut dict = doc.get(doc.trailer().reference(b"Encrypt").unwrap()).as_dict().cloned().unwrap();
|
||||
dict.set(b"Junk".to_vec(), Object::Ref(info));
|
||||
doc.trailer_mut().set(b"Encrypt".to_vec(), Object::Dict(dict));
|
||||
let bytes = write_full(&doc, &SaveOptions::default()).unwrap();
|
||||
let mut doc = Document::open(Arc::new(bytes)).unwrap();
|
||||
let info = doc.trailer().reference(b"Info").unwrap();
|
||||
doc.update_dict(info, |d| d.set(b"Title".to_vec(), PdfString::text("Merger plan"))).unwrap();
|
||||
for (save, saved) in [("incremental", write_incremental(&doc, &SaveOptions::default())), ("full", write_full(&doc, &SaveOptions::default()))] {
|
||||
assert!(!holds(&saved.unwrap(), "Merger plan"), "{save}: the edited title was written in the clear");
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream can select its crypt filter by reference at every step: the chain, an entry of it, the
|
||||
/// parameters, an entry of those, the name (ISO 32000-2 §7.3.10, §7.4.10). Read as absent, the
|
||||
/// filter was `/Identity`, so such a stream was saved in the clear and, in a file from elsewhere,
|
||||
/// its ciphertext read as data. A full save renumbers: what it follows has the document's numbers.
|
||||
#[test]
|
||||
fn a_crypt_filter_selected_by_reference_is_used() {
|
||||
for alg in [Algorithm::Aes128, Algorithm::Aes256] {
|
||||
for form in 0.. {
|
||||
let mut doc = Document::open(Arc::new(protect(alg, "", "owner", -1))).unwrap();
|
||||
let Some(selected) = selector(&mut doc, form, "StdCF") else { break };
|
||||
select(&mut doc, selected);
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(!holds(&saved, "Secret page"), "{alg:?} form {form}: saved encrypted");
|
||||
let back = Document::open(Arc::new(saved)).unwrap();
|
||||
assert_eq!(raw_content(&back), SECRET, "{alg:?} form {form}: read back");
|
||||
let full = write_full(&back, &SaveOptions::default()).unwrap();
|
||||
assert!(!holds(&full, "Secret page"), "{alg:?} form {form}: renumbered, encrypted");
|
||||
assert_eq!(raw_content(&Document::open(Arc::new(full)).unwrap()), SECRET, "{alg:?} form {form}: renumbered");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a stream that takes `follows` references to read cold, this one included: its
|
||||
/// nine filters are all read, the last leading to the next stream.
|
||||
fn spends(doc: &mut Document, follows: usize) -> Object {
|
||||
let left = follows - 1;
|
||||
let mut filters: Vec<Object> = (0..left.min(8)).map(|i| Object::Ref(doc.add(Object::Int(i as i64)))).collect();
|
||||
if left > 8 {
|
||||
filters.push(spends(doc, left - 8));
|
||||
}
|
||||
let mut dict = pdfcraft_cos::Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::Array(filters));
|
||||
Object::Ref(doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(dict, Vec::new()))))
|
||||
}
|
||||
|
||||
/// Gives the content stream a `/Type` that takes `follows` references to read, ahead of its filter.
|
||||
fn spend_first(doc: &mut Document, follows: usize) {
|
||||
let spent = spends(doc, follows);
|
||||
let contents = contents_of(doc);
|
||||
let Object::Stream(mut s) = doc.get(contents).as_ref().clone() else { panic!("no content stream") };
|
||||
s.dict.set(b"Type".to_vec(), spent);
|
||||
doc.set(contents, Object::Stream(s));
|
||||
}
|
||||
|
||||
/// Past what one read may follow a reference reads as absent, and a crypt filter with no `/Name` is
|
||||
/// `/Identity`. Together those would read this stream's ciphertext as data, so what can't be told
|
||||
/// is the document's own filter, as a `/Filter` that can't be read is.
|
||||
#[test]
|
||||
fn a_crypt_filter_past_what_a_read_may_follow_is_the_documents_own() {
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let selected = selector(&mut doc, 1, "StdCF").unwrap();
|
||||
select(&mut doc, selected);
|
||||
spend_first(&mut doc, 40);
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(!holds(&saved, "Secret page"), "saved encrypted");
|
||||
let back = Document::open(Arc::new(saved)).unwrap();
|
||||
assert_eq!(raw_content(&back), SECRET);
|
||||
let full = write_full(&back, &SaveOptions::default()).unwrap();
|
||||
assert!(!holds(&full, "Secret page"), "renumbered, encrypted");
|
||||
assert_eq!(raw_content(&Document::open(Arc::new(full)).unwrap()), SECRET, "renumbered");
|
||||
}
|
||||
|
||||
/// A read that takes all it may follow has refused nothing: a crypt filter with no `/Name` after it
|
||||
/// is still `/Identity`, and the stream is in the clear.
|
||||
#[test]
|
||||
fn a_crypt_filter_with_no_name_after_all_a_read_may_follow_is_identity() {
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
select(&mut doc, (Object::name("Crypt"), Object::Dict(pdfcraft_cos::Dict::new())));
|
||||
spend_first(&mut doc, 32);
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(holds(&saved, "Secret page"), "written as the stream asks");
|
||||
assert_eq!(raw_content(&Document::open(Arc::new(saved)).unwrap()), SECRET);
|
||||
}
|
||||
|
||||
/// `/Identity` selected by reference keeps a stream in the clear in a document that encrypts the
|
||||
/// rest, in every spelling, on both saves.
|
||||
#[test]
|
||||
fn an_identity_crypt_filter_selected_by_reference_leaves_a_stream_alone() {
|
||||
for form in 0.. {
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let Some(selected) = selector(&mut doc, form, "Identity") else { break };
|
||||
select(&mut doc, selected);
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(holds(&saved, "Secret page"), "form {form}: written as the stream asks");
|
||||
let back = Document::open(Arc::new(saved)).unwrap();
|
||||
assert_eq!(raw_content(&back), SECRET, "form {form}");
|
||||
let full = write_full(&back, &SaveOptions::default()).unwrap();
|
||||
assert!(holds(&full, "Secret page"), "form {form}: renumbered");
|
||||
assert_eq!(raw_content(&Document::open(Arc::new(full)).unwrap()), SECRET, "form {form}: renumbered");
|
||||
}
|
||||
}
|
||||
|
||||
/// New protection drops a crypt filter it doesn't define, whichever way the stream selected it. The
|
||||
/// chain comes out direct. Parameters in the stream's own dictionary stay, encrypted with it; of an
|
||||
/// array in an object of its own, which every stream can share, only what decoders read is copied.
|
||||
#[test]
|
||||
fn protecting_drops_a_crypt_filter_selected_by_reference() {
|
||||
let protected = |doc: &mut Document| {
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [7; 32],
|
||||
})
|
||||
.unwrap();
|
||||
write_full(doc, &SaveOptions::default()).unwrap()
|
||||
};
|
||||
for form in 0.. {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let Some(selected) = selector(&mut doc, form, "MyCF") else { break };
|
||||
select(&mut doc, selected);
|
||||
let bytes = protected(&mut doc);
|
||||
assert!(!holds(&bytes, "Secret page"), "form {form}: encrypted");
|
||||
assert!(!holds(&bytes, "MyCF"), "form {form}: no selector left in the stream");
|
||||
let back = Document::open(Arc::new(bytes.clone())).unwrap();
|
||||
assert_eq!(raw_content(&back), SECRET, "form {form}");
|
||||
let contents = back.get(contents_of(&back));
|
||||
let filter = contents.as_dict().unwrap().get(b"Filter");
|
||||
match form {
|
||||
8 => assert!(
|
||||
matches!(filter, Some(Object::Array(a)) if matches!(a.as_slice(), [Object::Ref(_)])),
|
||||
"form 8: the other filter stays: {filter:?}"
|
||||
),
|
||||
_ => assert_eq!(filter, None, "form {form}"),
|
||||
}
|
||||
assert!(hayro_syntax::Pdf::new(bytes).is_ok(), "form {form}");
|
||||
}
|
||||
for shared in [false, true] {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let mut named = pdfcraft_cos::Dict::new();
|
||||
named.set(b"Name".to_vec(), Object::name("MyCF"));
|
||||
let mut other = pdfcraft_cos::Dict::new();
|
||||
other.set(b"Note".to_vec(), Object::String(PdfString::text("Private note")));
|
||||
other.set(b"Predictor".to_vec(), Object::Int(1));
|
||||
let parms = Object::Array(vec![Object::Dict(named), Object::Dict(other)]);
|
||||
let parms = if shared { Object::Ref(doc.add(parms)) } else { parms };
|
||||
select(&mut doc, (Object::Array(vec![Object::name("Crypt"), Object::name("ASCIIHexDecode")]), parms));
|
||||
let bytes = protected(&mut doc);
|
||||
assert!(!holds(&bytes, "Private note"), "shared: {shared}");
|
||||
let back = Document::open(Arc::new(bytes)).unwrap();
|
||||
let contents = back.get(contents_of(&back));
|
||||
let kept = contents.as_dict().unwrap().get(b"DecodeParms").unwrap().as_array().unwrap();
|
||||
assert_eq!(kept.len(), 1, "one filter left, one entry");
|
||||
let kept = kept[0].as_dict().unwrap();
|
||||
assert_eq!(kept.get(b"Predictor"), Some(&Object::Int(1)), "shared: {shared}");
|
||||
assert_eq!(kept.get(b"Note").is_some(), !shared, "shared: {shared}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every stream of a file can share one large object as its `/DecodeParms` or `/Filter`, which
|
||||
/// costs nothing until new protection drops a crypt filter from each. Copied whole, a 1 MB file
|
||||
/// came out at 1 GB.
|
||||
#[test]
|
||||
fn protecting_copies_no_large_object_into_every_stream() {
|
||||
let pad = Object::Array(vec![Object::Int(1); 100_000]);
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let mut large = pdfcraft_cos::Dict::new();
|
||||
large.set(b"Pad".to_vec(), pad.clone());
|
||||
let parms = doc.add(Object::Array(vec![Object::Null, Object::Dict(large)]));
|
||||
let filter = doc.add(Object::Array(vec![Object::name("Crypt"), Object::Name(vec![b'A'; 100_000]), pad]));
|
||||
let mut streams = Vec::new();
|
||||
for _ in 0..200 {
|
||||
let mut by_parms = pdfcraft_cos::Dict::new();
|
||||
by_parms.set(b"Filter".to_vec(), Object::Array(vec![Object::name("Crypt"), Object::name("ASCIIHexDecode")]));
|
||||
by_parms.set(b"DecodeParms".to_vec(), Object::Ref(parms));
|
||||
streams.push(Object::Ref(doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(by_parms, b"41>".to_vec())))));
|
||||
let mut by_filter = pdfcraft_cos::Dict::new();
|
||||
by_filter.set(b"Filter".to_vec(), Object::Ref(filter));
|
||||
streams.push(Object::Ref(doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(by_filter, b"41>".to_vec())))));
|
||||
}
|
||||
doc.update_dict(doc.root().unwrap(), |c| c.set(b"Test".to_vec(), Object::Array(streams))).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [7; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
assert!(bytes.len() < 1 << 20, "{} bytes", bytes.len());
|
||||
}
|
||||
|
||||
/// A file can name one large string thousands of times in `/ID`, which holds two. An encrypted file
|
||||
/// keeps its `/ID`, so each copy would be in the file a save writes.
|
||||
#[test]
|
||||
fn a_file_identifier_of_many_references_is_two_strings() {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes128,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [9; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let large = doc.add(Object::String(PdfString { bytes: vec![0xAB; 100_000], hex: true }));
|
||||
doc.trailer_mut().set(b"ID".to_vec(), Object::Array(vec![Object::Ref(large); 500]));
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
assert!(bytes.len() < 1 << 20, "{} bytes", bytes.len());
|
||||
}
|
||||
|
||||
/// What a file can do with those references: point a stream's selector at the stream itself, or at
|
||||
/// nothing. They read as absent, so a stream with `/Crypt` is `/Identity` (ISO 32000-2 §7.4.10),
|
||||
/// and one that names no filter follows the document's. Loading ends either way.
|
||||
#[test]
|
||||
fn a_crypt_filter_reference_to_itself_or_nothing_reads_as_absent() {
|
||||
let named = |n: Object| {
|
||||
let mut d = pdfcraft_cos::Dict::new();
|
||||
d.set(b"Name".to_vec(), n);
|
||||
Object::Dict(d)
|
||||
};
|
||||
for (what, encrypted) in [
|
||||
("/Filter to itself", true),
|
||||
("/Filter nowhere", true),
|
||||
("/DecodeParms to itself", false),
|
||||
("/DecodeParms nowhere", false),
|
||||
("/Name to itself", false),
|
||||
("/Name nowhere", false),
|
||||
] {
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let me = Object::Ref(contents_of(&doc));
|
||||
let nowhere = Object::Ref(ObjRef::new(9_999, 0));
|
||||
let crypt = Object::name("Crypt");
|
||||
select(
|
||||
&mut doc,
|
||||
match what {
|
||||
"/Filter to itself" => (me, Object::Null),
|
||||
"/Filter nowhere" => (nowhere, Object::Null),
|
||||
"/DecodeParms to itself" => (crypt, me),
|
||||
"/DecodeParms nowhere" => (crypt, nowhere),
|
||||
"/Name to itself" => (crypt, named(me)),
|
||||
_ => (crypt, named(nowhere)),
|
||||
},
|
||||
);
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
assert_eq!(!holds(&saved, "Secret page"), encrypted, "{what}");
|
||||
assert_eq!(raw_content(&Document::open(Arc::new(saved)).unwrap()), SECRET, "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A lone parameter dictionary belongs to the first filter, so `/Crypt` behind another filter has
|
||||
/// none of its own and is `/Identity` (ISO 32000-2 §7.4.1), whatever that dictionary says.
|
||||
#[test]
|
||||
fn a_lone_parameter_dictionary_is_the_first_filters() {
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let mut parms = pdfcraft_cos::Dict::new();
|
||||
parms.set(b"Name".to_vec(), Object::name("StdCF"));
|
||||
let first = Object::Array(vec![Object::name("Crypt"), Object::name("ASCIIHexDecode")]);
|
||||
let second = Object::Array(vec![Object::name("ASCIIHexDecode"), Object::name("Crypt")]);
|
||||
select(&mut doc, (first, Object::Dict(parms.clone())));
|
||||
assert!(!holds(&write_incremental(&doc, &SaveOptions::default()).unwrap(), "Secret page"), "first: it names StdCF");
|
||||
select(&mut doc, (second, Object::Dict(parms)));
|
||||
assert!(holds(&write_incremental(&doc, &SaveOptions::default()).unwrap(), "Secret page"), "second: it names nothing");
|
||||
}
|
||||
|
||||
/// A lone parameter dictionary is the first filter's, so dropping a `/Crypt` behind that filter
|
||||
/// leaves it, and dropping one in front of it takes it away.
|
||||
#[test]
|
||||
fn protecting_keeps_the_parameters_of_the_filter_before_a_crypt_filter() {
|
||||
let protected = |filters: Vec<Object>| {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let mut parms = pdfcraft_cos::Dict::new();
|
||||
parms.set(b"Name".to_vec(), Object::name("MyCF"));
|
||||
select(&mut doc, (Object::Array(filters), Object::Dict(parms)));
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [7; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let back = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap();
|
||||
back.get(contents_of(&back)).as_dict().unwrap().contains(b"DecodeParms")
|
||||
};
|
||||
let (hex, crypt) = (Object::name("ASCIIHexDecode"), Object::name("Crypt"));
|
||||
assert!(protected(vec![hex.clone(), crypt.clone()]), "the first filter's, which stays");
|
||||
assert!(!protected(vec![crypt, hex]), "/Crypt's, which goes");
|
||||
}
|
||||
|
||||
/// Dropping `/Crypt` takes its entry out of `/DecodeParms`, and an array too short to have one is
|
||||
/// left as it is, given directly or by reference.
|
||||
#[test]
|
||||
fn protecting_leaves_a_parameter_array_too_short_for_the_crypt_filter() {
|
||||
for by_reference in [false, true] {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let short = Object::Array(vec![Object::Null]);
|
||||
let parms = if by_reference { Object::Ref(doc.add(short)) } else { short };
|
||||
select(&mut doc, (Object::Array(vec![Object::name("ASCIIHexDecode"), Object::name("Crypt")]), parms));
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [7; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(!holds(&bytes, "Secret page"), "by reference: {by_reference}");
|
||||
let back = Document::open(Arc::new(bytes)).unwrap();
|
||||
assert_eq!(raw_content(&back), SECRET, "by reference: {by_reference}");
|
||||
let contents = back.get(contents_of(&back));
|
||||
let filter = contents.as_dict().unwrap().get(b"Filter").unwrap().as_array().unwrap();
|
||||
assert_eq!(filter.len(), 1, "only /Crypt goes");
|
||||
assert_eq!(filter[0].as_name(), Some(b"ASCIIHexDecode".as_slice()), "the other filter stays as it was");
|
||||
}
|
||||
}
|
||||
|
||||
/// A chain of up to [`pdfcraft_filters::MAX_CHAIN`] filters and `/Crypt` is read; a longer one
|
||||
/// names no crypt filter, so its entries aren't followed for every stream sharing it.
|
||||
#[test]
|
||||
fn a_chain_longer_than_any_decoder_takes_names_no_crypt_filter() {
|
||||
let chain = |others: usize| {
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let mut filters = vec![Object::name("ASCIIHexDecode"); others];
|
||||
filters.push(Object::name("Crypt"));
|
||||
let array = doc.add(Object::Array(filters));
|
||||
select(&mut doc, (Object::Ref(array), Object::Null));
|
||||
holds(&write_incremental(&doc, &SaveOptions::default()).unwrap(), "Secret page")
|
||||
};
|
||||
assert!(chain(pdfcraft_filters::MAX_CHAIN), "the longest chain read: /Identity, in the clear");
|
||||
assert!(!chain(pdfcraft_filters::MAX_CHAIN + 1), "one more: the document's filter");
|
||||
}
|
||||
|
||||
/// A stream's `/Type` by reference picks its handling like one given directly: metadata left
|
||||
/// readable under `/EncryptMetadata false`, an embedded file under `/EFF`.
|
||||
#[test]
|
||||
fn a_stream_type_given_by_reference_picks_its_crypt_filter() {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let catalog = doc.get(doc.root().unwrap()).as_dict().cloned().unwrap();
|
||||
let meta = catalog.reference(b"Metadata").unwrap();
|
||||
let ty = doc.add(Object::name("Metadata"));
|
||||
let Object::Stream(mut s) = doc.get(meta).as_ref().clone() else { panic!("no metadata stream") };
|
||||
s.dict.set(b"Type".to_vec(), Object::Ref(ty));
|
||||
doc.set(meta, Object::Stream(s));
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "o",
|
||||
permissions: -1,
|
||||
encrypt_metadata: false,
|
||||
seed: [5; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(holds(&bytes, "<x:xmpmeta>meta</x:xmpmeta>"), "search engines can read XMP");
|
||||
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let encrypt = doc.trailer().reference(b"Encrypt").unwrap();
|
||||
doc.update_dict(encrypt, |e| e.set(b"EFF".to_vec(), Object::name("Identity"))).unwrap();
|
||||
let mut doc = Document::open(Arc::new(write_incremental(&doc, &SaveOptions::default()).unwrap())).unwrap();
|
||||
let contents = contents_of(&doc);
|
||||
let ty = doc.add(Object::name("EmbeddedFile"));
|
||||
let Object::Stream(mut s) = doc.get(contents).as_ref().clone() else { panic!("no content stream") };
|
||||
s.dict.set(b"Type".to_vec(), Object::Ref(ty));
|
||||
doc.set(contents, Object::Stream(s));
|
||||
assert!(holds(&write_incremental(&doc, &SaveOptions::default()).unwrap(), "Secret page"), "/EFF /Identity leaves it alone");
|
||||
}
|
||||
|
||||
/// What makes a dictionary a signature's can be given by reference, and what is null or refers to
|
||||
/// nothing is absent: `/Contents` stays as written in each of these.
|
||||
#[test]
|
||||
fn a_signature_dictionary_is_known_in_every_form() {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let ty = doc.add(Object::name("Sig"));
|
||||
let range = Object::Array([0, 10, 20, 30].into_iter().map(Object::Int).collect());
|
||||
let by_reference = doc.add(range.clone());
|
||||
let nowhere = Object::Ref(ObjRef::new(9_999, 0));
|
||||
let forms: [(Vec<(&str, Object)>, &str); 5] = [
|
||||
(vec![("Type", Object::Ref(ty))], "DEAD"),
|
||||
(vec![("ByteRange", Object::Ref(by_reference))], "BEEF"),
|
||||
(vec![("Type", Object::Null), ("ByteRange", range.clone())], "CAFE"),
|
||||
(vec![("Type", nowhere.clone()), ("ByteRange", range.clone())], "F00D"),
|
||||
(vec![("Subtype", nowhere), ("ByteRange", range)], "D00D"),
|
||||
];
|
||||
let mut signatures = Vec::new();
|
||||
for (entries, contents) in forms {
|
||||
let mut sig = pdfcraft_cos::Dict::new();
|
||||
for (key, value) in entries {
|
||||
sig.set(key.as_bytes().to_vec(), value);
|
||||
}
|
||||
let bytes = (0..contents.len()).step_by(2).map(|i| u8::from_str_radix(&contents[i..i + 2], 16).unwrap()).collect();
|
||||
sig.set(b"Contents".to_vec(), Object::String(PdfString { bytes, hex: true }));
|
||||
signatures.push(Object::Ref(doc.add(Object::Dict(sig))));
|
||||
}
|
||||
// Nested in another object, it follows nothing, but known directly it is one all the same.
|
||||
let mut nested = pdfcraft_cos::Dict::new();
|
||||
nested.set(b"Type".to_vec(), Object::name("Sig"));
|
||||
nested.set(b"Contents".to_vec(), Object::String(PdfString { bytes: vec![0xAB, 0xBA], hex: true }));
|
||||
let mut field = pdfcraft_cos::Dict::new();
|
||||
field.set(b"V".to_vec(), Object::Dict(nested));
|
||||
signatures.push(Object::Ref(doc.add(Object::Dict(field))));
|
||||
// Neither of what makes one: a string /Contents alone is anybody's, so it's encrypted.
|
||||
let mut other = pdfcraft_cos::Dict::new();
|
||||
other.set(b"Contents".to_vec(), Object::String(PdfString { bytes: vec![0xAB, 0xCD], hex: true }));
|
||||
signatures.push(Object::Ref(doc.add(Object::Dict(other))));
|
||||
doc.update_dict(doc.root().unwrap(), |c| c.set(b"Test".to_vec(), Object::Array(signatures))).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [3; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
for contents in ["DEAD", "BEEF", "CAFE", "F00D", "D00D", "ABBA"] {
|
||||
assert!(holds(&bytes, &format!("<{contents}>")), "{contents}: written as is");
|
||||
}
|
||||
assert!(!holds(&bytes, "<ABCD>"), "without a /ByteRange it is no signature");
|
||||
}
|
||||
|
||||
/// What a test needs to write an update of a classic file protected with AES-128.
|
||||
struct Classic {
|
||||
bytes: Vec<u8>,
|
||||
root: ObjRef,
|
||||
pages: ObjRef,
|
||||
info: ObjRef,
|
||||
encrypt: ObjRef,
|
||||
id: Vec<u8>,
|
||||
dict: pdfcraft_cos::Dict,
|
||||
}
|
||||
|
||||
fn classic_file() -> Classic {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes128,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [9; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
let doc = Document::open(Arc::new(bytes.clone())).unwrap();
|
||||
let encrypt = doc.trailer().reference(b"Encrypt").unwrap();
|
||||
let id = doc.trailer().get(b"ID").unwrap().as_array().unwrap()[0].as_string().unwrap().bytes.clone();
|
||||
let dict = doc.get(encrypt).as_dict().cloned().unwrap();
|
||||
let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap();
|
||||
Classic { root: doc.root().unwrap(), pages, info: doc.trailer().reference(b"Info").unwrap(), bytes, encrypt, id, dict }
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02X}")).collect()
|
||||
}
|
||||
|
||||
/// `bytes` with an update appended: `objects`, and a trailer of `trailer` entries, in which `@n`
|
||||
/// stands for the offset of object `n`. An object with nothing in it gets an entry of the
|
||||
/// cross-reference table that points at nothing.
|
||||
fn updated(bytes: &[u8], objects: &[(u32, Vec<u8>)], trailer: &str) -> Vec<u8> {
|
||||
let at = bytes.windows(9).rposition(|w| w == b"startxref").unwrap();
|
||||
let prev: usize = String::from_utf8_lossy(&bytes[at + 9..]).split_whitespace().next().unwrap().parse().unwrap();
|
||||
let mut out = bytes.to_vec();
|
||||
let mut table = String::from("xref\n");
|
||||
let mut trailer = trailer.to_string();
|
||||
for (num, body) in objects {
|
||||
if body.is_empty() {
|
||||
table.push_str(&format!("{num} 1\n0000000010 00000 n \n"));
|
||||
continue;
|
||||
}
|
||||
trailer = trailer.replace(&format!("@{num}"), &out.len().to_string());
|
||||
table.push_str(&format!("{num} 1\n{:010} 00000 n \n", out.len()));
|
||||
out.extend_from_slice(format!("{num} 0 obj\n").as_bytes());
|
||||
out.extend_from_slice(body);
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(table.as_bytes());
|
||||
out.extend_from_slice(format!("trailer\n<< {trailer} /Size 2000 /Prev {prev} >>\nstartxref\n{xref}\n%%EOF\n").as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
/// The `/Encrypt` dictionary of `file`, with `/O`, `/StmF` and `/StrF` as given.
|
||||
fn encrypt_with(file: &Classic, o: &str, filter: &str) -> Vec<u8> {
|
||||
let string = |key: &[u8]| hex(&file.dict.get(key).unwrap().as_string().unwrap().bytes);
|
||||
format!(
|
||||
"<< /Filter /Standard /V 4 /R 4 /Length 128 /P {} /O {o} /U <{}> /CF << /StdCF << /CFM /AESV2 /Length 16 /AuthEvent /DocOpen >> >> /StmF /{filter} /StrF /{filter} >>",
|
||||
file.dict.int(b"P").unwrap(),
|
||||
string(b"U")
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
/// An object stream can have its parameters by reference, and a stream's crypt filter can be
|
||||
/// selected by a member of it: legal together, though each is rare. What reads the stream first
|
||||
/// goes through the object stream, and members must come out of it as they do read directly. They
|
||||
/// didn't, while a read made for a selector wasn't allowed to follow its own: the object stream was
|
||||
/// kept undecoded, its members lost for good, and a full save wrote nulls without saying so.
|
||||
#[test]
|
||||
fn an_object_stream_reached_from_a_selector_keeps_its_members() {
|
||||
let file = classic_file();
|
||||
let parms = "[<< /Name /Identity >> null]";
|
||||
let header = format!("902 0 903 {} ", parms.len() + 1);
|
||||
let members = format!("{header}{parms} << /Keep (yes) >>");
|
||||
let (mut rows, mut before) = (vec![1u8], 0u8);
|
||||
for b in members.bytes() {
|
||||
rows.push(b.wrapping_sub(before));
|
||||
before = b;
|
||||
}
|
||||
let coded = pdfcraft_cos::Stream::flate(pdfcraft_cos::Dict::new(), &rows).raw;
|
||||
let mut object_stream =
|
||||
format!("<< /Type /ObjStm /N 2 /First {} /Filter /FlateDecode /DecodeParms 911 0 R /Length {} >>\nstream\n", header.len(), coded.len())
|
||||
.into_bytes();
|
||||
object_stream.extend_from_slice(&coded);
|
||||
object_stream.extend_from_slice(b"\nendstream");
|
||||
let data = [2u8, 0x03, 0x8E, 0, 2, 0x03, 0x8E, 1];
|
||||
let mut xref = format!("<< /Type /XRef /Size 2000 /W [1 2 1] /Index [902 2] /Length {} >>\nstream\n", data.len()).into_bytes();
|
||||
xref.extend_from_slice(&data);
|
||||
xref.extend_from_slice(b"\nendstream");
|
||||
let string = |key: &[u8]| format!("<{}>", hex(&file.dict.get(key).unwrap().as_string().unwrap().bytes));
|
||||
let objects = [
|
||||
(file.root.num, format!("<< /Type /Catalog /Pages {} 0 R /A 900 0 R /B 903 0 R >>", file.pages.num).into_bytes()),
|
||||
(file.encrypt.num, encrypt_with(&file, &string(b"O"), "Identity")),
|
||||
(900, b"<< /Filter [/Crypt /ASCIIHexDecode] /DecodeParms 902 0 R /Length 1 >>\nstream\n>\nendstream".to_vec()),
|
||||
(910, object_stream),
|
||||
(911, format!("<< /Predictor 11 /Columns {} >>", members.len()).into_bytes()),
|
||||
(912, xref),
|
||||
];
|
||||
let trailer = format!("/Root {} 0 R /Encrypt {} 0 R /ID [<{id}> <{id}>] /XRefStm @912", file.root.num, file.encrypt.num, id = hex(&file.id));
|
||||
let bytes = updated(&file.bytes, &objects, &trailer);
|
||||
let keep =
|
||||
|doc: &Document, r: &Object| doc.resolve(r).as_dict().and_then(|d| d.get(b"Keep")).and_then(Object::as_string).map(|s| s.bytes.clone());
|
||||
let member = Object::Ref(ObjRef::new(903, 0));
|
||||
let doc = Document::open(Arc::new(bytes.clone())).unwrap();
|
||||
assert_eq!(keep(&doc, &member).as_deref(), Some(b"yes".as_slice()), "read first");
|
||||
let doc = Document::open(Arc::new(bytes.clone())).unwrap();
|
||||
let _ = doc.get(ObjRef::new(900, 0));
|
||||
assert_eq!(keep(&doc, &member).as_deref(), Some(b"yes".as_slice()), "read after the stream selecting from it");
|
||||
let saved = write_full(&Document::open(Arc::new(bytes)).unwrap(), &SaveOptions::default()).unwrap();
|
||||
let back = Document::open(Arc::new(saved)).unwrap();
|
||||
let catalog = back.get(back.root().unwrap());
|
||||
assert_eq!(keep(&back, catalog.as_dict().unwrap().get(b"B").unwrap()).as_deref(), Some(b"yes".as_slice()), "after a full save");
|
||||
}
|
||||
|
||||
/// `walk` goes as deep as an object nests, so following a reference from down there adds that depth
|
||||
/// again for each object it loads: this file, under 10 KB, overflowed a 2 MiB stack and took the
|
||||
/// process with it. Only the object itself follows references, and it does so in very little stack.
|
||||
#[test]
|
||||
fn nested_dictionaries_load_nothing_on_a_small_stack() {
|
||||
let file = classic_file();
|
||||
let first = 960u32;
|
||||
let objects: Vec<(u32, Vec<u8>)> = (first..first + 40)
|
||||
.map(|num| {
|
||||
let open = "[".repeat(120);
|
||||
let close = "]".repeat(120);
|
||||
(num, format!("{open}<< /Contents () /Type {} 0 R >>{close}", num + 1).into_bytes())
|
||||
})
|
||||
.collect();
|
||||
let trailer = format!("/Root {} 0 R /Encrypt {} 0 R /ID [<{id}> <{id}>]", file.root.num, file.encrypt.num, id = hex(&file.id));
|
||||
let bytes = updated(&file.bytes, &objects, &trailer);
|
||||
let reading = std::thread::Builder::new().stack_size(1 << 20).spawn(move || {
|
||||
let doc = Document::open(Arc::new(bytes)).unwrap();
|
||||
doc.get(ObjRef::new(first, 0)).as_array().is_some()
|
||||
});
|
||||
assert_eq!(reading.unwrap().join().ok(), Some(true));
|
||||
}
|
||||
|
||||
/// The object itself can follow references, a ladder of them as long as loading may nest, and that
|
||||
/// fits a small stack too.
|
||||
#[test]
|
||||
fn a_ladder_of_signature_checks_fits_a_small_stack() {
|
||||
let file = classic_file();
|
||||
let first = 960u32;
|
||||
let objects: Vec<(u32, Vec<u8>)> =
|
||||
(first..first + 40).map(|num| (num, format!("<< /Contents () /Type {} 0 R >>", num + 1).into_bytes())).collect();
|
||||
let trailer = format!("/Root {} 0 R /Encrypt {} 0 R /ID [<{id}> <{id}>]", file.root.num, file.encrypt.num, id = hex(&file.id));
|
||||
let bytes = updated(&file.bytes, &objects, &trailer);
|
||||
let reading = std::thread::Builder::new().stack_size(1 << 20).spawn(move || {
|
||||
let doc = Document::open(Arc::new(bytes)).unwrap();
|
||||
doc.get(ObjRef::new(first, 0)).as_dict().is_some()
|
||||
});
|
||||
assert_eq!(reading.unwrap().join().ok(), Some(true));
|
||||
}
|
||||
|
||||
/// The strings of `/Encrypt` are never encrypted (ISO 32000-2 §7.6.2), and can be objects of their
|
||||
/// own. Read as ordinary strings they were decrypted, and a full save encrypted them again under
|
||||
/// another number, so the file it wrote no longer opened with its own password.
|
||||
#[test]
|
||||
fn encrypt_strings_given_by_reference_stay_as_stored() {
|
||||
let file = classic_file();
|
||||
let string = |key: &[u8]| hex(&file.dict.get(key).unwrap().as_string().unwrap().bytes);
|
||||
let replaced = format!(
|
||||
"<< /Filter /Standard /V 4 /R 4 /Length 128 /P {} /O 900 0 R /U 901 0 R /CF << /StdCF << /CFM /AESV2 /Length 16 /AuthEvent /DocOpen >> >> /StmF /StdCF /StrF /StdCF >>",
|
||||
file.dict.int(b"P").unwrap()
|
||||
);
|
||||
let objects = [
|
||||
(file.encrypt.num, replaced.into_bytes()),
|
||||
(900, format!("<{}>", string(b"O")).into_bytes()),
|
||||
(901, format!("<{}>", string(b"U")).into_bytes()),
|
||||
];
|
||||
let trailer =
|
||||
format!("/Root {} 0 R /Info {} 0 R /Encrypt {} 0 R /ID [<{id}> <{id}>]", file.root.num, file.info.num, file.encrypt.num, id = hex(&file.id));
|
||||
let source = Document::open(Arc::new(updated(&file.bytes, &objects, &trailer))).unwrap();
|
||||
assert_eq!(content(&source), SECRET, "the file reads");
|
||||
assert_eq!(title(&source).as_deref(), Some("Quarterly numbers"));
|
||||
for object_streams in [false, true] {
|
||||
let saved = write_full(&source, &SaveOptions { object_streams, ..SaveOptions::default() }).unwrap();
|
||||
let back = Document::open(Arc::new(saved)).unwrap();
|
||||
assert_eq!(content(&back), SECRET, "object streams: {object_streams}");
|
||||
assert_eq!(title(&back).as_deref(), Some("Quarterly numbers"), "object streams: {object_streams}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A save keeps what a file is protected with: its `/Encrypt`, which holds the handler and the
|
||||
/// permissions, its `/ID`, and so its file key, under every algorithm and by either kind of save.
|
||||
#[test]
|
||||
fn a_save_keeps_the_handler_the_key_and_the_permissions() {
|
||||
for alg in ALL {
|
||||
let doc = Document::open_with_password(Arc::new(protect(alg, "user", "owner", -1 ^ 0b1_0000)), Some("user")).unwrap();
|
||||
let held = |d: &Document| {
|
||||
let encrypt = d.resolve(d.trailer().get(b"Encrypt").unwrap()).as_dict().unwrap().clone();
|
||||
(encrypt, d.trailer().get(b"ID").cloned(), d.security().unwrap().file_key().to_vec(), format!("{:?}", d.permissions()))
|
||||
};
|
||||
let before = held(&doc);
|
||||
let mut edited = doc.clone();
|
||||
let _ = edited.add(Object::Int(1));
|
||||
for (save, saved) in [
|
||||
("incremental", write_incremental(&edited, &SaveOptions::default()).unwrap()),
|
||||
("full", write_full(&doc, &SaveOptions::default()).unwrap()),
|
||||
] {
|
||||
let back = Document::open_with_password(Arc::new(saved), Some("user")).unwrap();
|
||||
assert!(held(&back) == before, "{alg:?}: a {save} save");
|
||||
assert_eq!(content(&back), SECRET, "{alg:?}: a {save} save");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `/ID` has to be direct in an encrypted file, but one that isn't has its key made from the same
|
||||
/// bytes. A full save writes it direct, and an incremental one leaves it as it was.
|
||||
#[test]
|
||||
fn a_file_identifier_given_by_reference_is_read() {
|
||||
let file = classic_file();
|
||||
let id = hex(&file.id);
|
||||
let reference = |entries: &str, objects: &[(u32, Vec<u8>)]| {
|
||||
let trailer = format!("/Root {} 0 R /Encrypt {} 0 R /ID {entries}", file.root.num, file.encrypt.num);
|
||||
updated(&file.bytes, objects, &trailer)
|
||||
};
|
||||
let whole = reference("900 0 R", &[(900, format!("[<{id}> <{id}>]").into_bytes())]);
|
||||
let entries = reference("[900 0 R 901 0 R]", &[(900, format!("<{id}>").into_bytes()), (901, format!("<{id}>").into_bytes())]);
|
||||
for file in [whole, entries] {
|
||||
let source = Document::open(Arc::new(file)).unwrap();
|
||||
assert_eq!(content(&source), SECRET);
|
||||
let back = Document::open(Arc::new(write_full(&source, &SaveOptions::default()).unwrap())).unwrap();
|
||||
assert_eq!(content(&back), SECRET);
|
||||
assert!(matches!(back.trailer().get(b"ID"), Some(Object::Array(_))), "written direct");
|
||||
let mut edited = source.clone();
|
||||
let _ = edited.add(Object::Int(1));
|
||||
let back = Document::open(Arc::new(write_incremental(&edited, &SaveOptions::default()).unwrap())).unwrap();
|
||||
assert_eq!(content(&back), SECRET, "an incremental save");
|
||||
assert_eq!(back.trailer().get(b"ID"), edited.trailer().get(b"ID"), "and leaves it as it was");
|
||||
}
|
||||
}
|
||||
|
||||
/// A direct `/Encrypt` can refer to objects. A full save dropped those it found only there, and kept
|
||||
/// its references as they were under the old numbers, so nothing in the file read afterwards.
|
||||
#[test]
|
||||
fn a_direct_encrypt_dictionary_keeps_what_it_refers_to() {
|
||||
for object_streams in [false, true] {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes128,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [9; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let mut dict = doc.get(doc.trailer().reference(b"Encrypt").unwrap()).as_dict().cloned().unwrap();
|
||||
let filter = doc.add(Object::name("StdCF"));
|
||||
let filters = doc.add(dict.get(b"CF").unwrap().clone());
|
||||
dict.set(b"StmF".to_vec(), Object::Ref(filter));
|
||||
dict.set(b"CF".to_vec(), Object::Ref(filters));
|
||||
doc.trailer_mut().set(b"Encrypt".to_vec(), Object::Dict(dict));
|
||||
let options = SaveOptions { object_streams, ..SaveOptions::default() };
|
||||
let saved = write_full(&doc, &options).unwrap();
|
||||
let back = Document::open(Arc::new(saved)).unwrap();
|
||||
assert_eq!(content(&back), SECRET, "object streams: {object_streams}");
|
||||
let again = write_full(&back, &options).unwrap();
|
||||
assert_eq!(content(&Document::open(Arc::new(again)).unwrap()), SECRET, "object streams: {object_streams}, saved again");
|
||||
}
|
||||
}
|
||||
|
||||
/// How a stream is treated can be given by reference, and what that loads can be a stream too. A
|
||||
/// file can make a ladder of those, where each rung reads the rung below, then two trees wider than
|
||||
/// the object cache, which push that rung out of it, then the rung below again. Each rung would
|
||||
/// take twice what the one below does, if one read could follow any number of references.
|
||||
#[test]
|
||||
fn selectors_that_lead_to_selectors_load_at_once() {
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let stream_of = |doc: &mut Document, filters: Vec<Object>| {
|
||||
let mut dict = pdfcraft_cos::Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::Array(filters));
|
||||
doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(dict, Vec::new())))
|
||||
};
|
||||
let tree = |doc: &mut Document| {
|
||||
let mut level: Vec<ObjRef> = (0..6_561).map(|i| doc.add(Object::Int(i))).collect();
|
||||
while level.len() > 1 {
|
||||
let chunks: Vec<Vec<Object>> = level.chunks(9).map(|c| c.iter().map(|r| Object::Ref(*r)).collect()).collect();
|
||||
level = chunks.into_iter().map(|c| stream_of(doc, c)).collect();
|
||||
}
|
||||
Object::Ref(level[0])
|
||||
};
|
||||
let (first, second) = (tree(&mut doc), tree(&mut doc));
|
||||
let mut top = doc.add(Object::Int(0));
|
||||
// Twelve rungs of two, and the trees' five levels, stay inside the 32 that loading may nest.
|
||||
for _ in 0..12 {
|
||||
let (a, b) = (stream_of(&mut doc, vec![Object::Ref(top)]), stream_of(&mut doc, vec![Object::Ref(top)]));
|
||||
top = stream_of(&mut doc, vec![Object::Ref(a), first.clone(), second.clone(), Object::Ref(b)]);
|
||||
}
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
let (done, finished) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let doc = Document::open(Arc::new(saved)).unwrap();
|
||||
let _ = done.send(doc.get(top).as_dict().is_some());
|
||||
});
|
||||
// With the budget it takes milliseconds, and without it longer than anyone would wait, so a
|
||||
// margin this wide can't fail on a busy machine.
|
||||
assert_eq!(finished.recv_timeout(std::time::Duration::from_secs(120)), Ok(true), "loading must not fan out");
|
||||
}
|
||||
|
||||
@@ -9127,3 +9127,271 @@ fn content_after_an_inline_image_holding_ei_is_still_read() {
|
||||
// `EI` inside the data was not taken for the end.
|
||||
assert!(page.contains("AFTER"), "the content after the image was lost: {page:?}");
|
||||
}
|
||||
|
||||
/// `/Crypt` has nothing to undo, so a chain naming it thousands of times costs no copy of the data
|
||||
/// for each. A chain of filters that does work has its own cost, which this isn't about.
|
||||
#[test]
|
||||
fn a_long_chain_of_crypt_filters_takes_no_time() {
|
||||
use super::*;
|
||||
let content = format!("1 0 0 rg 0 0 200 200 re f\n{}", " ".repeat(16 << 20));
|
||||
let chain = "/Crypt ".repeat(100_000);
|
||||
let pdf = format!(
|
||||
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Filter [{chain}] /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
);
|
||||
let (done, finished) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let page = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
let start = (100 * page.width as usize + 100) * 4;
|
||||
let _ = done.send(page.rgba.get(start..start + 4).map(<[u8]>::to_vec));
|
||||
});
|
||||
// Skipped, this takes a moment; copied, 1.6 TB, which a margin this wide tells apart however
|
||||
// busy the machine.
|
||||
let pixel = finished.recv_timeout(std::time::Duration::from_secs(120)).expect("the chain costs no copy for each entry");
|
||||
assert_eq!(pixel, Some(vec![255, 0, 0, 255]));
|
||||
}
|
||||
|
||||
/// A lone filter takes its parameters as a dictionary or as an array of one, and given as an array
|
||||
/// of one itself it takes either too. The form of `/Filter` decided which was read, so a predictor
|
||||
/// in the other form was lost and the content came out as noise. `/StmF` and `/StrF` are optional,
|
||||
/// meaning `/Identity`: such a file didn't open at all, though a stream can still name a filter.
|
||||
#[test]
|
||||
fn a_filters_parameters_are_read_in_either_form() {
|
||||
use super::*;
|
||||
use pdfcraft_cos::{Algorithm, Dict, Document, NewEncryption, Object, SaveOptions, Stream, write_full, write_incremental};
|
||||
let content = b"1 0 0 rg 0 0 200 200 re f";
|
||||
let centre = |bytes: Vec<u8>| -> [u8; 4] {
|
||||
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
let start = (100 * page.width as usize + 100) * 4;
|
||||
let mut pixel = [0; 4];
|
||||
pixel.copy_from_slice(page.rgba.get(start..start + 4).expect("centre pixel"));
|
||||
pixel
|
||||
};
|
||||
let page = |stream: &[u8]| {
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(stream);
|
||||
pdf.extend_from_slice(b"\ntrailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
pdf
|
||||
};
|
||||
// PNG Sub over one row, which reads as noise if it isn't undone.
|
||||
let mut rows = vec![1u8];
|
||||
let mut before = 0u8;
|
||||
for b in content {
|
||||
rows.push(b.wrapping_sub(before));
|
||||
before = *b;
|
||||
}
|
||||
let coded = Stream::flate(Dict::new(), &rows).raw;
|
||||
let parms = format!("<< /Predictor 11 /Columns {} >>", content.len());
|
||||
for (filter, decode_parms) in [
|
||||
("/FlateDecode", parms.clone()),
|
||||
("/FlateDecode", format!("[{parms}]")),
|
||||
("[/FlateDecode]", parms.clone()),
|
||||
("[/FlateDecode]", format!("[{parms}]")),
|
||||
] {
|
||||
let mut stream = format!("4 0 obj << /Filter {filter} /DecodeParms {decode_parms} /Length {} >> stream\n", coded.len()).into_bytes();
|
||||
stream.extend_from_slice(&coded);
|
||||
stream.extend_from_slice(b"\nendstream endobj");
|
||||
assert_eq!(centre(page(&stream)), [255, 0, 0, 255], "{filter} with {decode_parms}");
|
||||
}
|
||||
|
||||
let plain = format!("4 0 obj << /Length {} >> stream\n{}\nendstream endobj", content.len(), String::from_utf8_lossy(content));
|
||||
let mut doc = Document::open(Arc::new(page(plain.as_bytes()))).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes128,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [9; 32],
|
||||
})
|
||||
.unwrap();
|
||||
// No object streams: they would be encrypted under the /StmF about to go.
|
||||
let saved = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
let mut doc = Document::open(Arc::new(saved)).unwrap();
|
||||
let encrypt = doc.trailer().reference(b"Encrypt").unwrap();
|
||||
doc.update_dict(encrypt, |e| {
|
||||
e.remove(b"StmF");
|
||||
e.remove(b"StrF");
|
||||
})
|
||||
.unwrap();
|
||||
let mut doc = Document::open(Arc::new(write_incremental(&doc, &SaveOptions::default()).unwrap())).unwrap();
|
||||
let catalog = doc.get(doc.root().unwrap());
|
||||
let pages = doc.get(catalog.as_dict().unwrap().reference(b"Pages").unwrap());
|
||||
let first = doc.resolve(&pages.as_dict().unwrap().get(b"Kids").unwrap().as_array().unwrap()[0]);
|
||||
let contents = first.as_dict().unwrap().reference(b"Contents").unwrap();
|
||||
let mut named = Dict::new();
|
||||
named.set(b"Name".to_vec(), Object::name("StdCF"));
|
||||
let mut dict = Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::name("Crypt"));
|
||||
dict.set(b"DecodeParms".to_vec(), Object::Dict(named));
|
||||
doc.set(contents, Object::Stream(Stream::from_raw(dict, content.to_vec())));
|
||||
assert_eq!(centre(write_incremental(&doc, &SaveOptions::default()).unwrap()), [255, 0, 0, 255], "no /StmF, and a stream naming StdCF");
|
||||
}
|
||||
|
||||
/// A stream can pick its own crypt filter: `/Crypt` in `/Filter`, with the filter's name in
|
||||
/// `/DecodeParms`, or `/Identity` without one (ISO 32000-2 §7.4.10). The renderer used the
|
||||
/// document's filter for every stream, and then couldn't decode `/Crypt`, so the page stayed blank.
|
||||
#[test]
|
||||
fn a_stream_with_a_crypt_filter_of_its_own_is_drawn() {
|
||||
use super::*;
|
||||
use pdfcraft_cos::{Algorithm, Dict, Document, NewEncryption, Object, SaveOptions, Stream, write_full, write_incremental};
|
||||
let content = "1 0 0 rg 0 0 200 200 re f";
|
||||
let page = format!(
|
||||
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
);
|
||||
let centre = |bytes: Vec<u8>| -> [u8; 4] {
|
||||
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
let start = (100 * page.width as usize + 100) * 4;
|
||||
let mut pixel = [0; 4];
|
||||
pixel.copy_from_slice(page.rgba.get(start..start + 4).expect("centre pixel"));
|
||||
pixel
|
||||
};
|
||||
let name = |n: &str| {
|
||||
let mut parms = Dict::new();
|
||||
parms.set(b"Name".to_vec(), Object::name(n));
|
||||
Object::Dict(parms)
|
||||
};
|
||||
let hex: String = content.bytes().map(|b| format!("{b:02X}")).chain([">".to_string()]).collect();
|
||||
let crypt = || Object::name("Crypt");
|
||||
let contents_of = |doc: &Document| {
|
||||
let catalog = doc.get(doc.root().unwrap());
|
||||
let pages = doc.get(catalog.as_dict().unwrap().reference(b"Pages").unwrap());
|
||||
let page = doc.resolve(&pages.as_dict().unwrap().get(b"Kids").unwrap().as_array().unwrap()[0]);
|
||||
page.as_dict().unwrap().reference(b"Contents").unwrap()
|
||||
};
|
||||
for (algorithm, filters) in [(Algorithm::Rc4_128, false), (Algorithm::Aes128, true), (Algorithm::Aes256, true)] {
|
||||
let mut doc = Document::open(Arc::new(page.clone().into_bytes())).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [9; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let protected = write_full(&doc, &SaveOptions::default()).unwrap();
|
||||
assert_eq!(centre(protected.clone()), [255, 0, 0, 255], "{algorithm:?}: the document's own filter");
|
||||
// A filter of its own name that stores streams as they are, beside the document's.
|
||||
let mut doc = Document::open(Arc::new(protected)).unwrap();
|
||||
if filters {
|
||||
let encrypt = doc.trailer().reference(b"Encrypt").unwrap();
|
||||
doc.update_dict(encrypt, |e| {
|
||||
let Some(Object::Dict(cf)) = e.get(b"CF") else { panic!("no /CF") };
|
||||
let mut cf = cf.clone();
|
||||
let mut stored = Dict::new();
|
||||
stored.set(b"CFM".to_vec(), Object::name("None"));
|
||||
cf.set(b"Stored".to_vec(), Object::Dict(stored));
|
||||
// `/Identity` is never one of these, whatever a file says.
|
||||
let mut named = Dict::new();
|
||||
named.set(b"CFM".to_vec(), Object::name("AESV2"));
|
||||
cf.set(b"Identity".to_vec(), Object::Dict(named));
|
||||
e.set(b"CF".to_vec(), Object::Dict(cf));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let protected = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
let spellings = [
|
||||
"no name",
|
||||
"/Identity",
|
||||
"StdCF",
|
||||
"Stored",
|
||||
"/DecodeParms by reference",
|
||||
"/Filter by reference",
|
||||
"an entry of /Filter by reference",
|
||||
"an entry of /DecodeParms by reference",
|
||||
"/Name by reference",
|
||||
"before another filter",
|
||||
"a lone dictionary for an array of one",
|
||||
"an array of one for a name",
|
||||
"a dictionary by reference for an array of one",
|
||||
"an array of one by reference for a dictionary",
|
||||
"a name by reference for an array",
|
||||
"a lone dictionary before another filter",
|
||||
"past the longest chain",
|
||||
];
|
||||
for what in spellings {
|
||||
let mut doc = Document::open(Arc::new(protected.clone())).unwrap();
|
||||
let (filter, parms) = match what {
|
||||
"no name" => (crypt(), None),
|
||||
"/Identity" => (crypt(), Some(name("Identity"))),
|
||||
"StdCF" => (crypt(), Some(name("StdCF"))),
|
||||
"Stored" => (crypt(), Some(name("Stored"))),
|
||||
"/DecodeParms by reference" => (crypt(), Some(Object::Ref(doc.add(name("StdCF"))))),
|
||||
"/Filter by reference" => (Object::Ref(doc.add(crypt())), Some(name("StdCF"))),
|
||||
"an entry of /Filter by reference" => (Object::Array(vec![Object::Ref(doc.add(crypt()))]), Some(Object::Array(vec![name("StdCF")]))),
|
||||
"an entry of /DecodeParms by reference" => {
|
||||
(Object::Array(vec![crypt()]), Some(Object::Array(vec![Object::Ref(doc.add(name("StdCF")))])))
|
||||
}
|
||||
"/Name by reference" => {
|
||||
let mut parms = Dict::new();
|
||||
parms.set(b"Name".to_vec(), Object::Ref(doc.add(Object::name("StdCF"))));
|
||||
(crypt(), Some(Object::Dict(parms)))
|
||||
}
|
||||
"a lone dictionary for an array of one" => (Object::Array(vec![crypt()]), Some(name("StdCF"))),
|
||||
"an array of one for a name" => (crypt(), Some(Object::Array(vec![name("StdCF")]))),
|
||||
"a dictionary by reference for an array of one" => (Object::Array(vec![crypt()]), Some(Object::Ref(doc.add(name("StdCF"))))),
|
||||
"an array of one by reference for a dictionary" => (Object::Ref(doc.add(Object::Array(vec![crypt()]))), Some(name("StdCF"))),
|
||||
"a name by reference for an array" => (Object::Ref(doc.add(crypt())), Some(Object::Array(vec![name("StdCF")]))),
|
||||
"a lone dictionary before another filter" => (Object::Array(vec![crypt(), Object::name("ASCIIHexDecode")]), Some(name("StdCF"))),
|
||||
// One filter more than cos decodes: it names none there, so here neither, and the
|
||||
// stream is read with the document's filter, not the `Stored` it says.
|
||||
"past the longest chain" => {
|
||||
let mut chain = vec![crypt()];
|
||||
chain.extend(std::iter::repeat_n(Object::name("ASCIIHexDecode"), 9));
|
||||
(Object::Array(chain), Some(Object::Array(vec![name("Stored")])))
|
||||
}
|
||||
_ => (Object::Array(vec![crypt(), Object::name("ASCIIHexDecode")]), Some(Object::Array(vec![name("StdCF"), Object::Null]))),
|
||||
};
|
||||
let contents = contents_of(&doc);
|
||||
let Object::Stream(s) = doc.get(contents).as_ref().clone() else { panic!("no content stream") };
|
||||
let mut dict = s.dict.clone();
|
||||
dict.set(b"Filter".to_vec(), filter);
|
||||
if let Some(parms) = parms {
|
||||
dict.set(b"DecodeParms".to_vec(), parms);
|
||||
}
|
||||
let raw = match what {
|
||||
"before another filter" | "a lone dictionary before another filter" => hex.clone().into_bytes(),
|
||||
"past the longest chain" => (0..9).fold(content.as_bytes().to_vec(), |data, _| {
|
||||
let mut coded: Vec<u8> = data.iter().flat_map(|b| format!("{b:02X}").into_bytes()).collect();
|
||||
coded.push(b'>');
|
||||
coded
|
||||
}),
|
||||
_ => content.as_bytes().to_vec(),
|
||||
};
|
||||
doc.set(contents, Object::Stream(Stream::from_raw(dict, raw)));
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
assert_eq!(centre(saved), [255, 0, 0, 255], "{algorithm:?}: {what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,8 +226,8 @@ milestone = "M1"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/crypt/src/tests.rs::identity_crypt_filters_leave_data_alone"]
|
||||
notes = ""
|
||||
evidence = ["crates/crypt/src/tests.rs::identity_crypt_filters_leave_data_alone", "crates/cos/tests/it/encryption.rs::a_crypt_filter_selected_by_reference_is_used", "crates/cos/tests/it/encryption.rs::an_identity_crypt_filter_selected_by_reference_leaves_a_stream_alone", "crates/render/src/raster/tests.rs::a_stream_with_a_crypt_filter_of_its_own_is_drawn"]
|
||||
notes = "a stream can name its own filter (/Crypt in /Filter, its name in /DecodeParms), read and written, each part given directly or by reference, in cos and in the renderer; /StmF, /StrF, /EFF and /CFM by reference too. A /Filter given by reference selects the crypt filter, but Stream::decoded still refuses it, so such a stream is decrypted and not decoded outside the renderer."
|
||||
|
||||
[[feature]]
|
||||
id = "core.attachment-only-encryption"
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+22
-9
@@ -74,9 +74,11 @@ pub(crate) enum Decryptor {
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) enum DecryptionTarget {
|
||||
pub(crate) enum DecryptionTarget<'a> {
|
||||
String,
|
||||
Stream,
|
||||
/// PdfCraft patch (30): a stream that names its own crypt filter.
|
||||
NamedStream(&'a str),
|
||||
}
|
||||
|
||||
impl Decryptor {
|
||||
@@ -84,7 +86,7 @@ impl Decryptor {
|
||||
&self,
|
||||
id: ObjectIdentifier,
|
||||
data: &[u8],
|
||||
target: DecryptionTarget,
|
||||
target: DecryptionTarget<'_>,
|
||||
) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
Self::None => Some(data.to_vec()),
|
||||
@@ -93,6 +95,8 @@ impl Decryptor {
|
||||
let crypt_dict = match target {
|
||||
DecryptionTarget::String => dict.string_filter,
|
||||
DecryptionTarget::Stream => dict.stream_filter,
|
||||
// `/Identity` isn't in the table, and a name it hasn't got reads as that.
|
||||
DecryptionTarget::NamedStream(name) => dict.named.get(name).copied().unwrap_or(CryptDictionary::identity(0)),
|
||||
};
|
||||
|
||||
match crypt_dict.cfm {
|
||||
@@ -292,10 +296,12 @@ fn decrypt_rc_aes(
|
||||
with_key(final_key)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct DecryptorData {
|
||||
stream_filter: CryptDictionary,
|
||||
string_filter: CryptDictionary,
|
||||
/// PdfCraft patch (30): `/CF`, for the streams that name a filter of their own.
|
||||
named: HashMap<String, CryptDictionary>,
|
||||
}
|
||||
|
||||
impl DecryptorData {
|
||||
@@ -312,16 +318,23 @@ impl DecryptorData {
|
||||
}
|
||||
}
|
||||
|
||||
let stm_f = *mappings
|
||||
.get(dict.get::<Name<'_>>(STM_F)?.as_str())
|
||||
.unwrap_or(&CryptDictionary::identity(default_length));
|
||||
let str_f = *mappings
|
||||
.get(dict.get::<Name<'_>>(STR_F)?.as_str())
|
||||
.unwrap_or(&CryptDictionary::identity(default_length));
|
||||
// PdfCraft patch (30): `/Identity` is never an entry of `/CF`, whatever a file puts there,
|
||||
// and `/StmF` and `/StrF` are optional, naming it by default (ISO 32000-2 Table 20).
|
||||
mappings.remove("Identity");
|
||||
let identity = CryptDictionary::identity(default_length);
|
||||
let stm_f = dict
|
||||
.get::<Name<'_>>(STM_F)
|
||||
.and_then(|n| mappings.get(n.as_str()).copied())
|
||||
.unwrap_or(identity);
|
||||
let str_f = dict
|
||||
.get::<Name<'_>>(STR_F)
|
||||
.and_then(|n| mappings.get(n.as_str()).copied())
|
||||
.unwrap_or(identity);
|
||||
|
||||
Some(Self {
|
||||
stream_filter: stm_f,
|
||||
string_filter: str_f,
|
||||
named: mappings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -130,7 +130,8 @@ impl Filter {
|
||||
warn!("image decoding is not supported (enable the `images` feature)");
|
||||
Err(DecodeFailure::ImageDecode)
|
||||
}
|
||||
_ => Err(DecodeFailure::StreamDecode),
|
||||
// PdfCraft patch (30): `Stream::raw_data` decrypted the data with the filter this names.
|
||||
Self::Crypt => Ok(FilterResult::from_data(data.to_vec())),
|
||||
};
|
||||
|
||||
if res.is_err() {
|
||||
|
||||
+49
-19
@@ -5,7 +5,7 @@ use crate::filter::Filter;
|
||||
use crate::object;
|
||||
use crate::object::Dict;
|
||||
use crate::object::Name;
|
||||
use crate::object::dict::keys::{DECODE_PARMS, DP, F, FILTER, LENGTH, TYPE};
|
||||
use crate::object::dict::keys::{DECODE_PARMS, DP, F, FILTER, LENGTH, NAME, TYPE};
|
||||
use crate::object::{Array, ObjectIdentifier};
|
||||
use crate::object::{Object, ObjectLike, ObjectRefLike};
|
||||
use crate::reader::Reader;
|
||||
@@ -13,6 +13,7 @@ use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable};
|
||||
use crate::trivia::is_white_space_character;
|
||||
use crate::util::{OptionLog, find_needle};
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt::{Debug, Formatter};
|
||||
use smallvec::SmallVec;
|
||||
@@ -73,38 +74,44 @@ impl<'a> Stream<'a> {
|
||||
let mut collected_filters = SmallVec::new();
|
||||
let mut collected_params = SmallVec::new();
|
||||
|
||||
// PdfCraft patch (30): `/DecodeParms` in either form, whichever form `/Filter` has: one filter
|
||||
// in an array with a lone dictionary is an everyday spelling, and a lone dictionary is the
|
||||
// first filter's. Read as the chain asks, so a long array costs no more than a short one.
|
||||
let parms = self
|
||||
.dict
|
||||
.get::<Object<'_>>(DP)
|
||||
.or_else(|| self.dict.get::<Object<'_>>(DECODE_PARMS));
|
||||
let mut in_array = match &parms {
|
||||
Some(Object::Array(a)) => Some(a.iter::<Object<'_>>()),
|
||||
_ => None,
|
||||
};
|
||||
let mut lone = match parms {
|
||||
Some(Object::Dict(d)) => Some(d),
|
||||
_ => None,
|
||||
};
|
||||
let mut next_params = || {
|
||||
lone.take()
|
||||
.or_else(|| in_array.as_mut()?.next()?.into_dict())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
if let Some(filter) = self
|
||||
.dict
|
||||
.get::<Name<'_>>(F)
|
||||
.or_else(|| self.dict.get::<Name<'_>>(FILTER))
|
||||
.and_then(Filter::from_name)
|
||||
{
|
||||
let params = self
|
||||
.dict
|
||||
.get::<Dict<'_>>(DP)
|
||||
.or_else(|| self.dict.get::<Dict<'_>>(DECODE_PARMS))
|
||||
.unwrap_or_default();
|
||||
|
||||
collected_filters.push(filter);
|
||||
collected_params.push(params);
|
||||
collected_params.push(next_params());
|
||||
} else if let Some(filters) = self
|
||||
.dict
|
||||
.get::<Array<'_>>(F)
|
||||
.or_else(|| self.dict.get::<Array<'_>>(FILTER))
|
||||
{
|
||||
let filters = filters.iter::<Name<'_>>().map(Filter::from_name);
|
||||
let mut params = self
|
||||
.dict
|
||||
.get::<Array<'_>>(DP)
|
||||
.or_else(|| self.dict.get::<Array<'_>>(DECODE_PARMS))
|
||||
.map(|a| a.iter::<Object<'_>>());
|
||||
|
||||
for filter in filters {
|
||||
let params = params
|
||||
.as_mut()
|
||||
.and_then(|p| p.next())
|
||||
.and_then(|p| p.into_dict())
|
||||
.unwrap_or_default();
|
||||
let params = next_params();
|
||||
|
||||
if let Some(filter) = filter {
|
||||
collected_filters.push(filter);
|
||||
@@ -119,6 +126,20 @@ impl<'a> Stream<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (30): the crypt filter the stream names for itself: `/Crypt` in its chain, its
|
||||
/// name in that filter's parameters, or `/Identity` without one (ISO 32000-2 §7.4.10).
|
||||
fn crypt_filter(&self) -> Option<String> {
|
||||
let FiltersAndParams { filters, params } = self.filters_and_params();
|
||||
// A chain longer than cos decodes names none there, so it names none here either:
|
||||
// `pdfcraft_filters::MAX_CHAIN`, and `/Crypt`.
|
||||
if filters.len() > 9 {
|
||||
return None;
|
||||
}
|
||||
let i = filters.iter().position(|f| matches!(f, Filter::Crypt))?;
|
||||
let name = params.get(i).and_then(|p| p.get::<Name<'_>>(NAME));
|
||||
Some(name.map_or_else(|| "Identity".to_string(), |n| n.as_str().to_string()))
|
||||
}
|
||||
|
||||
/// Return the raw, decrypted data of the stream.
|
||||
///
|
||||
/// Stream filters will not be applied.
|
||||
@@ -132,9 +153,13 @@ impl<'a> Stream<'a> {
|
||||
.map(|t| t.as_ref() != b"XRef")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let own = self.crypt_filter();
|
||||
let target = own
|
||||
.as_deref()
|
||||
.map_or(DecryptionTarget::Stream, DecryptionTarget::NamedStream);
|
||||
Cow::Owned(
|
||||
ctx.xref()
|
||||
.decrypt(self.obj_id(), self.data, DecryptionTarget::Stream)
|
||||
.decrypt(self.obj_id(), self.data, target)
|
||||
// TODO: MAybe an error would be better?
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
@@ -241,6 +266,11 @@ impl<'a> Stream<'a> {
|
||||
.zip(filters_and_params.params.iter())
|
||||
.enumerate()
|
||||
{
|
||||
// PdfCraft patch (30): `raw_data` decrypted the data, so there's nothing to undo, and a
|
||||
// chain can name this thousands of times.
|
||||
if matches!(filter, Filter::Crypt) {
|
||||
continue;
|
||||
}
|
||||
// PdfCraft patch: see `filter::decode_limit`.
|
||||
let limit = crate::filter::decode_limit(image_params, i + 1 == count).min(max);
|
||||
let new = filter.apply(
|
||||
|
||||
Vendored
+1
-1
@@ -651,7 +651,7 @@ impl XRef {
|
||||
&self,
|
||||
id: ObjectIdentifier,
|
||||
data: &[u8],
|
||||
target: DecryptionTarget,
|
||||
target: DecryptionTarget<'_>,
|
||||
) -> Option<Vec<u8>> {
|
||||
match &self.0 {
|
||||
Inner::Dummy => Some(data.to_vec()),
|
||||
|
||||
Reference in New Issue
Block a user