1use super::{Name, NameError, status::status_name};
4use crate::{
5 container::{Container, ErrorOf},
6 material::{HistoryError, ParsedHistory, Value},
7 model::admit,
8 query::Query,
9 range::RangeSource,
10 source::Location,
11 timestamp::{
12 Node,
13 sources::{Models, Slot},
14 },
15};
16use structured_asn1::memory::{OwnedMap, OwnedVec, Work};
17use structured_ltv::source::{Query as _, ScopeError};
18use structured_pkix::{algorithm::digest::AsnDigest, signature::SignatureValue};
19
20pub struct Association {
23 revision: usize,
24 ordinal: usize,
25 location: Location,
26 named: bool,
27 timely: bool,
28}
29impl Association {
30 #[must_use]
32 pub fn revision(&self) -> usize {
33 self.revision
34 }
35 #[must_use]
37 pub fn ordinal(&self) -> usize {
38 self.ordinal
39 }
40 #[must_use]
42 pub fn location(&self) -> Location {
43 self.location
44 }
45 #[must_use]
47 pub fn has_named_input(&self) -> bool {
48 self.named
49 }
50 #[must_use]
52 pub fn all_placement_requirements_met(&self) -> bool {
53 self.timely
54 }
55}
56pub struct Associations {
58 values: OwnedVec<Association>,
59 complete: bool,
60}
61impl Associations {
62 #[must_use]
64 pub fn entries(&self) -> &[Association] {
65 &self.values
66 }
67 #[must_use]
70 pub fn all_requirements_met(&self) -> bool {
71 self.complete
72 }
73}
74
75#[derive(Debug)]
77pub enum SourceError<E, P> {
78 History(HistoryError),
80 Scope(ScopeError<Node>),
82 Contents(ErrorOf<E>),
84 Name(NameError<P>),
86 Cms(structured_cades::Error),
88 UnsupportedName,
90}
91impl<E, P> From<HistoryError> for SourceError<E, P> {
92 fn from(cause: HistoryError) -> Self {
93 Self::History(cause)
94 }
95}
96
97struct Row {
98 value: Association,
99 name: Name,
100}
101struct Available {
102 dictionary: Option<usize>,
103 status: Option<usize>,
104}
105struct Table<W: Work> {
106 heads: OwnedMap<Name, Available, W>,
107 rows: OwnedVec<Row, W>,
108}
109enum StatusNameError<P> {
110 Unsupported,
111 Name(NameError<P>),
112}
113enum NamingError<P> {
114 Scope(ScopeError<Node>),
115 Name(NameError<P>),
116 Unsupported,
117}
118impl<P> From<StatusNameError<P>> for NamingError<P> {
119 fn from(value: StatusNameError<P>) -> Self {
120 match value {
121 StatusNameError::Unsupported => Self::Unsupported,
122 StatusNameError::Name(cause) => Self::Name(cause),
123 }
124 }
125}
126impl<E, P> From<NamingError<P>> for SourceError<E, P> {
127 fn from(value: NamingError<P>) -> Self {
128 match value {
129 NamingError::Scope(cause) => Self::Scope(cause),
130 NamingError::Name(cause) => Self::Name(cause),
131 NamingError::Unsupported => Self::UnsupportedName,
132 }
133 }
134}
135impl<E, P> From<StatusNameError<P>> for SourceError<E, P> {
136 fn from(cause: StatusNameError<P>) -> Self {
137 match cause {
138 StatusNameError::Unsupported => Self::UnsupportedName,
139 StatusNameError::Name(cause) => Self::Name(cause),
140 }
141 }
142}
143impl<E, P> From<crate::Error> for SourceError<E, P> {
144 fn from(cause: crate::Error) -> Self {
145 Self::Name(NameError::Pdf(cause))
146 }
147}
148impl<W: Work> Table<W> {
149 fn mark(
150 &mut self,
151 name: Name,
152 available: usize,
153 dictionary: bool,
154 work: &W,
155 location: Location,
156 ) -> crate::Result<()> {
157 let resource = |cause| crate::Error {
158 kind: crate::ErrorKind::Resource(cause),
159 location,
160 };
161 if let Some(value) = self.heads.get_mut(&name, work).map_err(resource)? {
162 admit(work, location, 1)?;
163 let earliest = if dictionary {
164 &mut value.dictionary
165 } else {
166 &mut value.status
167 };
168 if earliest.is_none_or(|previous| available < previous) {
169 *earliest = Some(available);
170 }
171 }
172 Ok(())
173 }
174 fn status<'p, P: AsnDigest<'p>>(
175 &mut self,
176 signature: SignatureValue<'_>,
177 available: usize,
178 provider: &'p P,
179 work: &W,
180 location: Location,
181 ) -> Result<(), StatusNameError<P::Error>> {
182 let name = match status_name(signature, provider, work, location) {
183 Ok(Some(name)) => name,
184 Ok(None) => return Err(StatusNameError::Unsupported),
185 Err(NameError::PaddedSignature) => return Ok(()),
186 Err(cause) => return Err(StatusNameError::Name(cause)),
187 };
188 self.mark(name, available, false, work, location)
189 .map_err(|cause| StatusNameError::Name(NameError::Pdf(cause)))
190 }
191}
192
193fn model_names<'q, 'p, P: AsnDigest<'p>, S: RangeSource>(
194 table: &mut Table<structured_asn1::memory::Execution<'q>>,
195 models: &Models<'_, '_, '_, '_, S::Document, S::Generation>,
196 provider: &'p P,
197 query: &Query<'q, '_, S>,
198 location: Location,
199) -> Result<(), NamingError<P::Error>> {
200 let work = query.execution();
201 for (ordinal, model) in models.values.iter().enumerate() {
202 query
203 .admit(Node::of(location), 1)
204 .map_err(NamingError::Scope)?;
205 let Some(Slot::Value(container)) = models
206 .sources
207 .get(ordinal, work)
208 .map_err(|cause| NamingError::Name(NameError::Pdf(cause)))?
209 else {
210 continue;
211 };
212 let signature = container.signature();
213 let name = container
214 .vri_name(provider, work)
215 .map_err(NamingError::Name)?
216 .ok_or(NamingError::Unsupported)?;
217 table
218 .mark(
219 name,
220 signature.defined_revision(),
221 true,
222 work,
223 signature.location(),
224 )
225 .map_err(|cause| NamingError::Name(NameError::Pdf(cause)))?;
226 let crate::timestamp::sources::Model::Known { revocations, .. } = model else {
227 continue;
228 };
229 for value in revocations {
230 query
231 .admit(Node::of(location), 1)
232 .map_err(NamingError::Scope)?;
233 match &value.value {
234 Ok(structured_cms::signed::revocation::Reading::Crl(value)) => table.status(
235 value.signature_value(),
236 signature.defined_revision(),
237 provider,
238 work,
239 signature.location(),
240 )?,
241 Ok(structured_cms::signed::revocation::Reading::Ocsp(value)) => table.status(
242 value.signature_value(),
243 signature.defined_revision(),
244 provider,
245 work,
246 signature.location(),
247 )?,
248 _ => {}
249 }
250 }
251 }
252 Ok(())
253}
254
255fn historical_names<'q, 'p, P: AsnDigest<'p>, S: RangeSource>(
256 table: &mut Table<structured_asn1::memory::Execution<'q>>,
257 history: &ParsedHistory<'_, '_, '_, S::Document, S::Generation>,
258 revision: usize,
259 provider: &'p P,
260 query: &Query<'q, '_, S>,
261 location: Location,
262) -> Result<(), NamingError<P::Error>> {
263 let work = query.execution();
264 for (revision, parsed) in history.revisions()[..=revision].iter().enumerate() {
265 for (ordinal, value) in parsed.values().iter().enumerate() {
266 query
267 .admit(Node::of(location), 1)
268 .map_err(NamingError::Scope)?;
269 let location = parsed.owner.items()[ordinal].location();
270 match value {
271 Value::Crl(value) => {
272 table.status(value.signature_value(), revision, provider, work, location)?;
273 }
274 Value::Ocsp {
275 basic: Ok(Some(value)),
276 ..
277 } => table.status(value.signature_value(), revision, provider, work, location)?,
278 Value::Timestamp(value) => {
279 for value in &value.revocations {
280 query
281 .admit(Node::of(location), 1)
282 .map_err(NamingError::Scope)?;
283 match &value.value {
284 Ok(structured_cms::signed::revocation::Reading::Crl(value)) => table
285 .status(
286 value.signature_value(),
287 revision,
288 provider,
289 work,
290 location,
291 )?,
292 Ok(structured_cms::signed::revocation::Reading::Ocsp(value)) => table
293 .status(
294 value.signature_value(),
295 revision,
296 provider,
297 work,
298 location,
299 )?,
300 _ => {}
301 }
302 }
303 }
304 _ => {}
305 }
306 }
307 }
308 Ok(())
309}
310
311fn finish_table<'q, S: RangeSource, E>(
312 table: Table<structured_asn1::memory::Execution<'q>>,
313 query: &Query<'q, '_, S>,
314 location: Location,
315) -> Result<Associations, NamingError<E>> {
316 let work = query.execution();
317 let resource = |cause| {
318 NamingError::Name(NameError::Pdf(crate::Error {
319 kind: crate::ErrorKind::Resource(cause),
320 location,
321 }))
322 };
323 let mut values = work.vector();
324 values
325 .try_reserve_exact(table.rows.len())
326 .map_err(resource)?;
327 let mut complete = true;
328 for mut row in table.rows {
329 query
330 .admit(Node::of(location), 1)
331 .map_err(NamingError::Scope)?;
332 let available = table
333 .heads
334 .get(&row.name, work)
335 .map_err(resource)?
336 .expect("original VRI name");
337 row.value.named = available.dictionary.is_some() || available.status.is_some();
338 row.value.timely = row.value.revision > 0
341 && (available
342 .dictionary
343 .is_some_and(|revision| row.value.revision > revision)
344 || available
345 .status
346 .is_some_and(|revision| row.value.revision >= revision));
347 if !row.value.timely {
348 complete = false;
349 }
350 values.try_push(row.value).map_err(resource)?;
351 }
352 Ok(Associations {
353 values: values.into_owned(),
354 complete,
355 })
356}
357
358fn seed_table<'q, S: RangeSource, E>(
359 history: &ParsedHistory<'_, '_, '_, S::Document, S::Generation>,
360 revision: usize,
361 location: Location,
362 query: &Query<'q, '_, S>,
363) -> Result<Table<structured_asn1::memory::Execution<'q>>, NamingError<E>> {
364 let work = query.execution();
365 let resource = |cause| {
366 NamingError::Name(NameError::Pdf(crate::Error {
367 kind: crate::ErrorKind::Resource(cause),
368 location,
369 }))
370 };
371 let mut table = Table {
372 heads: work.map(),
373 rows: work.vector(),
374 };
375 for (revision, parsed) in history.revisions()[..=revision].iter().enumerate() {
376 for (ordinal, vri) in parsed.owner.vri().iter().enumerate() {
377 query
378 .admit(Node::dss(vri.location(), revision), 41)
379 .map_err(NamingError::Scope)?;
380 table
381 .heads
382 .try_insert(
383 Name(*vri.name()),
384 Available {
385 dictionary: None,
386 status: None,
387 },
388 )
389 .map_err(resource)?;
390 table
391 .rows
392 .try_push(Row {
393 value: Association {
394 revision,
395 ordinal,
396 location: vri.location(),
397 named: false,
398 timely: false,
399 },
400 name: Name(*vri.name()),
401 })
402 .map_err(resource)?;
403 }
404 }
405 Ok(table)
406}
407
408#[expect(
409 clippy::result_large_err,
410 reason = "historical association failures preserve original item and nested format causes without allocating a refusal envelope"
411)]
412pub(in crate::material) async fn associate<'p, P, S>(
413 history: &ParsedHistory<'_, '_, '_, S::Document, S::Generation>,
414 root: &Container<'_, '_, S::Document, S::Generation>,
415 pool: &structured_cades::material::Pool<'_, '_, '_>,
416 models: &Models<'_, '_, '_, '_, S::Document, S::Generation>,
417 provider: &'p P,
418 query: &Query<'_, '_, S>,
419 revision: usize,
420) -> Result<Associations, SourceError<S::Error, P::Error>>
421where
422 S: RangeSource,
423 P: AsnDigest<'p>,
424{
425 let work = query.execution();
426 let location = root.signature().location();
427 let mut table = seed_table(history, revision, location, query).map_err(SourceError::from)?;
428 if table.rows.is_empty() {
429 return Ok(Associations {
430 values: work.vector().into_owned(),
431 complete: true,
432 });
433 }
434 let name = root
435 .vri_name(provider, work)
436 .map_err(SourceError::Name)?
437 .ok_or(SourceError::UnsupportedName)?;
438 table.mark(
439 name,
440 root.signature().defined_revision(),
441 true,
442 work,
443 location,
444 )?;
445 let catalogue = root.signature().catalogue();
446 for ordinal in 0..catalogue.len() {
447 query
448 .admit(Node::of(location), 1)
449 .map_err(SourceError::Scope)?;
450 let signature = catalogue.get(ordinal).expect("bounded original signature");
451 if signature.location() == location
452 || signature.is_document_timestamp()
453 || signature.defined_revision() > revision
454 {
455 continue;
456 }
457 if !matches!(
458 signature.sub_filter(),
459 Some(
460 crate::signature::SubFilter::CadesDetached
461 | crate::signature::SubFilter::Pkcs7Detached
462 )
463 ) {
464 continue;
465 }
466 let container = match query.document().container(signature, work).await {
467 Ok(value) => value,
468 Err(cause) if cause.is_terminal() => return Err(SourceError::Contents(cause)),
469 Err(_) => continue,
470 };
471 let name = container
472 .vri_name(provider, work)
473 .map_err(SourceError::Name)?
474 .ok_or(SourceError::UnsupportedName)?;
475 table.mark(
476 name,
477 signature.defined_revision(),
478 true,
479 work,
480 signature.location(),
481 )?;
482 }
483 let available = root.signature().defined_revision();
484 for ordinal in 0..pool.crls() {
485 let crl = pool
486 .crl(ordinal, work)
487 .map_err(SourceError::Cms)?
488 .expect("bounded actual CRL");
489 table.status(crl.signature_value(), available, provider, work, location)?;
490 }
491 for ordinal in 0..pool.ocsp_responses() {
492 let ocsp = pool
493 .ocsp(ordinal, work)
494 .map_err(SourceError::Cms)?
495 .expect("bounded actual basic response");
496 table.status(ocsp.signature_value(), available, provider, work, location)?;
497 }
498 model_names(&mut table, models, provider, query, location).map_err(SourceError::from)?;
499 historical_names(&mut table, history, revision, provider, query, location)
500 .map_err(SourceError::from)?;
501 finish_table(table, query, location).map_err(SourceError::from)
502}