Skip to main content

rpdfium_doc/
signature.rs

1// Derived from PDFium's fpdfsdk/fpdf_signature.cpp
2// Original: Copyright 2020 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Digital signature field enumeration and metadata extraction.
7//!
8//! Corresponds to upstream `fpdf_signature.h` / `fpdf_signature.cpp`.
9//!
10//! This module provides **read-only** access to digital signatures embedded
11//! in a PDF document. Cryptographic verification (PKCS#7/CMS) and signature
12//! creation are out of scope.
13
14use rpdfium_core::{Name, PdfSource};
15use rpdfium_parser::{Object, ObjectStore};
16
17use crate::error::{DocError, DocResult};
18
19/// Maximum number of signature fields collected from a single document.
20const MAX_SIGNATURES: usize = 1_000;
21
22// ---------------------------------------------------------------------------
23// DocMdpPermission
24// ---------------------------------------------------------------------------
25
26/// The DocMDP (Modification Detection and Prevention) permission level.
27///
28/// Corresponds to the `/P` integer in the DocMDP transform parameters.
29/// See ISO 32000-2 §12.8.2.2.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DocMdpPermission {
32    /// No changes allowed (P=1).
33    NoChanges,
34    /// Form filling and signing only (P=2).
35    FormFillingAndSigning,
36    /// Annotations, form filling, and signing (P=3).
37    AnnotationFormFillingAndSigning,
38}
39
40impl DocMdpPermission {
41    /// Convert from the PDF `/P` integer value.
42    ///
43    /// Returns `None` for values that are not 1, 2, or 3.
44    pub fn from_p_value(p: i64) -> Option<Self> {
45        match p {
46            1 => Some(Self::NoChanges),
47            2 => Some(Self::FormFillingAndSigning),
48            3 => Some(Self::AnnotationFormFillingAndSigning),
49            _ => None,
50        }
51    }
52
53    /// Convert back to the integer `/P` value.
54    pub fn as_u32(self) -> u32 {
55        match self {
56            Self::NoChanges => 1,
57            Self::FormFillingAndSigning => 2,
58            Self::AnnotationFormFillingAndSigning => 3,
59        }
60    }
61}
62
63// ---------------------------------------------------------------------------
64// SignatureObject
65// ---------------------------------------------------------------------------
66
67/// A digital signature field parsed from a PDF's AcroForm.
68///
69/// Corresponds to the data obtainable via the eight
70/// `FPDF_GetSignatureObject` / `FPDFSignatureObj_Get*` functions in
71/// PDFium's `fpdf_signature.h`.
72#[derive(Debug, Clone)]
73pub struct SignatureObject {
74    contents: Option<Vec<u8>>,
75    byte_range: Vec<(u64, u64)>,
76    sub_filter: Option<String>,
77    reason: Option<String>,
78    time: Option<String>,
79    doc_mdp_permission: Option<DocMdpPermission>,
80}
81
82impl SignatureObject {
83    /// The raw DER-encoded PKCS#7/CMS signature bytes from `/Contents`.
84    ///
85    /// Returns `None` if the field has no `/Contents` entry.
86    ///
87    /// Corresponds to `FPDFSignatureObj_GetContents()`.
88    pub fn contents(&self) -> Option<&[u8]> {
89        self.contents.as_deref()
90    }
91
92    /// ADR-019 T2 alias for [`contents()`](Self::contents).
93    ///
94    /// Corresponds to `FPDFSignatureObj_GetContents`.
95    #[inline]
96    pub fn signature_obj_get_contents(&self) -> Option<&[u8]> {
97        self.contents()
98    }
99
100    /// Deprecated — use [`signature_obj_get_contents()`](Self::signature_obj_get_contents).
101    ///
102    /// Corresponds to `FPDFSignatureObj_GetContents`.
103    #[deprecated(
104        note = "use `signature_obj_get_contents()` — matches upstream `FPDFSignatureObj_GetContents`"
105    )]
106    #[inline]
107    pub fn get_contents(&self) -> Option<&[u8]> {
108        self.contents()
109    }
110
111    /// The byte ranges that the signature covers, as `(offset, length)` pairs.
112    ///
113    /// The `/ByteRange` array is a flat sequence of integers interpreted as
114    /// pairs: `[offset0, length0, offset1, length1, ...]`. An odd number of
115    /// elements results in the trailing element being dropped.
116    ///
117    /// Corresponds to `FPDFSignatureObj_GetByteRange()`.
118    pub fn byte_range(&self) -> &[(u64, u64)] {
119        &self.byte_range
120    }
121
122    /// ADR-019 T2 alias for [`byte_range()`](Self::byte_range).
123    ///
124    /// Corresponds to `FPDFSignatureObj_GetByteRange`.
125    #[inline]
126    pub fn signature_obj_get_byte_range(&self) -> &[(u64, u64)] {
127        self.byte_range()
128    }
129
130    /// Deprecated — use [`signature_obj_get_byte_range()`](Self::signature_obj_get_byte_range).
131    ///
132    /// Corresponds to `FPDFSignatureObj_GetByteRange`.
133    #[deprecated(
134        note = "use `signature_obj_get_byte_range()` — matches upstream `FPDFSignatureObj_GetByteRange`"
135    )]
136    #[inline]
137    pub fn get_byte_range(&self) -> &[(u64, u64)] {
138        self.byte_range()
139    }
140
141    /// The sub-filter name from `/SubFilter` (e.g. `"adbe.pkcs7.detached"`).
142    ///
143    /// Returns `None` if the field has no `/SubFilter` entry.
144    ///
145    /// Corresponds to `FPDFSignatureObj_GetSubFilter()`.
146    pub fn sub_filter(&self) -> Option<&str> {
147        self.sub_filter.as_deref()
148    }
149
150    /// ADR-019 T2 alias for [`sub_filter()`](Self::sub_filter).
151    ///
152    /// Corresponds to `FPDFSignatureObj_GetSubFilter`.
153    #[inline]
154    pub fn signature_obj_get_sub_filter(&self) -> Option<&str> {
155        self.sub_filter()
156    }
157
158    /// Deprecated — use [`signature_obj_get_sub_filter()`](Self::signature_obj_get_sub_filter).
159    ///
160    /// Corresponds to `FPDFSignatureObj_GetSubFilter`.
161    #[deprecated(
162        note = "use `signature_obj_get_sub_filter()` — matches upstream `FPDFSignatureObj_GetSubFilter`"
163    )]
164    #[inline]
165    pub fn get_sub_filter(&self) -> Option<&str> {
166        self.sub_filter()
167    }
168
169    /// The human-readable signing reason from `/Reason`.
170    ///
171    /// Returns `None` if the field has no `/Reason` entry.
172    ///
173    /// Corresponds to `FPDFSignatureObj_GetReason()`.
174    pub fn reason(&self) -> Option<&str> {
175        self.reason.as_deref()
176    }
177
178    /// ADR-019 T2 alias for [`reason()`](Self::reason).
179    ///
180    /// Corresponds to `FPDFSignatureObj_GetReason`.
181    #[inline]
182    pub fn signature_obj_get_reason(&self) -> Option<&str> {
183        self.reason()
184    }
185
186    /// Deprecated — use [`signature_obj_get_reason()`](Self::signature_obj_get_reason).
187    ///
188    /// Corresponds to `FPDFSignatureObj_GetReason`.
189    #[deprecated(
190        note = "use `signature_obj_get_reason()` — matches upstream `FPDFSignatureObj_GetReason`"
191    )]
192    #[inline]
193    pub fn get_reason(&self) -> Option<&str> {
194        self.reason()
195    }
196
197    /// The signing time string from `/M` (format: `"D:YYYYMMDDHHmmSSOHH'mm'"`).
198    ///
199    /// Returns `None` if the field has no `/M` entry.
200    ///
201    /// Corresponds to `FPDFSignatureObj_GetTime()`.
202    pub fn time(&self) -> Option<&str> {
203        self.time.as_deref()
204    }
205
206    /// ADR-019 T2 alias for [`time()`](Self::time).
207    ///
208    /// Corresponds to `FPDFSignatureObj_GetTime`.
209    #[inline]
210    pub fn signature_obj_get_time(&self) -> Option<&str> {
211        self.time()
212    }
213
214    /// Deprecated — use [`signature_obj_get_time()`](Self::signature_obj_get_time).
215    ///
216    /// Corresponds to `FPDFSignatureObj_GetTime`.
217    #[deprecated(
218        note = "use `signature_obj_get_time()` — matches upstream `FPDFSignatureObj_GetTime`"
219    )]
220    #[inline]
221    pub fn get_time(&self) -> Option<&str> {
222        self.time()
223    }
224
225    /// Deprecated — use [`signature_obj_get_time()`](Self::signature_obj_get_time).
226    #[deprecated(
227        note = "use `signature_obj_get_time()` — matches upstream `FPDFSignatureObj_GetTime`"
228    )]
229    #[inline]
230    pub fn signing_time(&self) -> Option<&str> {
231        self.time()
232    }
233
234    /// The DocMDP permission level, if a DocMDP transform is present.
235    ///
236    /// Returns `None` if the signature does not include a DocMDP reference.
237    ///
238    /// Corresponds to `FPDFSignatureObj_GetDocMDPPermission()`.
239    pub fn doc_mdp_permission(&self) -> Option<DocMdpPermission> {
240        self.doc_mdp_permission
241    }
242
243    /// ADR-019 T2 alias for [`doc_mdp_permission()`](Self::doc_mdp_permission).
244    ///
245    /// Corresponds to `FPDFSignatureObj_GetDocMDPPermission`.
246    #[inline]
247    pub fn signature_obj_get_doc_mdp_permission(&self) -> Option<DocMdpPermission> {
248        self.doc_mdp_permission()
249    }
250
251    /// Deprecated — use [`signature_obj_get_doc_mdp_permission()`](Self::signature_obj_get_doc_mdp_permission).
252    ///
253    /// Corresponds to `FPDFSignatureObj_GetDocMDPPermission`.
254    #[deprecated(
255        note = "use `signature_obj_get_doc_mdp_permission()` — matches upstream `FPDFSignatureObj_GetDocMDPPermission`"
256    )]
257    #[inline]
258    pub fn get_doc_mdp_permission(&self) -> Option<DocMdpPermission> {
259        self.doc_mdp_permission()
260    }
261}
262
263// ---------------------------------------------------------------------------
264// collect_signatures
265// ---------------------------------------------------------------------------
266
267/// Collect all digital signature fields from the document's AcroForm.
268///
269/// Iterates the top-level `/AcroForm/Fields[]` array (non-recursive) and
270/// collects every entry whose `/FT` is `/Sig`. Returns an empty `Vec` if the
271/// document has no AcroForm or no signature fields.
272///
273/// Corresponds to `CollectSignatures()` in PDFium's `fpdf_signature.cpp`.
274pub fn collect_signatures<S: PdfSource>(
275    catalog: &Object,
276    store: &ObjectStore<S>,
277) -> DocResult<Vec<SignatureObject>> {
278    // Resolve the catalog dictionary.
279    let catalog_dict = store
280        .deep_resolve(catalog)
281        .map_err(|e| DocError::Parser(e.to_string()))?;
282    let catalog_dict = catalog_dict.as_dict().ok_or(DocError::UnexpectedType)?;
283
284    // Navigate to /AcroForm.
285    let acroform_obj = match catalog_dict.get(&Name::acro_form()) {
286        Some(obj) => obj,
287        None => return Ok(Vec::new()),
288    };
289    let acroform = store
290        .deep_resolve(acroform_obj)
291        .map_err(|e| DocError::Parser(e.to_string()))?;
292    let acroform_dict = match acroform.as_dict() {
293        Some(d) => d,
294        None => return Ok(Vec::new()),
295    };
296
297    // Get the top-level /Fields array.
298    let fields_obj = match acroform_dict.get(&Name::fields()) {
299        Some(obj) => obj,
300        None => return Ok(Vec::new()),
301    };
302    let fields_resolved = store
303        .deep_resolve(fields_obj)
304        .map_err(|e| DocError::Parser(e.to_string()))?;
305    let fields_arr = match fields_resolved.as_array() {
306        Some(arr) => arr,
307        None => return Ok(Vec::new()),
308    };
309
310    let mut signatures = Vec::new();
311
312    for field_ref in fields_arr {
313        if signatures.len() >= MAX_SIGNATURES {
314            break;
315        }
316
317        // Resolve the field dictionary.
318        let field_obj = store
319            .deep_resolve(field_ref)
320            .map_err(|e| DocError::Parser(e.to_string()))?;
321        let field_dict = match field_obj.as_dict() {
322            Some(d) => d,
323            None => continue,
324        };
325
326        // Check /FT == /Sig.
327        let ft = match field_dict.get(&Name::ft()) {
328            Some(obj) => obj,
329            None => continue,
330        };
331        let ft_resolved = store
332            .deep_resolve(ft)
333            .map_err(|e| DocError::Parser(e.to_string()))?;
334        let ft_name = match ft_resolved.as_name() {
335            Some(n) => n,
336            None => continue,
337        };
338        if *ft_name != Name::sig() {
339            continue;
340        }
341
342        // Resolve /V (the signature dictionary).
343        let v_obj = match field_dict.get(&Name::v()) {
344            Some(obj) => obj,
345            None => continue,
346        };
347        let sig_dict_obj = store
348            .deep_resolve(v_obj)
349            .map_err(|e| DocError::Parser(e.to_string()))?;
350        let sig_dict = match sig_dict_obj.as_dict() {
351            Some(d) => d,
352            None => continue,
353        };
354
355        let sig = parse_sig_dict(sig_dict, store)?;
356        signatures.push(sig);
357    }
358
359    Ok(signatures)
360}
361
362/// Parse a signature dictionary into a [`SignatureObject`].
363fn parse_sig_dict<S: PdfSource>(
364    sig_dict: &std::collections::HashMap<Name, Object>,
365    store: &ObjectStore<S>,
366) -> DocResult<SignatureObject> {
367    // /Contents — binary DER-encoded PKCS#7
368    let contents = sig_dict
369        .get(&Name::contents())
370        .and_then(|obj| store.deep_resolve(obj).ok())
371        .and_then(|obj| obj.as_string().map(|s| s.as_bytes().to_vec()));
372
373    // /ByteRange — flat integer array → [(offset, len)] pairs
374    let byte_range = sig_dict
375        .get(&Name::byte_range())
376        .and_then(|obj| store.deep_resolve(obj).ok())
377        .and_then(|obj| {
378            obj.as_array().map(|arr| {
379                arr.iter()
380                    .filter_map(|item| {
381                        store
382                            .deep_resolve(item)
383                            .ok()
384                            .and_then(|o| o.as_i64())
385                            .map(|v| v.max(0) as u64)
386                    })
387                    .collect::<Vec<u64>>()
388            })
389        })
390        .map(|flat| {
391            flat.chunks_exact(2)
392                .map(|pair| (pair[0], pair[1]))
393                .collect::<Vec<(u64, u64)>>()
394        })
395        .unwrap_or_default();
396
397    // /SubFilter — ASCII Name
398    let sub_filter = sig_dict
399        .get(&Name::sub_filter())
400        .and_then(|obj| store.deep_resolve(obj).ok())
401        .and_then(|obj| obj.as_name().map(|n| n.as_str().to_string()));
402
403    // /Reason — PDF text string
404    let reason = sig_dict
405        .get(&Name::reason())
406        .and_then(|obj| store.deep_resolve(obj).ok())
407        .and_then(|obj| obj.as_string().map(|s| s.to_string_lossy()));
408
409    // /M — PDF date string (ASCII)
410    let time = sig_dict
411        .get(&Name::m())
412        .and_then(|obj| store.deep_resolve(obj).ok())
413        .and_then(|obj| obj.as_string().map(|s| s.to_string_lossy()));
414
415    // /Reference[] — scan for DocMDP transform
416    let doc_mdp_permission = parse_doc_mdp(sig_dict, store);
417
418    Ok(SignatureObject {
419        contents,
420        byte_range,
421        sub_filter,
422        reason,
423        time,
424        doc_mdp_permission,
425    })
426}
427
428/// Scan `/Reference[]` for a DocMDP transform and extract the permission level.
429fn parse_doc_mdp<S: PdfSource>(
430    sig_dict: &std::collections::HashMap<Name, Object>,
431    store: &ObjectStore<S>,
432) -> Option<DocMdpPermission> {
433    let ref_obj = sig_dict.get(&Name::reference())?;
434    let ref_resolved = store.deep_resolve(ref_obj).ok()?;
435    let ref_arr = ref_resolved.as_array()?;
436
437    for entry_ref in ref_arr {
438        let entry_obj = store.deep_resolve(entry_ref).ok()?;
439        let entry_dict = entry_obj.as_dict()?;
440
441        // Check /TransformMethod == /DocMDP
442        let tm_obj = entry_dict.get(&Name::transform_method())?;
443        let tm_resolved = store.deep_resolve(tm_obj).ok()?;
444        let tm_name = tm_resolved.as_name()?;
445        if *tm_name != Name::doc_mdp() {
446            continue;
447        }
448
449        // Get /TransformParams/P
450        // Upstream uses GetIntegerFor("P", 2): absent /P defaults to 2
451        // (FormFillingAndSigning), then validates range 1–3.
452        let tp_obj = entry_dict.get(&Name::transform_params())?;
453        let tp_resolved = store.deep_resolve(tp_obj).ok()?;
454        let tp_dict = tp_resolved.as_dict()?;
455
456        let p_val = tp_dict
457            .get(&Name::p())
458            .and_then(|obj| store.deep_resolve(obj).ok())
459            .and_then(|obj| obj.as_i64())
460            .unwrap_or(2); // PDF spec default when /P is absent
461
462        return DocMdpPermission::from_p_value(p_val);
463    }
464
465    None
466}
467
468// ---------------------------------------------------------------------------
469// Tests
470// ---------------------------------------------------------------------------
471
472#[cfg(test)]
473mod tests {
474    use std::collections::HashMap;
475    use std::sync::Arc;
476
477    use rpdfium_core::{Name, PdfString};
478    use rpdfium_parser::object::Object;
479    use rpdfium_parser::store::ObjectStore;
480
481    use super::*;
482
483    /// Build a raw PDF byte stream with the given extra objects appended and
484    /// an AcroForm pointing to `fields_refs` (object numbers).
485    fn build_pdf_with_sig(field_obj_num: u32) -> Vec<u8> {
486        // We construct minimal PDF objects by hand.
487        // Objects:
488        //   1 0 obj — Catalog  (has /AcroForm 4 0 R)
489        //   2 0 obj — Pages
490        //   3 0 obj — Sig field (/FT /Sig, /V 5 0 R)
491        //   4 0 obj — AcroForm (/Fields [3 0 R])
492        //   5 0 obj — Sig dict (/Type /Sig, /Contents <DEADBEEF>,
493        //                        /ByteRange [0 10 20 30],
494        //                        /SubFilter /adbe.pkcs7.detached,
495        //                        /Reason (test reason),
496        //                        /M (D:20240101000000),
497        //                        /Reference [6 0 R])
498        //   6 0 obj — Ref dict (/TransformMethod /DocMDP,
499        //                        /TransformParams 7 0 R)
500        //   7 0 obj — TransformParams (/P 2)
501        let _ = field_obj_num; // used to select the field ref, but we hardcode obj 3
502
503        let mut pdf = Vec::new();
504        pdf.extend_from_slice(b"%PDF-1.7\n");
505
506        let obj1_offset = pdf.len();
507        pdf.extend_from_slice(
508            b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n",
509        );
510
511        let obj2_offset = pdf.len();
512        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
513
514        let obj3_offset = pdf.len();
515        pdf.extend_from_slice(b"3 0 obj\n<< /FT /Sig /V 5 0 R /T (Signature1) >>\nendobj\n");
516
517        let obj4_offset = pdf.len();
518        pdf.extend_from_slice(b"4 0 obj\n<< /Fields [3 0 R] >>\nendobj\n");
519
520        let obj5_offset = pdf.len();
521        // /Contents with hex string, /ByteRange, /SubFilter, /Reason, /M, /Reference
522        pdf.extend_from_slice(
523            b"5 0 obj\n\
524              << /Type /Sig\n\
525                 /Contents <DEADBEEF>\n\
526                 /ByteRange [0 10 20 30]\n\
527                 /SubFilter /adbe.pkcs7.detached\n\
528                 /Reason (test reason)\n\
529                 /M (D:20240101000000)\n\
530                 /Reference [6 0 R]\n\
531              >>\nendobj\n",
532        );
533
534        let obj6_offset = pdf.len();
535        pdf.extend_from_slice(
536            b"6 0 obj\n<< /TransformMethod /DocMDP /TransformParams 7 0 R >>\nendobj\n",
537        );
538
539        let obj7_offset = pdf.len();
540        pdf.extend_from_slice(b"7 0 obj\n<< /P 2 >>\nendobj\n");
541
542        let xref_offset = pdf.len();
543        pdf.extend_from_slice(b"xref\n");
544        pdf.extend_from_slice(b"0 8\n");
545        pdf.extend_from_slice(b"0000000000 65535 f \n");
546        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj1_offset).as_bytes());
547        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj2_offset).as_bytes());
548        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj3_offset).as_bytes());
549        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj4_offset).as_bytes());
550        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj5_offset).as_bytes());
551        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj6_offset).as_bytes());
552        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj7_offset).as_bytes());
553        pdf.extend_from_slice(b"trailer\n<< /Size 8 /Root 1 0 R >>\n");
554        pdf.extend_from_slice(format!("startxref\n{}\n%%%%EOF\n", xref_offset).as_bytes());
555        pdf
556    }
557
558    fn build_minimal_pdf(extra: &[u8]) -> Vec<u8> {
559        let mut pdf = Vec::new();
560        pdf.extend_from_slice(b"%PDF-1.7\n");
561        let obj1_offset = pdf.len();
562        pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
563        let obj2_offset = pdf.len();
564        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
565        pdf.extend_from_slice(extra);
566        let xref_offset = pdf.len();
567        pdf.extend_from_slice(b"xref\n0 3\n");
568        pdf.extend_from_slice(b"0000000000 65535 f \n");
569        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj1_offset).as_bytes());
570        pdf.extend_from_slice(format!("{:010} 00000 n \n", obj2_offset).as_bytes());
571        pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
572        pdf.extend_from_slice(format!("startxref\n{}\n%%%%EOF\n", xref_offset).as_bytes());
573        pdf
574    }
575
576    fn open_arc(data: Vec<u8>) -> (ObjectStore<Arc<[u8]>>, rpdfium_core::error::ObjectId) {
577        let arc: Arc<[u8]> = Arc::from(data);
578        let store = ObjectStore::open(arc, rpdfium_core::ParsingMode::Lenient).unwrap();
579        let cat_id = store.trailer().root;
580        (store, cat_id)
581    }
582
583    // -----------------------------------------------------------------------
584    // Unit tests using in-memory Object construction
585    // -----------------------------------------------------------------------
586
587    /// Build a SignatureObject by directly calling parse_sig_dict via a
588    /// manually constructed sig dict + empty store (Vec<u8> source).
589    fn make_empty_store() -> ObjectStore<Vec<u8>> {
590        let pdf = build_minimal_pdf(&[]);
591        ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
592    }
593
594    #[test]
595    fn test_no_acroform_returns_empty() {
596        // PDF has no /AcroForm → expect empty vec.
597        let pdf = build_minimal_pdf(&[]);
598        let (store, cat_id) = open_arc(pdf);
599        let cat = store.resolve(cat_id).unwrap();
600        let sigs = collect_signatures(cat, &store).unwrap();
601        assert!(sigs.is_empty());
602    }
603
604    #[test]
605    fn test_collects_one_signature() {
606        let pdf = build_pdf_with_sig(3);
607        let (store, cat_id) = open_arc(pdf);
608        let cat = store.resolve(cat_id).unwrap();
609        let sigs = collect_signatures(cat, &store).unwrap();
610        assert_eq!(sigs.len(), 1);
611        let sig = &sigs[0];
612        // contents
613        assert_eq!(sig.contents(), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
614        // byte_range
615        assert_eq!(sig.byte_range(), &[(0, 10), (20, 30)]);
616        // sub_filter
617        assert_eq!(sig.sub_filter(), Some("adbe.pkcs7.detached"));
618        // reason
619        assert_eq!(sig.reason(), Some("test reason"));
620        // time
621        assert_eq!(sig.time(), Some("D:20240101000000"));
622        // doc_mdp
623        assert_eq!(
624            sig.doc_mdp_permission(),
625            Some(DocMdpPermission::FormFillingAndSigning)
626        );
627    }
628
629    #[test]
630    fn test_empty_byte_range() {
631        // Build a sig dict without /ByteRange.
632        let mut sig_dict: HashMap<Name, Object> = HashMap::new();
633        sig_dict.insert(
634            Name::contents(),
635            Object::String(PdfString::from_bytes(vec![0xAA])),
636        );
637        let store = make_empty_store();
638        let sig = parse_sig_dict(&sig_dict, &store).unwrap();
639        assert_eq!(sig.byte_range(), &[]);
640    }
641
642    #[test]
643    fn test_odd_byte_range_truncates() {
644        // Odd number of elements → last one dropped.
645        let mut sig_dict: HashMap<Name, Object> = HashMap::new();
646        sig_dict.insert(
647            Name::byte_range(),
648            Object::Array(vec![
649                Object::Integer(0),
650                Object::Integer(10),
651                Object::Integer(20), // orphan — no pair
652            ]),
653        );
654        let store = make_empty_store();
655        let sig = parse_sig_dict(&sig_dict, &store).unwrap();
656        // Only one complete pair: (0, 10); the 20 is dropped.
657        assert_eq!(sig.byte_range(), &[(0, 10)]);
658    }
659
660    #[test]
661    fn test_doc_mdp_none() {
662        // Sig dict without /Reference → None.
663        let sig_dict: HashMap<Name, Object> = HashMap::new();
664        let store = make_empty_store();
665        let sig = parse_sig_dict(&sig_dict, &store).unwrap();
666        assert_eq!(sig.doc_mdp_permission(), None);
667    }
668
669    #[test]
670    fn test_doc_mdp_level_2() {
671        assert_eq!(
672            DocMdpPermission::from_p_value(2),
673            Some(DocMdpPermission::FormFillingAndSigning)
674        );
675    }
676
677    #[test]
678    fn test_doc_mdp_level_3() {
679        assert_eq!(
680            DocMdpPermission::from_p_value(3),
681            Some(DocMdpPermission::AnnotationFormFillingAndSigning)
682        );
683    }
684
685    #[test]
686    fn test_invalid_p_value() {
687        assert_eq!(DocMdpPermission::from_p_value(99), None);
688        assert_eq!(DocMdpPermission::from_p_value(0), None);
689        assert_eq!(DocMdpPermission::from_p_value(-1), None);
690    }
691
692    #[test]
693    fn test_non_sig_fields_skipped() {
694        // Build a PDF with a Tx field (obj 3) and a Sig field (obj 4).
695        // Only the Sig field should be collected.
696        let mut pdf = Vec::new();
697        pdf.extend_from_slice(b"%PDF-1.7\n");
698
699        let obj1_offset = pdf.len();
700        pdf.extend_from_slice(
701            b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 5 0 R >>\nendobj\n",
702        );
703        let obj2_offset = pdf.len();
704        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
705        // Tx field (should be skipped)
706        let obj3_offset = pdf.len();
707        pdf.extend_from_slice(b"3 0 obj\n<< /FT /Tx /T (TextField) /V (hello) >>\nendobj\n");
708        // Sig field
709        let obj4_offset = pdf.len();
710        pdf.extend_from_slice(b"4 0 obj\n<< /FT /Sig /V 6 0 R /T (Signature1) >>\nendobj\n");
711        // AcroForm with both fields
712        let obj5_offset = pdf.len();
713        pdf.extend_from_slice(b"5 0 obj\n<< /Fields [3 0 R 4 0 R] >>\nendobj\n");
714        // Sig dict for the Sig field
715        let obj6_offset = pdf.len();
716        pdf.extend_from_slice(
717            b"6 0 obj\n<< /Type /Sig /Contents <AABB> /SubFilter /test >>\nendobj\n",
718        );
719
720        let xref_offset = pdf.len();
721        pdf.extend_from_slice(b"xref\n0 7\n");
722        pdf.extend_from_slice(b"0000000000 65535 f \n");
723        for offset in &[
724            obj1_offset,
725            obj2_offset,
726            obj3_offset,
727            obj4_offset,
728            obj5_offset,
729            obj6_offset,
730        ] {
731            pdf.extend_from_slice(format!("{:010} 00000 n \n", offset).as_bytes());
732        }
733        pdf.extend_from_slice(b"trailer\n<< /Size 7 /Root 1 0 R >>\n");
734        pdf.extend_from_slice(format!("startxref\n{}\n%%%%EOF\n", xref_offset).as_bytes());
735
736        let (store, cat_id) = open_arc(pdf);
737        let cat = store.resolve(cat_id).unwrap();
738        let sigs = collect_signatures(cat, &store).unwrap();
739        assert_eq!(sigs.len(), 1, "only the Sig field should be collected");
740        assert_eq!(sigs[0].sub_filter(), Some("test"));
741    }
742
743    #[test]
744    fn test_doc_mdp_as_u32() {
745        assert_eq!(DocMdpPermission::NoChanges.as_u32(), 1);
746        assert_eq!(DocMdpPermission::FormFillingAndSigning.as_u32(), 2);
747        assert_eq!(
748            DocMdpPermission::AnnotationFormFillingAndSigning.as_u32(),
749            3
750        );
751    }
752
753    // -----------------------------------------------------------------------
754    // Gap #1: DocMDP /P default value
755    // -----------------------------------------------------------------------
756
757    #[test]
758    fn test_doc_mdp_missing_p_defaults_to_level_2() {
759        // /TransformParams dict exists but has no /P key.
760        // Upstream default: GetIntegerFor("P", 2) → FormFillingAndSigning.
761        let mut sig_dict: HashMap<Name, Object> = HashMap::new();
762        // /Reference [ << /TransformMethod /DocMDP /TransformParams << >> >> ]
763        let transform_params = Object::Dictionary(HashMap::new()); // empty — no /P
764        let mut ref_entry: HashMap<Name, Object> = HashMap::new();
765        ref_entry.insert(Name::transform_method(), Object::Name(Name::doc_mdp()));
766        ref_entry.insert(Name::transform_params(), transform_params);
767        sig_dict.insert(
768            Name::reference(),
769            Object::Array(vec![Object::Dictionary(ref_entry)]),
770        );
771        let store = make_empty_store();
772        let sig = parse_sig_dict(&sig_dict, &store).unwrap();
773        assert_eq!(
774            sig.doc_mdp_permission(),
775            Some(DocMdpPermission::FormFillingAndSigning),
776            "/P absent in /TransformParams should default to level 2"
777        );
778    }
779
780    // -----------------------------------------------------------------------
781    // Gap #3: Malformed field edge cases
782    // -----------------------------------------------------------------------
783
784    #[test]
785    fn test_contents_non_string_returns_none() {
786        // /Contents is an integer (not a string) → contents() should be None.
787        let mut sig_dict: HashMap<Name, Object> = HashMap::new();
788        sig_dict.insert(Name::contents(), Object::Integer(42));
789        let store = make_empty_store();
790        let sig = parse_sig_dict(&sig_dict, &store).unwrap();
791        assert_eq!(
792            sig.contents(),
793            None,
794            "/Contents as integer should yield None"
795        );
796    }
797
798    #[test]
799    fn test_byte_range_non_array_returns_empty() {
800        // /ByteRange is a dict (not an array) → byte_range() should be empty.
801        let mut sig_dict: HashMap<Name, Object> = HashMap::new();
802        sig_dict.insert(Name::byte_range(), Object::Dictionary(HashMap::new()));
803        let store = make_empty_store();
804        let sig = parse_sig_dict(&sig_dict, &store).unwrap();
805        assert!(
806            sig.byte_range().is_empty(),
807            "/ByteRange as dict should yield empty slice"
808        );
809    }
810
811    #[test]
812    fn test_reference_non_array_is_ignored() {
813        // /Reference is a single dict (not an array) → doc_mdp_permission() should be None.
814        let mut ref_entry: HashMap<Name, Object> = HashMap::new();
815        ref_entry.insert(Name::transform_method(), Object::Name(Name::doc_mdp()));
816        let mut tp: HashMap<Name, Object> = HashMap::new();
817        tp.insert(Name::p(), Object::Integer(1));
818        ref_entry.insert(Name::transform_params(), Object::Dictionary(tp));
819
820        let mut sig_dict: HashMap<Name, Object> = HashMap::new();
821        // Single dict instead of array — malformed per spec.
822        sig_dict.insert(Name::reference(), Object::Dictionary(ref_entry));
823        let store = make_empty_store();
824        let sig = parse_sig_dict(&sig_dict, &store).unwrap();
825        assert_eq!(
826            sig.doc_mdp_permission(),
827            None,
828            "/Reference as bare dict (not array) should yield None"
829        );
830    }
831}