Skip to main content

structured_asic/
material.rs

1//! Original CMS/TST validation material, reusing existing first-read grammars.
2
3use crate::{
4    Asic, ISSUER,
5    content::Content,
6    error::{Error as ArchiveError, resource},
7    source::{DocumentId, Node},
8    zip::MemberId,
9};
10use structured_asn1::{
11    Reader, Span,
12    memory::{Execution, OwnedVec, Work},
13    range::RangeSource,
14};
15use structured_cades::properties::Properties;
16use structured_cms::{
17    SignedData,
18    signed::{CertificateChoice, revocation},
19};
20use structured_ltv::{
21    material::{ArtifactRef, ArtifactSource, Occurrence},
22    source::{Location, Query},
23};
24
25/// Actual enclosing position, independently of whether a later archive covers it.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Placement {
28    /// Original `SignedData` certificate choice.
29    Certificate(usize),
30    /// Original `SignedData` CRL/other-revocation choice.
31    Revocation(usize),
32    /// Original certificate-values attribute member, with signed/unsigned role.
33    CertificateValues {
34        /// Original root `SignerInfo` ordinal.
35        signer: usize,
36        /// Original certificate ordinal in its values attribute.
37        ordinal: usize,
38        /// Whether the original values attribute is in `unsignedAttrs`.
39        unsigned: bool,
40    },
41    /// Original revocation-values CRL member, with signed/unsigned role.
42    CrlValues {
43        /// Original root `SignerInfo` ordinal.
44        signer: usize,
45        /// Original CRL ordinal in its values attribute.
46        ordinal: usize,
47        /// Whether the original values attribute is in `unsignedAttrs`.
48        unsigned: bool,
49    },
50    /// Original revocation-values bare `BasicOCSPResponse` member.
51    OcspValues {
52        /// Original root `SignerInfo` ordinal.
53        signer: usize,
54        /// Original basic-response ordinal in its values attribute.
55        ordinal: usize,
56        /// Whether the original values attribute is in `unsignedAttrs`.
57        unsigned: bool,
58    },
59}
60/// Original uninterpreted or malformed material remains visible.
61#[derive(Debug)]
62pub enum Finding {
63    /// Open certificate choice, not a fabricated X.509 certificate.
64    Certificate {
65        /// Original `SignedData` certificate-choice ordinal.
66        choice: usize,
67        /// Original logical member enclosure, not a fabricated ZIP position.
68        span: Span,
69    },
70    /// Unsupported original CMS revocation format.
71    Revocation {
72        /// Original `SignedData` revocation-choice ordinal.
73        choice: usize,
74        /// Original logical member enclosure.
75        span: Span,
76    },
77    /// Original known revocation syntax failed; processing refusals are terminal.
78    RevocationSyntax {
79        /// Original `SignedData` revocation-choice ordinal.
80        choice: usize,
81        /// Original logical member enclosure.
82        span: Span,
83        /// Exact original shared grammar failure.
84        cause: revocation::Error,
85    },
86    /// Original attributes could not be read as validation-value syntax.
87    Properties {
88        /// Actual original root `SignerInfo` ordinal.
89        signer: usize,
90        /// Exact original attribute-reading failure.
91        cause: structured_cades::Error,
92    },
93}
94/// Original source, grammar and capacity failures retain their classes.
95#[derive(Debug)]
96pub enum Error<E> {
97    /// Actual source/current operation refused.
98    Archive(ArchiveError<E>),
99    /// Existing common CMS/PKIX revocation processing refused.
100    Revocation(revocation::Error),
101    /// Existing common attribute-value processing refused.
102    Properties(structured_cades::Error),
103    /// Existing original CMS reader/session refused.
104    Cms(structured_cms::Error),
105}
106struct Status<'a> {
107    choice: usize,
108    value: revocation::Reading<'a>,
109}
110struct Owner<'a> {
111    signer: usize,
112    unsigned: Option<Span>,
113    properties: Properties<'a>,
114}
115/// Complete original material at this one actual member. Models and source
116/// bytes are borrowed/moved, never reparsed to construct an artifact index.
117pub struct Pool<'a, 'v, 's, S: RangeSource> {
118    content: &'a Content<'v, 's, S>,
119    message: &'a SignedData<'a>,
120    statuses: OwnedVec<Status<'a>>,
121    owners: OwnedVec<Owner<'a>>,
122    findings: OwnedVec<Finding>,
123}
124impl<'v, 's, S: RangeSource> Content<'v, 's, S> {
125    /// Reads every original certificate/revocation choice and every signer's
126    /// validation values through the existing CMS/CAdES kernels. This applies
127    /// to carried TST `SignedData` without asserting that it is a `CAdES` signature.
128    ///
129    /// # Panics
130    /// An internal CMS invariant fails if its retained revocation catalogue
131    /// cannot supply a reader for an ordinal taken from that same catalogue.
132    pub fn material<'a>(
133        &'a self,
134        message: &'a SignedData<'a>,
135        parent: &Reader<'a>,
136    ) -> Result<Pool<'a, 'v, 's, S>, Error<S::Error>> {
137        let work = parent.execution();
138        self.require(message, parent, &work)
139            .map_err(Error::Archive)?;
140        let refusal = |cause| Error::Archive(resource(self.enclosure(), cause));
141        let mut findings = work.vector();
142        let mut statuses = work.vector();
143        let mut owners = work.vector();
144        for (choice, value) in message.certificate_choices().iter().enumerate() {
145            work.charge_work(1).map_err(refusal)?;
146            let span = match value {
147                CertificateChoice::X509(_) => continue,
148                CertificateChoice::Extended(value)
149                | CertificateChoice::AttributeV1(value)
150                | CertificateChoice::AttributeV2(value)
151                | CertificateChoice::Other(value) => value.span(),
152            };
153            findings
154                .try_push(Finding::Certificate { choice, span })
155                .map_err(refusal)?;
156        }
157        for (choice, value) in message.revocations().iter().enumerate() {
158            work.charge_work(1).map_err(refusal)?;
159            let reader = message
160                .revocation_reader_in(parent, choice)
161                .map_err(Error::Cms)?
162                .expect("existing original revocation ordinal");
163            match revocation::read(&reader, value) {
164                Ok(revocation::Reading::Open { .. }) => findings
165                    .try_push(Finding::Revocation {
166                        choice,
167                        span: value.span(),
168                    })
169                    .map_err(refusal)?,
170                Ok(value) => statuses
171                    .try_push(Status { choice, value })
172                    .map_err(refusal)?,
173                Err(cause) if cause.is_terminal() => return Err(Error::Revocation(cause)),
174                Err(cause) => findings
175                    .try_push(Finding::RevocationSyntax {
176                        choice,
177                        span: value.span(),
178                        cause,
179                    })
180                    .map_err(refusal)?,
181            }
182        }
183        for (signer, value) in message.signers().iter().enumerate() {
184            work.charge_work(1).map_err(refusal)?;
185            match Properties::read(value, parent) {
186                Ok(properties) => owners
187                    .try_push(Owner {
188                        signer,
189                        unsigned: value
190                            .unsigned_attributes()
191                            .map(|attributes| attributes.element().span()),
192                        properties,
193                    })
194                    .map_err(refusal)?,
195                Err(cause) if cause.is_terminal() => return Err(Error::Properties(cause)),
196                Err(cause) => findings
197                    .try_push(Finding::Properties { signer, cause })
198                    .map_err(refusal)?,
199            }
200        }
201        self.member()
202            .archive()
203            .admit(&work, self.enclosure())
204            .map_err(Error::Archive)?;
205        Ok(Pool {
206            content: self,
207            message,
208            statuses: statuses.into_owned(),
209            owners: owners.into_owned(),
210            findings: findings.into_owned(),
211        })
212    }
213}
214/// Existing exact-artifact index, in the actual borrowed archive identity domain.
215pub type Index<'a, 'q, 'v, 's, S> = structured_ltv::material::Index<
216    'a,
217    Node,
218    DocumentId<'v, 's, S>,
219    (),
220    Placement,
221    Finding,
222    Execution<'q>,
223    Asic,
224>;
225type IndexOf<'a, 'q, 'v, 's, S, O> = structured_ltv::material::Index<
226    'a,
227    Node,
228    DocumentId<'v, 's, S>,
229    (),
230    Placement,
231    O,
232    Execution<'q>,
233    Asic,
234>;
235impl<'a, 'v, 's, S: RangeSource> Pool<'a, 'v, 's, S> {
236    /// Actual original complete material source.
237    #[must_use]
238    pub fn member(&self) -> MemberId<S::Document, S::Generation> {
239        self.content.member().id()
240    }
241    /// All malformed/open original source choices, never a successful subset.
242    #[must_use]
243    pub fn findings(&self) -> &[Finding] {
244        &self.findings
245    }
246    /// Existing attribute inspections retain every readable syntax/open finding.
247    pub fn properties(&self) -> impl Iterator<Item = (usize, &Properties<'a>)> {
248        self.owners
249            .iter()
250            .map(|owner| (owner.signer, &owner.properties))
251    }
252    /// Builds the SAME exact-artifact index. All occurrences are located at
253    /// their complete original member: an A.7 reference protects that complete
254    /// member, not a guessed contiguous physical position of an inner value.
255    pub fn index<'p, 'q, W: Work>(
256        &'p self,
257        work: &'q W,
258    ) -> Result<Index<'p, 'q, 'v, 's, S>, structured_ltv::material::IndexError<Node>>
259    where
260        'v: 'q,
261        'a: 'p,
262    {
263        let archive = self.content.member().archive();
264        let query = archive.ltv_query(work);
265        let node = Node::Root;
266        let mut index = structured_ltv::material::Index::new(ISSUER, node, &self.findings, &query)?;
267        let mut ordinal = 0usize;
268        self.append(&mut index, &query, &mut ordinal)?;
269        Ok(index)
270    }
271    fn append<'p, 'q, O>(
272        &'p self,
273        index: &mut IndexOf<'p, 'q, 'v, 's, S, O>,
274        query: &crate::source::Query<'q, 'v, 's, S>,
275        ordinal: &mut usize,
276    ) -> Result<(), structured_ltv::material::IndexError<Node>>
277    where
278        'a: 'p,
279    {
280        let node = Node::Root;
281        let mut add = |artifact: ArtifactRef<'_, 'p>, placement| {
282            index.admit(query)?;
283            let location = Location {
284                snapshot: query.snapshot(),
285                node: Node::Member(self.member().ordinal()),
286            };
287            let source = ArtifactSource::Material(*ordinal);
288            *ordinal =
289                ordinal
290                    .checked_add(1)
291                    .ok_or(structured_ltv::material::IndexError::Resource {
292                        signature: node,
293                        cause: structured_asn1::memory::MemoryError::WorkExceeded,
294                    })?;
295            index.insert(
296                ISSUER,
297                artifact,
298                Occurrence::new(source, location, placement),
299                query,
300            )
301        };
302        for (choice, value) in self.message.certificate_choices().iter().enumerate() {
303            query
304                .admit(node, 1)
305                .map_err(structured_ltv::material::IndexError::Scope)?;
306            if let CertificateChoice::X509(certificate) = value {
307                add(
308                    ArtifactRef::Certificate(&self.message.certificates()[*certificate]),
309                    Placement::Certificate(choice),
310                )?;
311            }
312        }
313        for status in &self.statuses {
314            query
315                .admit(node, 1)
316                .map_err(structured_ltv::material::IndexError::Scope)?;
317            match &status.value {
318                revocation::Reading::Crl(value) => add(
319                    ArtifactRef::Crl(value),
320                    Placement::Revocation(status.choice),
321                )?,
322                revocation::Reading::Ocsp(value) => add(
323                    ArtifactRef::Ocsp(value),
324                    Placement::Revocation(status.choice),
325                )?,
326                revocation::Reading::Open { .. } => {
327                    unreachable!("open choices are retained as findings")
328                }
329            }
330        }
331        for owner in &self.owners {
332            query
333                .admit(node, 1)
334                .map_err(structured_ltv::material::IndexError::Scope)?;
335            let Some(values) = owner.properties.legacy_values() else {
336                continue;
337            };
338            let unsigned = |property: Span| {
339                owner.unsigned.is_some_and(|span| {
340                    property.start >= span.start && property.end() <= span.end()
341                })
342            };
343            for entry in values.certificates() {
344                add(
345                    ArtifactRef::Certificate(&entry.value),
346                    Placement::CertificateValues {
347                        signer: owner.signer,
348                        ordinal: entry.ordinal,
349                        unsigned: unsigned(entry.property.span()),
350                    },
351                )?;
352            }
353            for entry in values.crls() {
354                add(
355                    ArtifactRef::Crl(&entry.value),
356                    Placement::CrlValues {
357                        signer: owner.signer,
358                        ordinal: entry.ordinal,
359                        unsigned: unsigned(entry.property.span()),
360                    },
361                )?;
362            }
363            for entry in values.ocsp_responses() {
364                add(
365                    ArtifactRef::Ocsp(&entry.value),
366                    Placement::OcspValues {
367                        signer: owner.signer,
368                        ordinal: entry.ordinal,
369                        unsigned: unsigned(entry.property.span()),
370                    },
371                )?;
372            }
373        }
374        Ok(())
375    }
376}
377
378/// Every original CMS/TST source slot, including unreadable/unprocessed files.
379pub struct SourceSlot<'p, 'a, 'v, 's, S: RangeSource> {
380    member: MemberId<S::Document, S::Generation>,
381    pool: Option<&'p Pool<'a, 'v, 's, S>>,
382}
383impl<'p, 'a, 'v, 's, S: RangeSource> SourceSlot<'p, 'a, 'v, 's, S> {
384    /// Original physical member occurrence, never caller-supplied success order.
385    #[must_use]
386    pub fn member(&self) -> MemberId<S::Document, S::Generation> {
387        self.member
388    }
389    /// Actual completed first-read material, absent for an unprocessed source.
390    #[must_use]
391    pub fn pool(&self) -> Option<&'p Pool<'a, 'v, 's, S>> {
392        self.pool
393    }
394}
395/// Original open/malformed material finding with its exact source member.
396pub struct Open<'p, D, G> {
397    /// Actual original source occurrence.
398    pub member: MemberId<D, G>,
399    /// Original retained cause, not a clone or a replacement diagnostic.
400    pub finding: &'p Finding,
401}
402/// Full original carrier source census and one reusable exact-artifact index.
403/// Participant completeness, source-negative semantics and usable status remain
404/// independent. Missing carrier pools cannot become successful source coverage.
405pub struct Sources<'p, 'a, 'v, 's, S: RangeSource> {
406    archive: &'v crate::zip::Archive<'s, S>,
407    slots: OwnedVec<SourceSlot<'p, 'a, 'v, 's, S>>,
408    open: OwnedVec<Open<'p, S::Document, S::Generation>>,
409    complete: bool,
410}
411/// Caller misassociation and original processing remain terminal.
412#[derive(Debug)]
413pub enum SourcesError<E> {
414    /// Actual source/current operation refused.
415    Archive(ArchiveError<E>),
416    /// A pool belongs to another archive or a non-CMS/TST original member.
417    Foreign,
418    /// Several supplied pools describe the same actual original source.
419    Duplicate,
420}
421impl<'p, 'a, 'v, 's, S: RangeSource> Sources<'p, 'a, 'v, 's, S> {
422    /// Derives slot order from the actual original census, not the caller's
423    /// successful pool list. Every `CAdES`/TST file remains a source obligation.
424    ///
425    /// # Panics
426    /// An internal census invariant fails if its own bounded ordinal is absent.
427    pub fn read<W: Work>(
428        census: &crate::layout::Census<'v, 's, S>,
429        pools: &[&'p Pool<'a, 'v, 's, S>],
430        work: &W,
431    ) -> Result<Self, SourcesError<S::Error>> {
432        use crate::layout::Role;
433        let archive = census.archive();
434        let execution = archive.session().query(work);
435        archive
436            .admit(&execution, archive.directory_span())
437            .map_err(SourcesError::Archive)?;
438        let refusal = |cause| SourcesError::Archive(resource(archive.directory_span(), cause));
439        let mut selected = execution.map();
440        for (index, pool) in pools.iter().enumerate() {
441            execution.charge_work(1).map_err(refusal)?;
442            if !core::ptr::eq(pool.content.member().archive(), archive) {
443                return Err(SourcesError::Foreign);
444            }
445            let role = census
446                .occurrence(pool.member().ordinal(), &execution)
447                .map_err(SourcesError::Archive)?
448                .map(|(_, role)| role);
449            if !matches!(role, Some(Role::Cades | Role::Timestamp)) {
450                return Err(SourcesError::Foreign);
451            }
452            if selected
453                .try_insert(pool.member().ordinal(), index)
454                .map_err(refusal)?
455                .is_some()
456            {
457                return Err(SourcesError::Duplicate);
458            }
459        }
460        let mut slots = execution.vector();
461        let mut open = execution.vector();
462        let mut complete = true;
463        for ordinal in 0..census.len() {
464            let (member, role) = census
465                .occurrence(ordinal, &execution)
466                .map_err(SourcesError::Archive)?
467                .expect("original census bounds its ordinal");
468            if !matches!(role, Role::Cades | Role::Timestamp) {
469                continue;
470            }
471            let pool = selected
472                .get(&ordinal, &execution)
473                .map_err(refusal)?
474                .map(|index| pools[*index]);
475            if let Some(pool) = pool {
476                for finding in pool.findings() {
477                    open.try_push(Open {
478                        member: member.id(),
479                        finding,
480                    })
481                    .map_err(refusal)?;
482                }
483            } else {
484                complete = false;
485            }
486            slots
487                .try_push(SourceSlot {
488                    member: member.id(),
489                    pool,
490                })
491                .map_err(refusal)?;
492        }
493        archive
494            .admit(&execution, archive.directory_span())
495            .map_err(SourcesError::Archive)?;
496        Ok(Self {
497            archive,
498            slots: slots.into_owned(),
499            open: open.into_owned(),
500            complete,
501        })
502    }
503    /// All original carrier slots, retaining missing and negative sources.
504    #[must_use]
505    pub fn slots(&self) -> &[SourceSlot<'p, 'a, 'v, 's, S>] {
506        &self.slots
507    }
508    /// Every original carrier has a completed first-read pool. This does not
509    /// assert conformance or complete participant/path/status interpretation.
510    #[must_use]
511    pub fn all_sources_inspected(&self) -> bool {
512        self.complete
513    }
514    /// Original malformed/open carrier findings, without replacing their causes.
515    #[must_use]
516    pub fn open(&self) -> &[Open<'p, S::Document, S::Generation>] {
517        &self.open
518    }
519    /// One exact-artifact index over all available original pools. The full
520    /// source census remains alongside it, so a missing source is never erased.
521    pub fn index<'g, 'q, W: Work>(
522        &'g self,
523        work: &'q W,
524    ) -> SourcesIndexResult<'g, 'q, 'p, 'v, 's, S>
525    where
526        'v: 'q,
527        'a: 'g,
528        'p: 'g,
529    {
530        let query = self.archive.ltv_query(work);
531        let mut index =
532            structured_ltv::material::Index::new(ISSUER, Node::Root, &self.open, &query)?;
533        let mut ordinal = 0usize;
534        for slot in &self.slots {
535            index.admit(&query)?;
536            if let Some(pool) = slot.pool {
537                pool.append(&mut index, &query, &mut ordinal)?;
538            }
539        }
540        Ok(index)
541    }
542}
543
544/// Exact source-census material index, retaining borrowed original findings.
545pub type SourcesIndexResult<'g, 'q, 'p, 'v, 's, S> = Result<
546    IndexOf<
547        'g,
548        'q,
549        'v,
550        's,
551        S,
552        Open<'p, <S as RangeSource>::Document, <S as RangeSource>::Generation>,
553    >,
554    structured_ltv::material::IndexError<Node>,
555>;