Skip to main content

pdfrum_doc/form/
field.rs

1//! The AcroForm field model: what a field *is*, what it holds, and what
2//! changing it implies.
3//!
4//! A field is a dictionary reachable from the catalog's `/AcroForm /Fields`,
5//! and the tree it sits in is not the annotation tree: an interior node may
6//! carry no widget at all and exist only to prefix its children's names, and
7//! a leaf may be merged with its own widget annotation into one dictionary.
8//! Both shapes are common and neither is an error, so the walk here collects
9//! **terminal** fields — the nodes that carry an `/FT`, inherited or not —
10//! and treats everything above them as naming structure.
11//!
12//! # Values are not stored here
13//!
14//! [`Field`] is a record of where a field lives, not a copy of what it holds:
15//! its value is read back out of the dictionary on demand. That is what makes
16//! [`FieldValues`] — the edit buffer — the only mutable thing in this module,
17//! and it is why reading a field never has to be told whether someone has
18//! written to it.
19
20use pdfrum_common::{Diagnostics, Limits};
21use pdfrum_object::{Dict, Name, ObjRef, Object, Resolve};
22
23use crate::ap::{self, GeneratedAp};
24use crate::form::attr::{field_attr, full_name};
25use crate::names;
26
27/// What kind of control a form field is (ISO 32000-1 §12.7.4).
28///
29/// The variants are the `/FT` values crossed with the two `/Ff` bits that
30/// split them: a `/Btn` is a push button, a radio button or a check box
31/// depending on bits 17 and 16, and a `/Ch` is a combo box or a list box
32/// depending on bit 18.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum FieldKind {
35    /// A text field (`/Tx`).
36    Text,
37    /// A check box (`/Btn` with neither the push-button nor the radio bit).
38    Check,
39    /// A radio button (`/Btn` with bit 16 set).
40    Radio,
41    /// A push button (`/Btn` with bit 17 set) — it holds no value.
42    Button,
43    /// A drop-down (`/Ch` with bit 18 set).
44    Combo,
45    /// A list box (`/Ch` without bit 18).
46    List,
47    /// A signature field (`/Sig`).
48    Signature,
49}
50
51impl FieldKind {
52    /// Classifies a field from its `/FT` and `/Ff`.
53    ///
54    /// Returns `None` for a node with no field type at all, which is how the
55    /// walk tells a naming-only interior node from a terminal field.
56    ///
57    /// ```
58    /// use pdfrum_doc::form::{FieldFlags, FieldKind};
59    ///
60    /// assert_eq!(FieldKind::classify(b"Tx", FieldFlags::default()), Some(FieldKind::Text));
61    /// // A `/Btn` splits three ways on its flags.
62    /// let radio = FieldFlags::from_bits(1 << 15);
63    /// assert_eq!(FieldKind::classify(b"Btn", radio), Some(FieldKind::Radio));
64    /// assert_eq!(FieldKind::classify(b"Btn", FieldFlags::default()), Some(FieldKind::Check));
65    /// // No field type at all: an interior naming node, not a field.
66    /// assert_eq!(FieldKind::classify(b"", FieldFlags::default()), None);
67    /// ```
68    #[must_use]
69    pub fn classify(field_type: &[u8], flags: FieldFlags) -> Option<FieldKind> {
70        match field_type {
71            b"Tx" => Some(FieldKind::Text),
72            b"Sig" => Some(FieldKind::Signature),
73            b"Btn" => Some(if flags.is_push_button() {
74                FieldKind::Button
75            } else if flags.is_radio() {
76                FieldKind::Radio
77            } else {
78                FieldKind::Check
79            }),
80            b"Ch" => Some(if flags.is_combo() {
81                FieldKind::Combo
82            } else {
83                FieldKind::List
84            }),
85            _ => None,
86        }
87    }
88
89    /// Whether the field holds a value a caller can write.
90    ///
91    /// False only for [`FieldKind::Button`], which fires an action rather
92    /// than storing anything, and [`FieldKind::Signature`], whose value is a
93    /// signature dictionary this crate does not synthesize.
94    ///
95    /// ```
96    /// use pdfrum_doc::form::FieldKind;
97    ///
98    /// assert!(FieldKind::Text.is_writable());
99    /// assert!(!FieldKind::Button.is_writable());
100    /// assert!(!FieldKind::Signature.is_writable());
101    /// ```
102    #[must_use]
103    pub fn is_writable(self) -> bool {
104        !matches!(self, FieldKind::Button | FieldKind::Signature)
105    }
106
107    /// Whether the field is one of the two on/off controls, whose value is a
108    /// state name rather than free text.
109    ///
110    /// ```
111    /// use pdfrum_doc::form::FieldKind;
112    ///
113    /// assert!(FieldKind::Check.is_toggle());
114    /// assert!(FieldKind::Radio.is_toggle());
115    /// assert!(!FieldKind::Combo.is_toggle());
116    /// ```
117    #[must_use]
118    pub fn is_toggle(self) -> bool {
119        matches!(self, FieldKind::Check | FieldKind::Radio)
120    }
121}
122
123/// A field's `/Ff` flag word (ISO 32000-1 tables 227–230).
124///
125/// Kept as the raw word rather than a set, and **deliberately without the
126/// `contains` / `union` algebra its two sibling flag types have**: the meaning
127/// of a bit depends on the field type, so the same bit 26 is "file select" on
128/// a text field and "sort" on a choice field, and `FieldFlags::COMBO |
129/// FieldFlags::MULTILINE` would be a lie. The predicates below — the ones
130/// whose reading is type-independent or whose type is implied by the name —
131/// are the API. [`FieldFlags::bits`] and [`FieldFlags::from_bits`] exist for
132/// round-tripping the word itself, unknown bits included.
133///
134/// ```
135/// use pdfrum_doc::form::FieldFlags;
136///
137/// // Bit 1 is `ReadOnly` on every field type.
138/// assert!(FieldFlags::from_bits(1).is_read_only());
139/// // A reserved bit survives the trip.
140/// assert_eq!(FieldFlags::from_bits(1 << 40).bits(), 1 << 40);
141/// ```
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
143pub struct FieldFlags(i64);
144
145impl FieldFlags {
146    /// The raw `/Ff` word, including every bit no predicate here reads.
147    ///
148    /// ```
149    /// use pdfrum_doc::form::FieldFlags;
150    ///
151    /// assert_eq!(FieldFlags::from_bits(1 << 12).bits(), 1 << 12);
152    /// ```
153    #[must_use]
154    pub const fn bits(self) -> i64 {
155        self.0
156    }
157
158    /// The word as written in the file. **Unknown bits are retained**: a bit
159    /// whose meaning belongs to a `/FT` this type knows nothing about is
160    /// kept, not dropped.
161    ///
162    /// ```
163    /// use pdfrum_doc::form::FieldFlags;
164    ///
165    /// assert!(FieldFlags::from_bits(1).is_read_only());
166    /// // A bit belonging to a `/FT` this type knows nothing about survives.
167    /// assert_eq!(FieldFlags::from_bits(1 << 40).bits(), 1 << 40);
168    /// ```
169    #[must_use]
170    pub const fn from_bits(bits: i64) -> Self {
171        Self(bits)
172    }
173
174    /// Bit 1: the field may not be changed.
175    ///
176    /// ```
177    /// use pdfrum_doc::form::FieldFlags;
178    ///
179    /// assert!(FieldFlags::from_bits(1 << 0).is_read_only());
180    /// ```
181    #[must_use]
182    pub const fn is_read_only(self) -> bool {
183        self.0 & (1 << 0) != 0
184    }
185
186    /// Bit 2: the field must have a value when the form is submitted.
187    ///
188    /// ```
189    /// use pdfrum_doc::form::FieldFlags;
190    ///
191    /// assert!(FieldFlags::from_bits(1 << 1).is_required());
192    /// ```
193    #[must_use]
194    pub const fn is_required(self) -> bool {
195        self.0 & (1 << 1) != 0
196    }
197
198    /// Bit 16, on a `/Btn`: the field is a radio button rather than a check
199    /// box.
200    ///
201    /// ```
202    /// use pdfrum_doc::form::FieldFlags;
203    ///
204    /// assert!(FieldFlags::from_bits(1 << 15).is_radio());
205    /// ```
206    #[must_use]
207    pub const fn is_radio(self) -> bool {
208        self.0 & (1 << 15) != 0
209    }
210
211    /// Bit 17, on a `/Btn`: the field is a push button and holds no value.
212    ///
213    /// ```
214    /// use pdfrum_doc::form::FieldFlags;
215    ///
216    /// assert!(FieldFlags::from_bits(1 << 16).is_push_button());
217    /// ```
218    #[must_use]
219    pub const fn is_push_button(self) -> bool {
220        self.0 & (1 << 16) != 0
221    }
222
223    /// Bit 18, on a `/Ch`: the field is a drop-down rather than a list box.
224    ///
225    /// ```
226    /// use pdfrum_doc::form::FieldFlags;
227    ///
228    /// assert!(FieldFlags::from_bits(1 << 17).is_combo());
229    /// ```
230    #[must_use]
231    pub const fn is_combo(self) -> bool {
232        self.0 & (1 << 17) != 0
233    }
234
235    /// Bit 19, on a `/Ch`: the combo box includes an editable text box.
236    ///
237    /// ```
238    /// use pdfrum_doc::form::FieldFlags;
239    ///
240    /// assert!(FieldFlags::from_bits(1 << 18).is_editable_combo());
241    /// ```
242    #[must_use]
243    pub const fn is_editable_combo(self) -> bool {
244        self.0 & (1 << 18) != 0
245    }
246
247    /// Bit 22, on a `/Ch`: more than one option may be selected at once.
248    ///
249    /// ```
250    /// use pdfrum_doc::form::FieldFlags;
251    ///
252    /// assert!(FieldFlags::from_bits(1 << 21).is_multi_select());
253    /// ```
254    #[must_use]
255    pub const fn is_multi_select(self) -> bool {
256        self.0 & (1 << 21) != 0
257    }
258
259    /// Bit 13, on a `/Tx`: the field accepts more than one line.
260    ///
261    /// ```
262    /// use pdfrum_doc::form::FieldFlags;
263    ///
264    /// assert!(FieldFlags::from_bits(1 << 12).is_multiline());
265    /// ```
266    #[must_use]
267    pub const fn is_multiline(self) -> bool {
268        self.0 & (1 << 12) != 0
269    }
270
271    /// Bit 14, on a `/Tx`: the field's contents are obscured as they are
272    /// typed.
273    ///
274    /// ```
275    /// use pdfrum_doc::form::FieldFlags;
276    ///
277    /// assert!(FieldFlags::from_bits(1 << 13).is_password());
278    /// ```
279    #[must_use]
280    pub const fn is_password(self) -> bool {
281        self.0 & (1 << 13) != 0
282    }
283
284    /// Bit 25, on a `/Tx`: the text is laid out in equally spaced cells.
285    ///
286    /// ```
287    /// use pdfrum_doc::form::FieldFlags;
288    ///
289    /// assert!(FieldFlags::from_bits(1 << 24).is_comb());
290    /// ```
291    #[must_use]
292    #[doc(alias = "Comb")]
293    pub const fn is_comb(self) -> bool {
294        self.0 & (1 << 24) != 0
295    }
296
297    /// Bit 24, on a `/Tx`: whether the field scrolls to fit more text than
298    /// its rectangle holds.
299    ///
300    /// The positive reading of the spec's `DoNotScroll` bit: a field scrolls
301    /// *unless* the bit is set.
302    ///
303    /// ```
304    /// use pdfrum_doc::form::FieldFlags;
305    ///
306    /// // The positive reading: a field scrolls unless `DoNotScroll` is set.
307    /// assert!(FieldFlags::default().scrolls());
308    /// assert!(!FieldFlags::from_bits(1 << 23).scrolls());
309    /// ```
310    #[must_use]
311    #[doc(alias = "DoNotScroll")]
312    pub const fn scrolls(self) -> bool {
313        self.0 & (1 << 23) == 0
314    }
315
316    /// Bit 23, on a `/Tx` or `/Ch`: whether the value is spell-checked.
317    ///
318    /// The positive reading of the spec's `DoNotSpellCheck` bit.
319    ///
320    /// ```
321    /// use pdfrum_doc::form::FieldFlags;
322    ///
323    /// assert!(FieldFlags::default().spell_checks());
324    /// assert!(!FieldFlags::from_bits(1 << 22).spell_checks());
325    /// ```
326    #[must_use]
327    #[doc(alias = "DoNotSpellCheck")]
328    pub const fn spell_checks(self) -> bool {
329        self.0 & (1 << 22) == 0
330    }
331}
332
333/// One terminal form field.
334///
335/// A record of *where* the field is — the dictionary, the reference that names
336/// it, its widgets — plus the classification derived once at load. What it
337/// currently holds is read back through [`Field::value`], because a
338/// [`FieldValues`] edit may have superseded the file's own `/V`.
339#[derive(Debug, Clone, PartialEq)]
340pub struct Field {
341    /// The field's own dictionary.
342    pub dict: Dict,
343    /// The reference that names it, when it has one. A field written inline
344    /// in its parent's `/Kids` has none, and cannot be written back.
345    pub reference: Option<ObjRef>,
346    /// The fully-qualified name: the ancestors' `/T` values and its own,
347    /// joined with dots.
348    pub name: String,
349    /// What kind of control it is.
350    pub kind: FieldKind,
351    /// The `/Ff` flag word, read through the inheritance chain.
352    pub flags: FieldFlags,
353    /// The widget annotations that draw it.
354    ///
355    /// Usually one. A radio group has one per button, and a field whose
356    /// dictionary *is* its widget has one that is the field itself.
357    pub widgets: Vec<Widget>,
358}
359
360/// One widget annotation drawing a field.
361#[derive(Debug, Clone, PartialEq)]
362pub struct Widget {
363    /// The widget's dictionary. Equal to the field's when the two are merged.
364    pub dict: Dict,
365    /// The reference naming it, when it has one.
366    pub reference: Option<ObjRef>,
367}
368
369impl Field {
370    /// The field's current value, as text.
371    ///
372    /// `values` is consulted first, so a field written through
373    /// [`FieldValues::set`] reads back as what was written rather than what
374    /// the file holds. Pass `None` to read the file's own `/V`.
375    ///
376    /// For a check box or radio button this is the *state name* — `Off` for
377    /// clear, and whatever the widget's `/AP /N` calls its on-state
378    /// otherwise. Use [`Field::is_checked`] for the boolean.
379    ///
380    /// ```
381    /// use pdfrum_common::{Diagnostics, Limits};
382    /// use pdfrum_doc::form::Form;
383    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
384    ///
385    /// let field = Dict::from_pairs([
386    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
387    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
388    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
389    /// ]);
390    /// let catalog = Dict::from_pairs([(
391    ///     Name::from("AcroForm"),
392    ///     Object::Dict(Dict::from_pairs([(
393    ///         Name::from("Fields"),
394    ///         Object::Array(Array::of([Object::Dict(field)])),
395    ///     )])),
396    /// )]);
397    ///
398    /// let mut diags = Diagnostics::default();
399    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
400    ///     .expect("the catalog declares an /AcroForm");
401    /// use pdfrum_doc::form::FieldValues;
402    ///
403    /// let field = form.field("name").expect("one terminal field");
404    /// assert_eq!(field.value(None, &NoResolve), "Ada");
405    ///
406    /// // An edit supersedes the file's own `/V`.
407    /// let mut values = FieldValues::new();
408    /// values.set("name", "Grace");
409    /// assert_eq!(field.value(Some(&values), &NoResolve), "Grace");
410    /// ```
411    #[must_use]
412    pub fn value<R: Resolve>(&self, values: Option<&FieldValues>, r: &R) -> String {
413        if let Some(edited) = values.and_then(|values| values.get(&self.name)) {
414            return edited.to_owned();
415        }
416        self.stored_value(r)
417    }
418
419    /// The value the *file* holds, ignoring any edit.
420    ///
421    /// ```
422    /// use pdfrum_common::{Diagnostics, Limits};
423    /// use pdfrum_doc::form::Form;
424    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
425    ///
426    /// let field = Dict::from_pairs([
427    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
428    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
429    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
430    /// ]);
431    /// let catalog = Dict::from_pairs([(
432    ///     Name::from("AcroForm"),
433    ///     Object::Dict(Dict::from_pairs([(
434    ///         Name::from("Fields"),
435    ///         Object::Array(Array::of([Object::Dict(field)])),
436    ///     )])),
437    /// )]);
438    ///
439    /// let mut diags = Diagnostics::default();
440    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
441    ///     .expect("the catalog declares an /AcroForm");
442    ///
443    /// let field = form.field("name").expect("one terminal field");
444    /// assert_eq!(field.stored_value(&NoResolve), "Ada");
445    /// ```
446    #[must_use]
447    pub fn stored_value<R: Resolve>(&self, r: &R) -> String {
448        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
449        field_attr(&self.dict, names::V, r, &limits, &mut diags)
450            .map(|value| value.to_text())
451            .unwrap_or_default()
452    }
453
454    /// The field's default value (`/DV`) — what a form reset restores.
455    #[must_use]
456    pub fn default_value<R: Resolve>(&self, r: &R) -> String {
457        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
458        field_attr(&self.dict, names::DV, r, &limits, &mut diags)
459            .map(|value| value.to_text())
460            .unwrap_or_default()
461    }
462
463    /// Whether a check box or radio button is on.
464    ///
465    /// Always false for a field that is not a toggle. A state of `Off`, and an
466    /// absent state, both read as clear — every other name is on, which is the
467    /// spec's own rule.
468    #[must_use]
469    pub fn is_checked<R: Resolve>(&self, values: Option<&FieldValues>, r: &R) -> bool {
470        if !self.kind.is_toggle() {
471            return false;
472        }
473        let value = self.value(values, r);
474        !value.is_empty() && value != "Off"
475    }
476
477    /// The states a check box or radio button can take, from its widgets'
478    /// `/AP /N` sub-dictionaries.
479    ///
480    /// `Off` is included when a widget offers it. The order is the widgets'
481    /// order, then each widget's own appearance-dictionary order.
482    #[must_use]
483    pub fn states<R: Resolve>(&self, r: &R) -> Vec<String> {
484        let mut out: Vec<String> = Vec::new();
485        for widget in &self.widgets {
486            let Some(ap) = widget.dict.dict(names::AP, r) else {
487                continue;
488            };
489            let Some(normal) = ap.dict(names::N, r) else {
490                continue;
491            };
492            for key in normal.keys() {
493                let state = String::from_utf8_lossy(key.as_bytes()).into_owned();
494                if !out.contains(&state) {
495                    out.push(state);
496                }
497            }
498        }
499        out
500    }
501
502    /// A choice field's selectable options (`/Opt`).
503    ///
504    /// An entry written as a two-element array is an export-value/label pair;
505    /// the label is what a reader shows, and that is what comes back here.
506    #[must_use]
507    pub fn options<R: Resolve>(&self, r: &R) -> Vec<String> {
508        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
509        let Some(opt) = field_attr(&self.dict, names::OPT, r, &limits, &mut diags) else {
510            return Vec::new();
511        };
512        let Some(array) = opt.as_array() else {
513            return Vec::new();
514        };
515        (0..array.len())
516            .map(|index| {
517                let Some(entry) = array.get(index, r) else {
518                    return String::new();
519                };
520                // A pair is [export, label]; a bare string is both.
521                match entry.get() {
522                    Object::Array(pair) => pair
523                        .raw_at(1)
524                        .or_else(|| pair.raw_at(0))
525                        .map(Object::to_text)
526                        .unwrap_or_default(),
527                    object => object.to_text(),
528                }
529            })
530            .collect()
531    }
532
533    /// The field's user-facing tooltip (`/TU`), when it has one.
534    #[must_use]
535    pub fn tooltip<R: Resolve>(&self, r: &R) -> Option<String> {
536        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
537        field_attr(&self.dict, names::TU, r, &limits, &mut diags).map(|value| value.to_text())
538    }
539}
540
541/// Every terminal field of a document's interactive form.
542///
543/// Loaded once from the catalog; the walk is the expensive part and nothing
544/// below repeats it.
545#[derive(Debug, Clone, Default, PartialEq)]
546pub struct Form {
547    /// The terminal fields, in the order the `/Fields` tree reaches them.
548    pub fields: Vec<Field>,
549    /// Whether the form asks a reader to regenerate every widget's appearance
550    /// (`/NeedAppearances`).
551    pub need_appearances: bool,
552}
553
554/// How deep the field tree may nest before the walk gives up.
555///
556/// The same cap the name-tree walks use, for the same reason: a `/Kids` cycle
557/// is stopped by the visited set, but a legitimately deep tree still has to
558/// end somewhere.
559const MAX_FIELD_DEPTH: u32 = 32;
560
561impl Form {
562    /// Loads the document's form, or nothing when the catalog declares none.
563    ///
564    /// A catalog with an `/AcroForm` whose `/Fields` is absent or empty still
565    /// yields a `Form` — an empty form is a different thing from no form, and
566    /// only the second means "this document is not interactive".
567    ///
568    /// ```
569    /// use pdfrum_common::{Diagnostics, Limits};
570    /// use pdfrum_doc::form::Form;
571    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
572    ///
573    /// let field = Dict::from_pairs([
574    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
575    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
576    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
577    /// ]);
578    /// let catalog = Dict::from_pairs([(
579    ///     Name::from("AcroForm"),
580    ///     Object::Dict(Dict::from_pairs([(
581    ///         Name::from("Fields"),
582    ///         Object::Array(Array::of([Object::Dict(field)])),
583    ///     )])),
584    /// )]);
585    ///
586    /// let mut diags = Diagnostics::default();
587    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
588    ///     .expect("the catalog declares an /AcroForm");
589    ///
590    /// assert_eq!(form.len(), 1);
591    /// assert_eq!(form.fields[0].name, "name");
592    /// ```
593    #[must_use]
594    pub fn load<R: Resolve>(
595        catalog: &Dict,
596        r: &R,
597        limits: &Limits,
598        diags: &mut Diagnostics,
599    ) -> Option<Form> {
600        let acro = catalog.dict(names::ACRO_FORM, r)?;
601        let need_appearances = acro
602            .get(names::NEED_APPEARANCES, r)
603            .and_then(|value| value.as_direct().and_then(Object::as_bool))
604            .unwrap_or(false);
605        let mut form = Form {
606            fields: Vec::new(),
607            need_appearances,
608        };
609        let Some(fields) = acro.array(names::FIELDS, r) else {
610            return Some(form);
611        };
612        let mut seen: Vec<ObjRef> = Vec::new();
613        for index in 0..fields.len() {
614            let reference = fields.reference_at(index);
615            let Some(dict) = fields.dict_at(index, r) else {
616                continue;
617            };
618            visit(
619                &dict,
620                reference,
621                0,
622                &mut seen,
623                &mut form.fields,
624                r,
625                limits,
626                diags,
627            );
628        }
629        Some(form)
630    }
631
632    /// How many terminal fields the form has.
633    ///
634    /// ```
635    /// use pdfrum_common::{Diagnostics, Limits};
636    /// use pdfrum_doc::form::Form;
637    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
638    ///
639    /// let field = Dict::from_pairs([
640    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
641    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
642    /// ]);
643    /// let catalog = Dict::from_pairs([(
644    ///     Name::from("AcroForm"),
645    ///     Object::Dict(Dict::from_pairs([(
646    ///         Name::from("Fields"),
647    ///         Object::Array(Array::of([Object::Dict(field)])),
648    ///     )])),
649    /// )]);
650    ///
651    /// let mut diags = Diagnostics::default();
652    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
653    ///     .expect("the catalog declares an /AcroForm");
654    ///
655    /// assert_eq!(form.len(), 1);
656    /// ```
657    #[must_use]
658    pub fn len(&self) -> usize {
659        self.fields.len()
660    }
661
662    /// Whether the form has no fields at all.
663    ///
664    /// ```
665    /// use pdfrum_common::{Diagnostics, Limits};
666    /// use pdfrum_doc::form::Form;
667    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
668    ///
669    /// let field = Dict::from_pairs([
670    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
671    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
672    /// ]);
673    /// let catalog = Dict::from_pairs([(
674    ///     Name::from("AcroForm"),
675    ///     Object::Dict(Dict::from_pairs([(
676    ///         Name::from("Fields"),
677    ///         Object::Array(Array::of([Object::Dict(field)])),
678    ///     )])),
679    /// )]);
680    ///
681    /// let mut diags = Diagnostics::default();
682    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
683    ///     .expect("the catalog declares an /AcroForm");
684    ///
685    /// assert!(!form.is_empty());
686    /// ```
687    #[must_use]
688    pub fn is_empty(&self) -> bool {
689        self.fields.is_empty()
690    }
691
692    /// The field with this fully-qualified name.
693    ///
694    /// ```
695    /// use pdfrum_common::{Diagnostics, Limits};
696    /// use pdfrum_doc::form::Form;
697    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
698    ///
699    /// let field = Dict::from_pairs([
700    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
701    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
702    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
703    /// ]);
704    /// let catalog = Dict::from_pairs([(
705    ///     Name::from("AcroForm"),
706    ///     Object::Dict(Dict::from_pairs([(
707    ///         Name::from("Fields"),
708    ///         Object::Array(Array::of([Object::Dict(field)])),
709    ///     )])),
710    /// )]);
711    ///
712    /// let mut diags = Diagnostics::default();
713    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
714    ///     .expect("the catalog declares an /AcroForm");
715    ///
716    /// assert!(form.field("name").is_some());
717    /// assert!(form.field("absent").is_none());
718    /// ```
719    #[must_use]
720    pub fn field(&self, name: &str) -> Option<&Field> {
721        self.fields.iter().find(|field| field.name == name)
722    }
723
724    /// The order a recalculation visits fields in, read from `/AcroForm /CO`.
725    ///
726    /// Indices into [`Form::fields`], in the order the array lists them.
727    ///
728    /// # An absent `/CO` is the answer, not a fallback
729    ///
730    /// A document with no `/CO` array recalculates **nothing**, however many
731    /// of its fields carry an `/AA /C` script: the sweep that drives
732    /// calculation walks exactly this list and nothing else. So an empty
733    /// answer here is "no calculation runs", and a reader tempted to fall
734    /// back to "every field, in `/Fields` order" would recalculate documents
735    /// that must be left alone — visibly, on any file with a calculation
736    /// script and no `/CO`.
737    ///
738    /// Entries that resolve to nothing, to a non-dictionary, or to a
739    /// dictionary that is not one of this form's terminal fields are dropped.
740    /// Duplicates are kept: the array is the order, and it is indexed
741    /// positionally.
742    ///
743    /// ```
744    /// use pdfrum_common::{Diagnostics, Limits};
745    /// use pdfrum_doc::form::Form;
746    /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
747    ///
748    /// let field = Dict::from_pairs([
749    ///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
750    ///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
751    ///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
752    /// ]);
753    /// let catalog = Dict::from_pairs([(
754    ///     Name::from("AcroForm"),
755    ///     Object::Dict(Dict::from_pairs([(
756    ///         Name::from("Fields"),
757    ///         Object::Array(Array::of([Object::Dict(field)])),
758    ///     )])),
759    /// )]);
760    ///
761    /// let mut diags = Diagnostics::default();
762    /// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
763    ///     .expect("the catalog declares an /AcroForm");
764    ///
765    /// // No `/CO`: nothing recalculates. That is the answer, not a fallback.
766    /// assert!(form.calculation_order(&catalog, &NoResolve).is_empty());
767    /// ```
768    #[must_use]
769    pub fn calculation_order<R: Resolve>(&self, catalog: &Dict, r: &R) -> Vec<usize> {
770        let Some(acro) = catalog.dict(names::ACRO_FORM, r) else {
771            return Vec::new();
772        };
773        let Some(order) = acro.array(names::CALCULATION_ORDER, r) else {
774            return Vec::new();
775        };
776        let mut out = Vec::new();
777        for index in 0..order.len() {
778            // The reference identifies the field where there is one, which is
779            // the ordinary shape — `/CO` holds indirect references to the same
780            // field dictionaries `/Fields` does. A directly-written entry is
781            // matched on the dictionary itself, which is what `GetFieldByDict`
782            // compares.
783            let reference = order.reference_at(index);
784            let dict = order.dict_at(index, r);
785            let found = self
786                .fields
787                .iter()
788                .position(|field| match (reference, &dict) {
789                    (Some(reference), _) if field.reference == Some(reference) => true,
790                    (_, Some(dict)) => field.reference.is_none() && &field.dict == dict,
791                    _ => false,
792                });
793            if let Some(found) = found {
794                out.push(found);
795            }
796        }
797        out
798    }
799}
800
801/// Walks one node of the field tree, collecting the terminal fields under it.
802#[allow(clippy::too_many_arguments)]
803fn visit<R: Resolve>(
804    dict: &Dict,
805    reference: Option<ObjRef>,
806    depth: u32,
807    seen: &mut Vec<ObjRef>,
808    out: &mut Vec<Field>,
809    r: &R,
810    limits: &Limits,
811    diags: &mut Diagnostics,
812) {
813    if depth > MAX_FIELD_DEPTH {
814        diags.record(
815            pdfrum_common::Severity::Suspicious,
816            pdfrum_common::DiagKind::TreeDepthExceeded,
817            None,
818        );
819        return;
820    }
821    // A `/Kids` cycle would otherwise spin forever. Only referenced nodes can
822    // close one; an inline dictionary is a fresh value every time.
823    if let Some(reference) = reference {
824        if seen.contains(&reference) {
825            diags.record(
826                pdfrum_common::Severity::Recovered,
827                pdfrum_common::DiagKind::NavigationCycle,
828                None,
829            );
830            return;
831        }
832        seen.push(reference);
833    }
834
835    let kids = dict.array(names::KIDS, r);
836    let field_type = field_attr(dict, names::FT, r, limits, diags)
837        .map(|value| value.to_byte_string())
838        .unwrap_or_default();
839    let flags = FieldFlags::from_bits(
840        field_attr(dict, names::FF, r, limits, diags)
841            .and_then(|value| value.as_int())
842            .unwrap_or(0),
843    );
844
845    // A node is terminal when it has a field type and its kids — if any — are
846    // widgets rather than further fields. A kid carrying its own `/T` is a
847    // field in its own right, and makes this node naming structure even
848    // though it has an `/FT` to inherit down.
849    let kids_are_fields = kids.as_ref().is_some_and(|kids| {
850        (0..kids.len()).any(|index| {
851            kids.dict_at(index, r)
852                .is_some_and(|kid| kid.contains_key(names::T))
853        })
854    });
855
856    if let Some(kind) = FieldKind::classify(&field_type, flags)
857        && !kids_are_fields
858    {
859        let name = full_name(dict, r);
860        // A field's fully-qualified name is its **identity**, not a label:
861        // the merge below keys on it, `Form::field` is the only public lookup,
862        // and `pdfrum-form` allocates one `FieldId` per distinct name. So an
863        // empty name is not merely an unaddressable field — it is a field that
864        // every *other* unnamed field in the document would be merged into.
865        // ISO 32000-1 §12.7.3.2 makes the fully qualified name the thing an
866        // action, an export or a JavaScript reference names a field by, and a
867        // node with no `/T` anywhere in its ancestry has none, so there is
868        // nothing a caller could do with the entry. Upstream drops it too
869        // (`cpdf_interactiveform.cpp:914-917`, `AddTerminalField`).
870        if name.is_empty() {
871            diags.record(
872                pdfrum_common::Severity::Suspicious,
873                pdfrum_common::DiagKind::FieldSkippedNoName,
874                None,
875            );
876            return;
877        }
878        let widgets = widgets_of(dict, reference, kids.as_ref(), r);
879        // A name already in the tree gets these widgets **added as further
880        // controls** rather than a second field of its own. Upstream's
881        // `AddTerminalField` looks the name up first and only builds a
882        // `CPDF_FormField` when it is new, so two `/Annots` entries sharing a
883        // `/T` are one field with two controls — and the value every one of
884        // them shows is the *field's*, which is the first dictionary's.
885        // `bug_733528` is exactly that: two widgets named `SharedField`, the
886        // first holding `/V (Hello, world)` and the second `/V ()`, and the
887        // golden reports the second drawing the first's text.
888        if let Some(existing) = out.iter_mut().find(|field| field.name == name) {
889            existing.widgets.extend(widgets);
890            return;
891        }
892        out.push(Field {
893            name,
894            kind,
895            flags,
896            widgets,
897            dict: dict.clone(),
898            reference,
899        });
900        return;
901    }
902
903    let Some(kids) = kids else {
904        // No `/FT` on this dictionary or its `/Parent`, and no `/Kids` to
905        // inherit one down: upstream's `AddTerminalField` returns here
906        // (`cpdf_interactiveform.cpp:905-912`, "Key \"FT\" is required for
907        // terminal fields") and the dictionary contributes no field at all.
908        if field_type.is_empty() {
909            diags.record(
910                pdfrum_common::Severity::Suspicious,
911                pdfrum_common::DiagKind::FieldSkippedNoType,
912                None,
913            );
914        }
915        return;
916    };
917    for index in 0..kids.len() {
918        let kid_ref = kids.reference_at(index);
919        // [oracle-bug] A `/Kids` entry that is not a dictionary costs *that
920        // entry* and nothing else. `CPDF_InteractiveForm::LoadField` reads
921        // `kids->GetDictAt(0)` and returns outright when it is null
922        // (`cpdf_interactiveform.cpp:871-874`), so one unresolvable first kid
923        // silently discards every sibling under the node — a whole page of
924        // fields lost to one broken reference. Nothing recovers them: the
925        // walk has already returned, and `FixPageFields` only re-enters
926        // through `/Annots`.
927        //
928        // That `GetDictAt(0)` is a **probe**, not a guard: the two lines after
929        // it (`:876-880`) ask whether the first kid has `/T` or `/Kids` to
930        // decide whether this node is the terminal field or a branch. The
931        // early return is what happens when the probe cannot be taken, and it
932        // throws away the siblings as a side effect rather than as a
933        // decision — a non-dict first kid says nothing about whether the
934        // *array* is a field tree. Our own probe (`kids_are_fields` above)
935        // scans every kid rather than only the first, so a null at index 0
936        // does not blind it and there is nothing to recover from.
937        //
938        // pdf.js is the tiebreaker and skips the entry: `#collectFieldObjects`
939        // (`src/core/document.js`) recurses per kid and its
940        // `if (!(fieldRef instanceof Ref) || visitedRefs.has(fieldRef))`
941        // guard returns from *that* kid alone, leaving the loop to continue
942        // with the siblings. ISO 32000-1 §12.7.3.1 says `/Kids` holds the
943        // field's children and gives no rule making the array's validity
944        // depend on its first element.
945        let Some(kid) = kids.dict_at(index, r) else {
946            continue;
947        };
948        visit(&kid, kid_ref, depth + 1, seen, out, r, limits, diags);
949    }
950}
951
952/// The widgets drawing a terminal field.
953///
954/// Either the field's kids — a radio group's buttons, or a field split across
955/// pages — or the field's own dictionary when the two are merged, which is
956/// the common single-widget shape.
957fn widgets_of<R: Resolve>(
958    dict: &Dict,
959    reference: Option<ObjRef>,
960    kids: Option<&pdfrum_object::Array>,
961    r: &R,
962) -> Vec<Widget> {
963    if let Some(kids) = kids
964        && !kids.is_empty()
965    {
966        let found: Vec<Widget> = (0..kids.len())
967            .filter_map(|index| {
968                let kid = kids.dict_at(index, r)?;
969                Some(Widget {
970                    reference: kids.reference_at(index),
971                    dict: kid,
972                })
973            })
974            .collect();
975        if !found.is_empty() {
976            return found;
977        }
978    }
979    // Merged field-and-widget: the field dictionary is the annotation.
980    if dict.byte_string(names::SUBTYPE, r).as_deref() == Some(b"Widget") {
981        return vec![Widget {
982            dict: dict.clone(),
983            reference,
984        }];
985    }
986    Vec::new()
987}
988
989/// Values written to a form's fields, keyed by fully-qualified name.
990///
991/// The edit buffer, and the reason this crate can fill a form without
992/// mutating anything: the parser's object store is immutable and its objects
993/// are values, so a write is recorded here and every reader consults it. It
994/// is the same shape as the appearance
995/// [`AnnotOverlay`](crate::AnnotOverlay), for the same reason.
996///
997/// Turning the buffer into a file is [`apply`]'s job.
998#[derive(Debug, Clone, Default, PartialEq, Eq)]
999pub struct FieldValues {
1000    entries: Vec<(String, String)>,
1001}
1002
1003impl FieldValues {
1004    /// An empty buffer.
1005    ///
1006    /// ```
1007    /// use pdfrum_doc::form::FieldValues;
1008    ///
1009    /// assert!(FieldValues::new().is_empty());
1010    /// ```
1011    #[must_use]
1012    pub fn new() -> FieldValues {
1013        FieldValues::default()
1014    }
1015
1016    /// Records a value for the field with this fully-qualified name,
1017    /// replacing any earlier one.
1018    ///
1019    /// ```
1020    /// use pdfrum_doc::form::FieldValues;
1021    ///
1022    /// let mut values = FieldValues::new();
1023    /// values.set("name", "Ada");
1024    /// // Writing again replaces, it does not append.
1025    /// values.set("name", "Grace");
1026    /// assert_eq!(values.get("name"), Some("Grace"));
1027    /// assert_eq!(values.len(), 1);
1028    /// ```
1029    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
1030        let name = name.into();
1031        let value = value.into();
1032        match self.entries.iter_mut().find(|(key, _)| *key == name) {
1033            Some(entry) => entry.1 = value,
1034            None => self.entries.push((name, value)),
1035        }
1036    }
1037
1038    /// What was written for this field, if anything.
1039    ///
1040    /// ```
1041    /// use pdfrum_doc::form::FieldValues;
1042    ///
1043    /// let mut values = FieldValues::new();
1044    /// values.set("name", "Ada");
1045    /// assert_eq!(values.get("name"), Some("Ada"));
1046    /// assert_eq!(values.get("absent"), None);
1047    /// ```
1048    #[must_use]
1049    pub fn get(&self, name: &str) -> Option<&str> {
1050        self.entries
1051            .iter()
1052            .find(|(key, _)| key == name)
1053            .map(|(_, value)| value.as_str())
1054    }
1055
1056    /// Every recorded write, in the order it was first made.
1057    ///
1058    /// ```
1059    /// use pdfrum_doc::form::FieldValues;
1060    ///
1061    /// let mut values = FieldValues::new();
1062    /// values.set("b", "2");
1063    /// values.set("a", "1");
1064    /// // First-write order, not sorted.
1065    /// assert_eq!(values.iter().collect::<Vec<_>>(), [("b", "2"), ("a", "1")]);
1066    /// ```
1067    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
1068        self.entries
1069            .iter()
1070            .map(|(name, value)| (name.as_str(), value.as_str()))
1071    }
1072
1073    /// How many fields have been written to.
1074    ///
1075    /// ```
1076    /// use pdfrum_doc::form::FieldValues;
1077    ///
1078    /// let mut values = FieldValues::new();
1079    /// values.set("name", "Ada");
1080    /// assert_eq!(values.len(), 1);
1081    /// ```
1082    #[must_use]
1083    pub fn len(&self) -> usize {
1084        self.entries.len()
1085    }
1086
1087    /// Whether nothing has been written.
1088    ///
1089    /// ```
1090    /// use pdfrum_doc::form::FieldValues;
1091    ///
1092    /// let mut values = FieldValues::new();
1093    /// values.set("name", "Ada");
1094    /// assert!(!values.is_empty());
1095    /// ```
1096    #[must_use]
1097    pub fn is_empty(&self) -> bool {
1098        self.entries.is_empty()
1099    }
1100}
1101
1102/// One field's edited dictionary, and the widget appearances that follow from
1103/// it.
1104#[derive(Debug, Clone, PartialEq)]
1105pub struct FieldEdit {
1106    /// The reference to replace.
1107    pub reference: ObjRef,
1108    /// The field dictionary with its `/V` — and, for a toggle, its `/AS` —
1109    /// rewritten.
1110    pub dict: Dict,
1111    /// Regenerated appearances for this field's widgets, each with the
1112    /// reference to replace. Empty when the widgets' own appearances already
1113    /// cover the new value, which is the case for a toggle whose `/AP /N`
1114    /// lists the state it was switched to.
1115    pub widgets: Vec<(ObjRef, Dict, GeneratedAp)>,
1116}
1117
1118/// Turns an edit buffer into the object replacements that write it to a file.
1119///
1120/// This is the whole "fill a form and save it" step: it rewrites each edited
1121/// field's `/V`, sets a toggle's widget `/AS` to the state chosen, and
1122/// regenerates the appearance of any widget whose own `/AP` cannot show the
1123/// new value.
1124///
1125/// A field the buffer names but the form does not have is skipped, as is one
1126/// whose dictionary is inline and therefore has no reference to replace.
1127///
1128/// ```
1129/// use pdfrum_common::{Diagnostics, Limits};
1130/// use pdfrum_doc::form::Form;
1131/// use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
1132///
1133/// let field = Dict::from_pairs([
1134///     (Name::from("FT"), Object::Name(Name::from("Tx"))),
1135///     (Name::from("T"), Object::Str(PdfString::literal(b"name"))),
1136///     (Name::from("V"), Object::Str(PdfString::literal(b"Ada"))),
1137/// ]);
1138/// let catalog = Dict::from_pairs([(
1139///     Name::from("AcroForm"),
1140///     Object::Dict(Dict::from_pairs([(
1141///         Name::from("Fields"),
1142///         Object::Array(Array::of([Object::Dict(field)])),
1143///     )])),
1144/// )]);
1145///
1146/// let mut diags = Diagnostics::default();
1147/// let form = Form::load(&catalog, &NoResolve, &Limits::default(), &mut diags)
1148///     .expect("the catalog declares an /AcroForm");
1149/// use pdfrum_doc::form::{FieldValues, apply};
1150///
1151/// let mut values = FieldValues::new();
1152/// values.set("name", "Grace");
1153///
1154/// // The field is written inline in `/Fields`, so it has no reference to
1155/// // replace and no edit is produced.
1156/// assert!(apply(&form, &values, &NoResolve, &mut diags).is_empty());
1157/// ```
1158#[must_use]
1159pub fn apply<R: Resolve>(
1160    form: &Form,
1161    values: &FieldValues,
1162    r: &R,
1163    diags: &mut Diagnostics,
1164) -> Vec<FieldEdit> {
1165    let mut out = Vec::new();
1166    for (name, value) in values.iter() {
1167        let Some(field) = form.field(name) else {
1168            continue;
1169        };
1170        let Some(reference) = field.reference else {
1171            continue;
1172        };
1173        if !field.kind.is_writable() {
1174            continue;
1175        }
1176
1177        let dict = rewrite(&field.dict, names::V, value_object(field.kind, value));
1178        let mut widgets = Vec::new();
1179        for widget in &field.widgets {
1180            let Some(widget_ref) = widget.reference else {
1181                continue;
1182            };
1183            // Merged field-and-widget: the value edit and the widget edit are
1184            // the same object, so the widget's edit starts from the field
1185            // dictionary that already carries the new `/V` — starting from
1186            // the widget's own copy would put its old `/V` back.
1187            let source = if widget_ref == reference {
1188                &dict
1189            } else {
1190                &widget.dict
1191            };
1192            // A toggle's widget selects its appearance with `/AS`; a text or
1193            // choice field's has to have one drawn.
1194            let widget_dict = if field.kind.is_toggle() {
1195                rewrite(
1196                    source,
1197                    names::AS,
1198                    Object::Name(Name::from(value.as_bytes())),
1199                )
1200            } else {
1201                source.clone()
1202            };
1203            if let Some(generated) = ap::widget::generate(&widget_dict, r) {
1204                widgets.push((widget_ref, widget_dict, generated));
1205            } else if widget_ref != reference && field.kind.is_toggle() {
1206                // No appearance needed to be drawn, but `/AS` still changed.
1207                widgets.push((
1208                    widget_ref,
1209                    widget_dict,
1210                    GeneratedAp {
1211                        stream: Vec::new(),
1212                        bbox: kurbo::Rect::ZERO,
1213                        matrix: kurbo::Affine::IDENTITY,
1214                        resources: Dict::new(),
1215                        rect_override: None,
1216                        as_override: None,
1217                    },
1218                ));
1219            }
1220        }
1221        let _ = diags;
1222        out.push(FieldEdit {
1223            reference,
1224            dict,
1225            widgets,
1226        });
1227    }
1228    out
1229}
1230
1231/// The object a value is stored as: a name for a toggle's state, a string for
1232/// everything else.
1233fn value_object(kind: FieldKind, value: &str) -> Object {
1234    if kind.is_toggle() {
1235        Object::Name(Name::from(value.as_bytes()))
1236    } else {
1237        Object::Str(pdfrum_object::PdfString::literal(value.as_bytes()))
1238    }
1239}
1240
1241/// A copy of `dict` with `key` set to `value`, keeping every other entry in
1242/// its original position.
1243fn rewrite(dict: &Dict, key: &Name, value: Object) -> Dict {
1244    let mut out = Dict::new();
1245    let mut replaced = false;
1246    for (existing, held) in dict.iter() {
1247        if existing == key {
1248            if !replaced {
1249                out.push(existing.clone(), value.clone());
1250                replaced = true;
1251            }
1252        } else {
1253            out.push(existing.clone(), held.clone());
1254        }
1255    }
1256    if !replaced {
1257        out.push(key.clone(), value);
1258    }
1259    out
1260}
1261
1262/// Which rows of a choice field an **interaction** treats as selected.
1263///
1264/// # Why this is not the appearance's answer
1265///
1266/// A choice field records its selection twice — `/I` as indices, `/V` as the
1267/// selected options' export values — and the pair is read *differently
1268/// depending on who is asking*. The two readers are not reconcilable and
1269/// pretending they are is how a corpus row moves in the wrong direction:
1270///
1271/// - **Interaction** — "is row `n` selected?", the question a click, an arrow
1272///   key or an embedder's query asks — consults **`/I` first**, as integer
1273///   indices, and falls back to `/V` only when `/I` is not usable. That is
1274///   this function.
1275/// - **Appearance** — what the generated `/AP` draws a band behind — reads
1276///   **`/V` first**, `/I` only when there is no `/V`, and then matches each
1277///   entry's *text* against the option values, so an integer index matches
1278///   nothing. That is `ap::field_body::selected_indices`, and it is
1279///   deliberately the other way round.
1280///
1281/// So `listbox_form.pdf`'s `Listbox_MultiSelectMultipleIndices` — `/I [1 3]`
1282/// and no `/V` — draws **no** selection band while an embedder asking about
1283/// its rows is told 1 and 3 are selected. Both are correct; they are answers
1284/// to different questions.
1285///
1286/// # What "usable" means
1287///
1288/// `indices_are_usable` is the test, and it is strict because its job is to
1289/// catch a stale `/I` left behind by an editor that rewrote `/V`. `/I` is
1290/// usable when either
1291///
1292/// - there is **no `/V` at all** — nothing can contradict it; or
1293/// - `/I` and `/V` **agree exactly**: the same number of entries, every index
1294///   in range, and the multiset of options those indices name equal to the
1295///   multiset `/V` lists. A duplicate on one side must be matched by a
1296///   duplicate on the other, which is why occurrences are counted rather than
1297///   membership tested.
1298///
1299/// One disagreement anywhere discards `/I` entirely — it is not repaired
1300/// entry by entry — and `/V` then decides alone, matched as text exactly as
1301/// the appearance reader does.
1302///
1303/// `options` is the field's `/Opt` in order, as the **values** a selection is
1304/// compared against: an `[export, label]` pair contributes its export, never
1305/// its label.
1306#[must_use]
1307pub fn selected_indices_for_interaction<R: Resolve>(
1308    dict: &Dict,
1309    options: &[String],
1310    r: &R,
1311) -> Vec<usize> {
1312    // `/V` and `/I` are inheritable field attributes, and a damaged
1313    // inheritance chain is not this function's to report on: it answers with
1314    // what it could reach.
1315    let (limits, mut diags) = (Limits::default(), Diagnostics::default());
1316    let value = field_attr(dict, names::V, r, &limits, &mut diags);
1317    let indices = field_attr(dict, names::I, r, &limits, &mut diags);
1318    if let Some(indices) = indices.as_ref()
1319        && indices_are_usable(indices, value.as_ref(), options, r)
1320    {
1321        return listed_indices(indices, r)
1322            .into_iter()
1323            .filter_map(|index| usize::try_from(index).ok())
1324            .filter(|index| *index < options.len())
1325            .collect();
1326    }
1327    let Some(value) = value else {
1328        return Vec::new();
1329    };
1330    let wanted: Vec<String> = match value.as_array() {
1331        Some(array) => (0..array.len())
1332            .map(|slot| {
1333                array
1334                    .get(slot, r)
1335                    .as_deref()
1336                    .map(Object::to_text)
1337                    .unwrap_or_default()
1338            })
1339            .collect(),
1340        None => vec![value.to_text()],
1341    };
1342    wanted
1343        .into_iter()
1344        .filter_map(|text| options.iter().position(|option| *option == text))
1345        .collect()
1346}
1347
1348/// `/I`'s entries as raw integers, or nothing when any entry is not a number.
1349///
1350/// A bare number stands for a one-entry array, which is the shape
1351/// `UseSelectedIndicesObject` admits alongside the array.
1352fn listed_indices<R: Resolve>(indices: &Object, r: &R) -> Vec<i64> {
1353    match indices.as_array() {
1354        Some(array) => (0..array.len())
1355            .map(|slot| array.get(slot, r).as_deref().and_then(Object::as_int))
1356            .collect::<Option<Vec<i64>>>()
1357            .unwrap_or_default(),
1358        None => indices.as_int().into_iter().collect(),
1359    }
1360}
1361
1362/// Whether `/I` may be believed in preference to `/V`.
1363///
1364/// See [`selected_indices_for_interaction`] for the rule and why it is all or
1365/// nothing.
1366fn indices_are_usable<R: Resolve>(
1367    indices: &Object,
1368    value: Option<&Object>,
1369    options: &[String],
1370    r: &R,
1371) -> bool {
1372    // No `/V` to contradict it.
1373    let Some(value) = value else {
1374        return true;
1375    };
1376    // A non-number entry anywhere fails outright: `/I` is trusted whole or
1377    // not at all, and an empty answer here would be indistinguishable from a
1378    // genuinely empty `/I`.
1379    let listed = listed_indices(indices, r);
1380    let declared = match indices.as_array() {
1381        Some(array) => array.len(),
1382        None => usize::from(indices.as_int().is_some()),
1383    };
1384    if listed.len() != declared || declared == 0 {
1385        return false;
1386    }
1387
1388    // `/V`'s texts, as counts, so a repeated value needs a repeated index.
1389    let mut wanted: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
1390    if let Some(array) = value.as_array() {
1391        if array.len() != listed.len() {
1392            return false;
1393        }
1394        for slot in 0..array.len() {
1395            // Only strings are counted — upstream ignores any other type
1396            // here, which then leaves a count `/I` cannot satisfy.
1397            if let Some(object) = array.get(slot, r)
1398                && object.as_string().is_some()
1399            {
1400                *wanted.entry(object.to_text()).or_default() += 1;
1401            }
1402        }
1403    } else {
1404        // A lone string is the one-selection spelling, so it can only ever
1405        // account for one index.
1406        if listed.len() != 1 {
1407            return false;
1408        }
1409        if value.as_string().is_some() {
1410            *wanted.entry(value.to_text()).or_default() += 1;
1411        }
1412    }
1413
1414    for index in listed {
1415        let Ok(index) = usize::try_from(index) else {
1416            return false;
1417        };
1418        let Some(option) = options.get(index) else {
1419            return false;
1420        };
1421        let Some(count) = wanted.get_mut(option) else {
1422            return false;
1423        };
1424        *count -= 1;
1425        if *count == 0 {
1426            wanted.remove(option);
1427        }
1428    }
1429    wanted.is_empty()
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434    use super::*;
1435    use pdfrum_object::{Array, NoResolve, PdfString};
1436
1437    fn dict(pairs: &[(&str, Object)]) -> Dict {
1438        Dict::from_pairs(
1439            pairs
1440                .iter()
1441                .map(|(k, v)| (Name::from(*k), v.clone()))
1442                .collect::<Vec<_>>(),
1443        )
1444    }
1445
1446    fn text(value: &str) -> Object {
1447        Object::Str(PdfString::literal(value.as_bytes()))
1448    }
1449
1450    fn name(value: &str) -> Object {
1451        Object::Name(Name::from(value))
1452    }
1453
1454    /// The four `listbox_form.pdf` shapes, as the interaction reader sees
1455    /// them. Contrast `ap::field_body::selected_indices`, which answers the
1456    /// appearance's question and disagrees on the first of these on purpose.
1457    fn opts() -> Vec<String> {
1458        ["Albania", "Belgium", "Croatia", "Denmark", "Estonia"]
1459            .iter()
1460            .map(|s| (*s).to_owned())
1461            .collect()
1462    }
1463
1464    fn selected(pairs: &[(&str, Object)]) -> Vec<usize> {
1465        selected_indices_for_interaction(&dict(pairs), &opts(), &NoResolve)
1466    }
1467
1468    fn strings(values: &[&str]) -> Object {
1469        Object::Array(Array::of(
1470            values.iter().map(|v| text(v)).collect::<Vec<_>>(),
1471        ))
1472    }
1473
1474    #[test]
1475    fn indices_alone_are_believed_because_nothing_contradicts_them() {
1476        // `Listbox_MultiSelectMultipleIndices`: `/I [1 3]`, no `/V`.
1477        assert_eq!(
1478            selected(&[(
1479                "I",
1480                Object::Array(Array::of([Object::Int(1), Object::Int(3)]))
1481            )]),
1482            vec![1, 3]
1483        );
1484        // A bare number is the one-entry spelling.
1485        assert_eq!(selected(&[("I", Object::Int(2))]), vec![2]);
1486    }
1487
1488    #[test]
1489    fn a_value_alone_selects_every_option_it_names() {
1490        // `Listbox_MultiSelectMultipleValues`, restated over these options.
1491        assert_eq!(
1492            selected(&[("V", strings(&["Belgium", "Denmark"]))]),
1493            vec![1, 3]
1494        );
1495        // And a lone string is the single-selection spelling.
1496        assert_eq!(selected(&[("V", text("Croatia"))]), vec![2]);
1497        // A value naming no option selects nothing rather than guessing.
1498        assert_eq!(selected(&[("V", text("Zambia"))]), Vec::<usize>::new());
1499    }
1500
1501    #[test]
1502    fn consistent_indices_win_over_the_values_they_agree_with() {
1503        // Same count, in range, naming exactly what `/V` lists.
1504        assert_eq!(
1505            selected(&[
1506                ("V", strings(&["Belgium", "Denmark"])),
1507                (
1508                    "I",
1509                    Object::Array(Array::of([Object::Int(1), Object::Int(3)]))
1510                ),
1511            ]),
1512            vec![1, 3]
1513        );
1514        // Occurrences are counted, not sequences compared, so the two may be
1515        // listed in different orders — and `/I`'s order is what comes back.
1516        assert_eq!(
1517            selected(&[
1518                ("V", strings(&["Denmark", "Belgium"])),
1519                (
1520                    "I",
1521                    Object::Array(Array::of([Object::Int(3), Object::Int(1)]))
1522                ),
1523            ]),
1524            vec![3, 1]
1525        );
1526    }
1527
1528    #[test]
1529    fn inconsistent_indices_are_discarded_whole_and_the_values_decide() {
1530        // `Listbox_MultiSelectMultipleMismatch`'s shape: three indices
1531        // against two values, so the counts differ and `/I` is rejected
1532        // before any index is looked up.
1533        assert_eq!(
1534            selected(&[
1535                ("V", strings(&["Albania", "Croatia"])),
1536                (
1537                    "I",
1538                    Object::Array(Array::of([Object::Int(1), Object::Int(3), Object::Int(4),])),
1539                ),
1540            ]),
1541            vec![0, 2]
1542        );
1543        // Equal counts, but an index naming an option `/V` does not list.
1544        assert_eq!(
1545            selected(&[
1546                ("V", strings(&["Albania"])),
1547                ("I", Object::Array(Array::of([Object::Int(1)]))),
1548            ]),
1549            vec![0]
1550        );
1551        // An index out of range poisons the whole array rather than being
1552        // dropped on its own.
1553        assert_eq!(
1554            selected(&[
1555                ("V", strings(&["Albania", "Croatia"])),
1556                (
1557                    "I",
1558                    Object::Array(Array::of([Object::Int(0), Object::Int(9)]))
1559                ),
1560            ]),
1561            vec![0, 2]
1562        );
1563        // Two indices naming one option cannot satisfy two distinct values.
1564        assert_eq!(
1565            selected(&[
1566                ("V", strings(&["Albania", "Belgium"])),
1567                (
1568                    "I",
1569                    Object::Array(Array::of([Object::Int(0), Object::Int(0)]))
1570                ),
1571            ]),
1572            vec![0, 1]
1573        );
1574        // A non-number entry fails the whole array too.
1575        assert_eq!(
1576            selected(&[
1577                ("V", strings(&["Albania"])),
1578                ("I", Object::Array(Array::of([text("0")]))),
1579            ]),
1580            vec![0]
1581        );
1582    }
1583
1584    #[test]
1585    fn a_field_declaring_neither_selects_nothing() {
1586        assert_eq!(selected(&[]), Vec::<usize>::new());
1587    }
1588
1589    fn load(catalog: &Dict) -> Option<Form> {
1590        load_with_diags(catalog).0
1591    }
1592
1593    /// `load`, plus the diagnostics the walk recorded.
1594    fn load_with_diags(catalog: &Dict) -> (Option<Form>, Diagnostics) {
1595        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
1596        let form = Form::load(catalog, &NoResolve, &limits, &mut diags);
1597        (form, diags)
1598    }
1599
1600    /// The single field a one-field fixture is expected to have.
1601    fn only_field(form: &Form) -> &Field {
1602        assert_eq!(form.len(), 1, "the fixture has exactly one field");
1603        form.fields.first().expect("one field")
1604    }
1605
1606    fn catalog_with(fields: Vec<Object>) -> Dict {
1607        let acro = dict(&[("Fields", Object::Array(Array::of(fields)))]);
1608        dict(&[("AcroForm", Object::Dict(acro))])
1609    }
1610
1611    #[test]
1612    fn a_catalog_without_an_acroform_has_no_form() {
1613        assert_eq!(load(&Dict::new()), None);
1614    }
1615
1616    #[test]
1617    fn an_acroform_without_fields_is_an_empty_form_not_an_absent_one() {
1618        let catalog = dict(&[("AcroForm", Object::Dict(Dict::new()))]);
1619        let form = load(&catalog).expect("an AcroForm is a form");
1620        assert!(form.is_empty());
1621    }
1622
1623    #[test]
1624    fn a_terminal_field_is_classified_from_its_type_and_flags() {
1625        let catalog = catalog_with(vec![Object::Dict(dict(&[
1626            ("FT", name("Tx")),
1627            ("T", text("greeting")),
1628            ("V", text("hello")),
1629        ]))]);
1630        let form = load(&catalog).expect("form");
1631        assert_eq!(form.len(), 1);
1632        let field = &only_field(&form);
1633        assert_eq!(field.name, "greeting");
1634        assert_eq!(field.kind, FieldKind::Text);
1635        assert_eq!(field.stored_value(&NoResolve), "hello");
1636    }
1637
1638    #[test]
1639    fn the_button_flags_split_the_three_button_kinds() {
1640        // No bits: a check box.
1641        assert_eq!(
1642            FieldKind::classify(b"Btn", FieldFlags::from_bits(0)),
1643            Some(FieldKind::Check)
1644        );
1645        // Bit 16: a radio button.
1646        assert_eq!(
1647            FieldKind::classify(b"Btn", FieldFlags::from_bits(1 << 15)),
1648            Some(FieldKind::Radio)
1649        );
1650        // Bit 17 wins over bit 16: a push button.
1651        assert_eq!(
1652            FieldKind::classify(b"Btn", FieldFlags::from_bits((1 << 16) | (1 << 15))),
1653            Some(FieldKind::Button)
1654        );
1655    }
1656
1657    #[test]
1658    fn a_choice_field_splits_on_the_combo_bit() {
1659        assert_eq!(
1660            FieldKind::classify(b"Ch", FieldFlags::from_bits(0)),
1661            Some(FieldKind::List)
1662        );
1663        assert_eq!(
1664            FieldKind::classify(b"Ch", FieldFlags::from_bits(1 << 17)),
1665            Some(FieldKind::Combo)
1666        );
1667    }
1668
1669    #[test]
1670    fn a_node_with_no_field_type_is_not_a_field() {
1671        assert_eq!(FieldKind::classify(b"", FieldFlags::from_bits(0)), None);
1672        assert_eq!(
1673            FieldKind::classify(b"Nonsense", FieldFlags::from_bits(0)),
1674            None
1675        );
1676    }
1677
1678    #[test]
1679    fn a_naming_node_contributes_its_children_and_its_name_prefix() {
1680        // An interior node with a `/T` but no `/FT`, whose kids are fields.
1681        // The kid carries the `/Parent` back-pointer a real file writes,
1682        // because that is the edge `full_name` walks — the tree is navigated
1683        // downwards to find fields and upwards to name them.
1684        let parent_dict = dict(&[("T", text("address"))]);
1685        let kid = dict(&[
1686            ("FT", name("Tx")),
1687            ("T", text("street")),
1688            ("Parent", Object::Dict(parent_dict)),
1689        ]);
1690        let parent = dict(&[
1691            ("T", text("address")),
1692            ("Kids", Object::Array(Array::of([Object::Dict(kid)]))),
1693        ]);
1694        let catalog = catalog_with(vec![Object::Dict(parent)]);
1695        let form = load(&catalog).expect("form");
1696        assert_eq!(form.len(), 1);
1697        // The name is qualified through the parent, which is the whole point
1698        // of the interior node.
1699        assert_eq!(only_field(&form).name, "address.street");
1700    }
1701
1702    #[test]
1703    fn a_field_whose_kids_are_widgets_stays_one_field() {
1704        // A radio group: the `/FT` is on the parent, the kids are widgets
1705        // with no `/T` of their own.
1706        let on = dict(&[("Subtype", name("Widget")), ("AS", name("A"))]);
1707        let off = dict(&[("Subtype", name("Widget")), ("AS", name("Off"))]);
1708        let group = dict(&[
1709            ("FT", name("Btn")),
1710            ("Ff", Object::Int(1 << 15)),
1711            ("T", text("choice")),
1712            ("V", name("A")),
1713            (
1714                "Kids",
1715                Object::Array(Array::of([Object::Dict(on), Object::Dict(off)])),
1716            ),
1717        ]);
1718        let catalog = catalog_with(vec![Object::Dict(group)]);
1719        let form = load(&catalog).expect("form");
1720        assert_eq!(form.len(), 1, "a radio group is one field, not two");
1721        let field = &only_field(&form);
1722        assert_eq!(field.kind, FieldKind::Radio);
1723        assert_eq!(field.widgets.len(), 2);
1724        assert!(field.is_checked(None, &NoResolve));
1725    }
1726
1727    #[test]
1728    fn a_merged_field_and_widget_reports_itself_as_its_widget() {
1729        let merged = dict(&[
1730            ("FT", name("Tx")),
1731            ("T", text("box")),
1732            ("Subtype", name("Widget")),
1733        ]);
1734        let catalog = catalog_with(vec![Object::Dict(merged)]);
1735        let form = load(&catalog).expect("form");
1736        assert_eq!(only_field(&form).widgets.len(), 1);
1737        assert_eq!(
1738            only_field(&form).widgets.first().expect("one widget").dict,
1739            only_field(&form).dict
1740        );
1741    }
1742
1743    #[test]
1744    fn a_toggle_reads_off_and_absent_as_clear_and_everything_else_as_set() {
1745        let make = |value: Option<Object>| {
1746            let mut pairs = vec![("FT", name("Btn")), ("T", text("t"))];
1747            if value.is_some() {
1748                pairs.push(("V", value.clone().unwrap_or(Object::Null)));
1749            }
1750            let catalog = catalog_with(vec![Object::Dict(dict(&pairs))]);
1751            let form = load(&catalog).expect("form");
1752            only_field(&form).is_checked(None, &NoResolve)
1753        };
1754        assert!(!make(None), "absent is clear");
1755        assert!(!make(Some(name("Off"))), "Off is clear");
1756        assert!(make(Some(name("Yes"))), "any other state is set");
1757    }
1758
1759    #[test]
1760    fn a_push_button_and_a_signature_hold_no_writable_value() {
1761        assert!(!FieldKind::Button.is_writable());
1762        assert!(!FieldKind::Signature.is_writable());
1763        assert!(FieldKind::Text.is_writable());
1764        assert!(FieldKind::Check.is_writable());
1765    }
1766
1767    #[test]
1768    fn writing_a_value_records_it_and_reading_sees_it() {
1769        let catalog = catalog_with(vec![Object::Dict(dict(&[
1770            ("FT", name("Tx")),
1771            ("T", text("greeting")),
1772            ("V", text("hello")),
1773        ]))]);
1774        let form = load(&catalog).expect("form");
1775        let mut values = FieldValues::new();
1776        values.set("greeting", "goodbye");
1777        // The edit wins over the file.
1778        assert_eq!(
1779            only_field(&form).value(Some(&values), &NoResolve),
1780            "goodbye"
1781        );
1782        // And the file is unchanged.
1783        assert_eq!(only_field(&form).stored_value(&NoResolve), "hello");
1784    }
1785
1786    #[test]
1787    fn setting_the_same_field_twice_keeps_the_last_write_and_one_entry() {
1788        let mut values = FieldValues::new();
1789        values.set("a", "one");
1790        values.set("a", "two");
1791        assert_eq!(values.len(), 1);
1792        assert_eq!(values.get("a"), Some("two"));
1793    }
1794
1795    #[test]
1796    fn an_option_pair_reports_its_label_rather_than_its_export_value() {
1797        let pair = Object::Array(Array::of([text("export"), text("Label")]));
1798        let catalog = catalog_with(vec![Object::Dict(dict(&[
1799            ("FT", name("Ch")),
1800            ("T", text("pick")),
1801            ("Opt", Object::Array(Array::of([text("Plain"), pair]))),
1802        ]))]);
1803        let form = load(&catalog).expect("form");
1804        assert_eq!(only_field(&form).options(&NoResolve), ["Plain", "Label"]);
1805    }
1806
1807    #[test]
1808    fn the_states_of_a_toggle_come_from_its_widgets_appearances() {
1809        let normal = dict(&[("Off", Object::Null), ("Yes", Object::Null)]);
1810        let ap = dict(&[("N", Object::Dict(normal))]);
1811        let widget = dict(&[
1812            ("FT", name("Btn")),
1813            ("T", text("t")),
1814            ("Subtype", name("Widget")),
1815            ("AP", Object::Dict(ap)),
1816        ]);
1817        let catalog = catalog_with(vec![Object::Dict(widget)]);
1818        let form = load(&catalog).expect("form");
1819        assert_eq!(only_field(&form).states(&NoResolve), ["Off", "Yes"]);
1820    }
1821
1822    #[test]
1823    fn a_field_with_no_reference_cannot_be_written_back() {
1824        // The field is written inline in `/Fields`, so nothing names it.
1825        let catalog = catalog_with(vec![Object::Dict(dict(&[
1826            ("FT", name("Tx")),
1827            ("T", text("inline")),
1828        ]))]);
1829        let form = load(&catalog).expect("form");
1830        assert_eq!(only_field(&form).reference, None);
1831        let mut values = FieldValues::new();
1832        values.set("inline", "x");
1833        let mut diags = Diagnostics::default();
1834        assert!(
1835            apply(&form, &values, &NoResolve, &mut diags).is_empty(),
1836            "an unnamed field produces no replacement"
1837        );
1838    }
1839
1840    #[test]
1841    fn rewriting_a_key_keeps_the_dictionary_order() {
1842        let source = dict(&[
1843            ("A", Object::Int(1)),
1844            ("V", text("old")),
1845            ("B", Object::Int(2)),
1846        ]);
1847        let out = rewrite(&source, &Name::from("V"), text("new"));
1848        let keys: Vec<&[u8]> = out.keys().map(pdfrum_object::Name::as_bytes).collect();
1849        assert_eq!(keys, [b"A".as_slice(), b"V".as_slice(), b"B".as_slice()]);
1850        assert_eq!(
1851            out.text(&Name::from("V"), &NoResolve).as_deref(),
1852            Some("new")
1853        );
1854    }
1855
1856    #[test]
1857    fn rewriting_an_absent_key_appends_it() {
1858        let out = rewrite(&Dict::new(), &Name::from("V"), text("v"));
1859        assert_eq!(out.len(), 1);
1860    }
1861
1862    #[test]
1863    fn a_field_with_no_name_anywhere_in_its_ancestry_is_dropped() {
1864        // ISO 32000-1 §12.7.3.2: a field is addressed by its fully qualified
1865        // name, and this one has none — no `/T` on itself and no `/Parent`
1866        // carrying one — so no action, export or script could ever name it.
1867        // `AddTerminalField` drops it (`cpdf_interactiveform.cpp:914-917`).
1868        let catalog = catalog_with(vec![Object::Dict(dict(&[
1869            ("FT", name("Tx")),
1870            ("V", text("unreachable")),
1871        ]))]);
1872        let (form, diags) = load_with_diags(&catalog);
1873        let form = form.expect("an AcroForm is still a form");
1874        assert!(form.is_empty(), "an unnamed terminal field is not a field");
1875        assert!(diags.contains(&pdfrum_common::DiagKind::FieldSkippedNoName));
1876    }
1877
1878    #[test]
1879    fn unnamed_fields_do_not_collapse_into_one() {
1880        // The reason the drop is the *correct* answer and not merely the
1881        // oracle's: `name` is the identity the merge below keys on, so
1882        // keeping the empty name would fold every unnamed field in the
1883        // document into a single field carrying all their widgets — a field
1884        // that is not in the file. Two unnamed entries plus a real one must
1885        // leave exactly the real one.
1886        let unnamed = || Object::Dict(dict(&[("FT", name("Tx")), ("V", text("a"))]));
1887        let catalog = catalog_with(vec![
1888            unnamed(),
1889            unnamed(),
1890            Object::Dict(dict(&[
1891                ("FT", name("Tx")),
1892                ("T", text("real")),
1893                ("V", text("b")),
1894            ])),
1895        ]);
1896        let form = load(&catalog).expect("a form");
1897        assert_eq!(only_field(&form).name, "real");
1898    }
1899
1900    #[test]
1901    fn a_field_named_only_by_an_ancestor_survives() {
1902        // The drop is about the *fully qualified* name, not about `/T` on the
1903        // node itself: a kid with no `/T` inherits its parent's name and is
1904        // addressable as it, so it must be kept.
1905        let kid = Object::Dict(dict(&[
1906            ("Subtype", name("Widget")),
1907            (
1908                "Parent",
1909                Object::Dict(dict(&[("FT", name("Tx")), ("T", text("parent"))])),
1910            ),
1911        ]));
1912        let catalog = catalog_with(vec![Object::Dict(dict(&[
1913            ("FT", name("Tx")),
1914            ("T", text("parent")),
1915            ("Kids", Object::Array(Array::of([kid]))),
1916        ]))]);
1917        let form = load(&catalog).expect("a form");
1918        assert_eq!(only_field(&form).name, "parent");
1919    }
1920
1921    #[test]
1922    fn two_fields_entries_sharing_a_name_are_one_field_with_two_widgets() {
1923        // `AddTerminalField` looks the fully-qualified name up before it
1924        // builds anything, so the second entry becomes another *control* of
1925        // the first's field rather than a field of its own. `bug_733528` is
1926        // that shape, and its golden has the second widget drawing the
1927        // first's value.
1928        let widget = |value: &str| {
1929            Object::Dict(dict(&[
1930                ("Type", name("Annot")),
1931                ("Subtype", name("Widget")),
1932                ("FT", name("Tx")),
1933                ("T", text("SharedField")),
1934                ("V", text(value)),
1935            ]))
1936        };
1937        let catalog = dict(&[(
1938            "AcroForm",
1939            Object::Dict(dict(&[(
1940                "Fields",
1941                Object::Array(Array::of([widget("Hello, world"), widget("")])),
1942            )])),
1943        )]);
1944        let form = load(&catalog).expect("a form");
1945        let field = only_field(&form);
1946        assert_eq!(field.name, "SharedField");
1947        assert_eq!(field.widgets.len(), 2);
1948        // The field's value is the **first** entry's, which is what both
1949        // controls show.
1950        assert_eq!(field.value(None, &NoResolve), "Hello, world");
1951    }
1952
1953    #[test]
1954    fn two_fields_entries_with_different_names_stay_two_fields() {
1955        let widget = |field_name: &str| {
1956            Object::Dict(dict(&[
1957                ("Type", name("Annot")),
1958                ("Subtype", name("Widget")),
1959                ("FT", name("Tx")),
1960                ("T", text(field_name)),
1961            ]))
1962        };
1963        let catalog = dict(&[(
1964            "AcroForm",
1965            Object::Dict(dict(&[(
1966                "Fields",
1967                Object::Array(Array::of([widget("one"), widget("two")])),
1968            )])),
1969        )]);
1970        assert_eq!(load(&catalog).expect("a form").len(), 2);
1971    }
1972
1973    #[test]
1974    fn the_need_appearances_flag_is_read_off_the_acroform() {
1975        let acro = dict(&[("NeedAppearances", Object::Bool(true))]);
1976        let catalog = dict(&[("AcroForm", Object::Dict(acro))]);
1977        assert!(load(&catalog).expect("form").need_appearances);
1978        // Absent reads as false.
1979        let catalog = dict(&[("AcroForm", Object::Dict(Dict::new()))]);
1980        assert!(!load(&catalog).expect("form").need_appearances);
1981    }
1982
1983    #[test]
1984    fn the_flag_word_accessors_read_the_documented_bits() {
1985        let f = FieldFlags::from_bits;
1986        assert!(f(1).is_read_only());
1987        assert!(f(2).is_required());
1988        assert!(f(1 << 12).is_multiline());
1989        assert!(f(1 << 13).is_password());
1990        assert!(f(1 << 18).is_editable_combo());
1991        assert!(f(1 << 21).is_multi_select());
1992        assert!(f(1 << 24).is_comb());
1993        assert!(!f(0).is_read_only());
1994        assert!(!f(0).is_editable_combo());
1995        assert!(!f(0).is_multi_select());
1996        assert!(!f(0).is_comb());
1997        // Neighbouring bits must not alias: combo (bit 18) is not editable
1998        // combo (bit 19), and do-not-spell-check (bit 23) is not do-not-scroll
1999        // (bit 24).
2000        assert!(!f(1 << 17).is_editable_combo());
2001    }
2002
2003    /// The two negative spec bits, read positively. `DoNotScroll` and
2004    /// `DoNotSpellCheck` are adjacent, so the alias test is also an
2005    /// anti-aliasing test.
2006    #[test]
2007    fn the_negative_spec_bits_read_positively() {
2008        let f = FieldFlags::from_bits;
2009        assert!(f(0).scrolls());
2010        assert!(!f(1 << 23).scrolls());
2011        assert!(f(1 << 22).scrolls());
2012        assert!(f(0).spell_checks());
2013        assert!(!f(1 << 22).spell_checks());
2014        assert!(f(1 << 23).spell_checks());
2015    }
2016
2017    /// `/Ff` has no set algebra by design (§6): the round trip is the whole
2018    /// contract, and a bit belonging to a `/FT` nothing here reads survives.
2019    #[test]
2020    fn the_flag_word_round_trips_unknown_bits() {
2021        let raw = (1 << 40) | (1 << 25) | 1;
2022        let f = FieldFlags::from_bits(raw);
2023        assert_eq!(f.bits(), raw);
2024        assert!(f.is_read_only());
2025        assert_eq!(FieldFlags::default().bits(), 0);
2026    }
2027
2028    // ---- `/CO`, the calculation order ----
2029
2030    /// A map-backed resolver, because `/CO` is a list of *references* and
2031    /// `NoResolve` cannot follow one.
2032    struct Store(std::collections::HashMap<u32, std::sync::Arc<Object>>);
2033
2034    impl Store {
2035        fn of(pairs: impl IntoIterator<Item = (u32, Object)>) -> Store {
2036            Store(
2037                pairs
2038                    .into_iter()
2039                    .map(|(num, obj)| (num, std::sync::Arc::new(obj)))
2040                    .collect(),
2041            )
2042        }
2043    }
2044
2045    impl Resolve for Store {
2046        fn fetch(&self, r: ObjRef) -> Result<std::sync::Arc<Object>, pdfrum_object::Error> {
2047            self.0
2048                .get(&r.num)
2049                .map(std::sync::Arc::clone)
2050                .ok_or(pdfrum_object::Error::UnresolvedRef(r))
2051        }
2052    }
2053
2054    fn reference(num: u32) -> Object {
2055        Object::Ref(ObjRef { num, generation: 0 })
2056    }
2057
2058    #[test]
2059    fn a_junk_first_kid_costs_that_kid_and_not_its_siblings() {
2060        // [oracle-bug] `LoadField` returns when `kids->GetDictAt(0)` is null
2061        // (`cpdf_interactiveform.cpp:871-874`), losing `real` along with the
2062        // broken entry. pdf.js skips the entry and keeps walking
2063        // (`#collectFieldObjects`, `src/core/document.js`), and so do we —
2064        // one unresolvable reference must not cost a page of fields.
2065        //
2066        // Object 9 is not in the store, so `/Kids[0]` resolves to nothing;
2067        // object 2 is a real text field.
2068        let store = Store::of([(
2069            2,
2070            Object::Dict(dict(&[
2071                ("FT", name("Tx")),
2072                ("T", text("real")),
2073                ("V", text("kept")),
2074            ])),
2075        )]);
2076        let catalog = catalog_with(vec![Object::Dict(dict(&[(
2077            "Kids",
2078            Object::Array(Array::of([reference(9), reference(2)])),
2079        )]))]);
2080
2081        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
2082        let form = Form::load(&catalog, &store, &limits, &mut diags).expect("a form");
2083
2084        let field = only_field(&form);
2085        assert_eq!(field.name, "real");
2086        assert_eq!(field.stored_value(&store), "kept");
2087    }
2088
2089    #[test]
2090    fn a_junk_first_kid_does_not_blind_the_terminal_probe() {
2091        // The oracle's `GetDictAt(0)` is a probe as well as a guard: the
2092        // lines after it (`:876-880`) ask the *first* kid whether it carries
2093        // `/T` or `/Kids` to decide branch-versus-terminal. Ours scans every
2094        // kid instead (`kids_are_fields`), so a null at index 0 cannot make a
2095        // branch node look terminal. Here `/Kids[1]` is a named field, so the
2096        // parent is naming structure and the kid is the field — even though
2097        // `/Kids[0]` says nothing.
2098        // The kid carries a `/Parent` back to the branch, which is how a
2099        // real file writes it: that is what its `/FT` and the first half of
2100        // its qualified name are inherited through.
2101        let parent = dict(&[("FT", name("Tx")), ("T", text("parent"))]);
2102        let store = Store::of([(
2103            2,
2104            Object::Dict(dict(&[
2105                ("T", text("kid")),
2106                ("V", text("v")),
2107                ("Parent", Object::Dict(parent.clone())),
2108            ])),
2109        )]);
2110        let catalog = catalog_with(vec![Object::Dict(dict(&[
2111            ("FT", name("Tx")),
2112            ("T", text("parent")),
2113            (
2114                "Kids",
2115                Object::Array(Array::of([reference(9), reference(2)])),
2116            ),
2117        ]))]);
2118
2119        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
2120        let form = Form::load(&catalog, &store, &limits, &mut diags).expect("a form");
2121
2122        // The parent is a branch, so the field is the kid, qualified by it.
2123        assert_eq!(only_field(&form).name, "parent.kid");
2124    }
2125
2126    /// Three text fields as objects 1, 2 and 3, and the catalog that lists
2127    /// them — `co` becomes the `/CO` array when it is `Some`.
2128    fn three_fields(co: Option<Object>) -> (Dict, Store) {
2129        let field_of = |title: &str| {
2130            Object::Dict(dict(&[
2131                ("FT", name("Tx")),
2132                ("T", text(title)),
2133                ("V", text("")),
2134            ]))
2135        };
2136        let store = Store::of([(1, field_of("a")), (2, field_of("b")), (3, field_of("c"))]);
2137        let mut acro = vec![(
2138            "Fields",
2139            Object::Array(Array::of([reference(1), reference(2), reference(3)])),
2140        )];
2141        if let Some(co) = co {
2142            acro.push(("CO", co));
2143        }
2144        let catalog = dict(&[("AcroForm", Object::Dict(dict(&acro)))]);
2145        (catalog, store)
2146    }
2147
2148    fn order_of(co: Option<Object>) -> Vec<usize> {
2149        let (catalog, store) = three_fields(co);
2150        let (limits, mut diags) = (Limits::default(), Diagnostics::default());
2151        let form = Form::load(&catalog, &store, &limits, &mut diags).expect("form");
2152        assert_eq!(form.len(), 3, "the fixture has three fields");
2153        form.calculation_order(&catalog, &store)
2154    }
2155
2156    /// The rule the whole feature turns on: no `/CO`, no calculation. Falling
2157    /// back to "every field" here would recalculate documents that must be
2158    /// left alone.
2159    #[test]
2160    fn a_document_with_no_calculation_order_calculates_nothing() {
2161        assert_eq!(order_of(None), Vec::<usize>::new());
2162        // An empty array is the same answer arrived at the other way.
2163        assert_eq!(order_of(Some(Object::Array(Array::new()))), Vec::new());
2164    }
2165
2166    /// The array's order is the answer, and it need not be `/Fields`' order.
2167    #[test]
2168    fn the_array_is_the_order() {
2169        assert_eq!(
2170            order_of(Some(Object::Array(Array::of([
2171                reference(3),
2172                reference(1),
2173                reference(2),
2174            ])))),
2175            vec![2, 0, 1]
2176        );
2177        // A subset is legal: only the fields listed are calculated.
2178        assert_eq!(
2179            order_of(Some(Object::Array(Array::of([reference(2)])))),
2180            vec![1]
2181        );
2182    }
2183
2184    /// `GetFieldByDict` answers null for anything it cannot map, and the
2185    /// sweep skips it rather than stopping.
2186    #[test]
2187    fn entries_that_resolve_to_nothing_are_dropped() {
2188        assert_eq!(
2189            order_of(Some(Object::Array(Array::of([
2190                reference(9),   // no such object
2191                Object::Int(7), // not a dictionary at all
2192                reference(2),
2193            ])))),
2194            vec![1]
2195        );
2196        // A `/CO` that is not an array is not an order.
2197        assert_eq!(order_of(Some(Object::Int(1))), Vec::<usize>::new());
2198    }
2199
2200    /// The oracle indexes the array positionally, so a repeated field is
2201    /// calculated twice rather than de-duplicated.
2202    #[test]
2203    fn duplicates_are_kept_because_the_array_is_indexed_positionally() {
2204        assert_eq!(
2205            order_of(Some(Object::Array(Array::of(
2206                [reference(1), reference(1),]
2207            )))),
2208            vec![0, 0]
2209        );
2210    }
2211}