//! The PDF object model (ISO 32000-2 §7.3). //! //! Dictionaries keep their key order so rewritten objects stay recognisable in diffs and //! byte-stable when re-serialized. Streams keep their *encoded* bytes; decoding is on demand. use pdfcraft_filters::{Filter, Params}; use crate::{Bytes, CosError}; /// An indirect reference: object number and generation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ObjRef { pub num: u32, pub generation: u16, } impl ObjRef { pub const fn new(num: u32, generation: u16) -> Self { Self { num, generation } } } impl std::fmt::Display for ObjRef { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} {} R", self.num, self.generation) } } /// A string object. `hex` records how it was written, so it is re-emitted the same way. #[derive(Clone, Debug, PartialEq)] pub struct PdfString { pub bytes: Vec, pub hex: bool, } impl PdfString { pub fn literal(bytes: impl Into>) -> Self { Self { bytes: bytes.into(), hex: false } } /// A text string (ISO 32000-2 §7.9.2.2): PDFDocEncoding when possible, else UTF-16BE with BOM. pub fn text(s: &str) -> Self { if s.chars().all(|c| (' '..='~').contains(&c) || c == '\n' || c == '\r' || c == '\t') { return Self::literal(s.as_bytes().to_vec()); } let mut bytes = vec![0xFE, 0xFF]; for u in s.encode_utf16() { bytes.extend_from_slice(&u.to_be_bytes()); } Self { bytes, hex: false } } /// Decode a text string: UTF-16BE (BOM), UTF-8 (BOM, PDF 2.0) or PDFDocEncoding. /// /// Lenient beyond the spec, as readers are: invalid sequences after a BOM decode lossily /// instead of dropping the string, a BOM-less string whose bytes are valid UTF-8 with /// non-ASCII bytes (written by some producers) is read as UTF-8 rather than as /// PDFDocEncoding mojibake, and stray leading U+FEFF (doubled BOMs) are removed. pub fn to_text(&self) -> String { let s = match self.bytes.as_slice() { [0xFE, 0xFF, rest @ ..] => { let units: Vec = rest.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect(); String::from_utf16_lossy(&units) } [0xEF, 0xBB, 0xBF, rest @ ..] => String::from_utf8_lossy(rest).into_owned(), b => match std::str::from_utf8(b) { // Pure ASCII is identical either way; only non-ASCII UTF-8 needs the switch. Ok(utf8) if !b.is_ascii() => utf8.to_owned(), _ => b.iter().map(|&c| pdfcraft_crypt::pdfdoc_char(c)).collect(), }, }; if s.starts_with('\u{FEFF}') { s.trim_start_matches('\u{FEFF}').to_owned() } else { s } } } /// A name object (without the leading `/`, with `#xx` escapes already decoded). pub type Name = Vec; #[cfg(test)] thread_local! { /// Key comparisons and index lookups made by [`Dict`] on this thread. static KEY_COMPARES: std::cell::Cell = const { std::cell::Cell::new(0) }; } /// Entries from which a [`Dict`] keeps a key index. Smaller dictionaries (nearly all of them) /// search their entries; a parsed file controls the key count, and a linear search per /// inserted key would make a dictionary of many distinct keys quadratic to build. const INDEX_FROM: usize = 32; /// An ordered dictionary. #[derive(Clone, Default)] pub struct Dict { entries: Vec<(Name, Object)>, /// Key to position in `entries`, kept once the dictionary reaches [`INDEX_FROM`] entries. /// Boxed so that every `Dict`, and so every `Object`, grows by one pointer, not by a map. #[allow(clippy::box_collection)] index: Option>>, } impl PartialEq for Dict { fn eq(&self, other: &Self) -> bool { self.entries == other.entries } } impl std::fmt::Debug for Dict { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Dict").field("entries", &self.entries).finish() } } impl Dict { pub fn new() -> Self { Self::default() } fn position(&self, key: &[u8]) -> Option { if let Some(index) = &self.index { #[cfg(test)] KEY_COMPARES.with(|c| c.set(c.get() + 1)); return index.get(key).copied(); } self.entries.iter().position(|(k, _)| { #[cfg(test)] KEY_COMPARES.with(|c| c.set(c.get() + 1)); k == key }) } pub fn get(&self, key: &[u8]) -> Option<&Object> { self.position(key).and_then(|i| self.entries.get(i)).map(|(_, v)| v) } pub fn get_mut(&mut self, key: &[u8]) -> Option<&mut Object> { let i = self.position(key)?; self.entries.get_mut(i).map(|(_, v)| v) } /// Insert or replace, keeping the position of an existing key. pub fn set(&mut self, key: impl Into, value: impl Into) { let key = key.into(); let value = value.into(); match self.position(&key).and_then(|i| self.entries.get_mut(i)) { Some((_, v)) => *v = value, None => { let at = self.entries.len(); if let Some(index) = &mut self.index { index.insert(key.clone(), at); } else if at + 1 >= INDEX_FROM { let index = self.entries.iter().enumerate().map(|(i, (k, _))| (k.clone(), i)).chain([(key.clone(), at)]).collect(); self.index = Some(Box::new(index)); } self.entries.push((key, value)); } } } pub fn remove(&mut self, key: &[u8]) -> Option { let i = self.position(key)?; if let Some(index) = &mut self.index { index.remove(key); index.values_mut().filter(|at| **at > i).for_each(|at| *at -= 1); } Some(self.entries.remove(i).1) } pub fn contains(&self, key: &[u8]) -> bool { self.get(key).is_some() } pub fn iter(&self) -> impl Iterator { self.entries.iter().map(|(k, v)| (k, v)) } pub fn len(&self) -> usize { self.entries.len() } pub fn is_empty(&self) -> bool { self.entries.is_empty() } pub fn name(&self, key: &[u8]) -> Option<&[u8]> { self.get(key).and_then(Object::as_name) } pub fn int(&self, key: &[u8]) -> Option { self.get(key).and_then(Object::as_int) } pub fn reference(&self, key: &[u8]) -> Option { self.get(key).and_then(Object::as_ref) } } impl FromIterator<(Name, Object)> for Dict { fn from_iter>(iter: T) -> Self { let mut d = Dict::new(); for (k, v) in iter { d.set(k, v); } d } } /// A stream: dictionary plus its data *as stored* (still filtered). #[derive(Clone, Debug, PartialEq)] pub struct Stream { pub dict: Dict, pub raw: Bytes, } /// Upper bound for a single decoded stream (decompression-bomb defence). pub const MAX_DECODED: usize = 1 << 30; impl Stream { /// A new stream from already-encoded bytes (`/Length` is set at write time). pub fn from_raw(dict: Dict, raw: Vec) -> Self { Self { dict, raw: raw.into() } } /// A new stream holding `data` compressed with Flate. pub fn flate(mut dict: Dict, data: &[u8]) -> Self { dict.set(b"Filter".to_vec(), Object::Name(b"FlateDecode".to_vec())); dict.remove(b"DecodeParms"); Self { dict, raw: pdfcraft_filters::encode_flate(data).into() } } /// The filter chain declared in the dictionary. pub fn filters(&self) -> Vec<(Filter, Params)> { let names: Vec<&[u8]> = match self.dict.get(b"Filter") { Some(Object::Name(n)) => vec![n], Some(Object::Array(a)) => a.iter().filter_map(Object::as_name).collect(), _ => Vec::new(), }; let parms: Vec> = match self.dict.get(b"DecodeParms") { Some(Object::Dict(d)) => vec![Some(d)], Some(Object::Array(a)) => a.iter().map(Object::as_dict).collect(), _ => Vec::new(), }; names .into_iter() .enumerate() // `/Crypt` is applied by the security handler when the object is loaded (§7.4.10). .filter(|(_, n)| *n != b"Crypt") .map(|(i, n)| { let p = parms.get(i).copied().flatten(); let get = |k: &[u8], d: i64| p.and_then(|p| p.int(k)).unwrap_or(d); let params = Params { predictor: get(b"Predictor", 1), colors: get(b"Colors", 1), bits_per_component: get(b"BitsPerComponent", 8), columns: get(b"Columns", 1), early_change: get(b"EarlyChange", 1), }; (Filter::from_name(n), params) }) .collect() } /// [`Stream::filters`], or an error when `/Filter` is present but is not a name or an /// array of names (an indirect reference, say): the data is not unfiltered, so it must not /// read as decoded. fn readable_filters(&self) -> Result, pdfcraft_filters::FilterError> { let readable = match self.dict.get(b"Filter") { None | Some(Object::Null | Object::Name(_)) => true, Some(Object::Array(a)) => a.iter().all(|f| f.as_name().is_some()), Some(_) => false, }; if !readable { return Err(pdfcraft_filters::FilterError::Unsupported("/Filter is not a name or an array of names".into())); } Ok(self.filters()) } /// Decoded data, tolerating truncated or corrupt encodings the way viewers do (the data /// decoded before the damage is returned). Streams with image codecs fail with /// `CosError::Filter` (keep them encoded). Use `decoded_strict` to detect damage. pub fn decoded(&self) -> Result, CosError> { self.decoded_within(MAX_DECODED) } /// Like [`Stream::decoded`], for a stream whose decoded size has a tighter bound than /// [`MAX_DECODED`]: decoding past `max` bytes is an error. pub fn decoded_within(&self, max: usize) -> Result, CosError> { let decode = |chain: &[(Filter, Params)], data: &[u8], max| { pdfcraft_filters::decode_tolerant(chain, data, max).map(|(v, _)| (v, 0)).map_err(|e| (e, 0)) }; self.decode_chain(max, decode).map(|(v, _)| v).map_err(|(e, _)| CosError::Filter(e.to_string())) } /// [`Stream::decoded_within`] for a caller charging an allowance: the filter error is kept /// typed, so a caller can tell a spent limit from corrupt data, `max` bounds all filter /// stages together, and the result gives the bytes they decoded (the work done, on success /// and on failure). pub(crate) fn decoded_within_counted(&self, max: usize) -> Result<(Vec, usize), (pdfcraft_filters::FilterError, usize)> { let decode = |chain: &[(Filter, Params)], data: &[u8], max| { pdfcraft_filters::decode_tolerant_counted(chain, data, max).map(|(v, _, spent)| (v, spent)) }; self.decode_chain(max, decode) } /// The stream decoded through its filters by `decode`, or copied when it has none, within /// `max` and [`MAX_DECODED`]. fn decode_chain( &self, max: usize, decode: impl FnOnce(&[(Filter, Params)], &[u8], usize) -> Result<(Vec, usize), (pdfcraft_filters::FilterError, usize)>, ) -> Result<(Vec, usize), (pdfcraft_filters::FilterError, usize)> { let max = max.min(MAX_DECODED); let chain = self.readable_filters().map_err(|e| (e, 0))?; if chain.is_empty() { return self .raw_within_typed(max) .map(|v| { let n = v.len(); (v, n) }) .map_err(|e| (e, 0)); } decode(&chain, &self.raw, max) } /// Decoded data, bounded by [`MAX_DECODED`]; any corruption is an error. pub fn decoded_strict(&self) -> Result, CosError> { let decode = |chain: &[(Filter, Params)], data: &[u8], max| pdfcraft_filters::decode(chain, data, max).map(|v| (v, 0)).map_err(|e| (e, 0)); self.decode_chain(MAX_DECODED, decode).map(|(v, _)| v).map_err(|(e, _)| CosError::Filter(e.to_string())) } /// [`Stream::decoded_strict`] bounded by `max` and counted like /// [`Stream::decoded_within_counted`]. pub(crate) fn decoded_strict_within_counted(&self, max: usize) -> Result<(Vec, usize), (pdfcraft_filters::FilterError, usize)> { self.decode_chain(max, pdfcraft_filters::decode_counted) } /// Data after the first filter alone, bounded by `max`: the rest of the chain stays encoded. /// Any corruption in that first filter is an error. pub fn decoded_first_filter_strict(&self, max: usize) -> Result, CosError> { let mut chain = self.readable_filters().map_err(|e| CosError::Filter(e.to_string()))?; chain.truncate(1); if chain.is_empty() { return self.raw_within(max); } pdfcraft_filters::decode(&chain, &self.raw, max.min(MAX_DECODED)).map_err(|e| CosError::Filter(e.to_string())) } fn raw_within(&self, max: usize) -> Result, CosError> { self.raw_within_typed(max).map_err(|e| CosError::Filter(e.to_string())) } fn raw_within_typed(&self, max: usize) -> Result, pdfcraft_filters::FilterError> { let max = max.min(MAX_DECODED); // An empty decoder chain still produces an owned output buffer: check its limit // before cloning, including streams whose /Crypt filter was applied on load. if self.raw.len() > max { return Err(pdfcraft_filters::FilterError::LimitExceeded(max)); } Ok(self.raw.to_vec()) } } #[derive(Clone, Debug, PartialEq)] pub enum Object { Null, Bool(bool), Int(i64), Real(f64), String(PdfString), Name(Name), Array(Vec), Dict(Dict), Stream(Stream), Ref(ObjRef), } impl Object { pub fn name(n: &str) -> Self { Object::Name(n.as_bytes().to_vec()) } pub fn as_int(&self) -> Option { match self { Object::Int(i) => Some(*i), Object::Real(r) if r.fract() == 0.0 && r.is_finite() => Some(*r as i64), _ => None, } } pub fn as_f64(&self) -> Option { match self { Object::Int(i) => Some(*i as f64), Object::Real(r) => Some(*r), _ => None, } } pub fn as_name(&self) -> Option<&[u8]> { match self { Object::Name(n) => Some(n), _ => None, } } pub fn as_ref(&self) -> Option { match self { Object::Ref(r) => Some(*r), _ => None, } } pub fn as_dict(&self) -> Option<&Dict> { match self { Object::Dict(d) => Some(d), Object::Stream(s) => Some(&s.dict), _ => None, } } pub fn as_dict_mut(&mut self) -> Option<&mut Dict> { match self { Object::Dict(d) => Some(d), Object::Stream(s) => Some(&mut s.dict), _ => None, } } pub fn as_array(&self) -> Option<&Vec> { match self { Object::Array(a) => Some(a), _ => None, } } pub fn as_string(&self) -> Option<&PdfString> { match self { Object::String(s) => Some(s), _ => None, } } } impl From for Object { fn from(v: i64) -> Self { Object::Int(v) } } impl From for Object { fn from(v: i32) -> Self { Object::Int(v as i64) } } impl From for Object { fn from(v: f64) -> Self { Object::Real(v) } } impl From for Object { fn from(v: bool) -> Self { Object::Bool(v) } } impl From for Object { fn from(v: ObjRef) -> Self { Object::Ref(v) } } impl From for Object { fn from(v: Dict) -> Self { Object::Dict(v) } } impl From> for Object { fn from(v: Vec) -> Self { Object::Array(v) } } impl From for Object { fn from(v: PdfString) -> Self { Object::String(v) } } impl From for Object { fn from(v: Stream) -> Self { Object::Stream(v) } } #[cfg(test)] mod tests { use super::*; #[test] fn a_filter_that_is_not_a_direct_name_is_not_decoded_as_absent() { let stream = |filter: Object| { let mut d = Dict::new(); d.set(b"Filter".to_vec(), filter); Stream::from_raw(d, b"48 69>".to_vec()) }; let hex = Object::Name(b"ASCIIHexDecode".to_vec()); assert_eq!(stream(hex.clone()).decoded_strict().ok(), Some(b"Hi".to_vec())); assert_eq!(stream(Object::Array(vec![hex.clone()])).decoded().ok(), Some(b"Hi".to_vec())); assert_eq!(stream(Object::Null).decoded_strict().ok(), Some(b"48 69>".to_vec())); for filter in [ Object::Ref(ObjRef::new(5, 0)), Object::Array(vec![Object::Ref(ObjRef::new(5, 0))]), Object::Int(3), Object::Array(vec![hex, Object::Int(1)]), ] { let s = stream(filter.clone()); assert!(s.decoded_strict().is_err(), "{filter:?}"); assert!(s.decoded().is_err(), "{filter:?}"); assert!(s.decoded_first_filter_strict(1 << 20).is_err(), "{filter:?}"); } } #[test] fn parsing_many_distinct_keys_is_not_quadratic() { let n = 20_000; let body: String = (0..n).map(|i| format!("/K{i} {i} ")).collect(); let text = format!("<< {body}/K7 true >>"); KEY_COMPARES.with(|c| c.set(0)); let Ok(Object::Dict(d)) = crate::parser::Lexer::new(text.as_bytes(), 0).object() else { panic!("not a dict") }; let compares = KEY_COMPARES.with(|c| c.get()); assert!(compares < 8 * n, "{compares} key comparisons for {n} keys"); // Order and replace-in-place semantics stay those of a plain ordered dictionary. assert_eq!(d.len(), n); assert_eq!(d.iter().next().map(|(k, _)| k.as_slice()), Some(&b"K0"[..])); assert_eq!(d.get(b"K7"), Some(&Object::Bool(true))); assert_eq!(d.iter().nth(7).map(|(_, v)| v), Some(&Object::Bool(true))); let mut d = d; assert_eq!(d.remove(b"K3"), Some(Object::Int(3))); assert_eq!(d.get(b"K4"), Some(&Object::Int(4))); assert_eq!(d.iter().nth(3).map(|(k, _)| k.as_slice()), Some(&b"K4"[..])); d.set(b"K3".to_vec(), Object::Null); assert_eq!(d.iter().last().map(|(k, _)| k.as_slice()), Some(&b"K3"[..])); assert!(d.get(b"K20000").is_none()); } #[test] fn text_strings_round_trip() { for s in ["Plain ASCII", "Café — naïve", "日本語", "emoji 📄"] { assert_eq!(PdfString::text(s).to_text(), s); } assert!(!PdfString::text("abc").bytes.starts_with(&[0xFE, 0xFF])); } #[test] fn text_string_encodings() { // UTF-16BE with BOM. let mut utf16 = vec![0xFE, 0xFF]; utf16.extend("第一章 概述".encode_utf16().flat_map(u16::to_be_bytes)); assert_eq!(PdfString::literal(utf16).to_text(), "第一章 概述"); // UTF-8 with BOM (PDF 2.0): the BOM is not part of the text. let mut utf8 = vec![0xEF, 0xBB, 0xBF]; utf8.extend_from_slice("第一章".as_bytes()); assert_eq!(PdfString::literal(utf8).to_text(), "第一章"); // Invalid UTF-8 after the BOM decodes lossily instead of vanishing. assert_eq!(PdfString::literal(vec![0xEF, 0xBB, 0xBF, b'A', 0xFF, b'B']).to_text(), "A\u{FFFD}B"); // BOM-less raw UTF-8 from producers that skip the BOM. assert_eq!(PdfString::literal("目录".as_bytes()).to_text(), "目录"); // PDFDocEncoding Latin-1 (not valid UTF-8) stays PDFDocEncoding. assert_eq!(PdfString::literal(vec![b'C', b'a', b'f', 0xE9]).to_text(), "Café"); // Doubled BOMs leave no U+FEFF behind. assert_eq!(PdfString::literal(vec![0xFE, 0xFF, 0xFE, 0xFF, 0x00, b'A']).to_text(), "A"); assert_eq!(PdfString::literal(vec![0xEF, 0xBB, 0xBF, 0xEF, 0xBB, 0xBF, b'A']).to_text(), "A"); // Odd trailing byte and empty strings never panic. assert_eq!(PdfString::literal(vec![0xFE, 0xFF, 0x00, b'A', 0x00]).to_text(), "A"); assert_eq!(PdfString::literal(vec![0xFE, 0xFF]).to_text(), ""); assert_eq!(PdfString::literal(Vec::new()).to_text(), ""); } #[test] fn pdfdoc_encoding_high_range() { assert_eq!(PdfString::literal(vec![0x84, 0x92, 0xA0, b'A']).to_text(), "—™€A"); } #[test] fn dict_preserves_order_and_replaces_in_place() { let mut d = Dict::new(); d.set(b"B".to_vec(), 1); d.set(b"A".to_vec(), 2); d.set(b"B".to_vec(), 3); let keys: Vec<_> = d.iter().map(|(k, _)| k.clone()).collect(); assert_eq!(keys, vec![b"B".to_vec(), b"A".to_vec()]); assert_eq!(d.int(b"B"), Some(3)); } #[test] fn raw_streams_refuse_decoding_past_a_caller_limit() { let stream = Stream::from_raw(Dict::new(), b"abc".to_vec()); for max in [0, 2] { assert!(matches!(stream.decoded_within(max), Err(CosError::Filter(_)))); } } #[test] fn empty_filter_arrays_refuse_decoding_past_a_caller_limit() { let mut dict = Dict::new(); dict.set(b"Filter".to_vec(), Object::Array(Vec::new())); let stream = Stream::from_raw(dict, b"abc".to_vec()); assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); } #[test] fn crypt_only_streams_refuse_decoding_past_a_caller_limit() { let mut dict = Dict::new(); dict.set(b"Filter".to_vec(), Object::name("Crypt")); let mut parms = Dict::new(); parms.set(b"Name".to_vec(), Object::name("Identity")); dict.set(b"DecodeParms".to_vec(), Object::Dict(parms)); let stream = Stream::from_raw(dict, b"abc".to_vec()); assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); } #[test] fn identity_streams_preserve_bytes_within_a_caller_limit() { let mut array = Dict::new(); array.set(b"Filter".to_vec(), Object::Array(Vec::new())); let mut crypt = Dict::new(); crypt.set(b"Filter".to_vec(), Object::name("Crypt")); for dict in [Dict::new(), array, crypt] { let stream = Stream::from_raw(dict.clone(), b"abc".to_vec()); assert_eq!(stream.decoded_within(3).unwrap(), b"abc"); assert_eq!(stream.decoded_within(usize::MAX).unwrap(), b"abc"); assert_eq!(stream.decoded().unwrap(), b"abc"); assert_eq!(stream.decoded_strict().unwrap(), b"abc"); assert!(Stream::from_raw(dict, Vec::new()).decoded_within(0).unwrap().is_empty()); } } #[test] fn filtered_streams_preserve_the_caller_limit() { let stream = Stream::flate(Dict::new(), b"abc"); assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); assert_eq!(stream.decoded_within(3).unwrap(), b"abc"); assert_eq!(stream.decoded_strict().unwrap(), b"abc"); } #[test] fn raw_flate_fallback_streams_preserve_the_caller_limit() { // Raw deflate with a stored byte and a final empty block. 78 01 also // passes the zlib header check, so decoding must retry the whole input. let mut dict = Dict::new(); dict.set(b"Filter".to_vec(), Object::name("FlateDecode")); let stream = Stream::from_raw(dict, vec![0x78, 0x01, 0x00, 0xfe, 0xff, b'A', 0x01, 0x00, 0x00, 0xff, 0xff]); let result = stream.decoded_within(0); assert!(matches!(&result, Err(CosError::Filter(detail)) if detail.contains("exceeds the limit of 0 bytes")), "{result:?}"); assert_eq!(stream.decoded_within(1).unwrap(), b"A"); assert_eq!(stream.decoded_strict().unwrap(), b"A"); } #[test] fn first_filter_decodes_only_the_outer_wrapper() { let data = "0 0 m 100 100 l S\n".repeat(64).into_bytes(); let inner = Stream::flate(Dict::new(), &data).raw.to_vec(); for (outer, name, corrupt) in [(Filter::Ascii85, "ASCII85Decode", &b"{~>"[..]), (Filter::AsciiHex, "ASCIIHexDecode", &b"G0>"[..])] { let encoded = pdfcraft_filters::encode(&outer, &Params::default(), &inner).unwrap(); let mut dict = Dict::new(); dict.set(b"Filter".to_vec(), Object::Array(vec![Object::name(name), Object::name("FlateDecode")])); let stream = Stream::from_raw(dict.clone(), encoded); assert_eq!(stream.decoded_first_filter_strict(usize::MAX).unwrap(), inner); assert_eq!(stream.decoded_strict().unwrap(), data); assert!(matches!(stream.decoded_first_filter_strict(inner.len() - 1), Err(CosError::Filter(_)))); assert!(matches!(Stream::from_raw(dict, corrupt.to_vec()).decoded_first_filter_strict(usize::MAX), Err(CosError::Filter(_)))); } } }