Skip to main content

structured_asic/
references.rs

1//! Scoped package URI lookup, independent of XML origin and authentication.
2
3mod checks;
4pub use checks::Checks;
5
6use crate::{
7    error::{Error as ArchiveError, resource},
8    uri::{self, Part},
9    zip::{Member, Names},
10};
11use structured_asn1::{memory::Work, range::RangeSource};
12use structured_xades::{
13    binary::{self, OwnedBinaryXml},
14    certificate::{Agreement, DigestProvider},
15    digest,
16    reference::{LexicalSource, VisitError},
17    scalar::{self, UriValue, UriXml},
18    xml::{Access, AccessError, Name, Xml},
19};
20
21/// All original manifest URI bindings. Construction resolves every original
22/// instruction, including malformed/ambiguous ones, without dropping siblings.
23/// This is inspection only; it does not attest that XML was produced from the
24/// selected base member or that a signature authenticated the manifest.
25pub struct Catalogue<'v, 's, N, D, G, S: RangeSource> {
26    base: Member<'v, 's, S>,
27    reading: crate::manifest::Reading<N, D, G>,
28    signatures: structured_asn1::memory::OwnedVec<Target<'v, 's, S>>,
29    objects: structured_asn1::memory::OwnedVec<Target<'v, 's, S>>,
30}
31
32/// Source/member, XML and storage admission failures retain their actual role.
33#[derive(Debug)]
34pub enum ResolveError<E, N, SE> {
35    /// Actual original archive view or current/source query refused.
36    Archive(ArchiveError<SE>),
37    /// Original XML lexical/source interpretation or target lookup refused.
38    Reference(AccessError<VisitError<E, ArchiveError<SE>>, N>),
39    /// Source-scoped retained metadata could not be admitted.
40    Storage {
41        /// Original owning XML instruction.
42        node: N,
43        /// Exact refusal cause.
44        cause: structured_asn1::memory::MemoryError,
45    },
46}
47
48/// Full original manifest catalogue or its unchanged processing failure.
49pub type ResolveResult<'v, 's, X, S> = Result<
50    Catalogue<'v, 's, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, S>,
51    ResolveError<<X as Xml>::Error, <X as Xml>::Id, <S as RangeSource>::Error>,
52>;
53type MemberResult<'v, 's, S> = crate::error::SourceResult<Option<Member<'v, 's, S>>, S>;
54
55/// Resolves every URI under the original manifest generation and the same
56/// current/archive execution. No decoder or provider state is started here.
57pub fn resolve_manifest<'v, 's, X: UriXml, S: RangeSource, W: Work>(
58    base: Member<'v, 's, S>,
59    reading: crate::manifest::Reading<X::Id, X::Document, X::Generation>,
60    xml: &X,
61    work: &W,
62) -> ResolveResult<'v, 's, X, S> {
63    let execution = base.session().query(work);
64    base.archive()
65        .admit(&execution, base.central_span())
66        .map_err(ResolveError::Archive)?;
67    let access = Access::new(xml, reading.snapshot(), &execution);
68    // The original XML owner sees its generation/query before any retained
69    // catalogue allocation, even when the reading contains no instructions.
70    access.inspect(reading.root(), |_| ()).map_err(|cause| {
71        ResolveError::Reference(match cause {
72            AccessError::Xml(cause) => AccessError::Xml(VisitError::Processing(cause)),
73            AccessError::Changed { node } => AccessError::Changed { node },
74            AccessError::Resource { node, cause } => AccessError::Resource { node, cause },
75        })
76    })?;
77    let mut signatures = execution.vector();
78    let mut objects = execution.vector();
79    signatures
80        .try_reserve_exact(reading.signatures().len())
81        .map_err(|cause| ResolveError::Storage {
82            node: reading.root(),
83            cause,
84        })?;
85    objects
86        .try_reserve_exact(reading.objects().len())
87        .map_err(|cause| ResolveError::Storage {
88            node: reading.root(),
89            cause,
90        })?;
91    for node in reading.signatures() {
92        let target = resolve(&base, &access, *node).map_err(ResolveError::Reference)?;
93        signatures
94            .try_push(target)
95            .map_err(|cause| ResolveError::Storage { node: *node, cause })?;
96    }
97    for object in reading.objects() {
98        let target = resolve(&base, &access, object.node).map_err(ResolveError::Reference)?;
99        objects
100            .try_push(target)
101            .map_err(|cause| ResolveError::Storage {
102                node: object.node,
103                cause,
104            })?;
105    }
106    base.archive()
107        .admit(&execution, base.central_span())
108        .map_err(ResolveError::Archive)?;
109    Ok(Catalogue {
110        base,
111        reading,
112        signatures: signatures.into_owned(),
113        objects: objects.into_owned(),
114    })
115}
116
117impl<'v, 's, N: Copy, D: Copy + Eq, G: Copy + Eq, S: RangeSource> Catalogue<'v, 's, N, D, G, S> {
118    /// Original selected base occurrence, without a claim of XML production.
119    #[must_use]
120    pub fn base(&self) -> &Member<'v, 's, S> {
121        &self.base
122    }
123    /// Complete original schema reading, including all faults/extensions.
124    #[must_use]
125    pub fn reading(&self) -> &crate::manifest::Reading<N, D, G> {
126        &self.reading
127    }
128    /// Every original signature-file target, preserving duplicate `SigReference`.
129    #[must_use]
130    pub fn signatures(&self) -> &[Target<'v, 's, S>] {
131        &self.signatures
132    }
133    /// Every original data target in exact `DataObjectReference` order.
134    #[must_use]
135    pub fn objects(&self) -> &[Target<'v, 's, S>] {
136        &self.objects
137    }
138
139    /// Selects an original whole member only when the signature-file singleton
140    /// and exact-name candidate are both unambiguous. All negatives remain in
141    /// the catalogue; None cannot become a fallback signature or ERS object.
142    pub fn signature_member<W: Work>(&self, work: &W) -> MemberResult<'v, 's, S> {
143        self.base.archive().admit(work, self.base.central_span())?;
144        if self.signatures.len() != 1 {
145            return Ok(None);
146        }
147        unique(&self.signatures[0], work)
148    }
149    /// Selects one original whole data member. None includes an out-of-range
150    /// caller ordinal and a retained negative/ambiguous original target.
151    pub fn object_member<W: Work>(&self, ordinal: usize, work: &W) -> MemberResult<'v, 's, S> {
152        self.base.archive().admit(work, self.base.central_span())?;
153        let Some(target) = self.objects.get(ordinal) else {
154            return Ok(None);
155        };
156        unique(target, work)
157    }
158    /// Produces the exact original ERS group only if EVERY object has one whole
159    /// input. Repeated original references remain repeated group occurrences.
160    /// No successful subset or partial group is returned for a negative target.
161    pub fn group<W: Work>(
162        &self,
163        work: &W,
164    ) -> crate::zip::Acquisition<Option<crate::evidence::Group<'v, 's, S>>, S> {
165        let archive = self.base.archive();
166        let execution = self.base.session().query(work);
167        let fail = |cause| crate::zip::ReadFailure::Processing {
168            member: self.base.id(),
169            cause,
170        };
171        archive
172            .admit(&execution, self.base.central_span())
173            .map_err(fail)?;
174        if self.objects.is_empty() {
175            return Ok(None);
176        }
177        let mut members = execution.vector();
178        for target in &self.objects {
179            archive
180                .admit(&execution, self.base.central_span())
181                .map_err(fail)?;
182            let Some(member) = unique(target, &execution).map_err(fail)? else {
183                return Ok(None);
184            };
185            members
186                .try_push(member.id())
187                .map_err(|cause| fail(resource(self.base.central_span(), cause)))?;
188        }
189        crate::evidence::Group::new(archive, members.into_owned(), &execution).map(Some)
190    }
191}
192
193fn unique<'v, 's, S: RangeSource, W: Work>(
194    target: &Target<'v, 's, S>,
195    work: &W,
196) -> Result<Option<Member<'v, 's, S>>, ArchiveError<S::Error>> {
197    if let Target::Members {
198        names,
199        part: Part::Whole,
200    } = target
201        && names.len() == 1
202    {
203        names.member(0, work)
204    } else {
205        Ok(None)
206    }
207}
208
209/// Why no digest was attempted for an original instruction.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum Unchecked {
212    /// The enclosing operation did not execute this original object's digest.
213    NotExecuted,
214    /// Resolution did not select exactly one whole original member.
215    Input,
216    /// `DigestMethod` is missing or ambiguous in the retained schema reading.
217    Method,
218    /// `DigestValue` is missing or ambiguous in the retained schema reading.
219    Value,
220}
221
222/// Independent completed hash fact, never an authenticated manifest verdict.
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum DigestReading {
225    /// Original selection/schema does not permit a unique full input.
226    Unchecked(Unchecked),
227    /// The selected provider completed the existing common digest check.
228    Checked(Agreement),
229}
230
231/// Actual source lookup and digest facts for one original XML instruction.
232/// These stable IDs can survive a scoped XML view; no DOM borrow is retained.
233pub struct ObjectCheck<'v, 's, N, D, G, S: RangeSource> {
234    snapshot: structured_xades::xml::Snapshot<D, G>,
235    root: N,
236    object: crate::manifest::Object<N>,
237    target: Target<'v, 's, S>,
238    digest: DigestReading,
239}
240impl<'v, 's, N: Copy, D: Copy, G: Copy, S: RangeSource> ObjectCheck<'v, 's, N, D, G, S> {
241    /// Exact original XML document/generation used by the digest method/value.
242    #[must_use]
243    pub fn snapshot(&self) -> structured_xades::xml::Snapshot<D, G> {
244        self.snapshot
245    }
246    /// Exact manifest root, not another sibling manifest with the same URI.
247    #[must_use]
248    pub fn root(&self) -> N {
249        self.root
250    }
251    /// The original instruction and all singleton/rootfile distinctions.
252    #[must_use]
253    pub fn object(&self) -> crate::manifest::Object<N> {
254        self.object
255    }
256    /// Complete exact-name candidate range and URI component outcome.
257    #[must_use]
258    pub fn target(&self) -> &Target<'v, 's, S> {
259        &self.target
260    }
261    /// Actual common digest answer, independent of XML origin/authentication.
262    #[must_use]
263    pub fn digest(&self) -> DigestReading {
264        self.digest
265    }
266}
267
268/// Original XML, source, callback and logical digest refusals stay distinct.
269#[derive(Debug)]
270pub enum CheckError<E, PE, N, D, G, SE, SD, SG> {
271    /// Original XML node/URI or ZIP source lookup refusal.
272    Reference(AccessError<VisitError<E, ArchiveError<SE>>, N>),
273    /// The original selected member cannot provide a complete input stream.
274    Input(crate::zip::ReadFailure<SE, SD, SG>),
275    /// Existing shared XML digest error with original value-node provenance.
276    Digest(ObjectDigestError<E, PE, N, D, G, SE, SD, SG>),
277}
278/// Original XML value location and unchanged native digest/source refusal.
279pub type ObjectDigestError<E, PE, N, D, G, SE, SD, SG> =
280    binary::Error<E, digest::Error<crate::zip::ReadFailure<SE, SD, SG>, PE>, N, D, G>;
281
282/// Complete original fact or original typed processing failure.
283pub type CheckResult<'v, 's, X, S, PE> = Result<
284    Option<ObjectCheck<'v, 's, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, S>>,
285    CheckError<
286        <X as Xml>::Error,
287        PE,
288        <X as Xml>::Id,
289        <X as Xml>::Document,
290        <X as Xml>::Generation,
291        <S as RangeSource>::Error,
292        <S as RangeSource>::Document,
293        <S as RangeSource>::Generation,
294    >,
295>;
296
297/// Checks one original manifest instruction through the existing XML digest
298/// provider and sealed member stream. Invalid/ambiguous targets remain facts;
299/// no first-name selection, whole-member promotion of fragments, or implicit
300/// external fetch is performed. This does not attest ZIP-to-XML provenance.
301///
302/// # Panics
303/// An internal immutable name-catalogue invariant fails if its unique
304/// original candidate cannot be selected from that same catalogue.
305pub async fn check_object<'p, 'v, 's, X, S, P, W>(
306    base: &Member<'v, 's, S>,
307    reading: &crate::manifest::Reading<X::Id, X::Document, X::Generation>,
308    ordinal: usize,
309    xml: &X,
310    provider: &'p P,
311    work: &W,
312) -> CheckResult<'v, 's, X, S, P::Error>
313where
314    X: UriXml + OwnedBinaryXml,
315    S: RangeSource,
316    P: DigestProvider<'p, X>,
317    W: Work,
318{
319    let access = Access::new(xml, reading.snapshot(), work);
320    // Even an out-of-range ordinal observes its original generation/query.
321    access.inspect(reading.root(), |_| ()).map_err(|cause| {
322        CheckError::Reference(match cause {
323            AccessError::Xml(cause) => AccessError::Xml(VisitError::Processing(cause)),
324            AccessError::Resource { node, cause } => AccessError::Resource { node, cause },
325            AccessError::Changed { node } => AccessError::Changed { node },
326        })
327    })?;
328    let Some(object) = reading.objects().get(ordinal).copied() else {
329        return Ok(None);
330    };
331    let target = resolve(base, &access, object.node).map_err(CheckError::Reference)?;
332    let answer = match (object.method, object.value) {
333        (crate::manifest::Slot::One(method), crate::manifest::Slot::One(value)) => {
334            if let Target::Members {
335                names,
336                part: Part::Whole,
337            } = &target
338                && names.len() == 1
339            {
340                let member = names
341                    .member(0, work)
342                    .map_err(|cause| {
343                        CheckError::Reference(AccessError::Xml(VisitError::Visitor(cause)))
344                    })?
345                    .expect("the immutable exact candidate range contains one original member");
346                let mut source = member.source(work).map_err(CheckError::Input)?;
347                DigestReading::Checked(
348                    digest::check_xml_async(
349                        xml,
350                        digest::Reference {
351                            snapshot: reading.snapshot(),
352                            method,
353                            value,
354                        },
355                        &mut source,
356                        provider,
357                        work,
358                    )
359                    .await
360                    .map_err(CheckError::Digest)?,
361                )
362            } else {
363                DigestReading::Unchecked(Unchecked::Input)
364            }
365        }
366        (crate::manifest::Slot::One(_), _) => DigestReading::Unchecked(Unchecked::Value),
367        _ => DigestReading::Unchecked(Unchecked::Method),
368    };
369    base.archive()
370        .admit(work, base.central_span())
371        .map_err(|cause| CheckError::Reference(AccessError::Xml(VisitError::Visitor(cause))))?;
372    Ok(Some(ObjectCheck {
373        snapshot: reading.snapshot(),
374        root: reading.root(),
375        object,
376        target,
377        digest: answer,
378    }))
379}
380
381/// Original URI outcome. A candidate range preserves every duplicated member;
382/// a fragment/query never becomes proof of a whole-member digest input.
383pub enum Target<'v, 's, S: RangeSource> {
384    /// The original optional URI attribute is absent.
385    Absent,
386    /// The owner found malformed/complex lexical content.
387    Malformed,
388    /// The owner cannot establish the anyURI datatype.
389    Unsupported,
390    /// A path-empty reference into the containing original XML document.
391    SameDocument(Part),
392    /// The package root, independent of an empty or missing ZIP filename.
393    Root(Part),
394    /// Exact original candidate range, possibly empty or ambiguous.
395    Members {
396        /// All original occurrences of the exact decoded ZIP name.
397        names: Names<'v, 's, S>,
398        /// Original query/fragment presence.
399        part: Part,
400    },
401    /// Completed URI interpretation failure, distinct from missing candidates.
402    Negative(uri::Negative),
403}
404
405/// XML node provenance and original ZIP/source causes remain independent.
406pub type ErrorOf<X, S> = AccessError<
407    VisitError<<X as Xml>::Error, ArchiveError<<S as RangeSource>::Error>>,
408    <X as Xml>::Id,
409>;
410
411/// Resolves the complete original URI attribute within its lexical visitor.
412/// This does not attest that `access` was produced from `base`: only the scoped
413/// producing owner can establish that association. No acquisition, signature
414/// check, XML reparse or normalized-name substitution takes place here.
415pub fn resolve<'v, 's, X: UriXml, S: RangeSource, W: Work>(
416    base: &Member<'v, 's, S>,
417    access: &Access<'_, X, W>,
418    node: X::Id,
419) -> Result<Target<'v, 's, S>, ErrorOf<X, S>> {
420    scalar::with_uri(
421        access,
422        node,
423        LexicalSource::Attribute(Name {
424            namespace: "",
425            local: "URI",
426        }),
427        |value| {
428            let work = access.execution();
429            let archive = base.archive();
430            archive.admit(work, base.central_span())?;
431            let target = match value {
432                UriValue::Absent => Target::Absent,
433                UriValue::ComplexContent | UriValue::Malformed => Target::Malformed,
434                UriValue::Unsupported => Target::Unsupported,
435                UriValue::Valid(value) => match uri::resolve(base.name(), value, work)
436                    .map_err(|cause| resource(base.central_span(), cause))?
437                {
438                    uri::Target::SameDocument(part) => Target::SameDocument(part),
439                    uri::Target::Root(part) => Target::Root(part),
440                    uri::Target::Negative(cause) => Target::Negative(cause),
441                    uri::Target::Member { name, part } => Target::Members {
442                        names: archive.find(name.as_ref(), work)?,
443                        part,
444                    },
445                },
446            };
447            archive.admit(work, base.central_span())?;
448            Ok(target)
449        },
450    )
451}