Skip to main content

structured_xades/
format.rs

1//! Baseline data-object format structure, reference coverage and object agreement.
2
3use crate::binding::{Bound, Coverage, Status};
4use crate::container::{DS, XADES};
5use crate::properties::{DataObjectRole, Error, Reading};
6use crate::reference::{Completeness, LexicalSource, Parent, ReferenceXml, Resolution, VisitError};
7use crate::xml::{Access, AccessError, Name, Node, Snapshot, Xml};
8use structured_asn1::memory::{OwnedVec, Work};
9
10/// Format structure, reference eligibility and exact object-metadata refusals.
11#[derive(Debug)]
12pub enum FormatError<E, N> {
13    /// Caller ordinal is absent or does not select a data-object format.
14    Selection {
15        /// Original selected signature.
16        signature: N,
17        /// Requested data-object property ordinal.
18        index: usize,
19    },
20    /// Original schema, source-generation or resource cause.
21    Property(Error<E, N>),
22    /// Lookup reached an instruction whose target digest was not successfully
23    /// checked, or which refers to `SignedProperties` rather than a data object.
24    Reference {
25        /// Original authenticated reference instruction.
26        node: N,
27        /// Actual completed or unchecked digest outcome.
28        status: Status,
29    },
30    /// Supplied reading belongs to another selected signature or source.
31    Foreign {
32        /// Original property node from the supplied reading.
33        node: N,
34    },
35    /// A present original `ds:Object` attribute differs from its format value.
36    Metadata {
37        /// Original property value element.
38        node: N,
39        /// Original referenced `ds:Object`.
40        object: N,
41        /// Exact unqualified attribute name.
42        local: &'static str,
43    },
44}
45
46/// Resolved original instruction, including present ds:Object metadata agreement.
47/// MIME syntax, URI lexical validation and trust are separate checks.
48#[derive(Debug, Clone, Copy)]
49pub struct Resolved<N> {
50    /// Original reference instruction.
51    pub reference: N,
52    /// Its exact authenticated original parent.
53    pub parent: Parent<N>,
54    /// Original same-document target, if present.
55    pub target: Option<N>,
56    /// Actual conservative transform coverage, independent of URI lookup.
57    pub coverage: Coverage,
58}
59
60/// Required object-format association failed before scalar/digest execution.
61#[derive(Debug)]
62pub enum CoverageError<E, N> {
63    /// Original format structure, resolution eligibility or source refusal.
64    Format(FormatError<E, N>),
65    /// Original guard/admission refusal.
66    Access(AccessError<E, N>),
67    /// Original XML catalogue traversal refusal, including its resource class.
68    Processing(E),
69    /// A format URI was not resolved to an authenticated instruction.
70    Lookup {
71        /// Original `DataObjectFormat`.
72        node: N,
73        /// Exact unsuccessful lookup outcome, distinct from processing refusal.
74        resolution: Resolution<()>,
75    },
76    /// Required object reference has no format property.
77    Missing {
78        /// Original Reference node.
79        reference: N,
80    },
81    /// Multiple formats identify the same reference instruction.
82    Repeated {
83        /// Original Reference node.
84        reference: N,
85    },
86    /// Format does not qualify any required object (including counter exemptions).
87    Unused {
88        /// Original `DataObjectFormat`.
89        node: N,
90    },
91    /// Supplied counter binding belongs to another source or child signature.
92    ForeignCounter {
93        /// Original selected signature.
94        signature: N,
95    },
96    /// Original XML request did not provide a complete reference catalogue.
97    Incomplete {
98        /// Original unprocessed instruction or container.
99        node: N,
100    },
101    /// Referenced data was not successfully checked by `XMLDSig`.
102    Unchecked {
103        /// Original Reference node.
104        reference: N,
105    },
106}
107
108/// Prepared original format with its unique required reference association.
109#[derive(Debug)]
110pub struct CoveredFormat<N, D, G> {
111    original: Format<N, D, G>,
112    reference: N,
113    seen: bool,
114}
115
116impl<N: Copy, D: Copy, G: Copy> CoveredFormat<N, D, G> {
117    /// Original property for subsequent scalar and object-metadata validation.
118    #[must_use]
119    pub fn original(&self) -> &Format<N, D, G> {
120        &self.original
121    }
122    /// Exact required reference, including signed Manifest children.
123    #[must_use]
124    pub fn reference(&self) -> N {
125        self.reference
126    }
127}
128
129/// Accounted complete associations and object metadata agreement; scalar syntax
130/// and MIME correctness are separate.
131pub type Covered<X, W = structured_asn1::memory::Memory> =
132    OwnedVec<CoveredFormat<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>, W>;
133/// Original cause retained through reference-catalogue visitor refusal.
134pub type CoverageResult<T, X> = Result<T, CoverageError<<X as Xml>::Error, <X as Xml>::Id>>;
135
136/// Checks EN319132-1 §6.3(k)'s complete per-object format coverage.
137/// A selected authenticated counter binding supplies the counter-value exemption;
138/// arbitrary caller booleans or nodes cannot claim that exception. All relevant
139/// authenticated Manifest instructions must be traversed, not just references
140/// for which a format happens to exist. Partial XML catalogues stay incomplete.
141/// This establishes association and exact ds:Object metadata agreement, not
142/// MIME syntax, Encoding URI validity or trust.
143/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
144pub fn check_references<X: ReferenceXml, W: Work>(
145    access: &Access<'_, X, W>,
146    bound: &Bound<X::Id, X::Document, X::Generation>,
147    signed: &Reading<X>,
148    counter: Option<&crate::counter::Binding<X>>,
149) -> CoverageResult<Covered<X>, X> {
150    let layout = bound.containers();
151    let counter_value = bind_sources(access, layout, signed, counter)?;
152    let mut formats = prepare_formats(access, bound, signed)?;
153    let mut indices = access
154        .query(layout.signature(), |_, _, work| Ok(work.map()))
155        .map_err(CoverageError::Access)?;
156    for index in 0..formats.len() {
157        admit(access, formats[index].original.node)?;
158        let reference = formats[index].reference;
159        if indices
160            .try_insert(reference, index)
161            .map_err(|cause| {
162                CoverageError::Access(AccessError::Resource {
163                    node: formats[index].original.node,
164                    cause,
165                })
166            })?
167            .is_some()
168        {
169            return Err(CoverageError::Repeated { reference });
170        }
171    }
172    let visited = access
173        .query(layout.signature(), |xml, snapshot, work| {
174            Ok(xml.visit_authenticated_references(
175                snapshot,
176                layout.signature(),
177                work,
178                |descriptor| {
179                    let r = descriptor.reference;
180                    if let Some(target) = r.target {
181                        let equal = |other| {
182                            access.equal_id(target, other).map_err(|cause| {
183                                CoverageError::Access(AccessError::Resource {
184                                    node: r.node,
185                                    cause,
186                                })
187                            })
188                        };
189                        if equal(layout.signed())? {
190                            return Ok(());
191                        }
192                        if let Some(value) = counter_value
193                            && equal(value)?
194                        {
195                            return Ok(());
196                        }
197                    }
198                    if r.status != Status::Valid {
199                        return Err(CoverageError::Unchecked { reference: r.node });
200                    }
201                    // Existing accounted map admits hash/equality and the
202                    // pinned engine's full possible hidden probe chain first.
203                    // Actual lookup avoids F×R identity scans; conservative
204                    // worst-probe admission is not claimed to be linear.
205                    let Some(&index) = indices.get(&r.node, work).map_err(|cause| {
206                        CoverageError::Access(AccessError::Resource {
207                            node: r.node,
208                            cause,
209                        })
210                    })?
211                    else {
212                        return Err(CoverageError::Missing { reference: r.node });
213                    };
214                    debug_assert!(index < formats.len());
215                    formats[index].seen = true;
216                    Ok(())
217                },
218            ))
219        })
220        .map_err(CoverageError::Access)?;
221    let completeness = match visited {
222        Ok(value) => value,
223        Err(VisitError::Processing(cause)) => return Err(CoverageError::Processing(cause)),
224        Err(VisitError::Visitor(cause)) => return Err(cause),
225    };
226    match completeness {
227        Completeness::Complete => {}
228        Completeness::Unchecked(node) | Completeness::Incomplete(node) => {
229            return Err(CoverageError::Incomplete { node });
230        }
231    }
232    for index in 0..formats.len() {
233        admit(access, signed.parent())?;
234        if !formats[index].seen {
235            return Err(CoverageError::Unused {
236                node: formats[index].original.node,
237            });
238        }
239    }
240    Ok(formats.into_owned())
241}
242
243fn bind_sources<X: Xml, W: Work>(
244    access: &Access<'_, X, W>,
245    layout: &crate::container::Layout<X>,
246    signed: &Reading<X>,
247    counter: Option<&crate::counter::Binding<X>>,
248) -> CoverageResult<Option<X::Id>, X> {
249    if access.snapshot() != layout.snapshot() {
250        return Err(CoverageError::Access(AccessError::Changed {
251            node: layout.signature(),
252        }));
253    }
254    if signed.snapshot() != layout.snapshot() {
255        return Err(CoverageError::Format(FormatError::Foreign {
256            node: signed.parent(),
257        }));
258    }
259    if !access
260        .equal_id(signed.signature(), layout.signature())
261        .map_err(|cause| {
262            CoverageError::Access(AccessError::Resource {
263                node: signed.parent(),
264                cause,
265            })
266        })?
267    {
268        return Err(CoverageError::Format(FormatError::Foreign {
269            node: signed.parent(),
270        }));
271    }
272    if let Some(counter) = counter {
273        if counter.original().snapshot() != layout.snapshot()
274            || !access
275                .equal_id(counter.original().child(), layout.signature())
276                .map_err(|cause| {
277                    CoverageError::Access(AccessError::Resource {
278                        node: counter.original().node(),
279                        cause,
280                    })
281                })?
282        {
283            return Err(CoverageError::ForeignCounter {
284                signature: layout.signature(),
285            });
286        }
287        Ok(Some(counter.original().value()))
288    } else {
289        Ok(None)
290    }
291}
292
293fn admit<X: Xml, W: Work>(access: &Access<'_, X, W>, node: X::Id) -> CoverageResult<(), X> {
294    access
295        .query(node, |_, _, work| Ok(work.charge_work(1)))
296        .map_err(CoverageError::Access)?
297        .map_err(|cause| CoverageError::Access(AccessError::Resource { node, cause }))
298}
299
300fn prepare_formats<X: ReferenceXml, W: Work>(
301    access: &Access<'_, X, W>,
302    bound: &Bound<X::Id, X::Document, X::Generation>,
303    signed: &Reading<X>,
304) -> CoverageResult<Covered<X, W>, X> {
305    let mut formats = access
306        .query(signed.parent(), |_, _, work| Ok(work.vector()))
307        .map_err(CoverageError::Access)?;
308    if let Some(objects) = signed.data_objects() {
309        for index in 0..objects.entries().len() {
310            admit(access, objects.parent())?;
311            if objects.entries()[index].role != DataObjectRole::Format {
312                continue;
313            }
314            let original = read(access, signed, index).map_err(CoverageError::Format)?;
315            let resolved = match resolve(access, bound, &original).map_err(CoverageError::Format)? {
316                Resolution::Resolved(value) => value,
317                other => {
318                    return Err(CoverageError::Lookup {
319                        node: original.node,
320                        resolution: other.map(|_| ()),
321                    });
322                }
323            };
324            let node = original.node;
325            formats
326                .try_push(CoveredFormat {
327                    original,
328                    reference: resolved.reference,
329                    seen: false,
330                })
331                .map_err(|cause| CoverageError::Access(AccessError::Resource { node, cause }))?;
332        }
333    }
334    Ok(formats)
335}
336
337/// Resolves `ObjectReference` with the same XML-owned `SignedInfo`/Manifest kernel
338/// used for timestamp Include. Namespace/URI processing, acquisition permission
339/// and original-context resolution are never reconstructed in this crate.
340/// EN319132-1 §5.2.4 and §6.3 note8 permit a signed Manifest reference.
341/// Present MimeType/Encoding values must exactly match corresponding attributes
342/// when the resolved target is ds:Object; no case folding or URI normalization
343/// is used, and no default is fabricated for an absent attribute.
344/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
345pub fn resolve<X: ReferenceXml, W: Work>(
346    access: &Access<'_, X, W>,
347    bound: &Bound<X::Id, X::Document, X::Generation>,
348    format: &Original<X>,
349) -> FormatResult<Resolution<Resolved<X::Id>>, X> {
350    let layout = bound.containers();
351    if format.snapshot != layout.snapshot() {
352        return Err(FormatError::Foreign { node: format.node });
353    }
354    if !access
355        .equal_id(format.signature, layout.signature())
356        .map_err(|cause| {
357            FormatError::Property(Error::Access(AccessError::Resource {
358                node: format.node,
359                cause,
360            }))
361        })?
362    {
363        return Err(FormatError::Foreign { node: format.node });
364    }
365    if access.snapshot() != format.snapshot {
366        return Err(FormatError::Property(Error::Access(AccessError::Changed {
367            node: format.node,
368        })));
369    }
370    let resolved = access
371        .query(format.node, |xml, snapshot, work| {
372            xml.resolve_reference(
373                snapshot,
374                format.signature,
375                format.node,
376                LexicalSource::Attribute(Name {
377                    namespace: "",
378                    local: "ObjectReference",
379                }),
380                work,
381                |descriptor| {
382                    let r = descriptor.reference;
383                    if r.status != Status::Valid {
384                        return Err(FormatError::Reference {
385                            node: r.node,
386                            status: r.status,
387                        });
388                    }
389                    if let Some(object) = r.target {
390                        if access.equal_id(object, layout.signed()).map_err(|cause| {
391                            FormatError::Property(Error::Access(AccessError::Resource {
392                                node: r.node,
393                                cause,
394                            }))
395                        })? {
396                            return Err(FormatError::Reference {
397                                node: r.node,
398                                status: r.status,
399                            });
400                        }
401                        object_metadata(access, format, object)?;
402                    }
403                    Ok(Resolved {
404                        reference: r.node,
405                        parent: descriptor.parent,
406                        target: r.target,
407                        coverage: r.coverage,
408                    })
409                },
410            )
411        })
412        .map_err(|cause| FormatError::Property(Error::Access(cause)))?;
413    Ok(match resolved {
414        Resolution::Resolved(value) => Resolution::Resolved(value?),
415        Resolution::Missing => Resolution::Missing,
416        Resolution::Unresolved => Resolution::Unresolved,
417        Resolution::Unauthenticated => Resolution::Unauthenticated,
418        Resolution::Unsupported => Resolution::Unsupported,
419    })
420}
421
422fn object_metadata<X: Xml, W: Work>(
423    access: &Access<'_, X, W>,
424    format: &Original<X>,
425    object: X::Id,
426) -> FormatResult<(), X> {
427    let access_error = |cause| FormatError::Property(Error::Access(cause));
428    if !access
429        .has_name(
430            object,
431            Name {
432                namespace: DS,
433                local: "Object",
434            },
435        )
436        .map_err(access_error)?
437    {
438        return Ok(());
439    }
440    // EN319132-1 V1.3.1 §5.2.4 requires exactly the same attribute values,
441    // not MIME equivalence or URI normalization. Only present format fields
442    // participate; no absent Encoding is manufactured from ds:Object.
443    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
444    for (node, local) in [
445        (Some(format.mime), "MimeType"),
446        (format.encoding, "Encoding"),
447    ] {
448        let Some(node) = node else { continue };
449        access
450            .attribute(
451                object,
452                Name {
453                    namespace: "",
454                    local,
455                },
456                |expected| {
457                    let Some(expected) = expected else {
458                        return Ok(());
459                    };
460                    if simple_content_matches(access, node, expected)? {
461                        Ok(())
462                    } else {
463                        Err(FormatError::Metadata {
464                            node,
465                            object,
466                            local,
467                        })
468                    }
469                },
470            )
471            .map_err(access_error)??;
472    }
473    Ok(())
474}
475
476fn simple_content_matches<X: Xml, W: Work>(
477    access: &Access<'_, X, W>,
478    parent: X::Id,
479    expected: &str,
480) -> FormatResult<bool, X> {
481    let access_error = |cause| FormatError::Property(Error::Access(cause));
482    let mut child = access.first_child(parent).map_err(access_error)?;
483    let mut offset = 0;
484    let mut same = true;
485    while let Some(node) = child {
486        let compared = access
487            .inspect(node, |value| match value {
488                Node::Element(_) => Err(FormatError::Property(Error::Unexpected { node })),
489                Node::Other => Ok(()),
490                Node::Text(text) => {
491                    if same {
492                        if text.len() > expected.len() - offset {
493                            same = false;
494                        } else {
495                            access
496                                .query(node, |_, _, work| {
497                                    Ok(u64::try_from(text.len())
498                                        .map_err(|_| {
499                                            structured_asn1::memory::MemoryError::WorkExceeded
500                                        })
501                                        .and_then(|length| work.charge_work(length)))
502                                })
503                                .map_err(access_error)?
504                                .map_err(|cause| {
505                                    access_error(AccessError::Resource { node, cause })
506                                })?;
507                            let end = offset + text.len();
508                            same = text.as_bytes() == &expected.as_bytes()[offset..end];
509                            offset = end;
510                        }
511                    }
512                    Ok(())
513                }
514            })
515            .map_err(access_error)?;
516        compared?;
517        child = access.next_sibling(parent, node).map_err(access_error)?;
518    }
519    Ok(same && offset == expected.len())
520}
521
522/// Original covered roles, before scalar/URI/object-metadata checks.
523#[derive(Debug)]
524pub struct Format<N, D, G> {
525    snapshot: Snapshot<D, G>,
526    signature: N,
527    node: N,
528    description: Option<N>,
529    identifier: Option<N>,
530    mime: N,
531    encoding: Option<N>,
532}
533
534impl<N: Copy, D: Copy, G: Copy> Format<N, D, G> {
535    /// Original XML identity and generation.
536    #[must_use]
537    pub fn snapshot(&self) -> Snapshot<D, G> {
538        self.snapshot
539    }
540    /// Signature whose whole `SignedProperties` coverage protects these nodes.
541    #[must_use]
542    pub fn signature(&self) -> N {
543        self.signature
544    }
545    /// Original `DataObjectFormat`; its unqualified `ObjectReference` attribute
546    /// still needs the XML owner's authenticated-reference resolver.
547    #[must_use]
548    pub fn node(&self) -> N {
549        self.node
550    }
551    /// Original optional Description string element.
552    #[must_use]
553    pub fn description(&self) -> Option<N> {
554        self.description
555    }
556    /// Original optional `ObjectIdentifier`, before its own content validation.
557    #[must_use]
558    pub fn identifier(&self) -> Option<N> {
559        self.identifier
560    }
561    /// Original baseline-mandatory `MimeType`, before MIME/scalar agreement checks.
562    #[must_use]
563    pub fn mime(&self) -> N {
564        self.mime
565    }
566    /// Original optional Encoding, before XML-owned anyURI validation.
567    #[must_use]
568    pub fn encoding(&self) -> Option<N> {
569        self.encoding
570    }
571}
572
573/// Source-aware result using the XML owner's original error/identity types.
574pub type FormatResult<T, X> = Result<T, FormatError<<X as Xml>::Error, <X as Xml>::Id>>;
575/// Original data-object format using the XML owner's stable identities.
576pub type Original<X> = Format<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
577
578/// Check the original optional Encoding's XSD1.0 anyURI syntax. Absence stays
579/// optional; a present empty value follows anyURI rules rather than being
580/// fabricated as absent. EN319132-1 §5.2.4 and Annex C define this datatype.
581/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
582///
583/// This does not resolve the URI, accept an encoding, check other attributes,
584/// or repeat the separate ds:Object metadata comparison. Retained format data
585/// stay on their original account; THIS query uses the supplied current Work.
586pub fn check_encoding<X: crate::scalar::ScalarXml, W: Work>(
587    access: &Access<'_, X, W>,
588    original: &Original<X>,
589) -> crate::scalar::ScalarResult<X> {
590    use crate::scalar::{Datatype, Error as ScalarError};
591    if original.snapshot != access.snapshot() {
592        return Err(ScalarError::Access(AccessError::Changed {
593            node: original.node,
594        }));
595    }
596    access
597        .query(original.node, |_, _, _| Ok(()))
598        .map_err(ScalarError::Access)?;
599    if let Some(node) = original.encoding {
600        crate::scalar::check(access, node, LexicalSource::SimpleContent, Datatype::AnyUri)?;
601    }
602    Ok(())
603}
604
605/// Reads one original format from the complete authenticated property catalogue.
606///
607/// EN319132-1 V1.3.1 §6.3/Table2 requires `MimeType` in the baseline even though
608/// the Annex C base schema makes it optional. `ObjectReference` must be present;
609/// §5.2.4's target binding and ds:Object metadata agreement remain separate.
610/// No scalar validation, successful reference resolution or MIME match is
611/// inferred from this structural reading, and no URI dispatcher is introduced.
612/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
613pub fn read<X: Xml, W: Work>(
614    access: &Access<'_, X, W>,
615    signed: &Reading<X>,
616    index: usize,
617) -> FormatResult<Original<X>, X> {
618    let select = || FormatError::Selection {
619        signature: signed.signature(),
620        index,
621    };
622    let property = FormatError::Property;
623    if access.snapshot() != signed.snapshot() {
624        return Err(property(Error::Access(AccessError::Changed {
625            node: signed.parent(),
626        })));
627    }
628    access
629        .query(signed.parent(), |_, _, work| Ok(work.charge_work(1)))
630        .map_err(|cause| property(Error::Access(cause)))?
631        .map_err(|cause| {
632            property(Error::Resource {
633                node: signed.parent(),
634                cause,
635            })
636        })?;
637    let objects = signed.data_objects().ok_or_else(select)?;
638    let selected = objects.entries().get(index).ok_or_else(select)?;
639    if selected.role != DataObjectRole::Format {
640        return Err(select());
641    }
642    let node = selected.node;
643    let has_reference = access
644        .attribute(
645            node,
646            Name {
647                namespace: "",
648                local: "ObjectReference",
649            },
650            |value| value.is_some(),
651        )
652        .map_err(|cause| property(Error::Access(cause)))?;
653    if !has_reference {
654        return Err(property(Error::Missing {
655            parent: node,
656            local: "ObjectReference",
657        }));
658    }
659    let mut fields = [None; 4];
660    let mut previous = 0;
661    for child in access.children(node) {
662        let child = child.map_err(|cause| property(Error::Child(cause)))?;
663        let mut selected = None;
664        for (rank, local) in ["Description", "ObjectIdentifier", "MimeType", "Encoding"]
665            .iter()
666            .enumerate()
667        {
668            if access
669                .has_name(
670                    child,
671                    Name {
672                        namespace: XADES,
673                        local,
674                    },
675                )
676                .map_err(|cause| property(Error::Access(cause)))?
677            {
678                selected = Some(rank);
679                break;
680            }
681        }
682        let rank = selected.ok_or_else(|| property(Error::Unexpected { node: child }))?;
683        if fields[rank].is_some() {
684            return Err(property(Error::Duplicate { node: child }));
685        }
686        if rank < previous {
687            return Err(property(Error::Order { node: child }));
688        }
689        fields[rank] = Some(child);
690        previous = rank;
691        match rank {
692            1 => {
693                crate::identifier::read(access, child).map_err(property)?;
694            }
695            2 => crate::mime::check(access, child).map_err(property)?,
696            _ => crate::properties::simple_content(access, child).map_err(property)?,
697        }
698    }
699    let mime = fields[2].ok_or_else(|| {
700        property(Error::Missing {
701            parent: node,
702            local: "MimeType",
703        })
704    })?;
705    Ok(Format {
706        snapshot: signed.snapshot(),
707        signature: signed.signature(),
708        node,
709        description: fields[0],
710        identifier: fields[1],
711        mime,
712        encoding: fields[3],
713    })
714}