Skip to main content

structured_xades/
timestamp.rs

1//! Timestamp input selection and streaming over original XML owner ranges.
2
3use crate::binary::{self, BinaryXml, Location};
4use crate::binding::{Bound, Coverage, Status};
5use crate::container::{DS, XADES};
6use crate::reference::{Boolean, LexicalSource, ReferenceXml, ReplayOutcome, Resolution};
7use crate::xml::{Access, AccessError, ChildError, Name, Snapshot, Xml};
8use crate::{ISSUER, Xades};
9use structured_asn1::{
10    Reader,
11    memory::{Execution, MemoryError, OwnedVec, Work},
12};
13use structured_cms::stream::{Receiver, Refused, Source};
14use structured_cms::{
15    provider::SignatureProvider,
16    timestamp::{CheckContext, CheckedTimestamp, TokenExtension},
17};
18use structured_ltv::timestamp::{CheckedActor, Purpose, Verified};
19
20/// Namespace of the current archive timestamp property.
21pub const XADES_141: &str = "http://uri.etsi.org/01903/v1.4.1#";
22
23mod counters;
24mod coverage;
25mod material;
26pub use counters::{
27    CounterStamp, CounterTimestampError, CounterTimestampResult, CounterTimestamps,
28    counter_timestamps,
29};
30pub use coverage::{CoverageError, CoverageFor};
31pub(crate) mod profile;
32mod selection;
33mod tokens;
34pub use material::{Carried, CarriedCause, CarriedError, CarriedMaterial, OtherCarried};
35pub use profile::{DigestError, DigestUse};
36pub use selection::{
37    RequirementError, RequirementResult, TimestampSelection, requirements,
38    requirements_with_counters,
39};
40pub use tokens::{
41    EncodingConformance, TokenEntry, TokenPipelineError, TokenPipelineResult, TokenProfile,
42    TokenReading, TokenSource, TokenView, with_counter_tokens, with_tokens,
43};
44
45/// An implicit timestamp input prescribed by the current baseline.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Role {
48    /// Canonical original `SignatureValue` element, not decoded signature octets.
49    Signature,
50    /// Direct `SignedInfo` reference outputs, excluding `SignedProperties`.
51    AllDataObjects,
52    /// Explicit Include order over authenticated SignedInfo/Manifest references.
53    IndividualDataObjects,
54    /// `SignatureValue`, earlier signature timestamps and validation references.
55    SignatureAndReferences,
56    /// Earlier validation references, without signature or data-object input.
57    ReferencesOnly,
58    /// Original reference outputs and earlier signature/validation components.
59    Archive,
60}
61
62/// Input for a timestamp that will be appended after all current unsigned
63/// properties. This is a construction role, not an existing token occurrence.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum AppendKind {
66    /// B-T: canonicalize the selected `SignatureValue` element.
67    Signature,
68    /// B-LTA or renewal: include every current unsigned property in order.
69    Archive,
70}
71
72impl Role {
73    /// What a timestamp of this role proves the existence of.
74    #[must_use]
75    pub fn purpose(self) -> Purpose {
76        match self {
77            Self::AllDataObjects | Self::IndividualDataObjects => Purpose::Content,
78            Self::Signature => Purpose::Signature,
79            Self::SignatureAndReferences | Self::ReferencesOnly => Purpose::ValidationData,
80            Self::Archive => Purpose::Archive,
81        }
82    }
83}
84
85/// Failure to select or acquire an original timestamp input.
86#[derive(Debug)]
87pub enum InputError<E, N> {
88    /// The streaming driver retains the original callback refusal.
89    Receiver(Refused),
90    /// XML processing, generation or current resource refusal.
91    Access(AccessError<E, N>),
92    /// Original detached bytes are not available in the immutable request.
93    /// This is incomplete input, not a resource refusal or successful timestamp.
94    Unavailable {
95        /// Actual original reference instruction.
96        node: N,
97        /// Original typed acquisition cause supplied by the XML owner.
98        cause: E,
99    },
100    /// Original direct-child traversal failure.
101    Child(ChildError<E, N>),
102    /// Accounted metadata retention refused before storage growth.
103    Resource {
104        /// Original timestamp container.
105        node: N,
106        /// Original admission refusal.
107        cause: MemoryError,
108    },
109    /// Required schema role is missing.
110    Missing {
111        /// Original enclosing node.
112        node: N,
113        /// Required role's local name.
114        local: &'static str,
115    },
116    /// A node has no role in this baseline timestamp container.
117    Unexpected {
118        /// Original node identity.
119        node: N,
120    },
121    /// A singleton field or `SignatureTimeStamp` token is repeated.
122    Duplicate {
123        /// Repeated original node.
124        node: N,
125    },
126    /// The original `CanonicalizationMethod` occurs after token content.
127    Order {
128        /// Original out-of-order method.
129        node: N,
130    },
131    /// Timestamp is not a direct unsigned property of the selected signature.
132    Foreign {
133        /// Original requested timestamp.
134        node: N,
135    },
136    /// Individual Include requires an explicitly written true boolean.
137    ReferencedData {
138        /// Original Include node.
139        node: N,
140        /// Presence/value classification from the XML lexical owner.
141        value: Boolean,
142    },
143    /// Include did not identify an authenticated eligible reference instruction.
144    Reference {
145        /// Original Include, not a fabricated URI byte offset.
146        node: N,
147        /// Precise original lookup outcome.
148        resolution: Resolution<()>,
149    },
150}
151
152/// XML-owned canonicalization and reference replay, using the actual source view.
153///
154/// Each call rejects foreign/stale identities at access and enforces both XML
155/// policy and current Work. Method is the original `CanonicalizationMethod` node,
156/// including parameters such as Exclusive C14N `PrefixList`; no second algorithm
157/// dispatcher or fabricated parameter representation is introduced here.
158pub trait TimestampXml: ReferenceXml {
159    /// Streams the original element and its content under the timestamp method.
160    /// Ancestor namespace/xml context and method parameters are owner obligations.
161    /// Successful return means complete output; receiver refusal must stop work.
162    fn canonicalize<W: Work>(
163        &self,
164        snapshot: Snapshot<Self::Document, Self::Generation>,
165        node: Self::Id,
166        method: Self::Id,
167        receiver: &mut impl Receiver,
168        execution: &W,
169    ) -> Result<(), Self::Error>;
170}
171
172/// One original timestamp container, before RFC3161 mathematical/authority checks.
173#[derive(Debug)]
174pub struct Stamp<N, D, G> {
175    snapshot: Snapshot<D, G>,
176    signature: N,
177    node: N,
178    parent: N,
179    method: N,
180    role: Role,
181    tokens: OwnedVec<N>,
182    includes: OwnedVec<N>,
183}
184
185impl<N: Copy, D: Copy, G: Copy> Stamp<N, D, G> {
186    /// Original root or nested signature whose actual input is timestamped.
187    #[must_use]
188    pub fn signature(&self) -> N {
189        self.signature
190    }
191    /// Original timestamp location in its exact XML generation.
192    #[must_use]
193    pub fn snapshot(&self) -> Snapshot<D, G> {
194        self.snapshot
195    }
196    /// Original timestamp container identity.
197    #[must_use]
198    pub fn node(&self) -> N {
199        self.node
200    }
201    /// Original canonicalization method and parameters, owned by the XML adapter.
202    #[must_use]
203    pub fn method(&self) -> N {
204        self.method
205    }
206    /// Exact input role; this is not proof of an attained signature level.
207    #[must_use]
208    pub fn role(&self) -> Role {
209        self.role
210    }
211    /// Original `EncapsulatedTimeStamp` nodes in their source order.
212    #[must_use]
213    pub fn tokens(&self) -> &[N] {
214        &self.tokens
215    }
216}
217
218/// Timestamp metadata using this XML owner's stable source identity types.
219pub type Timestamp<X> = Stamp<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
220/// Independently retained positive timestamp using original XML identities.
221pub type VerifiedTimestamp<X> =
222    Verified<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, Xades>;
223/// Successful timestamp bound to the exact certificate/key used for checking.
224pub type TimestampActor<'certificate, 'document, X> = CheckedActor<
225    'certificate,
226    'document,
227    <X as Xml>::Id,
228    <X as Xml>::Document,
229    <X as Xml>::Generation,
230    Xades,
231>;
232/// Original cause and node location survive timestamp preparation failures.
233pub type InputResult<T, X> = Result<T, InputError<<X as Xml>::Error, <X as Xml>::Id>>;
234
235/// Complete common timestamp execution with original XML input selection.
236///
237/// The caller selects the TSA certificate/key and supplies explicit UTC facts.
238/// The resulting common facts/evidence still require caller path, revocation and
239/// algorithm-policy acceptance; this object never chooses a trusted certificate.
240pub struct Check<'xml, 'document, 'certificate, 'context, X, P> {
241    /// Original XML owner, not a serialized or reconstructed document.
242    pub xml: &'xml X,
243    /// One selected provider for token signature, ESS and covered-input imprint.
244    pub provider: &'context P,
245    /// Original candidate certificate/key, producing reader and explicit time facts.
246    pub context: &'context CheckContext<'document, 'certificate>,
247}
248
249/// Original timestamp selection or complete-check refusal.
250#[derive(Debug)]
251pub enum CheckError<E, N, D, G, PE, RE> {
252    /// Original input selection, snapshot or resource refusal.
253    Input(InputError<E, N>),
254    /// Caller requested a token ordinal not present in this parsed timestamp.
255    TokenIndex {
256        /// Original timestamp container.
257        node: N,
258        /// Requested ordinal; it is not a document byte offset.
259        index: usize,
260    },
261    /// Unchanged common cause and binary-source/XML coordinate context.
262    Token(TokenCheckError<E, N, D, G, PE, RE>),
263}
264
265/// Common timestamp-check cause nested in its binary-source/XML enclosure.
266pub type TokenCheckError<E, N, D, G, PE, RE> = binary::Error<
267    E,
268    binary::TokenError<
269        structured_cms::timestamp::ProfileCheckError<InputError<E, N>, PE, RE, DigestError>,
270    >,
271    N,
272    D,
273    G,
274>;
275
276/// Complete-check result using the XML owner's original identity types.
277pub type CheckResult<R, X, PE, RE> = Result<
278    R,
279    CheckError<
280        <X as Xml>::Error,
281        <X as Xml>::Id,
282        <X as Xml>::Document,
283        <X as Xml>::Generation,
284        PE,
285        RE,
286    >,
287>;
288
289impl<'xml, 'document, 'certificate, X: TimestampXml + BinaryXml, P>
290    Check<'xml, 'document, 'certificate, '_, X, P>
291{
292    /// Retains positive evidence with the actual selected certificate/key.
293    ///
294    /// A later path/status phase cannot silently replace this signer or its
295    /// inherited parameters. The same complete token/input kernel runs once;
296    /// no certificate bytes, provider state or temporary context are copied.
297    /// Path/status/UTC chronology and application trust remain separate.
298    #[expect(
299        clippy::result_large_err,
300        reason = "retain original opaque causes and source maps without allocating on refusal"
301    )]
302    pub fn retain_actor<PE, RE>(
303        &self,
304        bound: &Bound<X::Id, X::Document, X::Generation>,
305        stamp: &Timestamp<X>,
306        index: usize,
307        operation: &Reader<'_>,
308        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
309    ) -> CheckResult<Option<TimestampActor<'certificate, 'document, X>>, X, PE, RE>
310    where
311        P: for<'scope> SignatureProvider<'scope, Error = PE>,
312    {
313        Ok(self
314            .retain(bound, stamp, index, operation, recognizes)?
315            .map(|verified| {
316                CheckedActor::new(ISSUER, verified, self.context.certificate, self.context.key)
317            }))
318    }
319
320    /// Retains positive common evidence together with its actual XML input role,
321    /// selected signature and original token identity, without copying evidence.
322    /// No positive record is returned for a completed negative check. TSA trust,
323    /// path/status, material completeness and baseline attainment remain separate.
324    #[expect(
325        clippy::result_large_err,
326        reason = "retain original opaque causes and source maps without allocating on refusal"
327    )]
328    pub fn retain<PE, RE>(
329        &self,
330        bound: &Bound<X::Id, X::Document, X::Generation>,
331        stamp: &Timestamp<X>,
332        index: usize,
333        operation: &Reader<'_>,
334        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
335    ) -> CheckResult<Option<VerifiedTimestamp<X>>, X, PE, RE>
336    where
337        P: for<'scope> SignatureProvider<'scope, Error = PE>,
338    {
339        fn evidence(checked: CheckedTimestamp<'_>) -> Option<structured_cms::timestamp::Evidence> {
340            checked.into_evidence()
341        }
342        let located = self.run(bound, stamp, index, operation, recognizes, evidence)?;
343        Ok(located.value.map(|evidence| {
344            Verified::new(
345                ISSUER,
346                located.location,
347                stamp.signature,
348                stamp.node,
349                stamp.role.purpose(),
350                evidence,
351            )
352        }))
353    }
354
355    /// Checks one actual RFC3161 token against its prescribed original XML input.
356    ///
357    /// The scoped visitor receives the common complete-check facts. It may move
358    /// independently retained Evidence out, but cannot retain borrowed token or
359    /// decoded XML-buffer models. A readable malformed token remains a common
360    /// nonconformance refusal rather than producing successful timestamp evidence.
361    #[expect(
362        clippy::result_large_err,
363        reason = "preserve opaque original failures and admitted source maps without allocating while handling a refusal"
364    )]
365    pub fn run<R, PE, RE>(
366        &self,
367        bound: &Bound<X::Id, X::Document, X::Generation>,
368        stamp: &Timestamp<X>,
369        index: usize,
370        operation: &Reader<'_>,
371        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
372        visitor: impl for<'checked> FnOnce(CheckedTimestamp<'checked>) -> R,
373    ) -> CheckResult<binary::LocatedValue<R, X>, X, PE, RE>
374    where
375        P: for<'scope> SignatureProvider<'scope, Error = PE>,
376    {
377        if index >= stamp.tokens().len() {
378            return Err(CheckError::TokenIndex {
379                node: stamp.node(),
380                index,
381            });
382        }
383        let execution = operation.execution();
384        let input =
385            Input::prepare(self.xml, bound, stamp, &execution).map_err(CheckError::Input)?;
386        self.run_prepared(stamp, index, operation, input, recognizes, visitor)
387    }
388
389    /// Check a timestamp belonging to an actual nested countersignature. Its
390    /// privately bound source scope selects the CHILD input through the root's
391    /// same token/profile kernel, without inventing a child baseline binding.
392    #[expect(
393        clippy::result_large_err,
394        reason = "retain original source maps and opaque callback causes"
395    )]
396    pub fn retain_counter_actor<PE, RE>(
397        &self,
398        counter: &CounterStamp<X::Id, X::Document, X::Generation>,
399        index: usize,
400        operation: &Reader<'_>,
401        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
402    ) -> CheckResult<Option<TimestampActor<'certificate, 'document, X>>, X, PE, RE>
403    where
404        P: for<'scope> SignatureProvider<'scope, Error = PE>,
405    {
406        if index >= counter.stamp().tokens().len() {
407            return Err(CheckError::TokenIndex {
408                node: counter.stamp().node(),
409                index,
410            });
411        }
412        let execution = operation.execution();
413        let input = counter
414            .input(self.xml, &execution)
415            .map_err(CheckError::Input)?;
416        let stamp = counter.stamp();
417        let located = self.run_prepared(stamp, index, operation, input, recognizes, |checked| {
418            checked.into_evidence()
419        })?;
420        Ok(located.value.map(|evidence| {
421            CheckedActor::new(
422                ISSUER,
423                Verified::new(
424                    ISSUER,
425                    located.location,
426                    stamp.signature,
427                    stamp.node,
428                    stamp.role.purpose(),
429                    evidence,
430                ),
431                self.context.certificate,
432                self.context.key,
433            )
434        }))
435    }
436
437    /// Retain evidence for a root timestamp from its original frozen pool entry.
438    /// The pool's selected encoding is reused without decoding or parsing the
439    /// CMS envelope again. The original token and input must belong to the same
440    /// timestamp on the bound signature. Declaration conformance remains a
441    /// separate pool fact.
442    #[expect(
443        clippy::result_large_err,
444        reason = "retain original source maps and opaque callback causes"
445    )]
446    pub fn retain_actor_from<PE, RE>(
447        &self,
448        bound: &Bound<X::Id, X::Document, X::Generation>,
449        stamp: &Timestamp<X>,
450        entry: TokenEntry<'_, '_, X::Id, X::Document, X::Generation>,
451        operation: &Reader<'_>,
452        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
453    ) -> CheckResult<Option<TimestampActor<'certificate, 'document, X>>, X, PE, RE>
454    where
455        P: for<'scope> SignatureProvider<'scope, Error = PE>,
456    {
457        self.retain_from(stamp, entry, operation, recognizes, |execution| {
458            Input::prepare(self.xml, bound, stamp, execution)
459        })
460    }
461
462    /// Reuse an already decoded/parsed original child token from the frozen
463    /// pool. The SAME common profile/check kernel supplies evidence; a fresh
464    /// source reader preserves original and current ceilings without reparsing.
465    /// Original signature/property/token/role are paid and matched BEFORE any
466    /// provider or input replay. No receipt can relabel a sibling/root token.
467    #[expect(
468        clippy::result_large_err,
469        reason = "retain original source maps and opaque callback causes"
470    )]
471    pub fn retain_counter_from<PE, RE>(
472        &self,
473        counter: &CounterStamp<X::Id, X::Document, X::Generation>,
474        entry: TokenEntry<'_, '_, X::Id, X::Document, X::Generation>,
475        operation: &Reader<'_>,
476        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
477    ) -> CheckResult<Option<TimestampActor<'certificate, 'document, X>>, X, PE, RE>
478    where
479        P: for<'scope> SignatureProvider<'scope, Error = PE>,
480    {
481        self.retain_from(counter.stamp(), entry, operation, recognizes, |execution| {
482            counter.input(self.xml, execution)
483        })
484    }
485
486    #[expect(
487        clippy::result_large_err,
488        reason = "retain original source maps and opaque callback causes"
489    )]
490    fn retain_from<PE, RE>(
491        &self,
492        stamp: &Timestamp<X>,
493        entry: TokenEntry<'_, '_, X::Id, X::Document, X::Generation>,
494        operation: &Reader<'_>,
495        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
496        prepare: impl FnOnce(&Execution<'_>) -> InputResult<Input<'xml, X>, X>,
497    ) -> CheckResult<Option<TimestampActor<'certificate, 'document, X>>, X, PE, RE>
498    where
499        P: for<'scope> SignatureProvider<'scope, Error = PE>,
500    {
501        let execution = operation.execution();
502        let access = Access::new(self.xml, stamp.snapshot, &execution);
503        access
504            .query(stamp.node, |_, _, work| Ok(work.charge_work(1)))
505            .map_err(|cause| CheckError::Input(InputError::Access(cause)))?
506            .map_err(|cause| {
507                CheckError::Input(InputError::Resource {
508                    node: stamp.node,
509                    cause,
510                })
511            })?;
512        let source = entry.source;
513        let equal = |left, right| {
514            access.equal_id(left, right).map_err(|cause| {
515                CheckError::Input(InputError::Resource {
516                    node: stamp.node,
517                    cause,
518                })
519            })
520        };
521        let expected = stamp.tokens.get(source.token_index());
522        if source.location().snapshot != stamp.snapshot
523            || !equal(source.signature(), stamp.signature)?
524            || !equal(source.property(), stamp.node)?
525            || source.role() != stamp.role
526            || match expected {
527                Some(expected) => !equal(source.location().node, *expected)?,
528                None => true,
529            }
530        {
531            return Err(CheckError::Input(InputError::Foreign {
532                node: source.location().node,
533            }));
534        }
535        let mut input = prepare(&execution).map_err(CheckError::Input)?;
536        let location = source.location();
537        let reader = entry.reader_in(operation).map_err(|cause| {
538            CheckError::Token(binary::Error::ReaderCreation { location, cause })
539        })?;
540        let checked = entry
541            .reading
542            .token()
543            .check_with_profile(
544                self.provider,
545                &reader,
546                self.context,
547                &mut input,
548                recognizes,
549                |fields| profile::check(&fields),
550            )
551            .map_err(|cause| {
552                CheckError::Token(binary::Error::Callback {
553                    location,
554                    cause: binary::TokenError::Callback(cause),
555                })
556            })?;
557        let evidence = checked.into_evidence();
558        access
559            .query(stamp.node, |_, _, _| Ok(()))
560            .map_err(|cause| CheckError::Input(InputError::Access(cause)))?;
561        Ok(evidence.map(|evidence| {
562            CheckedActor::new(
563                ISSUER,
564                Verified::new(
565                    ISSUER,
566                    location,
567                    stamp.signature,
568                    stamp.node,
569                    stamp.role.purpose(),
570                    evidence,
571                ),
572                self.context.certificate,
573                self.context.key,
574            )
575        }))
576    }
577
578    #[expect(
579        clippy::result_large_err,
580        reason = "retain original source maps and opaque callback causes"
581    )]
582    fn run_prepared<R, PE, RE>(
583        &self,
584        stamp: &Timestamp<X>,
585        index: usize,
586        operation: &Reader<'_>,
587        mut input: Input<'_, X>,
588        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
589        visitor: impl for<'checked> FnOnce(CheckedTimestamp<'checked>) -> R,
590    ) -> CheckResult<binary::LocatedValue<R, X>, X, PE, RE>
591    where
592        P: for<'scope> SignatureProvider<'scope, Error = PE>,
593    {
594        let Some(&node) = stamp.tokens.get(index) else {
595            return Err(CheckError::TokenIndex {
596                node: stamp.node,
597                index,
598            });
599        };
600        let location = Location {
601            snapshot: stamp.snapshot,
602            node,
603        };
604        binary::with_token(self.xml, location, operation, |token, reader| {
605            let checked = token.check_with_profile(
606                self.provider,
607                reader,
608                self.context,
609                &mut input,
610                recognizes,
611                |fields| profile::check(&fields),
612            )?;
613            Ok(binary::Located {
614                location,
615                value: visitor(checked),
616            })
617        })
618        .map_err(CheckError::Token)
619    }
620}
621
622/// Reads an implicit baseline timestamp belonging to the bound selected signature.
623///
624/// Explicit/distributed Include forms are not baseline direct-incorporation inputs.
625/// XML timestamps are not RFC3161 tokens. Missing canonicalization is preserved
626/// as a baseline violation rather than selecting an undocumented method default.
627pub fn read<X: Xml, W: Work>(
628    access: &Access<'_, X, W>,
629    bound: &Bound<X::Id, X::Document, X::Generation>,
630    node: X::Id,
631) -> InputResult<Timestamp<X>, X> {
632    read_in(access, bound.containers(), node)
633}
634
635fn read_in<X: Xml, W: Work>(
636    access: &Access<'_, X, W>,
637    layout: &crate::container::Layout<X>,
638    node: X::Id,
639) -> InputResult<Timestamp<X>, X> {
640    let parent = unsigned_parent_in(access, layout, node)?;
641    let mut contained = false;
642    for child in access.children(parent) {
643        let child = child.map_err(InputError::Child)?;
644        if access
645            .equal_id(child, node)
646            .map_err(|cause| InputError::Resource { node: child, cause })?
647        {
648            contained = true;
649        }
650    }
651    if !contained {
652        return Err(InputError::Foreign { node });
653    }
654    let role = if named(access, node, XADES, "SignatureTimeStamp")? {
655        Role::Signature
656    } else if named(access, node, XADES_141, "ArchiveTimeStamp")? {
657        Role::Archive
658    } else if named(access, node, XADES_141, "SigAndRefsTimeStampV2")? {
659        Role::SignatureAndReferences
660    } else if named(access, node, XADES_141, "RefsOnlyTimeStampV2")? {
661        Role::ReferencesOnly
662    } else {
663        return Err(InputError::Unexpected { node });
664    };
665    read_fields(access, layout.signature(), parent, node, role)
666}
667
668pub(crate) fn unsigned_parent<X: Xml, W: Work>(
669    access: &Access<'_, X, W>,
670    bound: &Bound<X::Id, X::Document, X::Generation>,
671    node: X::Id,
672) -> InputResult<X::Id, X> {
673    unsigned_parent_in(access, bound.containers(), node)
674}
675
676fn unsigned_parent_in<X: Xml, W: Work>(
677    access: &Access<'_, X, W>,
678    layout: &crate::container::Layout<X>,
679    node: X::Id,
680) -> InputResult<X::Id, X> {
681    if layout.snapshot() != access.snapshot() {
682        return Err(InputError::Access(AccessError::Changed { node }));
683    }
684    let unsigned = layout.unsigned().ok_or(InputError::Foreign { node })?;
685    singleton(access, unsigned, XADES, "UnsignedSignatureProperties")
686}
687
688/// Reads an implicit `AllDataObjectsTimeStamp` from the selected signature's
689/// authenticated data-object properties. Explicit `Individual` Include selection
690/// is a different processing contract and cannot be inferred from this input.
691pub fn read_all_data_objects<X: Xml, W: Work>(
692    access: &Access<'_, X, W>,
693    bound: &Bound<X::Id, X::Document, X::Generation>,
694    signed: &crate::properties::Reading<X>,
695    node: X::Id,
696) -> InputResult<Timestamp<X>, X> {
697    read_data_objects(access, bound, signed, node, Role::AllDataObjects)
698}
699
700/// Reads explicit Individual inputs only from authenticated signed properties.
701/// Include resolution is deferred to input preparation, before replay or hashing.
702pub fn read_individual_data_objects<X: Xml, W: Work>(
703    access: &Access<'_, X, W>,
704    bound: &Bound<X::Id, X::Document, X::Generation>,
705    signed: &crate::properties::Reading<X>,
706    node: X::Id,
707) -> InputResult<Timestamp<X>, X> {
708    read_data_objects(access, bound, signed, node, Role::IndividualDataObjects)
709}
710
711fn read_data_objects<X: Xml, W: Work>(
712    access: &Access<'_, X, W>,
713    bound: &Bound<X::Id, X::Document, X::Generation>,
714    signed: &crate::properties::Reading<X>,
715    node: X::Id,
716    role: Role,
717) -> InputResult<Timestamp<X>, X> {
718    let layout = bound.containers();
719    if signed.snapshot() != layout.snapshot() {
720        return Err(InputError::Foreign { node });
721    }
722    if !access
723        .equal_id(signed.signature(), layout.signature())
724        .map_err(|cause| InputError::Resource { node, cause })?
725    {
726        return Err(InputError::Foreign { node });
727    }
728    if access.snapshot() != signed.snapshot() {
729        return Err(InputError::Access(AccessError::Changed { node }));
730    }
731    let objects = signed.data_objects().ok_or(InputError::Foreign { node })?;
732    read_object_fields(access, layout, objects, node, role)
733}
734
735fn read_object_fields<X: Xml, W: Work>(
736    access: &Access<'_, X, W>,
737    layout: &crate::container::Layout<X>,
738    objects: &crate::properties::DataObjects<X::Id>,
739    node: X::Id,
740    role: Role,
741) -> InputResult<Timestamp<X>, X> {
742    let mut contained = false;
743    for index in 0..objects.entries().len() {
744        access
745            .query(node, |_, _, work| Ok(work.charge_work(1)))
746            .map_err(InputError::Access)?
747            .map_err(|cause| InputError::Resource { node, cause })?;
748        let entry = objects.entries()[index];
749        if access
750            .equal_id(entry.node, node)
751            .map_err(|cause| InputError::Resource { node, cause })?
752        {
753            let expected = if role == Role::IndividualDataObjects {
754                crate::properties::DataObjectRole::IndividualTimestamp
755            } else {
756                crate::properties::DataObjectRole::AllTimestamp
757            };
758            if entry.role != expected {
759                return Err(InputError::Unexpected { node });
760            }
761            contained = true;
762            break;
763        }
764    }
765    if !contained {
766        return Err(InputError::Foreign { node });
767    }
768    read_fields(access, layout.signature(), objects.parent(), node, role)
769}
770
771fn read_fields<X: Xml, W: Work>(
772    access: &Access<'_, X, W>,
773    signature: X::Id,
774    parent: X::Id,
775    node: X::Id,
776    role: Role,
777) -> InputResult<Timestamp<X>, X> {
778    let mut method = None;
779    let mut tokens = access
780        .query(node, |_, _, work| Ok(work.vector()))
781        .map_err(InputError::Access)?;
782    let mut includes = access
783        .query(node, |_, _, work| Ok(work.vector()))
784        .map_err(InputError::Access)?;
785    for child in access.children(node) {
786        let child = child.map_err(InputError::Child)?;
787        if named(access, child, XADES, "Include")? {
788            if role != Role::IndividualDataObjects {
789                return Err(InputError::Unexpected { node: child });
790            }
791            if method.is_some() || !tokens.is_empty() {
792                return Err(InputError::Order { node: child });
793            }
794            includes
795                .try_push(child)
796                .map_err(|cause| InputError::Resource { node, cause })?;
797        } else if named(access, child, DS, "CanonicalizationMethod")? {
798            if method.is_some() {
799                return Err(InputError::Duplicate { node: child });
800            }
801            if !tokens.is_empty() {
802                return Err(InputError::Order { node: child });
803            }
804            method = Some(child);
805        } else if named(access, child, XADES, "EncapsulatedTimeStamp")? {
806            if role == Role::Signature && !tokens.is_empty() {
807                return Err(InputError::Duplicate { node: child });
808            }
809            tokens
810                .try_push(child)
811                .map_err(|cause| InputError::Resource { node, cause })?;
812        } else {
813            return Err(InputError::Unexpected { node: child });
814        }
815    }
816    // EN319132-1 §§4.5/5.3/6.3(n): explicit method for generated baseline,
817    // and exactly one RFC3161 token per SignatureTimeStamp. Archive and
818    // AllDataObjectsTimeStamp (§5.2.8.1) may each carry multiple tokens.
819    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
820    let method = method.ok_or(InputError::Missing {
821        node,
822        local: "CanonicalizationMethod",
823    })?;
824    if tokens.is_empty() {
825        return Err(InputError::Missing {
826            node,
827            local: "EncapsulatedTimeStamp",
828        });
829    }
830    if role == Role::IndividualDataObjects && includes.is_empty() {
831        return Err(InputError::Missing {
832            node,
833            local: "Include",
834        });
835    }
836    Ok(Stamp {
837        snapshot: access.snapshot(),
838        signature,
839        node,
840        parent,
841        method,
842        role,
843        tokens: tokens.into_owned(),
844        includes: includes.into_owned(),
845    })
846}
847
848#[derive(Debug, Clone, Copy)]
849enum Part<N> {
850    Element(N),
851    Reference { node: N, covered: Option<N> },
852}
853
854/// Prepared original input sequence; no canonical XML is buffered here.
855pub struct Input<'a, X: TimestampXml> {
856    xml: &'a X,
857    snapshot: Snapshot<X::Document, X::Generation>,
858    signature: X::Id,
859    timestamp: X::Id,
860    method: X::Id,
861    role: Role,
862    parts: OwnedVec<Part<X::Id>>,
863}
864
865impl<'a, X: TimestampXml> Input<'a, X> {
866    /// Select the input before the new timestamp exists. `method` is an actual
867    /// owner-validated `CanonicalizationMethod` in this immutable snapshot,
868    /// including its parameters. It may be in the owner's unpublished staging
869    /// area; it must not be a new unsigned property or an extra ds:Object.
870    ///
871    /// EN319132-1 §§5.3/5.5.2.2–3 distinguish creation's full unsigned tail
872    /// from validation's prefix before an existing archive. Any material or
873    /// `RenewedDigestsV2` must already be staged before preparing this input.
874    /// Mutation invalidates it; obtain a new input/request after any change.
875    /// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
876    pub fn prepare_append<W: Work>(
877        xml: &'a X,
878        bound: &Bound<X::Id, X::Document, X::Generation>,
879        kind: AppendKind,
880        method: X::Id,
881        execution: &W,
882    ) -> InputResult<Self, X> {
883        let layout = bound.containers();
884        let access = Access::new(xml, layout.snapshot(), execution);
885        if !named(&access, method, DS, "CanonicalizationMethod")? {
886            return Err(InputError::Unexpected { node: method });
887        }
888        let mut parts = execution.vector();
889        let role = match kind {
890            AppendKind::Signature => {
891                let value = singleton(&access, layout.signature(), DS, "SignatureValue")?;
892                parts
893                    .try_push(Part::Element(value))
894                    .map_err(|cause| InputError::Resource {
895                        node: method,
896                        cause,
897                    })?;
898                Role::Signature
899            }
900            AppendKind::Archive => {
901                append_archive(&access, layout, method, None, &mut parts)?;
902                Role::Archive
903            }
904        };
905        Ok(Self {
906            xml,
907            snapshot: layout.snapshot(),
908            signature: layout.signature(),
909            timestamp: method,
910            method,
911            role,
912            parts: parts.into_owned(),
913        })
914    }
915
916    /// Selects every original input part before C14N, hashing or source callbacks.
917    pub fn prepare<W: Work>(
918        xml: &'a X,
919        bound: &Bound<X::Id, X::Document, X::Generation>,
920        stamp: &Timestamp<X>,
921        execution: &W,
922    ) -> InputResult<Self, X> {
923        Self::prepare_in(xml, bound.containers(), stamp, execution)
924    }
925
926    fn prepare_in<W: Work>(
927        xml: &'a X,
928        layout: &crate::container::Layout<X>,
929        stamp: &Timestamp<X>,
930        execution: &W,
931    ) -> InputResult<Self, X> {
932        if stamp.snapshot != layout.snapshot() {
933            return Err(InputError::Foreign { node: stamp.node });
934        }
935        let access = Access::new(xml, stamp.snapshot, execution);
936        let refusal = |cause| InputError::Resource {
937            node: stamp.node,
938            cause,
939        };
940        if !access
941            .equal_id(stamp.signature, layout.signature())
942            .map_err(refusal)?
943        {
944            return Err(InputError::Foreign { node: stamp.node });
945        }
946        let mut parts = execution.vector();
947        match stamp.role {
948            Role::Signature => {
949                let value = singleton(&access, stamp.signature, DS, "SignatureValue")?;
950                parts.try_push(Part::Element(value)).map_err(refusal)?;
951            }
952            Role::AllDataObjects => {
953                // EN319132-1 §5.2.8.1: direct SignedInfo references only, in order,
954                // except those referencing SignedProperties. A signed Manifest's
955                // referenced targets do not add another traversal to this input.
956                // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
957                append_references(
958                    &access,
959                    stamp.signature,
960                    stamp.node,
961                    Some(layout.signed()),
962                    &mut parts,
963                )?;
964            }
965            Role::IndividualDataObjects => {
966                append_includes(&access, stamp, layout.signed(), &mut parts)?;
967            }
968            Role::SignatureAndReferences | Role::ReferencesOnly => {
969                append_validation_references(&access, stamp, &mut parts)?;
970            }
971            Role::Archive => {
972                append_archive(
973                    &access,
974                    layout,
975                    stamp.node,
976                    Some((stamp.parent, stamp.node)),
977                    &mut parts,
978                )?;
979            }
980        }
981        Ok(Self {
982            xml,
983            snapshot: stamp.snapshot,
984            signature: stamp.signature,
985            timestamp: stamp.node,
986            method: stamp.method,
987            role: stamp.role,
988            parts: parts.into_owned(),
989        })
990    }
991
992    /// Original enclosing XML location for a logical streamed-source failure.
993    /// A logical stream offset must never be reported as a physical XML byte offset.
994    #[must_use]
995    pub fn location(&self) -> (Snapshot<X::Document, X::Generation>, X::Id) {
996        (self.snapshot, self.timestamp)
997    }
998}
999
1000fn append_validation_references<X: TimestampXml, W: Work>(
1001    access: &Access<'_, X, W>,
1002    stamp: &Timestamp<X>,
1003    parts: &mut OwnedVec<Part<X::Id>, W>,
1004) -> InputResult<(), X> {
1005    // EN319132-1 A.1.5.1.2/A.1.5.2.2: implicit same-parent input uses
1006    // ORIGINAL preceding property order, not the order of the role list.
1007    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
1008    let refusal = |cause| InputError::Resource {
1009        node: stamp.node,
1010        cause,
1011    };
1012    let with_signature = stamp.role == Role::SignatureAndReferences;
1013    if with_signature {
1014        let value = singleton(access, stamp.signature, DS, "SignatureValue")?;
1015        parts.try_push(Part::Element(value)).map_err(refusal)?;
1016    }
1017    let mut found = [false; 4];
1018    let roles = [
1019        (XADES_141, "CompleteCertificateRefsV2"),
1020        (XADES, "CompleteRevocationRefs"),
1021        (XADES_141, "AttributeCertificateRefsV2"),
1022        (XADES, "AttributeRevocationRefs"),
1023    ];
1024    let mut reached = false;
1025    for child in access.children(stamp.parent) {
1026        let node = child.map_err(InputError::Child)?;
1027        if access.equal_id(node, stamp.node).map_err(refusal)? {
1028            reached = true;
1029            break;
1030        }
1031        let mut selected = with_signature && named(access, node, XADES, "SignatureTimeStamp")?;
1032        for (index, (namespace, local)) in roles.iter().enumerate() {
1033            if named(access, node, namespace, local)? {
1034                if found[index] {
1035                    return Err(InputError::Duplicate { node });
1036                }
1037                found[index] = true;
1038                selected = true;
1039                break;
1040            }
1041        }
1042        if selected {
1043            parts.try_push(Part::Element(node)).map_err(refusal)?;
1044        }
1045    }
1046    if !reached {
1047        return Err(InputError::Foreign { node: stamp.node });
1048    }
1049    for index in 0..2 {
1050        if !found[index] {
1051            return Err(InputError::Missing {
1052                node: stamp.node,
1053                local: roles[index].1,
1054            });
1055        }
1056    }
1057    Ok(())
1058}
1059
1060fn append_includes<X: TimestampXml, W: Work>(
1061    access: &Access<'_, X, W>,
1062    stamp: &Timestamp<X>,
1063    signed_properties: X::Id,
1064    parts: &mut OwnedVec<Part<X::Id>, W>,
1065) -> InputResult<(), X> {
1066    // EN319132-1 §5.2.8.2: explicit Include order, true referencedData,
1067    // SignedInfo OR signed Manifest references; SignedProperties excluded.
1068    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
1069    for index in 0..stamp.includes.len() {
1070        access
1071            .query(stamp.node, |_, _, work| Ok(work.charge_work(1)))
1072            .map_err(InputError::Access)?
1073            .map_err(|cause| InputError::Resource {
1074                node: stamp.node,
1075                cause,
1076            })?;
1077        let include = stamp.includes[index];
1078        let value = access
1079            .query(include, |xml, snapshot, work| {
1080                xml.boolean_attribute(
1081                    snapshot,
1082                    include,
1083                    Name {
1084                        namespace: "",
1085                        local: "referencedData",
1086                    },
1087                    work,
1088                )
1089            })
1090            .map_err(InputError::Access)?;
1091        if value != Boolean::Value(true) {
1092            return Err(InputError::ReferencedData {
1093                node: include,
1094                value,
1095            });
1096        }
1097        let resolved = access
1098            .query(include, |xml, snapshot, work| {
1099                xml.resolve_reference(
1100                    snapshot,
1101                    stamp.signature,
1102                    include,
1103                    LexicalSource::Attribute(Name {
1104                        namespace: "",
1105                        local: "URI",
1106                    }),
1107                    work,
1108                    |descriptor| {
1109                        let r = descriptor.reference;
1110                        (r.node, r.target, r.status, r.coverage)
1111                    },
1112                )
1113            })
1114            .map_err(InputError::Access)?;
1115        let reference = match resolved {
1116            Resolution::Resolved((node, target, Status::Valid, coverage)) => {
1117                if let Some(target) = target
1118                    && access
1119                        .equal_id(target, signed_properties)
1120                        .map_err(|cause| InputError::Resource {
1121                            node: include,
1122                            cause,
1123                        })?
1124                {
1125                    return Err(InputError::Reference {
1126                        node: include,
1127                        resolution: Resolution::Resolved(()),
1128                    });
1129                }
1130                Part::Reference {
1131                    node,
1132                    covered: if coverage == Coverage::ElementContent {
1133                        target
1134                    } else {
1135                        None
1136                    },
1137                }
1138            }
1139            other => {
1140                return Err(InputError::Reference {
1141                    node: include,
1142                    resolution: other.map(|_| ()),
1143                });
1144            }
1145        };
1146        parts
1147            .try_push(reference)
1148            .map_err(|cause| InputError::Resource {
1149                node: include,
1150                cause,
1151            })?;
1152    }
1153    Ok(())
1154}
1155
1156fn append_references<X: TimestampXml, W: Work>(
1157    access: &Access<'_, X, W>,
1158    signature: X::Id,
1159    context: X::Id,
1160    excluded: Option<X::Id>,
1161    parts: &mut OwnedVec<Part<X::Id>, W>,
1162) -> InputResult<(), X> {
1163    let count = access
1164        .query(signature, |xml, snapshot, work| {
1165            xml.reference_count(snapshot, signature, work)
1166        })
1167        .map_err(InputError::Access)?;
1168    for ordinal in 0..count {
1169        let (part, target) = access
1170            .query(signature, |xml, snapshot, work| {
1171                xml.reference(snapshot, signature, ordinal, work, |r| {
1172                    (
1173                        Part::Reference {
1174                            node: r.node,
1175                            covered: if r.status == Status::Valid
1176                                && r.coverage == Coverage::ElementContent
1177                            {
1178                                r.target
1179                            } else {
1180                                None
1181                            },
1182                        },
1183                        r.target,
1184                    )
1185                })
1186            })
1187            .map_err(InputError::Access)?;
1188        if let (Some(target), Some(excluded)) = (target, excluded)
1189            && access
1190                .equal_id(target, excluded)
1191                .map_err(|cause| InputError::Resource {
1192                    node: context,
1193                    cause,
1194                })?
1195        {
1196            continue;
1197        }
1198        parts.try_push(part).map_err(|cause| InputError::Resource {
1199            node: context,
1200            cause,
1201        })?;
1202    }
1203    Ok(())
1204}
1205
1206// One input-order kernel for creation's complete tail and validation's original
1207// prefix, EN319132-1 §5.5.2.3. The optional boundary is a source node, not a count.
1208fn append_archive<X: TimestampXml, W: Work>(
1209    access: &Access<'_, X, W>,
1210    layout: &crate::container::Layout<X>,
1211    context: X::Id,
1212    before: Option<(X::Id, X::Id)>,
1213    parts: &mut OwnedVec<Part<X::Id>, W>,
1214) -> InputResult<(), X> {
1215    let refusal = |cause| InputError::Resource {
1216        node: context,
1217        cause,
1218    };
1219    let signature = layout.signature();
1220    append_references(access, signature, context, None, parts)?;
1221    parts
1222        .try_push(Part::Element(singleton(
1223            access,
1224            signature,
1225            DS,
1226            "SignedInfo",
1227        )?))
1228        .map_err(refusal)?;
1229    parts
1230        .try_push(Part::Element(singleton(
1231            access,
1232            signature,
1233            DS,
1234            "SignatureValue",
1235        )?))
1236        .map_err(refusal)?;
1237    if let Some(key) = optional(access, signature, DS, "KeyInfo")? {
1238        parts.try_push(Part::Element(key)).map_err(refusal)?;
1239    }
1240    let parent = match before {
1241        Some((parent, _)) => Some(parent),
1242        None => match layout.unsigned() {
1243            Some(unsigned) => optional(access, unsigned, XADES, "UnsignedSignatureProperties")?,
1244            None => None,
1245        },
1246    };
1247    let mut reached = before.is_none();
1248    if let Some(parent) = parent {
1249        for child in access.children(parent) {
1250            let node = child.map_err(InputError::Child)?;
1251            if let Some((_, boundary)) = before
1252                && access.equal_id(node, boundary).map_err(refusal)?
1253            {
1254                reached = true;
1255                break;
1256            }
1257            parts.try_push(Part::Element(node)).map_err(refusal)?;
1258        }
1259    }
1260    if !reached {
1261        return Err(InputError::Foreign { node: context });
1262    }
1263    for child in access.children(signature) {
1264        let node = child.map_err(InputError::Child)?;
1265        if !access.equal_id(node, layout.object()).map_err(refusal)?
1266            && named(access, node, DS, "Object")?
1267        {
1268            parts.try_push(Part::Element(node)).map_err(refusal)?;
1269        }
1270    }
1271    Ok(())
1272}
1273
1274impl<X: TimestampXml> Source for Input<'_, X> {
1275    type Error = InputError<X::Error, X::Id>;
1276
1277    fn stream<W: Work>(
1278        &mut self,
1279        receiver: &mut impl Receiver,
1280        execution: &W,
1281    ) -> Result<(), Self::Error> {
1282        let access = Access::new(self.xml, self.snapshot, execution);
1283        let mut receiver = Stop {
1284            receiver,
1285            refused: false,
1286        };
1287        for index in 0..self.parts.len() {
1288            execution
1289                .charge_work(1)
1290                .map_err(|cause| InputError::Resource {
1291                    node: self.timestamp,
1292                    cause,
1293                })?;
1294            match self.parts[index] {
1295                Part::Element(node) => access
1296                    .query(node, |xml, snapshot, work| {
1297                        xml.canonicalize(snapshot, node, self.method, &mut receiver, work)
1298                    })
1299                    .map_err(InputError::Access)?,
1300                Part::Reference { node, .. } => {
1301                    let outcome = access
1302                        .query(node, |xml, snapshot, work| {
1303                            xml.replay_reference(
1304                                snapshot,
1305                                self.signature,
1306                                node,
1307                                crate::reference::Replay::Timestamp(self.method),
1308                                &mut receiver,
1309                                work,
1310                            )
1311                        })
1312                        .map_err(InputError::Access)?;
1313                    if let ReplayOutcome::Unavailable(cause) = outcome {
1314                        return Err(InputError::Unavailable { node, cause });
1315                    }
1316                }
1317            }
1318            if receiver.refused {
1319                return Err(InputError::Receiver(Refused));
1320            }
1321        }
1322        Ok(())
1323    }
1324}
1325
1326struct Stop<'a, R: Receiver> {
1327    receiver: &'a mut R,
1328    refused: bool,
1329}
1330
1331impl<R: Receiver> Receiver for Stop<'_, R> {
1332    fn write(&mut self, bytes: &[u8]) -> Result<(), Refused> {
1333        if self.refused {
1334            return Err(Refused);
1335        }
1336        if self.receiver.write(bytes).is_err() {
1337            self.refused = true;
1338            return Err(Refused);
1339        }
1340        Ok(())
1341    }
1342}
1343
1344fn named<X: Xml, W: Work>(
1345    access: &Access<'_, X, W>,
1346    node: X::Id,
1347    namespace: &str,
1348    local: &str,
1349) -> InputResult<bool, X> {
1350    access
1351        .has_name(node, Name { namespace, local })
1352        .map_err(InputError::Access)
1353}
1354
1355pub(crate) fn singleton<X: Xml, W: Work>(
1356    access: &Access<'_, X, W>,
1357    parent: X::Id,
1358    namespace: &str,
1359    local: &'static str,
1360) -> InputResult<X::Id, X> {
1361    optional(access, parent, namespace, local)?.ok_or(InputError::Missing {
1362        node: parent,
1363        local,
1364    })
1365}
1366
1367pub(crate) fn optional<X: Xml, W: Work>(
1368    access: &Access<'_, X, W>,
1369    parent: X::Id,
1370    namespace: &str,
1371    local: &str,
1372) -> InputResult<Option<X::Id>, X> {
1373    let mut found = None;
1374    for child in access.children(parent) {
1375        let node = child.map_err(InputError::Child)?;
1376        if named(access, node, namespace, local)? {
1377            if found.is_some() {
1378                return Err(InputError::Duplicate { node });
1379            }
1380            found = Some(node);
1381        }
1382    }
1383    Ok(found)
1384}