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) ordinal: usize,
90    pub(crate) token: Token<'d>,
91    pub(crate) properties: Option<crate::properties::Properties<'d>>,
92}
93pub(crate) struct Cache<'m, 'd> {
94    pub(crate) models: OwnedVec<Model<'d>>,
95    pub(crate) ordinals: OwnedVec<Option<usize>>,
96    pub(crate) faults: OwnedVec<(Node, ReadError)>,
97    pub(crate) related: Option<OwnedVec<Finding<'m, 'd>>>,
98}
99
100pub(super) struct Findings<'a, 'm, 'd, W: Work> {
101    pub(super) rows: &'a mut OwnedVec<Finding<'m, 'd>, W>,
102    pub(super) work: &'a W,
103}
104struct Reading<'d> {
105    model: Option<Model<'d>>,
106    fault: Option<ReadError>,
107}
108
109#[expect(
110    clippy::result_large_err,
111    reason = "retain the original source map without allocating during resource refusal"
112)]
113fn read_one<'m, 'd, W: Work>(
114    document: &Document<'m, 'd>,
115    ordinal: usize,
116    entry: &Timestamp<'d>,
117    parent: &Reader<'d>,
118    findings: &mut OwnedVec<Finding<'m, 'd>, W>,
119) -> Result<Reading<'d>, ReadError> {
120    let at = entry.property.span().start;
121    let cause = if let Some(source) = entry.value {
122        let mut reader = source.reader_in(parent)?;
123        match Token::read_from(&mut reader) {
124            Ok(token) => {
125                let properties =
126                    if crate::properties::Properties::has_legacy_values(token.signer(), &reader)? {
127                        // The shared property reader retains AnnexA cardinality
128                        // faults too, before skipping an unreadable value.
129                        // Timestamp signers must not rely on root-only rules.
130                        let properties =
131                            crate::properties::Properties::read(token.signer(), &reader)?;
132                        super::property_findings(document, &properties, &reader, findings)?;
133                        Some(properties)
134                    } else {
135                        None
136                    };
137                let fault = if let Err(cause) = inspect(document, entry, &token, &reader, findings)
138                {
139                    if cause.is_terminal() {
140                        return Err(cause);
141                    }
142                    let ReadError::Information(source) = cause else {
143                        return Err(cause);
144                    };
145                    finding(
146                        document,
147                        entry.property.span(),
148                        Severity::Required,
149                        Rule::Schema {
150                            code: information_cause(&source).code(),
151                            field: information_cause(&source).field(),
152                        },
153                        findings,
154                    )?;
155                    Some(ReadError::Information(source))
156                } else {
157                    None
158                };
159                return Ok(Reading {
160                    model: Some(Model {
161                        ordinal,
162                        token,
163                        properties,
164                    }),
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    findings: Findings<'_, 'm, 'd, W>,
220) -> Result<Cache<'m, 'd>, ReadError> {
221    let Findings {
222        rows: findings,
223        work: findings_work,
224    } = findings;
225    let work = parent.execution();
226    let mut models = work.vector();
227    let mut ordinals = work.vector();
228    let mut faults = work.vector();
229    let mut related = None;
230    ordinals
231        .try_reserve_exact(timestamps.len())
232        .map_err(|cause| {
233            Error::resource(cause, "cades.timestamp", document.message().span().start)
234        })?;
235    for (ordinal, entry) in timestamps.iter().enumerate() {
236        let at = entry.property.span().start;
237        let resource = |cause| Error::resource(cause, "cades.timestamp", at);
238        work.charge_work(1).map_err(resource)?;
239        let selected_owner = entry.owner == root
240            || counters
241                .get(&entry.owner, &work)
242                .map_err(resource)?
243                .is_some();
244        // Other roots' preservation findings stay visible without becoming
245        // this selected root's B-B syntax. Their actual tokens STILL enter
246        // the complete historical TSA census and must independently validate.
247        let entry_findings = if selected_owner {
248            &mut *findings
249        } else {
250            related.get_or_insert_with(|| findings_work.vector())
251        };
252        let reading = read_one(document, ordinal, entry, parent, entry_findings)?;
253        if let Some(fault) = reading.fault {
254            faults.try_push((entry.property, fault)).map_err(resource)?;
255        }
256        let selected = if let Some(model) = reading.model {
257            let index = models.len();
258            models.try_push(model).map_err(resource)?;
259            Some(index)
260        } else {
261            None
262        };
263        ordinals.try_push(selected).map_err(resource)?;
264    }
265    Ok(Cache {
266        models: models.into_owned(),
267        ordinals: ordinals.into_owned(),
268        faults: faults.into_owned(),
269        related: related
270            .filter(|rows| !rows.is_empty())
271            .map(OwnedVec::into_owned),
272    })
273}
274
275#[expect(
276    clippy::result_large_err,
277    reason = "retain the original information refusal and source map without allocating while refusing resources"
278)]
279fn inspect<'m, 'd>(
280    document: &Document<'m, 'd>,
281    entry: &Timestamp<'d>,
282    token: &Token<'d>,
283    reader: &Reader<'d>,
284    findings: &mut OwnedVec<Finding<'m, 'd>, impl Work>,
285) -> Result<(), ReadError> {
286    let index_algorithm = {
287        let signing_fields = signing::read(document, entry, token, reader, findings)?;
288        index::read(document, entry, token, &signing_fields, reader, findings)?
289    };
290    // ESS/index catalogues are temporary inspection scratch. Release their
291    // capacity before opening potentially fragmented carried information.
292    // The envelope model is retained once. A subsequent cryptographic check
293    // opens information under CURRENT work: temporary joined TSTInfo cannot
294    // be pinned in this cache. Its faults use a physical attribute enclosure,
295    // never a logical offset disguised as an original document position.
296    let inspection = token.with_info(reader, |info, inside, _| {
297        if let structured_cms::timestamp::HashAlgorithmReading::Identifier(algorithm) =
298            info.imprint().algorithm()
299        {
300            let imprint = signing::digest(
301                document,
302                entry.property.span(),
303                algorithm,
304                Some(info.imprint().hashed().len()),
305                "tstInfo.messageImprint.hashedMessage",
306                inside,
307                findings,
308            )?;
309            if let Some((structured_asn1::digest::Resolution::Supported(index), span)) =
310                &index_algorithm
311                && let structured_asn1::digest::Resolution::Supported(imprint) = imprint
312            {
313                reader.charge(1).map_err(|cause| {
314                    signing::InspectionError::Physical(Error::encoding(
315                        cause,
316                        "cades.archive.index",
317                    ))
318                })?;
319                if *index != imprint {
320                    finding(
321                        document,
322                        *span,
323                        Severity::Required,
324                        Rule::ArchiveAlgorithm,
325                        findings,
326                    )
327                    .map_err(signing::InspectionError::Physical)?;
328                }
329            }
330        }
331        for cause in token
332            .faults()
333            .iter()
334            .chain(token.message().faults())
335            .chain(token.signer().faults())
336            .chain(info.faults())
337        {
338            reader.charge(1).map_err(|cause| {
339                signing::InspectionError::Physical(Error::encoding(cause, "cades.timestamp"))
340            })?;
341            finding(
342                document,
343                entry.property.span(),
344                Severity::Required,
345                Rule::Schema {
346                    code: cause.code(),
347                    field: cause.field(),
348                },
349                findings,
350            )
351            .map_err(signing::InspectionError::Physical)?;
352        }
353        Ok::<_, signing::InspectionError>(())
354    });
355    let source = match inspection {
356        Ok(()) => return Ok(()),
357        Err(ContentError::Callback {
358            source: InfoError::Callback(cause),
359            context,
360        }) => {
361            let coordinates = match &cause {
362                signing::InspectionError::Reader(_) => context.coordinates(),
363                signing::InspectionError::Physical(_) => Coordinates::Original,
364            };
365            return Err(ReadError::Inspection {
366                source: cause.into_source(),
367                context,
368                coordinates,
369            });
370        }
371        Err(ContentError::Processing(cause)) => ContentError::Processing(cause),
372        Err(ContentError::Decoding { source, context }) => {
373            ContentError::Decoding { source, context }
374        }
375        Err(ContentError::Callback {
376            source: InfoError::Processing(source),
377            context,
378        }) => ContentError::Callback {
379            source: InfoError::Processing(source),
380            context,
381        },
382    };
383    Err(ReadError::Information(source))
384}