Skip to main content

structured_cades/prepare/
tokens.rs

1//! One original timestamp envelope reading shared by preparation and material.
2
3use super::{Finding, Rule, Severity, Timestamp, finding};
4use crate::{
5    Error,
6    source::{Document, Node},
7};
8use core::convert::Infallible;
9use structured_asn1::{
10    Reader,
11    memory::{OwnedMap, OwnedVec, Work},
12};
13use structured_cms::{
14    content_source::{ContentError, Coordinates, SourceMap},
15    timestamp::{InfoError, Token},
16};
17
18mod index;
19mod signing;
20
21/// An original timestamp syntax refusal with independently retained source maps.
22#[derive(Debug)]
23pub enum ReadError {
24    /// Original outer-source or processing refusal.
25    Processing(Error),
26    /// Original carried-information refusal. The retained map describes the
27    /// cause's coordinate system; the physical carrier is an honest enclosure.
28    Information(ContentError<InfoError<Infallible>>),
29    /// Inspection callback refusal with its independent carried source map.
30    /// The coordinate discriminator separates reader-derived locations from
31    /// failures retaining findings at an already physical carrier location.
32    Inspection {
33        /// Unchanged callback cause, including resource refusal.
34        source: Error,
35        /// Original physical fragments, never a retained temporary join.
36        context: SourceMap,
37        /// Coordinate system of the callback cause's own offset.
38        coordinates: Coordinates,
39    },
40}
41impl From<Error> for ReadError {
42    fn from(cause: Error) -> Self {
43        Self::Processing(cause)
44    }
45}
46impl From<structured_cms::Error> for ReadError {
47    fn from(cause: structured_cms::Error) -> Self {
48        Self::Processing(Error::Cms(cause))
49    }
50}
51impl ReadError {
52    /// Resource/source misuse aborts preparation rather than becoming a finding.
53    #[must_use]
54    pub fn is_terminal(&self) -> bool {
55        match self {
56            Self::Processing(cause) => cause.is_terminal(),
57            Self::Information(cause) => information_cause(cause).is_terminal(),
58            Self::Inspection { source, .. } => source.is_terminal(),
59        }
60    }
61    /// Original physical failure position or carried-source enclosure start.
62    #[must_use]
63    pub fn offset(&self) -> u64 {
64        match self {
65            Self::Processing(cause) => cause.offset(),
66            Self::Information(ContentError::Processing(cause)) => cause.offset(),
67            Self::Inspection { context, .. }
68            | Self::Information(
69                ContentError::Decoding { context, .. } | ContentError::Callback { context, .. },
70            ) => context.carrier().start,
71        }
72    }
73}
74fn information_cause(cause: &ContentError<InfoError<Infallible>>) -> &structured_cms::Error {
75    match cause {
76        ContentError::Processing(cause)
77        | ContentError::Decoding { source: cause, .. }
78        | ContentError::Callback {
79            source: InfoError::Processing(cause),
80            ..
81        } => cause,
82        ContentError::Callback {
83            source: InfoError::Callback(never),
84            ..
85        } => match *never {},
86    }
87}
88pub(crate) struct Model<'d> {
89    pub(crate) token: Token<'d>,
90    pub(crate) properties: Option<crate::properties::Properties<'d>>,
91}
92pub(crate) struct Cache<'m, 'd> {
93    pub(crate) models: OwnedVec<Model<'d>>,
94    pub(crate) ordinals: OwnedVec<Option<TokenIndex>>,
95    pub(crate) proofs: super::proofs::Proofs<'d>,
96    pub(crate) faults: OwnedVec<(Node, ReadError)>,
97    pub(crate) related: Option<OwnedVec<Finding<'m, 'd>>>,
98}
99#[derive(Clone, Copy)]
100pub(crate) enum TokenIndex {
101    Direct(usize),
102    Proof(super::proofs::Reference),
103}
104pub(crate) struct ModelView<'a, 'd> {
105    pub(crate) ordinal: usize,
106    pub(crate) token: &'a Token<'d>,
107    pub(crate) properties: Option<&'a crate::properties::Properties<'d>>,
108}
109impl<'d> Cache<'_, 'd> {
110    pub(crate) fn token(&self, ordinal: usize) -> Option<ModelView<'_, 'd>> {
111        let (token, properties) = match self.ordinals.get(ordinal)?.as_ref()? {
112            TokenIndex::Direct(index) => {
113                let model = &self.models[*index];
114                (&model.token, model.properties.as_ref())
115            }
116            TokenIndex::Proof(reference) => self.proofs.token(*reference)?,
117        };
118        Some(ModelView {
119            ordinal,
120            token,
121            properties,
122        })
123    }
124}
125
126pub(super) struct Findings<'a, 'm, 'd, W: Work> {
127    pub(super) rows: &'a mut OwnedVec<Finding<'m, 'd>, W>,
128    pub(super) work: &'a W,
129}
130struct Reading<'d> {
131    model: Option<Model<'d>>,
132    fault: Option<ReadError>,
133}
134
135#[expect(
136    clippy::result_large_err,
137    reason = "retain the original source map without allocating during resource refusal"
138)]
139fn read_one<'m, 'd, W: Work>(
140    document: &Document<'m, 'd>,
141    entry: &Timestamp<'d>,
142    parent: &Reader<'d>,
143    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
144) -> Result<Reading<'d>, ReadError> {
145    let at = entry.property.span().start;
146    let cause = if let Some(source) = entry.value {
147        let mut reader = source.reader_in(parent)?;
148        match Token::read_from(&mut reader) {
149            Ok(token) => {
150                let properties =
151                    if crate::properties::Properties::has_legacy_values(token.signer(), &reader)? {
152                        // The shared property reader retains AnnexA cardinality
153                        // faults too, before skipping an unreadable value.
154                        // Timestamp signers must not rely on root-only rules.
155                        let properties =
156                            crate::properties::Properties::read(token.signer(), &reader)?;
157                        super::property_findings(document, &properties, &reader, findings)?;
158                        Some(properties)
159                    } else {
160                        None
161                    };
162                let fault = inspect_cached(document, entry, &token, &reader, findings)?;
163                return Ok(Reading {
164                    model: Some(Model { token, properties }),
165                    fault,
166                });
167            }
168            Err(cause) => {
169                if cause.is_terminal() {
170                    return Err(cause.into());
171                }
172                finding(
173                    document,
174                    entry.property.span(),
175                    Severity::Required,
176                    Rule::Schema {
177                        code: cause.code(),
178                        field: cause.field(),
179                    },
180                    findings,
181                )?;
182                ReadError::from(cause)
183            }
184        }
185    } else {
186        let cause = Error::Syntax {
187            code: structured_cms::ErrorCode::MissingField,
188            field: "cades.timestamp",
189            at,
190        };
191        finding(
192            document,
193            entry.property.span(),
194            Severity::Required,
195            Rule::Schema {
196                code: structured_cms::ErrorCode::MissingField,
197                field: "cades.timestamp",
198            },
199            findings,
200        )?;
201        ReadError::from(cause)
202    };
203    Ok(Reading {
204        model: None,
205        fault: Some(cause),
206    })
207}
208
209#[expect(
210    clippy::result_large_err,
211    reason = "retain the original information refusal and source map without allocating while refusing resources"
212)]
213pub(super) fn read<'m, 'd, W: Work>(
214    document: &Document<'m, 'd>,
215    timestamps: &[Timestamp<'d>],
216    root: Node,
217    counters: &OwnedMap<Node, usize, impl Work>,
218    parent: &Reader<'d>,
219    proofs: super::proofs::Proofs<'d>,
220    findings: Findings<'_, 'm, 'd, W>,
221) -> Result<Cache<'m, 'd>, ReadError> {
222    let Findings {
223        rows: findings,
224        work: findings_work,
225    } = findings;
226    let work = parent.execution();
227    let mut models = work.vector();
228    let mut ordinals = work.vector();
229    let mut faults = work.vector();
230    let mut related = None;
231    ordinals
232        .try_reserve_exact(timestamps.len())
233        .map_err(|cause| {
234            Error::resource(cause, "cades.timestamp", document.message().span().start)
235        })?;
236    for entry in timestamps {
237        let at = entry.property.span().start;
238        let resource = |cause| Error::resource(cause, "cades.timestamp", at);
239        work.charge_work(1).map_err(resource)?;
240        let selected_owner = entry.owner == root
241            || counters
242                .get(&entry.owner, &work)
243                .map_err(resource)?
244                .is_some();
245        // Other roots' preservation findings stay visible without becoming
246        // this selected root's B-B syntax. Their actual tokens STILL enter
247        // the complete historical TSA census and must independently validate.
248        let entry_findings = if selected_owner {
249            &mut *findings
250        } else {
251            related.get_or_insert_with(|| findings_work.vector())
252        };
253        if let Some(source) = entry.value {
254            let reference = proofs
255                .index
256                .get(&Node::of(source.element().span()), &work)
257                .map_err(resource)?;
258            if let Some(reference) = reference {
259                let selected = if let Some((token, properties)) = proofs.token(*reference) {
260                    let reader = source.reader_in(parent)?;
261                    if let Some(properties) = properties {
262                        super::property_findings(document, properties, &reader, entry_findings)?;
263                    }
264                    if let Some(fault) =
265                        inspect_cached(document, entry, token, &reader, entry_findings)?
266                    {
267                        faults.try_push((entry.property, fault)).map_err(resource)?;
268                    }
269                    Some(TokenIndex::Proof(*reference))
270                } else {
271                    faults
272                        .try_push((
273                            entry.property,
274                            ReadError::Processing(Error::Syntax {
275                                code: structured_cms::ErrorCode::UnexpectedContentType,
276                                field: "cades.evidence.timestamp",
277                                at: source.element().span().start,
278                            }),
279                        ))
280                        .map_err(resource)?;
281                    None
282                };
283                ordinals.try_push(selected).map_err(resource)?;
284                continue;
285            }
286        }
287        let reading = read_one(document, entry, parent, entry_findings)?;
288        if let Some(fault) = reading.fault {
289            faults.try_push((entry.property, fault)).map_err(resource)?;
290        }
291        let selected = if let Some(model) = reading.model {
292            let index = models.len();
293            models.try_push(model).map_err(resource)?;
294            Some(TokenIndex::Direct(index))
295        } else {
296            None
297        };
298        ordinals.try_push(selected).map_err(resource)?;
299    }
300    Ok(Cache {
301        models: models.into_owned(),
302        ordinals: ordinals.into_owned(),
303        proofs,
304        faults: faults.into_owned(),
305        related: related
306            .filter(|rows| !rows.is_empty())
307            .map(OwnedVec::into_owned),
308    })
309}
310
311#[expect(
312    clippy::result_large_err,
313    reason = "Inspection retains original source mapping inline so an exhausted operation does not allocate an error box."
314)]
315fn inspect_cached<'m, 'd>(
316    document: &Document<'m, 'd>,
317    entry: &Timestamp<'d>,
318    token: &Token<'d>,
319    reader: &Reader<'d>,
320    findings: &mut OwnedVec<Finding<'m, 'd>, impl Work>,
321) -> Result<Option<ReadError>, ReadError> {
322    if let Err(cause) = inspect(document, entry, token, reader, findings) {
323        if cause.is_terminal() {
324            return Err(cause);
325        }
326        let ReadError::Information(source) = cause else {
327            return Err(cause);
328        };
329        finding(
330            document,
331            entry.property.span(),
332            Severity::Required,
333            Rule::Schema {
334                code: information_cause(&source).code(),
335                field: information_cause(&source).field(),
336            },
337            findings,
338        )?;
339        return Ok(Some(ReadError::Information(source)));
340    }
341    Ok(None)
342}
343
344#[expect(
345    clippy::result_large_err,
346    reason = "retain the original information refusal and source map without allocating while refusing resources"
347)]
348fn inspect<'m, 'd>(
349    document: &Document<'m, 'd>,
350    entry: &Timestamp<'d>,
351    token: &Token<'d>,
352    reader: &Reader<'d>,
353    findings: &mut OwnedVec<Finding<'m, 'd>, impl Work>,
354) -> Result<(), ReadError> {
355    let index_algorithm = {
356        let signing_fields = signing::read(document, entry, token, reader, findings)?;
357        index::read(document, entry, token, &signing_fields, reader, findings)?
358    };
359    // ESS/index catalogues are temporary inspection scratch. Release their
360    // capacity before opening potentially fragmented carried information.
361    // The envelope model is retained once. A subsequent cryptographic check
362    // opens information under CURRENT work: temporary joined TSTInfo cannot
363    // be pinned in this cache. Its faults use a physical attribute enclosure,
364    // never a logical offset disguised as an original document position.
365    let inspection = token.with_info(reader, |info, inside, _| {
366        if let structured_cms::timestamp::HashAlgorithmReading::Identifier(algorithm) =
367            info.imprint().algorithm()
368        {
369            let imprint = signing::digest(
370                document,
371                entry.property.span(),
372                algorithm,
373                Some(info.imprint().hashed().len()),
374                "tstInfo.messageImprint.hashedMessage",
375                inside,
376                findings,
377            )?;
378            if let Some((structured_asn1::digest::Resolution::Supported(index), span)) =
379                &index_algorithm
380                && let structured_asn1::digest::Resolution::Supported(imprint) = imprint
381            {
382                reader.charge(1).map_err(|cause| {
383                    signing::InspectionError::Physical(Error::encoding(
384                        cause,
385                        "cades.archive.index",
386                    ))
387                })?;
388                if *index != imprint {
389                    finding(
390                        document,
391                        *span,
392                        Severity::Required,
393                        Rule::ArchiveAlgorithm,
394                        findings,
395                    )
396                    .map_err(signing::InspectionError::Physical)?;
397                }
398            }
399        }
400        for cause in token
401            .faults()
402            .iter()
403            .chain(token.message().faults())
404            .chain(token.signer().faults())
405            .chain(info.faults())
406        {
407            reader.charge(1).map_err(|cause| {
408                signing::InspectionError::Physical(Error::encoding(cause, "cades.timestamp"))
409            })?;
410            finding(
411                document,
412                entry.property.span(),
413                Severity::Required,
414                Rule::Schema {
415                    code: cause.code(),
416                    field: cause.field(),
417                },
418                findings,
419            )
420            .map_err(signing::InspectionError::Physical)?;
421        }
422        Ok::<_, signing::InspectionError>(())
423    });
424    let source = match inspection {
425        Ok(()) => return Ok(()),
426        Err(ContentError::Callback {
427            source: InfoError::Callback(cause),
428            context,
429        }) => {
430            let coordinates = match &cause {
431                signing::InspectionError::Reader(_) => context.coordinates(),
432                signing::InspectionError::Physical(_) => Coordinates::Original,
433            };
434            return Err(ReadError::Inspection {
435                source: cause.into_source(),
436                context,
437                coordinates,
438            });
439        }
440        Err(ContentError::Processing(cause)) => ContentError::Processing(cause),
441        Err(ContentError::Decoding { source, context }) => {
442            ContentError::Decoding { source, context }
443        }
444        Err(ContentError::Callback {
445            source: InfoError::Processing(source),
446            context,
447        }) => ContentError::Callback {
448            source: InfoError::Processing(source),
449            context,
450        },
451    };
452    Err(ReadError::Information(source))
453}