pdfrum_parser/decode.rs
1//! Getting a stream's real bytes.
2//!
3//! The decoding itself lives in `pdfrum-filters`, including the fallback
4//! ladder that turns every failure into the undecoded bytes. This module is
5//! only the parser's side of that boundary: two places inside the reader —
6//! cross-reference streams and object streams — need decoded bytes before
7//! anything else can happen, and consumers above need them on demand.
8
9use pdfrum_common::{Diagnostics, Limits};
10use pdfrum_object::{Resolve, Stream};
11
12/// A stream's data with its filters applied.
13///
14/// Never fails: a stream whose filter chain is unusable yields its raw bytes
15/// and a diagnostic, which is what makes a damaged file's content still
16/// render. A chain ending in an image codec yields the codec's *input*; the
17/// image path takes it from there.
18///
19/// ```
20/// use pdfrum_common::{Diagnostics, Limits};
21/// use pdfrum_object::{ByteSpan, Dict, Name, NoResolve, Object, Stream, names};
22/// use pdfrum_parser::decoded_stream;
23///
24/// let dict = Dict::from_pairs([(
25/// names::FILTER.clone(),
26/// Object::Name(Name::from("ASCIIHexDecode")),
27/// )]);
28/// let stream = Stream::new(dict, ByteSpan::from(b"48656c6c6f>".to_vec()));
29/// let mut diags = Diagnostics::default();
30/// let bytes = decoded_stream(&stream, &NoResolve, &Limits::default(), &mut diags);
31/// assert_eq!(bytes, b"Hello");
32/// ```
33#[must_use]
34pub fn decoded_stream(
35 stream: &Stream,
36 r: &impl Resolve,
37 limits: &Limits,
38 diags: &mut Diagnostics,
39) -> Vec<u8> {
40 decoded_bytes(stream, r, limits, diags)
41}
42
43/// The same, over a `?Sized` resolver so the store can call it behind a
44/// reference.
45pub(crate) fn decoded_bytes<R: Resolve + ?Sized>(
46 stream: &Stream,
47 r: &R,
48 limits: &Limits,
49 diags: &mut Diagnostics,
50) -> Vec<u8> {
51 pdfrum_filters::decode_chain(stream, 0, &r, limits, diags).data
52}
53
54/// Decoded bytes for a stream the *reader itself* has to understand — a
55/// cross-reference stream or an object stream.
56///
57/// Unlike a content stream, these are useless unless the whole chain
58/// produced real bytes. A chain that stops at an image codec has handed back
59/// that codec's input rather than the fields the reader is looking for, so
60/// this answers `None` and the caller falls through to its next repair
61/// instead of reading pixels as offsets.
62pub(crate) fn structural_bytes<R: Resolve + ?Sized>(
63 stream: &Stream,
64 r: &R,
65 limits: &Limits,
66 diags: &mut Diagnostics,
67) -> Option<Vec<u8>> {
68 let decoded = pdfrum_filters::decode_chain(stream, 0, &r, limits, diags);
69 decoded.image.is_none().then_some(decoded.data)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::decoded_stream;
75 use pdfrum_common::{DiagKind, Diagnostics, Limits};
76 use pdfrum_object::{ByteSpan, Dict, Name, NoResolve, Object, Stream, names};
77
78 #[test]
79 fn an_unfiltered_stream_is_its_own_data() {
80 let stream = Stream::new(Dict::new(), ByteSpan::from(b"raw".to_vec()));
81 let mut diags = Diagnostics::default();
82 assert_eq!(
83 decoded_stream(&stream, &NoResolve, &Limits::default(), &mut diags),
84 b"raw"
85 );
86 }
87
88 #[test]
89 fn an_unusable_filter_declaration_yields_the_raw_bytes() {
90 let dict = Dict::from_pairs([(names::FILTER.clone(), Object::Int(7))]);
91 let stream = Stream::new(dict, ByteSpan::from(b"raw".to_vec()));
92 let mut diags = Diagnostics::default();
93 assert_eq!(
94 decoded_stream(&stream, &NoResolve, &Limits::default(), &mut diags),
95 b"raw"
96 );
97 assert!(diags.contains(&DiagKind::UndecodableStream));
98 }
99
100 #[test]
101 fn a_hex_filter_decodes() {
102 let dict = Dict::from_pairs([(
103 names::FILTER.clone(),
104 Object::Name(Name::from("ASCIIHexDecode")),
105 )]);
106 let stream = Stream::new(dict, ByteSpan::from(b"414243>".to_vec()));
107 let mut diags = Diagnostics::default();
108 assert_eq!(
109 decoded_stream(&stream, &NoResolve, &Limits::default(), &mut diags),
110 b"ABC"
111 );
112 }
113}