Skip to main content

pdfrum_object/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4// Every byte in this crate came from an untrusted file: index with `get()`.
5#![warn(clippy::indexing_slicing)]
6
7mod array;
8mod dict;
9mod error;
10mod name;
11pub mod names;
12mod number;
13mod object;
14mod resolve;
15mod stream;
16mod string;
17
18#[cfg(test)]
19mod test_resolve;
20
21pub use array::Array;
22pub use dict::Dict;
23pub use error::Error;
24pub use name::{Name, name_decode, name_encode};
25pub use number::{
26    INT_RANGE, fmt_int, fmt_number, narrow_to_signed32, truncate_to_signed32, widen_to_f32,
27};
28pub use object::{ObjRef, Object};
29pub use resolve::{NoResolve, Resolve, Resolved};
30pub use stream::{ByteSpan, Stream};
31pub use string::{
32    PDF_DOC_ENCODING, PdfString, StringSyntax, decode_text, encode_string_hex,
33    encode_string_literal, encode_text,
34};
35
36#[cfg(test)]
37mod tests {
38    use crate::test_resolve::TestStore;
39    use crate::{Array, ByteSpan, Dict, Name, NoResolve, ObjRef, Object, PdfString, Stream, names};
40
41    fn every_variant() -> Dict {
42        Dict::from_pairs([
43            (Name::from("null"), Object::Null),
44            (Name::from("bool"), Object::Bool(true)),
45            (Name::from("int"), Object::Int(1245)),
46            (Name::from("real"), Object::Real(9.003_45)),
47            (
48                Name::from("str"),
49                Object::Str(PdfString::literal(b"A simple test")),
50            ),
51            (
52                Name::from("hexstr"),
53                Object::Str(PdfString::hex(b"\x12\xAC")),
54            ),
55            (Name::from("name"), Object::Name(Name::from("space"))),
56            (
57                Name::from("array"),
58                Object::Array(Array::of([
59                    Object::Int(8902),
60                    Object::Name(Name::from("address")),
61                ])),
62            ),
63            (
64                Name::from("dict"),
65                Object::Dict(Dict::from_pairs([(Name::from("k"), Object::Int(1))])),
66            ),
67            (Name::from("ref"), Object::Ref(ObjRef::new(7, 0))),
68        ])
69    }
70
71    // From cpdf_object_unittest.cpp:210-236: the GetString table, restated.
72    #[test]
73    fn byte_string_spelling_per_type() {
74        let dict = every_variant();
75        let spell = |k: &str| {
76            dict.byte_string(&Name::from(k), &NoResolve)
77                .unwrap_or_default()
78        };
79        assert_eq!(spell("bool"), b"true");
80        assert_eq!(spell("int"), b"1245");
81        assert_eq!(spell("real"), b"9.00345");
82        assert_eq!(spell("str"), b"A simple test");
83        assert_eq!(spell("name"), b"space");
84        // Composites and null have no spelling.
85        assert_eq!(spell("null"), b"");
86        assert_eq!(spell("array"), b"");
87        assert_eq!(spell("dict"), b"");
88    }
89
90    // From cpdf_object_unittest.cpp:227-240: the GetUnicodeText table.
91    #[test]
92    fn text_reading_per_type() {
93        let dict = every_variant();
94        let text = |k: &str| dict.text(&Name::from(k), &NoResolve).unwrap_or_default();
95        assert_eq!(text("str"), "A simple test");
96        assert_eq!(text("name"), "space");
97        assert_eq!(text("bool"), "");
98        assert_eq!(text("int"), "");
99        assert_eq!(text("real"), "");
100        assert_eq!(text("array"), "");
101        assert_eq!(text("dict"), "");
102        assert_eq!(text("null"), "");
103    }
104
105    // From cpdf_object_unittest.cpp:242-272: GetNumber and GetInteger.
106    #[test]
107    fn numeric_readings_per_type() {
108        let dict = every_variant();
109        let num = |k: &str| dict.number(&Name::from(k), &NoResolve);
110        let int = |k: &str| dict.int(&Name::from(k), &NoResolve);
111
112        assert_eq!(num("int"), Some(1245.0));
113        assert_eq!(num("real"), Some(9.003_45));
114        // Booleans are not numbers...
115        assert_eq!(num("bool"), None);
116        assert_eq!(num("str"), None);
117        assert_eq!(num("name"), None);
118        assert_eq!(num("null"), None);
119
120        // ...but do have an integer reading.
121        assert_eq!(int("bool"), Some(1));
122        assert_eq!(int("int"), Some(1245));
123        assert_eq!(int("real"), Some(9));
124        assert_eq!(int("str"), None);
125        assert_eq!(int("name"), None);
126        assert_eq!(int("null"), None);
127    }
128
129    #[test]
130    fn stream_reads_as_its_own_dictionary_and_keeps_its_bytes() {
131        let dict = Dict::from_pairs([(names::LENGTH.clone(), Object::Int(3))]);
132        let stream = Object::Stream(Box::new(Stream::new(
133            dict.clone(),
134            ByteSpan::from(b"abc".to_vec()),
135        )));
136        assert_eq!(stream.as_dict(), Some(&dict));
137        assert_eq!(stream.as_stream().map(|s| &*s.data), Some(&b"abc"[..]));
138        assert_eq!(stream.to_byte_string(), b"");
139        assert_eq!(stream.number(), None);
140    }
141
142    // From cpdf_object_unittest.cpp:845-862 and :947-1013 — clone_direct
143    // flattens references and drops the edges that would close a cycle.
144    #[test]
145    fn clone_direct_flattens_references() {
146        let store = TestStore::from_pairs([(7, Object::Int(42))]);
147        let obj = Object::Array(Array::of([Object::Ref(ObjRef::new(7, 0)), Object::Int(1)]));
148        assert_eq!(
149            obj.clone_direct(&store),
150            Object::Array(Array::of([Object::Int(42), Object::Int(1)]))
151        );
152    }
153
154    #[test]
155    fn clone_direct_cuts_self_referential_edges() {
156        // Object 1 is an array whose only element points back at object 1.
157        let store = TestStore::from_pairs([(
158            1,
159            Object::Array(Array::of([Object::Ref(ObjRef::new(1, 0))])),
160        )]);
161        let root = Object::Ref(ObjRef::new(1, 0));
162        // The cycle edge disappears; the array survives, empty.
163        assert_eq!(root.clone_direct(&store), Object::Array(Array::new()));
164    }
165
166    #[test]
167    fn clone_direct_cuts_a_dictionary_stream_loop() {
168        // A stream whose dictionary refers back to the stream's own object.
169        let inner = Dict::from_pairs([(Name::from("Self"), Object::Ref(ObjRef::new(2, 0)))]);
170        let store = TestStore::from_pairs([(
171            2,
172            Object::Stream(Box::new(Stream::new(
173                inner,
174                ByteSpan::from(b"data".to_vec()),
175            ))),
176        )]);
177        let cloned = Object::Ref(ObjRef::new(2, 0)).clone_direct(&store);
178        let stream = cloned.as_stream().expect("still a stream");
179        assert!(stream.dict.is_empty(), "the looping key was dropped");
180        assert_eq!(&*stream.data, b"data");
181    }
182
183    #[test]
184    fn clone_direct_keeps_shared_substructure_for_siblings() {
185        // Two siblings pointing at the same object is not a cycle.
186        let store = TestStore::from_pairs([(3, Object::Int(5))]);
187        let obj = Object::Array(Array::of([
188            Object::Ref(ObjRef::new(3, 0)),
189            Object::Ref(ObjRef::new(3, 0)),
190        ]));
191        assert_eq!(
192            obj.clone_direct(&store),
193            Object::Array(Array::of([Object::Int(5), Object::Int(5)]))
194        );
195    }
196
197    #[test]
198    fn clone_direct_drops_dangling_references() {
199        let store = TestStore::default();
200        let obj = Object::Dict(Dict::from_pairs([
201            (Name::from("gone"), Object::Ref(ObjRef::new(9, 0))),
202            (Name::from("here"), Object::Int(1)),
203        ]));
204        assert_eq!(
205            obj.clone_direct(&store),
206            Object::Dict(Dict::from_pairs([(Name::from("here"), Object::Int(1))]))
207        );
208    }
209
210    // The shape that made `clone_direct` panic on ordinary corpus files: a
211    // `/Resources` whose `/XObject` entries are indirect streams. Flattening
212    // stores each stream as a *direct* dictionary value, which is exactly
213    // what `CPDF_Dictionary::CloneNonCyclic` does — its loop inserts into
214    // `map_` directly and so never reaches the `CHECK(!IsStream())` that
215    // guards the ordinary setters. §7.3.8.1 binds the writer, not this type.
216    #[test]
217    fn clone_direct_stores_a_resolved_stream_as_a_direct_dict_value() {
218        let image = Stream::new(
219            Dict::from_pairs([
220                (names::TYPE.clone(), Object::Name(Name::from("XObject"))),
221                (names::SUBTYPE.clone(), Object::Name(Name::from("Image"))),
222                (names::LENGTH.clone(), Object::Int(4)),
223            ]),
224            ByteSpan::from(b"\xDE\xAD\xBE\xEF".to_vec()),
225        );
226        let store = TestStore::from_pairs([(9, Object::Stream(Box::new(image.clone())))]);
227
228        let resources = Object::Dict(Dict::from_pairs([(
229            Name::from("XObject"),
230            Object::Dict(Dict::from_pairs([(
231                Name::from("Image9"),
232                Object::Ref(ObjRef::new(9, 0)),
233            )])),
234        )]));
235
236        let cloned = resources.clone_direct(&store);
237        let xobject = cloned
238            .as_dict()
239            .and_then(|d| d.raw(&Name::from("XObject")))
240            .and_then(Object::as_dict)
241            .expect("the /XObject sub-dictionary survives");
242        // Stored directly, not as a reference and not dropped.
243        assert_eq!(
244            xobject.raw(&Name::from("Image9")),
245            Some(&Object::Stream(Box::new(image.clone())))
246        );
247        // And the resolving accessor reads it back, as `GetStreamFor` does.
248        assert_eq!(
249            xobject.stream(&Name::from("Image9"), &NoResolve),
250            Some(image)
251        );
252    }
253
254    #[test]
255    fn clone_direct_stores_a_resolved_stream_as_a_direct_array_element() {
256        let stream = Stream::new(
257            Dict::from_pairs([(names::LENGTH.clone(), Object::Int(2))]),
258            ByteSpan::from(b"hi".to_vec()),
259        );
260        let store = TestStore::from_pairs([(4, Object::Stream(Box::new(stream.clone())))]);
261        let array = Object::Array(Array::of([Object::Ref(ObjRef::new(4, 0))]));
262
263        let cloned = array.clone_direct(&store);
264        let cloned = cloned.as_array().expect("still an array");
265        assert_eq!(
266            cloned.raw_at(0),
267            Some(&Object::Stream(Box::new(stream.clone())))
268        );
269        assert_eq!(cloned.stream_at(0, &NoResolve), Some(stream));
270    }
271
272    // The cloned stream carries its **raw**, still-encoded bytes, matching
273    // `CPDF_Stream::CloneNonCyclic`'s `LoadAllDataRaw()` — no filter is run
274    // and `/Filter` stays in the cloned dictionary.
275    #[test]
276    fn clone_direct_keeps_a_streams_raw_bytes_and_filter() {
277        let stream = Stream::new(
278            Dict::from_pairs([
279                (
280                    names::FILTER.clone(),
281                    Object::Name(Name::from("FlateDecode")),
282                ),
283                (names::LENGTH.clone(), Object::Int(3)),
284            ]),
285            ByteSpan::from(b"\x78\x9C\x03".to_vec()),
286        );
287        let store = TestStore::from_pairs([(1, Object::Stream(Box::new(stream)))]);
288
289        let cloned = Object::Ref(ObjRef::new(1, 0)).clone_direct(&store);
290        let cloned = cloned.as_stream().expect("a stream");
291        assert_eq!(&*cloned.data, b"\x78\x9C\x03");
292        assert_eq!(
293            cloned.dict.name(names::FILTER).map(Name::as_str),
294            Some(Some("FlateDecode"))
295        );
296    }
297
298    // A stream reachable by two sibling paths is not a cycle: both clone.
299    #[test]
300    fn clone_direct_clones_a_shared_stream_down_both_sibling_paths() {
301        let stream = Stream::new(Dict::new(), ByteSpan::from(b"xy".to_vec()));
302        let store = TestStore::from_pairs([(6, Object::Stream(Box::new(stream.clone())))]);
303        let dict = Object::Dict(Dict::from_pairs([
304            (Name::from("a"), Object::Ref(ObjRef::new(6, 0))),
305            (Name::from("b"), Object::Ref(ObjRef::new(6, 0))),
306        ]));
307
308        assert_eq!(
309            dict.clone_direct(&store),
310            Object::Dict(Dict::from_pairs([
311                (Name::from("a"), Object::Stream(Box::new(stream.clone()))),
312                (Name::from("b"), Object::Stream(Box::new(stream))),
313            ]))
314        );
315    }
316
317    // A stream whose own dictionary points back at the dictionary holding it
318    // still cuts only the cycle edge — the stream itself survives inline.
319    #[test]
320    fn clone_direct_cuts_only_the_cycle_edge_of_an_inlined_stream() {
321        let inner = Stream::new(
322            Dict::from_pairs([(Name::from("Up"), Object::Ref(ObjRef::new(1, 0)))]),
323            ByteSpan::from(b"body".to_vec()),
324        );
325        let store = TestStore::from_pairs([
326            (
327                1,
328                Object::Dict(Dict::from_pairs([(
329                    Name::from("Down"),
330                    Object::Ref(ObjRef::new(2, 0)),
331                )])),
332            ),
333            (2, Object::Stream(Box::new(inner))),
334        ]);
335
336        let cloned = Object::Ref(ObjRef::new(1, 0)).clone_direct(&store);
337        let down = cloned
338            .as_dict()
339            .and_then(|d| d.raw(&Name::from("Down")))
340            .and_then(Object::as_stream)
341            .expect("the stream is stored directly under /Down");
342        assert_eq!(&*down.data, b"body");
343        assert!(down.dict.is_empty(), "the /Up cycle edge was cut");
344    }
345
346    // Containers accept a stream value in memory: the file-format rule of
347    // §7.3.8.1 is the writer's to enforce, not this type's.
348    #[test]
349    fn containers_accept_a_stream_value_in_memory() {
350        let stream = Object::Stream(Box::new(Stream::new(
351            Dict::new(),
352            ByteSpan::from(b"z".to_vec()),
353        )));
354        let mut dict = Dict::new();
355        dict.push(Name::from("S"), stream.clone());
356        assert!(dict.stream(&Name::from("S"), &NoResolve).is_some());
357
358        let mut array = Array::new();
359        array.push(stream);
360        assert!(array.stream_at(0, &NoResolve).is_some());
361    }
362
363    #[test]
364    fn plain_clone_keeps_references_as_references() {
365        let obj = every_variant();
366        let copy = obj.clone();
367        assert_eq!(obj, copy);
368        assert_eq!(copy.reference(&Name::from("ref")), Some(ObjRef::new(7, 0)));
369    }
370
371    #[test]
372    fn objects_are_send_and_sync() {
373        const fn assert_send_sync<T: Send + Sync>() {}
374        assert_send_sync::<Object>();
375        assert_send_sync::<Dict>();
376        assert_send_sync::<Array>();
377        assert_send_sync::<Stream>();
378        assert_send_sync::<ByteSpan>();
379        assert_send_sync::<Name>();
380        assert_send_sync::<PdfString>();
381    }
382
383    #[test]
384    fn reference_identity_is_by_number_and_generation() {
385        assert_eq!(ObjRef::new(1, 0), ObjRef::new(1, 0));
386        assert_ne!(ObjRef::new(1, 0), ObjRef::new(1, 1));
387        // The sentinel is private, so the test names the value the *file*
388        // would contain rather than importing a constant to compare against —
389        // which is the whole point of keeping it private.
390        assert!(ObjRef::new(0xFFFF_FFFF, 0).is_invalid());
391        assert!(!ObjRef::new(1, 0).is_invalid());
392    }
393
394    #[test]
395    fn debug_dump_of_a_composite_tree_is_readable() {
396        // Guards against an accidental representation change: every variant
397        // shows its payload, and a stream shows its length rather than its
398        // bytes.
399        let dump = format!("{:?}", every_variant());
400        for expected in [
401            "/null",
402            "Null",
403            "Bool(true)",
404            "Int(1245)",
405            "Real(9.00345)",
406            "(A simple test)",
407            "<12AC>",
408            "Name(/space)",
409            "Ref(ObjRef { num: 7, generation: 0 })",
410        ] {
411            assert!(dump.contains(expected), "missing {expected} in {dump}");
412        }
413        let stream = Stream::new(Dict::new(), ByteSpan::from(vec![0u8; 4096]));
414        let stream_dump = format!("{stream:?}");
415        assert!(stream_dump.contains("len: 4096"), "{stream_dump}");
416        assert!(stream_dump.len() < 200, "a stream dump stays short");
417    }
418}