1use 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#[derive(Debug)]
23pub enum ReadError {
24 Processing(Error),
26 Information(ContentError<InfoError<Infallible>>),
29 Inspection {
33 source: Error,
35 context: SourceMap,
37 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 #[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 #[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 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 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 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}