Files
pdfcraft/crates/cos/src/object.rs
T

668 lines
25 KiB
Rust

//! 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<u8>,
pub hex: bool,
}
impl PdfString {
pub fn literal(bytes: impl Into<Vec<u8>>) -> 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<u16> = 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<u8>;
#[cfg(test)]
thread_local! {
/// Key comparisons and index lookups made by [`Dict`] on this thread.
static KEY_COMPARES: std::cell::Cell<usize> = 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<Box<std::collections::HashMap<Name, usize>>>,
}
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<usize> {
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<Name>, value: impl Into<Object>) {
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<Object> {
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<Item = (&Name, &Object)> {
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<i64> {
self.get(key).and_then(Object::as_int)
}
pub fn reference(&self, key: &[u8]) -> Option<ObjRef> {
self.get(key).and_then(Object::as_ref)
}
}
impl FromIterator<(Name, Object)> for Dict {
fn from_iter<T: IntoIterator<Item = (Name, Object)>>(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<u8>) -> 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<Option<&Dict>> = 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<Vec<(Filter, Params)>, 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<Vec<u8>, 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<Vec<u8>, 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<u8>, 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<u8>, usize), (pdfcraft_filters::FilterError, usize)>,
) -> Result<(Vec<u8>, 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<Vec<u8>, 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<u8>, 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<Vec<u8>, 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<Vec<u8>, CosError> {
self.raw_within_typed(max).map_err(|e| CosError::Filter(e.to_string()))
}
fn raw_within_typed(&self, max: usize) -> Result<Vec<u8>, 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<Object>),
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<i64> {
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<f64> {
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<ObjRef> {
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<Object>> {
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<i64> for Object {
fn from(v: i64) -> Self {
Object::Int(v)
}
}
impl From<i32> for Object {
fn from(v: i32) -> Self {
Object::Int(v as i64)
}
}
impl From<f64> for Object {
fn from(v: f64) -> Self {
Object::Real(v)
}
}
impl From<bool> for Object {
fn from(v: bool) -> Self {
Object::Bool(v)
}
}
impl From<ObjRef> for Object {
fn from(v: ObjRef) -> Self {
Object::Ref(v)
}
}
impl From<Dict> for Object {
fn from(v: Dict) -> Self {
Object::Dict(v)
}
}
impl From<Vec<Object>> for Object {
fn from(v: Vec<Object>) -> Self {
Object::Array(v)
}
}
impl From<PdfString> for Object {
fn from(v: PdfString) -> Self {
Object::String(v)
}
}
impl From<Stream> 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(_))));
}
}
}