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) 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 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 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 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}