Skip to main content

rpdfium_parser/
object.rs

1// Derived from PDFium's cpdf_object.h / cpdf_object.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! PDF object model — ObjectId, StreamData, and the Object enum.
7
8use std::collections::HashMap;
9
10pub use rpdfium_core::error::ObjectId;
11use rpdfium_core::{Name, PdfString};
12
13/// Represents the raw (undecoded) data of a PDF stream.
14///
15/// Stream data is not read or decoded at parse time. Only the byte range
16/// within the source data is recorded for later on-demand decoding.
17#[derive(Debug, Clone)]
18pub enum StreamData {
19    /// Undecoded: byte range within source data.
20    Raw { offset: u64, length: u64 },
21    /// Already decoded/generated data (used by editing operations).
22    Decoded { data: Vec<u8> },
23}
24
25/// A PDF object value.
26///
27/// `Object::Reference` holds an unresolved indirect reference — it is **not**
28/// recursively resolved at parse time to avoid `OnceLock` deadlocks.
29#[derive(Debug, Clone)]
30pub enum Object {
31    /// The PDF null object.
32    Null,
33    /// A boolean value.
34    Boolean(bool),
35    /// An integer number.
36    Integer(i64),
37    /// A real (floating-point) number.
38    Real(f64),
39    /// A PDF string (encoding-aware).
40    String(PdfString),
41    /// A PDF name.
42    Name(Name),
43    /// An array of objects.
44    Array(Vec<Object>),
45    /// A dictionary mapping names to objects.
46    Dictionary(HashMap<Name, Object>),
47    /// A stream object with dictionary metadata and raw data reference.
48    Stream {
49        dict: HashMap<Name, Object>,
50        data: StreamData,
51    },
52    /// An unresolved indirect reference to another object.
53    Reference(ObjectId),
54}
55
56impl Object {
57    /// Returns the boolean value if this is `Object::Boolean`.
58    pub fn as_bool(&self) -> Option<bool> {
59        match self {
60            Object::Boolean(b) => Some(*b),
61            _ => None,
62        }
63    }
64
65    /// ADR-019 alias for [`as_bool()`](Self::as_bool).
66    ///
67    /// Corresponds to `CPDF_Object::GetBooleanValue`.
68    #[inline]
69    pub fn as_boolean(&self) -> Option<bool> {
70        self.as_bool()
71    }
72
73    /// Returns the integer value if this is `Object::Integer`.
74    pub fn as_i64(&self) -> Option<i64> {
75        match self {
76            Object::Integer(n) => Some(*n),
77            _ => None,
78        }
79    }
80
81    /// Returns the floating-point value if this is `Object::Real` or `Object::Integer`.
82    pub fn as_f64(&self) -> Option<f64> {
83        match self {
84            Object::Real(f) => Some(*f),
85            Object::Integer(n) => Some(*n as f64),
86            _ => None,
87        }
88    }
89
90    /// Returns a reference to the `PdfString` if this is `Object::String`.
91    pub fn as_string(&self) -> Option<&PdfString> {
92        match self {
93            Object::String(s) => Some(s),
94            _ => None,
95        }
96    }
97
98    /// Returns a reference to the `Name` if this is `Object::Name`.
99    pub fn as_name(&self) -> Option<&Name> {
100        match self {
101            Object::Name(n) => Some(n),
102            _ => None,
103        }
104    }
105
106    /// Returns a reference to the array if this is `Object::Array`.
107    pub fn as_array(&self) -> Option<&[Object]> {
108        match self {
109            Object::Array(a) => Some(a),
110            _ => None,
111        }
112    }
113
114    /// Returns a reference to the dictionary if this is `Object::Dictionary`.
115    pub fn as_dict(&self) -> Option<&HashMap<Name, Object>> {
116        match self {
117            Object::Dictionary(d) => Some(d),
118            _ => None,
119        }
120    }
121
122    /// ADR-019 alias for [`as_dict()`](Self::as_dict).
123    ///
124    /// Corresponds to `CPDF_Object::AsDictionary`.
125    #[inline]
126    pub fn as_dictionary(&self) -> Option<&HashMap<Name, Object>> {
127        self.as_dict()
128    }
129
130    /// Returns a reference to the stream dictionary if this is `Object::Stream`.
131    pub fn as_stream_dict(&self) -> Option<&HashMap<Name, Object>> {
132        match self {
133            Object::Stream { dict, .. } => Some(dict),
134            _ => None,
135        }
136    }
137
138    /// Returns the `ObjectId` if this is `Object::Reference`.
139    pub fn as_reference(&self) -> Option<ObjectId> {
140        match self {
141            Object::Reference(id) => Some(*id),
142            _ => None,
143        }
144    }
145
146    /// Returns a mutable reference to the dictionary if this is `Object::Dictionary`.
147    pub fn as_dict_mut(&mut self) -> Option<&mut HashMap<Name, Object>> {
148        match self {
149            Object::Dictionary(d) => Some(d),
150            _ => None,
151        }
152    }
153
154    /// Returns a mutable reference to the array if this is `Object::Array`.
155    pub fn as_array_mut(&mut self) -> Option<&mut Vec<Object>> {
156        match self {
157            Object::Array(a) => Some(a),
158            _ => None,
159        }
160    }
161
162    /// Returns `true` if this is `Object::Null`.
163    pub fn is_null(&self) -> bool {
164        matches!(self, Object::Null)
165    }
166
167    /// Returns `true` if this is `Object::Reference`.
168    pub fn is_reference(&self) -> bool {
169        matches!(self, Object::Reference(_))
170    }
171
172    /// Returns `true` if this is `Object::Boolean`.
173    ///
174    /// Corresponds to `CPDF_Object::IsBoolean()` in PDFium.
175    pub fn is_boolean(&self) -> bool {
176        matches!(self, Object::Boolean(_))
177    }
178
179    /// Returns `true` if this is `Object::Integer` or `Object::Real`.
180    ///
181    /// Corresponds to `CPDF_Object::IsNumber()` in PDFium.
182    pub fn is_number(&self) -> bool {
183        matches!(self, Object::Integer(_) | Object::Real(_))
184    }
185
186    /// Returns `true` if this is `Object::String`.
187    ///
188    /// Corresponds to `CPDF_Object::IsString()` in PDFium.
189    pub fn is_string(&self) -> bool {
190        matches!(self, Object::String(_))
191    }
192
193    /// Returns `true` if this is `Object::Name`.
194    ///
195    /// Corresponds to `CPDF_Object::IsName()` in PDFium.
196    pub fn is_name(&self) -> bool {
197        matches!(self, Object::Name(_))
198    }
199
200    /// Returns `true` if this is `Object::Array`.
201    ///
202    /// Corresponds to `CPDF_Object::IsArray()` in PDFium.
203    pub fn is_array(&self) -> bool {
204        matches!(self, Object::Array(_))
205    }
206
207    /// Returns `true` if this is `Object::Dictionary`.
208    ///
209    /// Corresponds to `CPDF_Object::IsDictionary()` in PDFium.
210    pub fn is_dictionary(&self) -> bool {
211        matches!(self, Object::Dictionary(_))
212    }
213
214    /// Returns `true` if this is `Object::Stream`.
215    ///
216    /// Corresponds to `CPDF_Object::IsStream()` in PDFium.
217    pub fn is_stream(&self) -> bool {
218        matches!(self, Object::Stream { .. })
219    }
220
221    /// Upstream-aligned alias for [`Self::as_i64()`].
222    ///
223    /// Returns the integer value if this is `Object::Integer`.
224    /// Corresponds to `CPDF_Object::GetInteger()` in PDFium.
225    #[inline]
226    pub fn get_integer(&self) -> Option<i64> {
227        self.as_i64()
228    }
229
230    /// Upstream-aligned alias for [`Self::as_f64()`].
231    ///
232    /// Returns the numeric value as `f64` for `Object::Real` or `Object::Integer`.
233    /// Corresponds to `CPDF_Object::GetNumber()` in PDFium.
234    #[inline]
235    pub fn get_number(&self) -> Option<f64> {
236        self.as_f64()
237    }
238
239    /// Upstream-aligned alias for [`Self::as_string()`].
240    ///
241    /// Returns a reference to the `PdfString` if this is `Object::String`.
242    /// Corresponds to `CPDF_Object::GetString()` in PDFium.
243    #[inline]
244    pub fn get_string(&self) -> Option<&PdfString> {
245        self.as_string()
246    }
247
248    /// Returns the string value decoded to a UTF-8 `String`, if this is a
249    /// `Object::String` or `Object::Name`.
250    ///
251    /// Corresponds to `CPDF_Object::GetUnicodeText()` in PDFium.
252    pub fn unicode_text(&self) -> Option<String> {
253        match self {
254            Object::String(s) => Some(s.to_string_lossy()),
255            Object::Name(n) => Some(n.as_str().to_string()),
256            _ => None,
257        }
258    }
259
260    /// Upstream-aligned alias for [`Self::unicode_text()`].
261    ///
262    /// Corresponds to `CPDF_Object::GetUnicodeText()` in PDFium.
263    #[inline]
264    pub fn get_unicode_text(&self) -> Option<String> {
265        self.unicode_text()
266    }
267
268    // -----------------------------------------------------------------------
269    // CPDF_Number-level methods
270    // -----------------------------------------------------------------------
271
272    /// Returns `true` if this is `Object::Integer` (a whole number, not real).
273    ///
274    /// Corresponds to `CPDF_Number::IsInteger()` in PDFium.
275    pub fn is_integer(&self) -> bool {
276        matches!(self, Object::Integer(_))
277    }
278
279    // -----------------------------------------------------------------------
280    // CPDF_Reference-level methods
281    // -----------------------------------------------------------------------
282
283    /// Returns the object number of the indirect reference if this is
284    /// `Object::Reference`.
285    ///
286    /// Corresponds to `CPDF_Reference::GetRefObjNum()` in PDFium.
287    pub fn ref_obj_num(&self) -> Option<u32> {
288        match self {
289            Object::Reference(id) => Some(id.number),
290            _ => None,
291        }
292    }
293
294    /// Upstream-aligned alias for [`Self::ref_obj_num()`].
295    ///
296    /// Corresponds to `CPDF_Reference::GetRefObjNum()` in PDFium.
297    #[inline]
298    pub fn get_ref_obj_num(&self) -> Option<u32> {
299        self.ref_obj_num()
300    }
301
302    // -----------------------------------------------------------------------
303    // CPDF_Stream-level methods
304    // -----------------------------------------------------------------------
305
306    /// Returns the raw (encoded) byte size of the stream data if this is
307    /// `Object::Stream`.
308    ///
309    /// - For `StreamData::Raw`, returns the `length` stored at parse time.
310    /// - For `StreamData::Decoded`, returns the byte length of the in-memory buffer.
311    ///
312    /// Corresponds to `CPDF_Stream::GetRawSize()` in PDFium.
313    pub fn raw_size(&self) -> Option<u64> {
314        match self {
315            Object::Stream { data, .. } => match data {
316                StreamData::Raw { length, .. } => Some(*length),
317                StreamData::Decoded { data } => Some(data.len() as u64),
318            },
319            _ => None,
320        }
321    }
322
323    /// Upstream-aligned alias for [`Self::raw_size()`].
324    ///
325    /// Corresponds to `CPDF_Stream::GetRawSize()` in PDFium.
326    #[inline]
327    pub fn get_raw_size(&self) -> Option<u64> {
328        self.raw_size()
329    }
330
331    /// Returns `true` if this stream has a `/Filter` entry in its dictionary.
332    ///
333    /// Corresponds to `CPDF_Stream::HasFilter()` in PDFium.
334    pub fn has_filter(&self) -> bool {
335        match self {
336            Object::Stream { dict, .. } => dict.contains_key(&Name::filter()),
337            _ => false,
338        }
339    }
340
341    // -----------------------------------------------------------------------
342    // CPDF_Array convenience methods
343    // -----------------------------------------------------------------------
344
345    /// Returns the number of elements if this is `Object::Array`, otherwise `None`.
346    ///
347    /// Corresponds to `CPDF_Array::size()` in PDFium.
348    pub fn array_len(&self) -> Option<usize> {
349        match self {
350            Object::Array(a) => Some(a.len()),
351            _ => None,
352        }
353    }
354
355    /// Variant name for [`Self::array_len()`].
356    ///
357    /// Corresponds to `CPDF_Array::size()` in PDFium, but the exact snake_case
358    /// of that method is `size()`, which would be ambiguous on the unified `Object`
359    /// type. Use [`array_len()`](Self::array_len) instead.
360    #[inline]
361    #[deprecated(since = "0.0.0", note = "use `array_len()`")]
362    pub fn array_size(&self) -> Option<usize> {
363        self.array_len()
364    }
365
366    /// Returns `true` if this is an `Object::Array` with no elements.
367    ///
368    /// Corresponds to `CPDF_Array::IsEmpty()` in PDFium.
369    pub fn array_is_empty(&self) -> bool {
370        match self {
371            Object::Array(a) => a.is_empty(),
372            _ => false,
373        }
374    }
375
376    // -----------------------------------------------------------------------
377    // CPDF_Dictionary convenience methods
378    // -----------------------------------------------------------------------
379
380    /// Returns the number of key–value pairs if this is `Object::Dictionary`,
381    /// otherwise `None`.
382    ///
383    /// Corresponds to `CPDF_Dictionary::size()` in PDFium.
384    pub fn dict_len(&self) -> Option<usize> {
385        match self {
386            Object::Dictionary(d) => Some(d.len()),
387            _ => None,
388        }
389    }
390
391    /// Variant name for [`Self::dict_len()`].
392    ///
393    /// Corresponds to `CPDF_Dictionary::size()` in PDFium, but the exact snake_case
394    /// of that method is `size()`, which would be ambiguous on the unified `Object`
395    /// type. Use [`dict_len()`](Self::dict_len) instead.
396    #[inline]
397    #[deprecated(since = "0.0.0", note = "use `dict_len()`")]
398    pub fn dict_size(&self) -> Option<usize> {
399        self.dict_len()
400    }
401
402    /// Returns `true` if the dictionary contains `key`.
403    ///
404    /// Returns `false` for non-dictionary objects.
405    ///
406    /// Corresponds to `CPDF_Dictionary::KeyExist()` in PDFium.
407    pub fn key_exist(&self, key: &Name) -> bool {
408        match self {
409            Object::Dictionary(d) => d.contains_key(key),
410            _ => false,
411        }
412    }
413
414    /// Returns the list of keys if this is `Object::Dictionary`, otherwise `None`.
415    ///
416    /// Corresponds to `CPDF_Dictionary::GetKeys()` in PDFium.
417    pub fn keys(&self) -> Option<Vec<&Name>> {
418        match self {
419            Object::Dictionary(d) => Some(d.keys().collect()),
420            _ => None,
421        }
422    }
423
424    /// Upstream-aligned alias for [`Self::keys()`].
425    ///
426    /// Corresponds to `CPDF_Dictionary::GetKeys()` in PDFium.
427    #[inline]
428    pub fn get_keys(&self) -> Option<Vec<&Name>> {
429        self.keys()
430    }
431
432    // -----------------------------------------------------------------------
433    // CPDF_Object::GetDict — unified dict accessor
434    // -----------------------------------------------------------------------
435
436    /// Returns the dictionary of this object:
437    /// - If `Object::Dictionary`, returns the dictionary.
438    /// - If `Object::Stream`, returns the stream dictionary.
439    /// - Otherwise returns `None`.
440    ///
441    /// Corresponds to `CPDF_Object::GetDict()` in PDFium.
442    pub fn dict(&self) -> Option<&HashMap<Name, Object>> {
443        match self {
444            Object::Dictionary(d) => Some(d),
445            Object::Stream { dict, .. } => Some(dict),
446            _ => None,
447        }
448    }
449
450    /// Upstream-aligned alias for [`Self::dict()`].
451    ///
452    /// Corresponds to `CPDF_Object::GetDict()` in PDFium.
453    #[inline]
454    pub fn get_dict(&self) -> Option<&HashMap<Name, Object>> {
455        self.dict()
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn test_object_id_display_format() {
465        let id = ObjectId::new(42, 0);
466        assert_eq!(format!("{}", id), "42 0 R");
467    }
468
469    #[test]
470    fn test_object_id_equality() {
471        let a = ObjectId::new(1, 0);
472        let b = ObjectId::new(1, 0);
473        let c = ObjectId::new(1, 1);
474        assert_eq!(a, b);
475        assert_ne!(a, c);
476    }
477
478    #[test]
479    fn test_object_null() {
480        let obj = Object::Null;
481        assert!(obj.is_null());
482        assert_eq!(obj.as_bool(), None);
483        assert_eq!(obj.as_i64(), None);
484    }
485
486    #[test]
487    fn test_object_boolean() {
488        let obj = Object::Boolean(true);
489        assert_eq!(obj.as_bool(), Some(true));
490        assert_eq!(obj.as_i64(), None);
491    }
492
493    #[test]
494    fn test_object_integer() {
495        let obj = Object::Integer(42);
496        assert_eq!(obj.as_i64(), Some(42));
497        assert_eq!(obj.as_f64(), Some(42.0));
498        assert_eq!(obj.as_bool(), None);
499    }
500
501    #[test]
502    #[allow(clippy::approx_constant)]
503    fn test_object_real() {
504        let obj = Object::Real(3.14);
505        assert_eq!(obj.as_f64(), Some(3.14));
506        assert_eq!(obj.as_i64(), None);
507    }
508
509    #[test]
510    fn test_object_string() {
511        let obj = Object::String(PdfString::from_bytes(b"hello".to_vec()));
512        assert!(obj.as_string().is_some());
513        assert_eq!(obj.as_string().unwrap().as_bytes(), b"hello");
514    }
515
516    #[test]
517    fn test_object_name() {
518        let obj = Object::Name(Name::from_bytes(b"Type".to_vec()));
519        assert!(obj.as_name().is_some());
520    }
521
522    #[test]
523    fn test_object_array() {
524        let obj = Object::Array(vec![Object::Integer(1), Object::Integer(2)]);
525        assert_eq!(obj.as_array().unwrap().len(), 2);
526    }
527
528    #[test]
529    fn test_object_dictionary() {
530        let mut dict = HashMap::new();
531        dict.insert(
532            Name::r#type(),
533            Object::Name(Name::from_bytes(b"Catalog".to_vec())),
534        );
535        let obj = Object::Dictionary(dict);
536        assert!(obj.as_dict().is_some());
537    }
538
539    #[test]
540    fn test_object_stream() {
541        let dict = HashMap::new();
542        let data = StreamData::Raw {
543            offset: 0,
544            length: 100,
545        };
546        let obj = Object::Stream { dict, data };
547        assert!(obj.as_stream_dict().is_some());
548    }
549
550    #[test]
551    fn test_object_reference() {
552        let id = ObjectId::new(5, 0);
553        let obj = Object::Reference(id);
554        assert!(obj.is_reference());
555        assert_eq!(obj.as_reference(), Some(id));
556    }
557
558    #[test]
559    fn test_object_unicode_text() {
560        let str_obj = Object::String(PdfString::from_bytes(b"hello".to_vec()));
561        assert_eq!(str_obj.unicode_text(), Some("hello".to_string()));
562        assert_eq!(str_obj.get_unicode_text(), Some("hello".to_string()));
563
564        let name_obj = Object::Name(Name::from("Foo"));
565        assert_eq!(name_obj.unicode_text(), Some("Foo".to_string()));
566
567        let int_obj = Object::Integer(42);
568        assert_eq!(int_obj.unicode_text(), None);
569    }
570
571    #[test]
572    fn test_object_get_aliases_delegate_to_as_methods() {
573        let int_obj = Object::Integer(99);
574        assert_eq!(int_obj.get_integer(), Some(99));
575        assert_eq!(int_obj.get_number(), Some(99.0));
576        assert_eq!(int_obj.get_string(), None);
577        assert_eq!(int_obj.as_name(), None);
578
579        let real_obj = Object::Real(3.14);
580        assert_eq!(real_obj.get_number(), Some(3.14));
581        assert_eq!(real_obj.get_integer(), None);
582
583        let str_obj = Object::String(PdfString::from_bytes(b"hello".to_vec()));
584        assert!(str_obj.get_string().is_some());
585        assert_eq!(str_obj.get_integer(), None);
586
587        let name_obj = Object::Name(Name::from("Foo"));
588        assert!(name_obj.as_name().is_some());
589        assert_eq!(name_obj.get_string(), None);
590    }
591
592    // ------------------------------------------------------------------
593    // Tests for new methods added in ADR-019 audit pass
594    // ------------------------------------------------------------------
595
596    #[test]
597    fn test_is_integer_distinguishes_integer_from_real() {
598        // Only Object::Integer → is_integer() == true
599        assert!(Object::Integer(5).is_integer());
600        assert!(!Object::Real(5.0).is_integer());
601        assert!(!Object::Null.is_integer());
602        assert!(!Object::Boolean(true).is_integer());
603    }
604
605    #[test]
606    fn test_ref_obj_num_and_alias() {
607        let id = ObjectId::new(7, 0);
608        let obj = Object::Reference(id);
609        assert_eq!(obj.ref_obj_num(), Some(7));
610        assert_eq!(obj.get_ref_obj_num(), Some(7));
611
612        // Non-reference → None
613        assert_eq!(Object::Integer(1).ref_obj_num(), None);
614        assert_eq!(Object::Null.get_ref_obj_num(), None);
615    }
616
617    #[test]
618    fn test_raw_size_and_alias() {
619        // Raw stream
620        let raw_stream = Object::Stream {
621            dict: HashMap::new(),
622            data: StreamData::Raw {
623                offset: 0,
624                length: 42,
625            },
626        };
627        assert_eq!(raw_stream.raw_size(), Some(42));
628        assert_eq!(raw_stream.get_raw_size(), Some(42));
629
630        // Decoded stream
631        let decoded_stream = Object::Stream {
632            dict: HashMap::new(),
633            data: StreamData::Decoded {
634                data: vec![0u8; 10],
635            },
636        };
637        assert_eq!(decoded_stream.raw_size(), Some(10));
638        assert_eq!(decoded_stream.get_raw_size(), Some(10));
639
640        // Non-stream → None
641        assert_eq!(Object::Integer(1).raw_size(), None);
642    }
643
644    #[test]
645    fn test_has_filter_detects_filter_key() {
646        let mut dict_with_filter = HashMap::new();
647        dict_with_filter.insert(Name::filter(), Object::Name(Name::from("FlateDecode")));
648        let stream_with_filter = Object::Stream {
649            dict: dict_with_filter,
650            data: StreamData::Raw {
651                offset: 0,
652                length: 0,
653            },
654        };
655        assert!(stream_with_filter.has_filter());
656
657        let stream_without_filter = Object::Stream {
658            dict: HashMap::new(),
659            data: StreamData::Raw {
660                offset: 0,
661                length: 0,
662            },
663        };
664        assert!(!stream_without_filter.has_filter());
665
666        // Non-stream → false
667        assert!(!Object::Integer(1).has_filter());
668    }
669
670    #[test]
671    #[allow(deprecated)]
672    fn test_array_len_size_is_empty() {
673        let arr = Object::Array(vec![Object::Integer(1), Object::Integer(2)]);
674        assert_eq!(arr.array_len(), Some(2));
675        assert_eq!(arr.array_size(), Some(2));
676        assert!(!arr.array_is_empty());
677
678        let empty_arr = Object::Array(vec![]);
679        assert_eq!(empty_arr.array_len(), Some(0));
680        assert!(empty_arr.array_is_empty());
681
682        // Non-array → None / false
683        assert_eq!(Object::Integer(1).array_len(), None);
684        assert_eq!(Object::Integer(1).array_size(), None);
685        assert!(!Object::Integer(1).array_is_empty());
686    }
687
688    #[test]
689    #[allow(deprecated)]
690    fn test_dict_len_size() {
691        let mut map = HashMap::new();
692        map.insert(Name::r#type(), Object::Name(Name::from("Page")));
693        let dict = Object::Dictionary(map);
694        assert_eq!(dict.dict_len(), Some(1));
695        assert_eq!(dict.dict_size(), Some(1));
696
697        let empty_dict = Object::Dictionary(HashMap::new());
698        assert_eq!(empty_dict.dict_len(), Some(0));
699
700        // Non-dict → None
701        assert_eq!(Object::Integer(1).dict_len(), None);
702    }
703
704    #[test]
705    fn test_key_exist_on_dictionary() {
706        let mut map = HashMap::new();
707        let type_name = Name::r#type();
708        map.insert(type_name.clone(), Object::Name(Name::from("Page")));
709        let dict = Object::Dictionary(map);
710
711        assert!(dict.key_exist(&type_name));
712        assert!(!dict.key_exist(&Name::from("Missing")));
713
714        // Non-dict → false
715        assert!(!Object::Integer(1).key_exist(&type_name));
716    }
717
718    #[test]
719    fn test_keys_and_get_keys_on_dictionary() {
720        let mut map = HashMap::new();
721        map.insert(Name::r#type(), Object::Name(Name::from("Page")));
722        let dict = Object::Dictionary(map);
723
724        let ks = dict.keys().expect("should return keys for dict");
725        assert_eq!(ks.len(), 1);
726        let gks = dict.get_keys().expect("get_keys alias should also work");
727        assert_eq!(gks.len(), 1);
728
729        // Non-dict → None
730        assert!(Object::Integer(1).keys().is_none());
731        assert!(Object::Integer(1).get_keys().is_none());
732    }
733
734    #[test]
735    fn test_dict_method_returns_dict_and_stream_dict() {
736        // Object::Dictionary
737        let mut map = HashMap::new();
738        map.insert(Name::r#type(), Object::Name(Name::from("Page")));
739        let dict_obj = Object::Dictionary(map.clone());
740        assert!(dict_obj.dict().is_some());
741        assert!(dict_obj.get_dict().is_some());
742
743        // Object::Stream
744        let stream_obj = Object::Stream {
745            dict: map,
746            data: StreamData::Raw {
747                offset: 0,
748                length: 0,
749            },
750        };
751        assert!(stream_obj.dict().is_some());
752        assert!(stream_obj.get_dict().is_some());
753
754        // Other variants → None
755        assert!(Object::Integer(1).dict().is_none());
756        assert!(Object::Null.get_dict().is_none());
757    }
758
759    // -----------------------------------------------------------------------
760    // Upstream-derived number tests (cpdf_number_unittest.cpp)
761    // -----------------------------------------------------------------------
762
763    /// Upstream: TEST(CPDFNumber, WriteToFloat)
764    ///
765    /// Verifies that float number objects produce correct string representations.
766    /// Note: rpdfium's serialization is in rpdfium-edit's serialize_object.
767    /// Here we test the Object's numeric accessor methods match expected values.
768    #[test]
769    fn test_number_float_values() {
770        // Integer 0 represented as float
771        let n = Object::Real(0.0);
772        assert_eq!(n.as_f64(), Some(0.0));
773        assert!(n.is_number());
774        assert!(!n.is_integer());
775
776        // Float 1.0
777        let n = Object::Real(1.0);
778        assert_eq!(n.as_f64(), Some(1.0));
779
780        // Float -7.5
781        let n = Object::Real(-7.5);
782        assert_eq!(n.as_f64(), Some(-7.5));
783
784        // Float with precision loss (38.895285f → 38.895287 in C++)
785        let n = Object::Real(38.895285_f64);
786        let val = n.as_f64().unwrap();
787        assert!((val - 38.895285).abs() < 0.001);
788
789        // Large float (f64::MAX is too large for PDF; test f32::MAX equivalent)
790        let n = Object::Real(f32::MAX as f64);
791        assert!(n.as_f64().unwrap() > 0.0);
792
793        // Small float (f32::MIN_POSITIVE equivalent)
794        let n = Object::Real(f32::MIN_POSITIVE as f64);
795        let val = n.as_f64().unwrap();
796        assert!(val > 0.0);
797        assert!(val < 0.001);
798    }
799
800    /// Upstream: TEST(CPDFNumber, WriteToInt)
801    ///
802    /// Verifies that integer number objects produce correct values.
803    #[test]
804    fn test_number_integer_values() {
805        // Integer 0
806        let n = Object::Integer(0);
807        assert_eq!(n.as_i64(), Some(0));
808        assert!(n.is_integer());
809        assert!(n.is_number());
810
811        // Integer 1
812        let n = Object::Integer(1);
813        assert_eq!(n.as_i64(), Some(1));
814
815        // Negative
816        let n = Object::Integer(-99);
817        assert_eq!(n.as_i64(), Some(-99));
818
819        // Larger positive
820        let n = Object::Integer(1234);
821        assert_eq!(n.as_i64(), Some(1234));
822
823        // Larger negative
824        let n = Object::Integer(-54321);
825        assert_eq!(n.as_i64(), Some(-54321));
826
827        // i32::MAX
828        let n = Object::Integer(i32::MAX as i64);
829        assert_eq!(n.as_i64(), Some(i32::MAX as i64));
830        assert_eq!(n.as_f64(), Some(i32::MAX as f64));
831
832        // i32::MIN
833        let n = Object::Integer(i32::MIN as i64);
834        assert_eq!(n.as_i64(), Some(i32::MIN as i64));
835        assert_eq!(n.as_f64(), Some(i32::MIN as f64));
836    }
837
838    // -----------------------------------------------------------------------
839    // Upstream-derived dictionary tests (cpdf_dictionary_unittest.cpp)
840    // -----------------------------------------------------------------------
841
842    /// Upstream: TEST(DictionaryTest, Iterators)
843    ///
844    /// Verifies that dictionary iteration yields all key-value pairs and
845    /// that the values have the correct types.
846    #[test]
847    fn test_dictionary_iterators() {
848        let mut dict = HashMap::new();
849        dict.insert(
850            Name::from("the-dictionary"),
851            Object::Dictionary(HashMap::new()),
852        );
853        dict.insert(Name::from("the-array"), Object::Array(vec![]));
854        dict.insert(Name::from("the-number"), Object::Integer(42));
855
856        let obj = Object::Dictionary(dict);
857        let d = obj.as_dict().unwrap();
858
859        // All three keys should be present
860        assert_eq!(d.len(), 3);
861
862        // Verify each key maps to the correct type
863        let dict_val = d.get(&Name::from("the-dictionary")).unwrap();
864        assert!(dict_val.is_dictionary());
865
866        let arr_val = d.get(&Name::from("the-array")).unwrap();
867        assert!(arr_val.is_array());
868
869        let num_val = d.get(&Name::from("the-number")).unwrap();
870        assert!(num_val.is_number());
871        assert_eq!(num_val.as_i64(), Some(42));
872
873        // Verify iteration covers all entries
874        let keys: Vec<&Name> = d.keys().collect();
875        assert_eq!(keys.len(), 3);
876        assert!(keys.contains(&&Name::from("the-dictionary")));
877        assert!(keys.contains(&&Name::from("the-array")));
878        assert!(keys.contains(&&Name::from("the-number")));
879
880        // Note: Unlike C++'s std::map (sorted by key), Rust's HashMap has no
881        // guaranteed iteration order, so we check membership rather than order.
882    }
883
884    // -----------------------------------------------------------------------
885    // Tests ported from upstream cpdf_object_unittest.cpp
886    // -----------------------------------------------------------------------
887
888    /// Build the set of direct objects matching upstream's PDFObjectsTest fixture.
889    fn make_direct_objects() -> Vec<Object> {
890        let mut dict = HashMap::new();
891        dict.insert(Name::from("bool"), Object::Boolean(false));
892        dict.insert(Name::from("num"), Object::Real(0.23));
893
894        let mut stream_dict = HashMap::new();
895        stream_dict.insert(
896            Name::from("key1"),
897            Object::String(PdfString::from_bytes(b" test dict".to_vec())),
898        );
899        stream_dict.insert(Name::from("key2"), Object::Integer(-1));
900
901        vec![
902            Object::Boolean(false),                                           // 0
903            Object::Boolean(true),                                            // 1
904            Object::Integer(1245),                                            // 2
905            Object::Real(9.00345_f64),                                        // 3
906            Object::String(PdfString::from_bytes(b"A simple test".to_vec())), // 4
907            Object::String(PdfString::from_bytes(b"\t\n".to_vec())),          // 5
908            Object::Name(Name::from("space")),                                // 6
909            Object::Array(vec![
910                Object::Integer(8902),
911                Object::Name(Name::from("address")),
912            ]), // 7
913            Object::Dictionary(dict),                                         // 8
914            Object::Stream {
915                dict: stream_dict,
916                data: StreamData::Decoded {
917                    data: b"abcdefghijklmnopqrstuvwxyz".to_vec(),
918                },
919            }, // 9
920            Object::Null,                                                     // 10
921        ]
922    }
923
924    /// Upstream: TEST_F(PDFObjectsTest, GetType)
925    ///
926    /// Verify type predicates for all direct object types.
927    #[test]
928    fn test_pdf_objects_get_type() {
929        let objs = make_direct_objects();
930        assert!(objs[0].is_boolean());
931        assert!(objs[1].is_boolean());
932        assert!(objs[2].is_number());
933        assert!(objs[2].is_integer());
934        assert!(objs[3].is_number());
935        assert!(!objs[3].is_integer());
936        assert!(objs[4].is_string());
937        assert!(objs[5].is_string());
938        assert!(objs[6].is_name());
939        assert!(objs[7].is_array());
940        assert!(objs[8].is_dictionary());
941        assert!(objs[9].is_stream());
942        assert!(objs[10].is_null());
943    }
944
945    /// Upstream: TEST_F(PDFObjectsTest, GetInteger)
946    ///
947    /// Verify get_integer() for each direct object type.
948    #[test]
949    fn test_pdf_objects_get_integer() {
950        let objs = make_direct_objects();
951        assert_eq!(objs[0].get_integer(), None);
952        assert_eq!(objs[1].get_integer(), None);
953        assert_eq!(objs[2].get_integer(), Some(1245));
954        assert_eq!(objs[3].get_integer(), None);
955        assert_eq!(objs[4].get_integer(), None);
956        assert_eq!(objs[6].get_integer(), None);
957        assert_eq!(objs[7].get_integer(), None);
958        assert_eq!(objs[8].get_integer(), None);
959        assert_eq!(objs[10].get_integer(), None);
960    }
961
962    /// Upstream: TEST_F(PDFObjectsTest, GetNumber)
963    ///
964    /// Verify get_number() for each direct object type.
965    #[test]
966    fn test_pdf_objects_get_number() {
967        let objs = make_direct_objects();
968        assert_eq!(objs[0].get_number(), None);
969        assert_eq!(objs[2].get_number(), Some(1245.0));
970        assert!(objs[3].get_number().is_some());
971        assert_eq!(objs[4].get_number(), None);
972        assert_eq!(objs[6].get_number(), None);
973        assert_eq!(objs[10].get_number(), None);
974    }
975
976    /// Upstream: TEST_F(PDFObjectsTest, GetString)
977    ///
978    /// Verify get_string() for each direct object type.
979    #[test]
980    fn test_pdf_objects_get_string() {
981        let objs = make_direct_objects();
982        assert_eq!(objs[0].get_string(), None);
983        assert_eq!(objs[1].get_string(), None);
984        assert_eq!(objs[2].get_string(), None);
985        assert_eq!(objs[3].get_string(), None);
986        assert!(objs[4].get_string().is_some());
987        assert_eq!(objs[4].get_string().unwrap().as_bytes(), b"A simple test");
988        assert!(objs[5].get_string().is_some());
989        assert_eq!(objs[5].get_string().unwrap().as_bytes(), b"\t\n");
990        assert_eq!(objs[6].get_string(), None);
991        assert_eq!(objs[7].get_string(), None);
992        assert_eq!(objs[10].get_string(), None);
993    }
994
995    /// Upstream: TEST_F(PDFObjectsTest, GetDict)
996    ///
997    /// Verify dict() for each direct object type.
998    #[test]
999    fn test_pdf_objects_get_dict() {
1000        let objs = make_direct_objects();
1001        assert!(objs[0].dict().is_none());
1002        assert!(objs[2].dict().is_none());
1003        assert!(objs[4].dict().is_none());
1004        assert!(objs[6].dict().is_none());
1005        assert!(objs[7].dict().is_none());
1006        assert!(objs[8].dict().is_some());
1007        assert!(objs[9].dict().is_some());
1008        assert!(objs[10].dict().is_none());
1009    }
1010
1011    /// Upstream: TEST_F(PDFObjectsTest, GetArray)
1012    ///
1013    /// Verify as_array() for each direct object type.
1014    #[test]
1015    fn test_pdf_objects_get_array() {
1016        let objs = make_direct_objects();
1017        assert!(objs[0].as_array().is_none());
1018        assert!(objs[2].as_array().is_none());
1019        assert!(objs[4].as_array().is_none());
1020        assert!(objs[6].as_array().is_none());
1021        assert!(objs[7].as_array().is_some());
1022        assert_eq!(objs[7].as_array().unwrap().len(), 2);
1023        assert!(objs[8].as_array().is_none());
1024        assert!(objs[9].as_array().is_none());
1025        assert!(objs[10].as_array().is_none());
1026    }
1027
1028    /// Upstream: TEST_F(PDFObjectsTest, IsTypeAndAsType)
1029    ///
1030    /// For each object type, verify is_X/as_X predicates and accessors.
1031    #[test]
1032    fn test_pdf_objects_is_type_and_as_type() {
1033        let objs = make_direct_objects();
1034
1035        for (i, obj) in objs.iter().enumerate() {
1036            if i <= 1 {
1037                assert!(obj.is_boolean(), "obj[{i}] should be boolean");
1038                assert!(obj.as_bool().is_some());
1039            } else {
1040                assert!(!obj.is_boolean(), "obj[{i}] should not be boolean");
1041                assert!(obj.as_bool().is_none());
1042            }
1043
1044            if i == 2 || i == 3 {
1045                assert!(obj.is_number(), "obj[{i}] should be number");
1046            } else {
1047                assert!(!obj.is_number(), "obj[{i}] should not be number");
1048            }
1049
1050            if i == 4 || i == 5 {
1051                assert!(obj.is_string(), "obj[{i}] should be string");
1052                assert!(obj.as_string().is_some());
1053            } else {
1054                assert!(!obj.is_string(), "obj[{i}] should not be string");
1055                assert!(obj.as_string().is_none());
1056            }
1057
1058            if i == 6 {
1059                assert!(obj.is_name(), "obj[{i}] should be name");
1060                assert!(obj.as_name().is_some());
1061            } else {
1062                assert!(!obj.is_name(), "obj[{i}] should not be name");
1063                assert!(obj.as_name().is_none());
1064            }
1065
1066            if i == 7 {
1067                assert!(obj.is_array(), "obj[{i}] should be array");
1068                assert!(obj.as_array().is_some());
1069            } else {
1070                assert!(!obj.is_array(), "obj[{i}] should not be array");
1071                assert!(obj.as_array().is_none());
1072            }
1073
1074            if i == 8 {
1075                assert!(obj.is_dictionary(), "obj[{i}] should be dictionary");
1076                assert!(obj.as_dict().is_some());
1077            } else {
1078                assert!(!obj.is_dictionary(), "obj[{i}] should not be dictionary");
1079                if i != 9 {
1080                    assert!(obj.as_dict().is_none());
1081                }
1082            }
1083
1084            if i == 9 {
1085                assert!(obj.is_stream(), "obj[{i}] should be stream");
1086                assert!(obj.as_stream_dict().is_some());
1087            } else {
1088                assert!(!obj.is_stream(), "obj[{i}] should not be stream");
1089                assert!(obj.as_stream_dict().is_none());
1090            }
1091
1092            assert!(!obj.is_reference(), "obj[{i}] should not be reference");
1093            assert!(obj.as_reference().is_none());
1094        }
1095    }
1096
1097    /// Upstream: TEST_F(PDFObjectsTest, GetDirect)
1098    ///
1099    /// Verify that Reference objects hold the correct ObjectId.
1100    #[test]
1101    fn test_pdf_objects_reference_deref() {
1102        let id = ObjectId::new(42, 0);
1103        let ref_obj = Object::Reference(id);
1104        assert!(ref_obj.is_reference());
1105        assert_eq!(ref_obj.as_reference(), Some(id));
1106        assert_eq!(ref_obj.ref_obj_num(), Some(42));
1107    }
1108
1109    // -----------------------------------------------------------------------
1110    // Tests ported from upstream cpdf_array_unittest.cpp (read-only)
1111    // -----------------------------------------------------------------------
1112
1113    /// Upstream: TEST(PDFArrayTest, GetMatrix)
1114    ///
1115    /// 6-element float array — verify element access.
1116    #[test]
1117    fn test_pdf_array_get_matrix_elements() {
1118        let elems: Vec<[f64; 6]> = vec![
1119            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1120            [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
1121            [2.3, 4.05, 3.0, -2.0, -3.0, 0.0],
1122            [0.05, 0.1, 0.56, 0.67, 1.34, 99.9],
1123        ];
1124        for elem in &elems {
1125            let arr = Object::Array(elem.iter().map(|&f| Object::Real(f)).collect());
1126            let items = arr.as_array().unwrap();
1127            assert_eq!(items.len(), 6);
1128            for (i, &val) in elem.iter().enumerate() {
1129                assert_eq!(items[i].as_f64(), Some(val), "mismatch at index {i}");
1130            }
1131        }
1132    }
1133
1134    /// Upstream: TEST(PDFArrayTest, GetRect)
1135    ///
1136    /// 4-element float array — verify element access.
1137    #[test]
1138    fn test_pdf_array_get_rect_elements() {
1139        let elems: Vec<[f64; 4]> = vec![
1140            [0.0, 0.0, 0.0, 0.0],
1141            [1.0, 2.0, 5.0, 6.0],
1142            [2.3, 4.05, -3.0, 0.0],
1143            [0.05, 0.1, 1.34, 99.9],
1144        ];
1145        for elem in &elems {
1146            let arr = Object::Array(elem.iter().map(|&f| Object::Real(f)).collect());
1147            let items = arr.as_array().unwrap();
1148            assert_eq!(items.len(), 4);
1149            for (i, &val) in elem.iter().enumerate() {
1150                assert_eq!(items[i].as_f64(), Some(val), "mismatch at index {i}");
1151            }
1152        }
1153    }
1154
1155    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Boolean sub-case
1156    #[test]
1157    fn test_pdf_array_get_type_at_boolean() {
1158        let vals = [true, false, false, true, true];
1159        let arr = Object::Array(vals.iter().map(|&b| Object::Boolean(b)).collect());
1160        let items = arr.as_array().unwrap();
1161        for (i, &expected) in vals.iter().enumerate() {
1162            assert_eq!(items[i].as_bool(), Some(expected), "bool mismatch at {i}");
1163            assert_eq!(items[i].as_i64(), None);
1164            assert_eq!(items[i].as_f64(), None);
1165            assert!(items[i].as_array().is_none());
1166            assert!(items[i].as_dict().is_none());
1167        }
1168    }
1169
1170    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Integer sub-case
1171    #[test]
1172    fn test_pdf_array_get_type_at_integer() {
1173        let vals: Vec<i64> = vec![10, 0, -345, 2089345456, -1000000000, 567, 93658767];
1174        let arr = Object::Array(vals.iter().map(|&n| Object::Integer(n)).collect());
1175        let items = arr.as_array().unwrap();
1176        for (i, &val) in vals.iter().enumerate() {
1177            assert_eq!(items[i].as_i64(), Some(val), "int mismatch at {i}");
1178            assert_eq!(items[i].as_f64(), Some(val as f64), "float mismatch at {i}");
1179            assert!(items[i].as_array().is_none());
1180            assert!(items[i].as_dict().is_none());
1181        }
1182    }
1183
1184    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Name sub-case
1185    #[test]
1186    fn test_pdf_array_get_type_at_name() {
1187        let vals = [
1188            "this",
1189            "adsde$%^",
1190            "\r\t",
1191            "\"012",
1192            ".",
1193            "EYREW",
1194            "It is a joke :)",
1195        ];
1196        let arr = Object::Array(vals.iter().map(|&s| Object::Name(Name::from(s))).collect());
1197        let items = arr.as_array().unwrap();
1198        for (i, &val) in vals.iter().enumerate() {
1199            let name = items[i].as_name().unwrap();
1200            assert_eq!(name.as_str().as_ref(), val, "name mismatch at {i}");
1201            assert_eq!(items[i].as_i64(), None);
1202            assert_eq!(items[i].as_f64(), None);
1203        }
1204    }
1205
1206    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Null sub-case
1207    #[test]
1208    fn test_pdf_array_get_type_at_null() {
1209        let arr = Object::Array(vec![Object::Null, Object::Null, Object::Null]);
1210        let items = arr.as_array().unwrap();
1211        for i in 0..3 {
1212            assert!(items[i].is_null());
1213            assert_eq!(items[i].as_i64(), None);
1214            assert_eq!(items[i].as_f64(), None);
1215            assert!(items[i].as_array().is_none());
1216            assert!(items[i].as_dict().is_none());
1217            assert!(items[i].as_string().is_none());
1218        }
1219    }
1220
1221    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Mixed array sub-case
1222    #[test]
1223    fn test_pdf_array_get_type_at_mixed() {
1224        let arr = Object::Array(vec![
1225            Object::Boolean(true),
1226            Object::Boolean(false),
1227            Object::Integer(0),
1228            Object::Integer(-1234),
1229            Object::Real(2345.0),
1230            Object::Real(0.05),
1231            Object::String(PdfString::from_bytes(b"".to_vec())),
1232            Object::String(PdfString::from_bytes(b"It is a test!".to_vec())),
1233            Object::Name(Name::from("NAME")),
1234            Object::Name(Name::from("test")),
1235            Object::Null,
1236        ]);
1237        let items = arr.as_array().unwrap();
1238        assert_eq!(items.len(), 11);
1239        assert_eq!(items[0].as_bool(), Some(true));
1240        assert_eq!(items[1].as_bool(), Some(false));
1241        assert_eq!(items[2].as_i64(), Some(0));
1242        assert_eq!(items[3].as_i64(), Some(-1234));
1243        assert_eq!(items[4].as_f64(), Some(2345.0));
1244        assert_eq!(items[5].as_f64(), Some(0.05));
1245        assert_eq!(items[6].as_string().unwrap().as_bytes(), b"");
1246        assert_eq!(items[7].as_string().unwrap().as_bytes(), b"It is a test!");
1247        assert_eq!(items[8].as_name().unwrap().as_str().as_ref(), "NAME");
1248        assert_eq!(items[9].as_name().unwrap().as_str().as_ref(), "test");
1249        assert!(items[10].is_null());
1250    }
1251
1252    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Array of arrays
1253    #[test]
1254    fn test_pdf_array_nested_arrays() {
1255        let inner: Vec<Object> = (0..3)
1256            .map(|_| Object::Array((100..103).map(Object::Integer).collect()))
1257            .collect();
1258        let arr = Object::Array(inner);
1259        let items = arr.as_array().unwrap();
1260        assert_eq!(items.len(), 3);
1261        for i in 0..3 {
1262            let sub = items[i].as_array().unwrap();
1263            assert_eq!(sub.len(), 3);
1264            assert_eq!(sub[0].as_i64(), Some(100));
1265            assert_eq!(sub[1].as_i64(), Some(101));
1266            assert_eq!(sub[2].as_i64(), Some(102));
1267        }
1268    }
1269
1270    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Dictionary array
1271    #[test]
1272    fn test_pdf_array_of_dicts() {
1273        let inner: Vec<Object> = (0..3)
1274            .map(|_| {
1275                let mut d = HashMap::new();
1276                for j in 0..3i64 {
1277                    d.insert(
1278                        Name::from(format!("key{j}").as_str()),
1279                        Object::Integer(j + 200),
1280                    );
1281                }
1282                Object::Dictionary(d)
1283            })
1284            .collect();
1285        let arr = Object::Array(inner);
1286        let items = arr.as_array().unwrap();
1287        assert_eq!(items.len(), 3);
1288        for i in 0..3 {
1289            let d = items[i].as_dict().unwrap();
1290            assert_eq!(d.len(), 3);
1291            assert_eq!(d.get(&Name::from("key0")).unwrap().as_i64(), Some(200));
1292        }
1293    }
1294
1295    /// Upstream: TEST(ArrayTest, Iterator)
1296    ///
1297    /// Verify iterating over array elements.
1298    #[test]
1299    fn test_pdf_array_iterator() {
1300        let elems: Vec<i64> = vec![
1301            -23, -11, 3, 455, 2345877, 0, 7895330, -12564334, 10000, -100000,
1302        ];
1303        let arr = Object::Array(elems.iter().map(|&n| Object::Integer(n)).collect());
1304        let items = arr.as_array().unwrap();
1305        for (i, item) in items.iter().enumerate() {
1306            assert_eq!(item.as_i64(), Some(elems[i]), "mismatch at index {i}");
1307        }
1308    }
1309
1310    /// Upstream: TEST_F(PDFObjectsTest, GetNameFor)
1311    ///
1312    /// Verify dictionary name/string lookup.
1313    #[test]
1314    fn test_pdf_objects_dict_name_for() {
1315        let mut d = HashMap::new();
1316        d.insert(Name::from("bool"), Object::Boolean(false));
1317        d.insert(Name::from("num"), Object::Real(0.23));
1318        d.insert(
1319            Name::from("string"),
1320            Object::String(PdfString::from_bytes(b"ium".to_vec())),
1321        );
1322        d.insert(Name::from("name"), Object::Name(Name::from("Pdf")));
1323        let dict = Object::Dictionary(d);
1324        let d = dict.as_dict().unwrap();
1325
1326        // GetNameFor: only returns value for Name entries
1327        assert_eq!(
1328            d.get(&Name::from("name"))
1329                .and_then(|o| o.as_name())
1330                .map(|n| n.as_str().to_string()),
1331            Some("Pdf".to_string())
1332        );
1333        // bool entry is not a Name
1334        assert!(
1335            d.get(&Name::from("bool"))
1336                .and_then(|o| o.as_name())
1337                .is_none()
1338        );
1339        // Missing key
1340        assert!(d.get(&Name::from("invalid")).is_none());
1341    }
1342
1343    // -----------------------------------------------------------------------
1344    // Tests ported from upstream cpdf_object_unittest.cpp — additional
1345    // -----------------------------------------------------------------------
1346
1347    /// Upstream: TEST_F(PDFObjectsTest, GetUnicodeText)
1348    ///
1349    /// Verify unicode_text() returns expected values for each object type.
1350    /// In upstream, strings and names return their unicode text, streams
1351    /// return their content as text, and other types return empty.
1352    /// Note: PdfString bytes are decoded via PDFDocEncoding, so raw bytes
1353    /// like 0x09 (\t) map to PDFDocEncoding codepoints, not ASCII.
1354    #[test]
1355    fn test_pdf_objects_get_unicode_text() {
1356        let objs = make_direct_objects();
1357        // Booleans → None
1358        assert!(objs[0].unicode_text().is_none());
1359        assert!(objs[1].unicode_text().is_none());
1360        // Numbers → None
1361        assert!(objs[2].unicode_text().is_none());
1362        assert!(objs[3].unicode_text().is_none());
1363        // String → text (decoded via PDFDocEncoding)
1364        assert_eq!(objs[4].unicode_text(), Some("A simple test".to_string()));
1365        // objs[5] is PdfString with bytes [0x09, 0x0A] — decoded via PDFDocEncoding
1366        assert!(objs[5].unicode_text().is_some());
1367        // Name → text
1368        assert_eq!(objs[6].unicode_text(), Some("space".to_string()));
1369        // Array → None
1370        assert!(objs[7].unicode_text().is_none());
1371        // Dictionary → None
1372        assert!(objs[8].unicode_text().is_none());
1373        // Null → None
1374        assert!(objs[10].unicode_text().is_none());
1375    }
1376
1377    // -----------------------------------------------------------------------
1378    // Tests ported from upstream cpdf_array_unittest.cpp — read-only
1379    // -----------------------------------------------------------------------
1380
1381    /// Upstream: TEST(ArrayTest, GetBooleanAt)
1382    ///
1383    /// Boolean values in an array — verify access by index.
1384    /// In rpdfium, we don't have GetBooleanAt with default, but we can
1385    /// check as_bool() on array elements.
1386    #[test]
1387    fn test_pdf_array_get_boolean_at() {
1388        let arr = Object::Array(vec![
1389            Object::Boolean(true),
1390            Object::Boolean(false),
1391            Object::Integer(100),
1392            Object::Integer(0),
1393        ]);
1394        let items = arr.as_array().unwrap();
1395        assert_eq!(items.len(), 4);
1396
1397        // Boolean true
1398        assert_eq!(items[0].as_bool(), Some(true));
1399        // Boolean false
1400        assert_eq!(items[1].as_bool(), Some(false));
1401        // Integer 100 is not a boolean
1402        assert_eq!(items[2].as_bool(), None);
1403        // Integer 0 is not a boolean
1404        assert_eq!(items[3].as_bool(), None);
1405    }
1406
1407    /// Upstream: TEST(PDFArrayTest, AddNumber)
1408    ///
1409    /// Float values in array — verify they round-trip correctly.
1410    #[test]
1411    fn test_pdf_array_add_number() {
1412        let vals: Vec<f64> = vec![1.0, -1.0, 0.0, 0.456734, 12345.54321, 0.5, 1000.0, 0.000045];
1413        let arr = Object::Array(vals.iter().map(|&f| Object::Real(f)).collect());
1414        let items = arr.as_array().unwrap();
1415        assert_eq!(items.len(), vals.len());
1416        for (i, &val) in vals.iter().enumerate() {
1417            assert!(items[i].is_number());
1418            assert_eq!(items[i].as_f64(), Some(val), "mismatch at index {i}");
1419        }
1420    }
1421
1422    /// Upstream: TEST(PDFArrayTest, AddInteger)
1423    ///
1424    /// Integer values in array — verify they round-trip correctly.
1425    #[test]
1426    fn test_pdf_array_add_integer() {
1427        let vals: Vec<i64> = vec![0, 1, 934435456, 876, 10000, -1, -24354656, -100];
1428        let arr = Object::Array(vals.iter().map(|&n| Object::Integer(n)).collect());
1429        let items = arr.as_array().unwrap();
1430        assert_eq!(items.len(), vals.len());
1431        for (i, &val) in vals.iter().enumerate() {
1432            assert!(items[i].is_number());
1433            assert_eq!(items[i].as_i64(), Some(val), "mismatch at index {i}");
1434        }
1435    }
1436
1437    /// Upstream: TEST(PDFArrayTest, AddStringAndName)
1438    ///
1439    /// String and Name values in array — verify they round-trip correctly.
1440    #[test]
1441    fn test_pdf_array_add_string_and_name() {
1442        let vals = [
1443            "",
1444            "a",
1445            "ehjhRIOYTTFdfcdnv",
1446            "122323",
1447            "$#%^&**",
1448            " ",
1449            "This is a test.\r\n",
1450        ];
1451        let string_arr = Object::Array(
1452            vals.iter()
1453                .map(|&s| Object::String(PdfString::from_bytes(s.as_bytes().to_vec())))
1454                .collect(),
1455        );
1456        let name_arr = Object::Array(vals.iter().map(|&s| Object::Name(Name::from(s))).collect());
1457        let str_items = string_arr.as_array().unwrap();
1458        let name_items = name_arr.as_array().unwrap();
1459        for (i, &val) in vals.iter().enumerate() {
1460            assert!(str_items[i].is_string(), "string item {i} should be string");
1461            assert_eq!(
1462                str_items[i].as_string().unwrap().as_bytes(),
1463                val.as_bytes(),
1464                "string mismatch at {i}"
1465            );
1466            assert!(name_items[i].is_name(), "name item {i} should be name");
1467            assert_eq!(
1468                name_items[i].as_name().unwrap().as_str().as_ref(),
1469                val,
1470                "name mismatch at {i}"
1471            );
1472        }
1473    }
1474
1475    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — Float sub-case
1476    ///
1477    /// Float values in array — verify accessor behavior.
1478    #[test]
1479    fn test_pdf_array_get_type_at_float() {
1480        let vals: Vec<f64> = vec![
1481            0.0, 0.0, 10.0, 10.0, 0.0345, 897.34, -2.5, -1.0, -345.0, -0.0,
1482        ];
1483        let arr = Object::Array(vals.iter().map(|&f| Object::Real(f)).collect());
1484        let items = arr.as_array().unwrap();
1485        assert_eq!(items.len(), vals.len());
1486        for (i, &val) in vals.iter().enumerate() {
1487            assert_eq!(items[i].as_f64(), Some(val), "float mismatch at {i}");
1488            assert!(items[i].as_array().is_none());
1489            assert!(items[i].as_dict().is_none());
1490        }
1491    }
1492
1493    // Upstream: TEST_F(PDFObjectsTest, KeyForCache)
1494    //
1495    // rpdfium does not have a KeyForCache equivalent — objects use ObjectId
1496    // for cache keys rather than a u64 derived from pointer addresses.
1497    // Skipped: no rpdfium equivalent.
1498
1499    /// Upstream: TEST(ArrayTest, Find)
1500    ///
1501    /// Find element by value in array.  rpdfium arrays are Vec<Object> and
1502    /// Object doesn't implement PartialEq, so there's no direct `find()`
1503    /// method.  We verify the equivalent using iter().position().
1504    #[test]
1505    fn test_pdf_array_find() {
1506        let arr = Object::Array(vec![
1507            Object::Integer(10),
1508            Object::Integer(20),
1509            Object::Integer(30),
1510        ]);
1511        let items = arr.as_array().unwrap();
1512
1513        // Find by value using iterator
1514        let pos = items.iter().position(|o| o.as_i64() == Some(20));
1515        assert_eq!(pos, Some(1));
1516
1517        let pos = items.iter().position(|o| o.as_i64() == Some(99));
1518        assert_eq!(pos, None);
1519    }
1520
1521    /// Upstream: TEST(ArrayTest, Contains)
1522    ///
1523    /// Check if array contains element by value.
1524    #[test]
1525    fn test_pdf_array_contains() {
1526        let arr = Object::Array(vec![Object::Integer(10), Object::Integer(20)]);
1527        let items = arr.as_array().unwrap();
1528        assert!(items.iter().any(|o| o.as_i64() == Some(10)));
1529        assert!(items.iter().any(|o| o.as_i64() == Some(20)));
1530        assert!(!items.iter().any(|o| o.as_i64() == Some(30)));
1531    }
1532
1533    /// Upstream: TEST(ArrayTest, SetAtBeyond)
1534    ///
1535    /// In upstream, SetNewAt fails when index is out of bounds.
1536    /// In rpdfium, arrays are Vec<Object> — setting beyond bounds would panic.
1537    /// This test verifies that valid index access works and documents the
1538    /// difference.
1539    #[test]
1540    fn test_pdf_array_set_at_beyond() {
1541        let mut arr = vec![Object::Integer(0)];
1542        // Setting at valid index works
1543        arr[0] = Object::Integer(42);
1544        assert_eq!(arr[0].as_i64(), Some(42));
1545        // Setting at index 1 would panic (out of bounds) — this matches
1546        // upstream where SetNewAt(1, ...) returns false on a size-1 array.
1547        assert_eq!(arr.len(), 1);
1548    }
1549
1550    /// Upstream: TEST(ArrayTest, InsertAtBeyond)
1551    ///
1552    /// In upstream, InsertNewAt fails when index > size.
1553    /// rpdfium arrays are Vec<Object> — insert panics on out-of-bounds.
1554    /// Verify valid insertion at end (index == len) works.
1555    #[test]
1556    fn test_pdf_array_insert_at_beyond() {
1557        let mut arr: Vec<Object> = Vec::new();
1558        // Insert at index 0 (== len) is valid
1559        arr.insert(0, Object::Integer(0));
1560        assert_eq!(arr.len(), 1);
1561        // Insert at index 1 (== len) is valid
1562        arr.insert(1, Object::Integer(1));
1563        assert_eq!(arr.len(), 2);
1564        assert_eq!(arr[0].as_i64(), Some(0));
1565        assert_eq!(arr[1].as_i64(), Some(1));
1566    }
1567
1568    /// Upstream: TEST(PDFArrayTest, GetTypeAt) — String sub-case
1569    ///
1570    /// String values in array — verify accessor behavior.
1571    #[test]
1572    fn test_pdf_array_get_type_at_string() {
1573        let vals = [
1574            "this",
1575            "adsde$%^",
1576            "\r\t",
1577            "\"012",
1578            ".",
1579            "EYREW",
1580            "It is a joke :)",
1581        ];
1582        let arr = Object::Array(
1583            vals.iter()
1584                .map(|&s| Object::String(PdfString::from_bytes(s.as_bytes().to_vec())))
1585                .collect(),
1586        );
1587        let items = arr.as_array().unwrap();
1588        for (i, &val) in vals.iter().enumerate() {
1589            assert_eq!(
1590                items[i].as_string().unwrap().as_bytes(),
1591                val.as_bytes(),
1592                "string mismatch at {i}"
1593            );
1594            assert_eq!(items[i].as_i64(), None);
1595            assert_eq!(items[i].as_f64(), None);
1596            assert!(items[i].as_array().is_none());
1597            assert!(items[i].as_dict().is_none());
1598        }
1599    }
1600}