Skip to main content

structured_pades/material/
history.rs

1//! Full historical DSS visibility and the unchanged inner CMS material pool.
2
3use super::{Parsed, Placement, Validation};
4use crate::{
5    basic::Basic,
6    binding::InnerQuery,
7    document::Document,
8    error::{Error, ErrorKind, ReadFailure},
9    query::Query,
10    range::RangeSource,
11    signature::Signature,
12    timestamp::{ISSUER, Node, Pades},
13};
14use structured_asn1::memory::{Execution, Memory, OwnedVec, Work};
15use structured_ltv::{
16    material::{ArtifactSource, Index, IndexError, ProjectionError},
17    source::{Location, Query as _},
18};
19mod token;
20
21/// Every historical DSS window, including explicit absent dictionaries. The
22/// original root/source membership cannot be replaced with a caller subset.
23pub struct History<'a, 's, D, G> {
24    pub(crate) root: Signature<'a, 's, D, G>,
25    pub(super) values: OwnedVec<Validation<'s, D, G>, Memory>,
26}
27/// Once-parsed material in every actual historical window.
28pub struct ParsedHistory<'h, 'a, 's, D, G> {
29    pub(super) owner: &'h History<'a, 's, D, G>,
30    pub(super) values: OwnedVec<Parsed<'h, 's, D, G>, Memory>,
31}
32
33type UnifiedIndex<'q, 'd, D, G> =
34    Index<'q, Node, D, G, Placement, structured_cades::material::Open<'d>, Execution<'q>, Pades>;
35
36struct TokenCoverage {
37    complete: bool,
38    generation_revision: Option<usize>,
39}
40
41enum TokenIndexError {
42    Foreign(Error),
43    Index(IndexError<Node>),
44}
45impl From<TokenIndexError> for HistoryError {
46    fn from(value: TokenIndexError) -> Self {
47        match value {
48            TokenIndexError::Foreign(cause) => Self::Foreign(cause),
49            TokenIndexError::Index(cause) => Self::Index(cause),
50        }
51    }
52}
53
54/// Exact common pool projected into the enclosing PDF domain, before any
55/// material/path/status acceptance. Open values retain their CMS coordinates.
56pub struct Unified<'q, 'd, D, G> {
57    pub(crate) session: &'q crate::Session,
58    pub(crate) index: UnifiedIndex<'q, 'd, D, G>,
59    pub(crate) root: Node,
60    pub(crate) complete: bool,
61    pub(crate) required: &'q structured_ltv::timestamp::TimestampRequirements<Node, D, G, Pades>,
62    pub(crate) revision: usize,
63    associations: super::vri::Associations,
64}
65impl<D, G> Unified<'_, '_, D, G> {
66    /// Exact historical material window, independent of the file's latest
67    /// revision and of whether any artifact lookup succeeds.
68    #[must_use]
69    pub fn revision(&self) -> usize {
70        self.revision
71    }
72    /// DSS presence and original source conformance in the selected window.
73    /// Actual participant incorporation, authentication and trust are separate.
74    #[must_use]
75    pub fn all_source_requirements_met(&self) -> bool {
76        self.complete
77    }
78    /// Complete nominal VRI associations, including unmatched or late inputs.
79    #[must_use]
80    pub fn vri_associations(&self) -> &super::vri::Associations {
81        &self.associations
82    }
83}
84
85/// Original acquisition, carried-format, projection or current-query refusal.
86#[derive(Debug)]
87pub enum HistoryError {
88    /// Original DSS/carried-format parser refusal.
89    Material(super::MaterialFailure),
90    /// Actual inner pool source/metadata admission failure.
91    CmsIndex {
92        /// Physical PDF enclosure of the logical CMS coordinates.
93        enclosure: crate::source::Location,
94        /// Unchanged inner source/resource cause.
95        cause: IndexError<structured_cades::source::Node>,
96    },
97    /// Original source/projection/current allocation refusal.
98    Projection(ProjectionError<Node, Error>),
99    /// Actual PDF material-index refusal.
100    Index(IndexError<Node>),
101    /// Source membership was substituted.
102    Foreign(Error),
103}
104
105impl<'s, S: RangeSource> Document<'s, S> {
106    /// Enumerates each actual revision, so a late DSS or replacement cannot
107    /// silently satisfy a timestamp whose original input lacks those values.
108    pub async fn validation_history<'a, W: Work>(
109        &self,
110        root: Signature<'a, 's, S::Document, S::Generation>,
111        operation: &W,
112    ) -> Result<History<'a, 's, S::Document, S::Generation>, ReadFailure<S::Error>> {
113        let work = self.session.query(operation);
114        let location = root.location();
115        self.check(&work, location)?;
116        if !core::ptr::eq(root.owner().value.session, self.session)
117            || root.owner().value.stamp != self.stamp
118        {
119            return Err(Error {
120                kind: ErrorKind::Foreign,
121                location,
122            }
123            .into());
124        }
125        let mut values = work.vector();
126        for revision in 0..self.revisions() {
127            crate::model::admit(&work, location, 1)?;
128            let value = self.validation(revision, &work).await?;
129            values.try_push(value).map_err(|cause| Error {
130                kind: ErrorKind::Resource(cause),
131                location,
132            })?;
133        }
134        Ok(History {
135            root,
136            values: values.into_owned(),
137        })
138    }
139}
140impl<'a, 's, D: Copy + Eq, G: Copy + Eq> History<'a, 's, D, G> {
141    /// Historical owners in original revision order, including absent DSS.
142    #[must_use]
143    pub fn revisions(&self) -> &[Validation<'s, D, G>] {
144        &self.values
145    }
146    /// Parses each actual original window once, keeping every negative source.
147    #[expect(
148        clippy::result_large_err,
149        reason = "historical material failure preserves the complete cause and actual original item location without an error box"
150    )]
151    pub fn parse<'h>(
152        &'h self,
153        operation: &'h impl Work,
154    ) -> Result<ParsedHistory<'h, 'a, 's, D, G>, HistoryError> {
155        let work = self.root.owner().value.session.query(operation);
156        let mut values = work.vector();
157        for value in &self.values {
158            let value = value.parse(operation).map_err(HistoryError::Material)?;
159            values.try_push(value).map_err(|cause| {
160                HistoryError::Foreign(Error {
161                    kind: ErrorKind::Resource(cause),
162                    location: self.root.location(),
163                })
164            })?;
165        }
166        Ok(ParsedHistory {
167            owner: self,
168            values: values.into_owned(),
169        })
170    }
171}
172impl<'h, 's, D: Copy + Eq, G: Copy + Eq> ParsedHistory<'h, '_, 's, D, G> {
173    /// Same full original material windows; no successful-prefix selection.
174    #[must_use]
175    pub fn revisions(&self) -> &[Parsed<'h, 's, D, G>] {
176        &self.values
177    }
178    fn append_document_tokens<
179        'q,
180        'm: 'q,
181        'd: 'q,
182        'tokens: 'q,
183        S: RangeSource<Document = D, Generation = G>,
184    >(
185        timestamps: &'q crate::timestamp::sources::Models<'tokens, '_, '_, 's, D, G>,
186        mapping: crate::binding::Mapping<'m, 'd, D, G>,
187        prefix: usize,
188        index: &mut UnifiedIndex<'q, 'd, D, G>,
189        query: &Query<'q, 's, S>,
190    ) -> Result<TokenCoverage, TokenIndexError>
191    where
192        'h: 'q,
193    {
194        let mut complete = true;
195        let mut generation_revision = None;
196        for (timestamp, value) in timestamps.values.iter().enumerate() {
197            index.admit(query).map_err(TokenIndexError::Index)?;
198            let crate::timestamp::sources::Model::Known { token, revocations } = value else {
199                complete = false;
200                continue;
201            };
202            let Some(crate::timestamp::sources::Slot::Value(container)) = timestamps
203                .sources
204                .get(timestamp, query.execution())
205                .map_err(TokenIndexError::Foreign)?
206            else {
207                unreachable!("a parsed token retains its original container")
208            };
209            if let crate::signature::Coverage::Revision(covered) = container.signature().coverage()
210                && generation_revision.is_none_or(|previous| covered > previous)
211            {
212                generation_revision = Some(covered);
213            }
214            let source_ordinal = prefix.checked_add(timestamp).ok_or(TokenIndexError::Index(
215                IndexError::Resource {
216                    signature: mapping.root,
217                    cause: structured_asn1::memory::MemoryError::CapacityOverflow,
218                },
219            ))?;
220            let contents = timestamps
221                .contents(timestamp)
222                .expect("readable original token source");
223            let location = Location {
224                snapshot: mapping.snapshot,
225                node: Node::of(contents),
226            };
227            if !token::append(
228                index,
229                token,
230                revocations,
231                source_ordinal,
232                location,
233                |choice| Placement::DocumentTimestamp { timestamp, choice },
234                query,
235            )
236            .map_err(TokenIndexError::Index)?
237            {
238                complete = false;
239            }
240        }
241        Ok(TokenCoverage {
242            complete,
243            generation_revision,
244        })
245    }
246
247    fn append_vri_tokens<
248        'q,
249        'm: 'q,
250        'd: 'q,
251        'tokens: 'q,
252        S: RangeSource<Document = D, Generation = G>,
253    >(
254        &'q self,
255        timestamps: &'q crate::timestamp::sources::Models<'tokens, '_, '_, 's, D, G>,
256        mapping: crate::binding::Mapping<'m, 'd, D, G>,
257        prefix: usize,
258        index: &mut UnifiedIndex<'q, 'd, D, G>,
259        query: &Query<'q, 's, S>,
260    ) -> Result<bool, TokenIndexError>
261    where
262        'h: 'q,
263    {
264        let mut complete = true;
265        for source in timestamps.vri_timestamps() {
266            index.admit(query).map_err(TokenIndexError::Index)?;
267            let parsed = &self.values[source.revision];
268            let super::Value::Timestamp(value) = &parsed.values()[source.item] else {
269                complete = false;
270                continue;
271            };
272            let source_ordinal = prefix
273                .checked_add(timestamps.total_pdf())
274                .and_then(|prefix| prefix.checked_add(source.ordinal))
275                .ok_or(TokenIndexError::Index(IndexError::Resource {
276                    signature: mapping.root,
277                    cause: structured_asn1::memory::MemoryError::CapacityOverflow,
278                }))?;
279            let item = &self.owner.values[source.revision].items()[source.item];
280            let location = Location {
281                snapshot: mapping.snapshot,
282                node: Node::dss(item.location(), source.revision),
283            };
284            if !token::append(
285                index,
286                &value.token,
287                &value.revocations,
288                source_ordinal,
289                location,
290                |choice| Placement::VriTimestamp {
291                    revision: source.revision,
292                    vri: source.vri,
293                    choice,
294                },
295                query,
296            )
297            .map_err(TokenIndexError::Index)?
298            {
299                complete = false;
300            }
301        }
302        Ok(complete)
303    }
304
305    fn selected_revision<S: RangeSource<Document = D, Generation = G>>(
306        &self,
307        basic: &Basic<'_, '_, '_, '_, '_, '_, 's, '_, D, G>,
308        pool: &structured_cades::material::Pool<'_, '_, '_>,
309        timestamps: &crate::timestamp::sources::Models<'_, '_, '_, 's, D, G>,
310        query: &Query<'_, 's, S>,
311    ) -> crate::Result<usize> {
312        let signature = self.owner.root;
313        let location = signature.location();
314        let mapping = basic.binding().mapping;
315        let foreign = || Error {
316            kind: ErrorKind::Foreign,
317            location,
318        };
319        if !query.belongs(signature.owner().value.session)
320            || query.snapshot() != mapping.snapshot
321            || mapping.root != Node::of(location)
322            || !core::ptr::eq(pool.prepared(), basic.binding().prepared().cms)
323        {
324            return Err(foreign());
325        }
326        if timestamps.root() != mapping.root || timestamps.snapshot() != mapping.snapshot {
327            return Err(foreign());
328        }
329        debug_assert!(
330            !self.values.is_empty(),
331            "a parsed document has an original revision"
332        );
333        let revision = timestamps.revision().unwrap_or(self.values.len() - 1);
334        if revision >= self.values.len()
335            || !matches!(signature.coverage(), crate::signature::Coverage::Revision(root) if root <= revision)
336        {
337            return Err(foreign());
338        }
339        Ok(revision)
340    }
341
342    fn source_complete<'q, 'm, 'd, S: RangeSource<Document = D, Generation = G>>(
343        &self,
344        pool: &structured_cades::material::Pool<'_, 'm, 'd>,
345        revision: usize,
346        index: &UnifiedIndex<'q, 'd, D, G>,
347        query: &Query<'q, 's, S>,
348    ) -> Result<bool, IndexError<Node>> {
349        let mut complete = true;
350        for value in pool.open() {
351            index.admit(query)?;
352            if value.cause.is_some() {
353                complete = false;
354            }
355        }
356        for value in &self.values[..=revision] {
357            index.admit(query)?;
358            if !value.all_values_readable() || !value.all_vri_material_in_dss() {
359                complete = false;
360            }
361        }
362        Ok(complete)
363    }
364
365    fn generation_covered<'q, 'd, S: RangeSource<Document = D, Generation = G>>(
366        &self,
367        revision: usize,
368        generation_revision: Option<usize>,
369        index: &UnifiedIndex<'q, 'd, D, G>,
370        query: &Query<'q, 's, S>,
371    ) -> Result<bool, IndexError<Node>> {
372        let mut complete = true;
373        // EN319142-1 §5.4.2.3(c): absent TU/TS leaves a subsequent-DTS
374        // obligation. An earlier whole-file timestamp cannot satisfy it.
375        // https://www.etsi.org/deliver/etsi_en/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf
376        for validation in &self.owner.values[..=revision] {
377            for vri in validation.vri() {
378                index.admit(query)?;
379                if validation.revision == 0
380                    || matches!(
381                        vri.generation_time(),
382                        super::GenerationTime::DocumentTimestampRequired
383                    ) && generation_revision.is_none_or(|covered| covered < validation.revision)
384                {
385                    complete = false;
386                }
387            }
388        }
389        Ok(complete)
390    }
391
392    /// Transfer the original complete inner index, preserving every embedded
393    /// certificate and original occurrence, then append all historical DSS.
394    ///
395    /// # Panics
396    /// Panics if a known parsed timestamp no longer has its retained original contents.
397    #[expect(
398        clippy::result_large_err,
399        reason = "indexing retains exact inner and enclosing refusal contexts; resource failure must not require another allocation"
400    )]
401    pub async fn index<'q, 'm: 'q, 'd, 'tokens: 'q, 'p, S, P>(
402        &'q self,
403        basic: &Basic<'_, '_, '_, 'm, 'd, '_, 's, '_, D, G>,
404        pool: &'q structured_cades::material::Pool<'_, 'm, 'd>,
405        timestamps: &'q crate::timestamp::sources::Models<'tokens, '_, '_, 's, D, G>,
406        provider: &'p P,
407        query: &Query<'q, 's, S>,
408    ) -> Result<Unified<'q, 'd, D, G>, super::vri::SourceError<S::Error, P::Error>>
409    where
410        S: RangeSource<Document = D, Generation = G>,
411        P: structured_pkix::algorithm::digest::AsnDigest<'p>,
412        'd: 'q,
413        'h: 'q,
414    {
415        let signature = self.owner.root;
416        let location = signature.location();
417        let mapping = basic.binding().mapping;
418        let revision = self
419            .selected_revision(basic, pool, timestamps, query)
420            .map_err(HistoryError::Foreign)?;
421        let inner_query = InnerQuery {
422            outer: query,
423            mapping,
424        };
425        let original = pool
426            .index(&inner_query)
427            .map_err(|cause| HistoryError::CmsIndex {
428                enclosure: mapping.carrier,
429                cause,
430            })?;
431        let mut ordinal = 0;
432        let mut index = original
433            .into_projected(ISSUER, mapping.root, query, |occurrence| {
434                if occurrence.location().snapshot != mapping.cms {
435                    return Err(Error {
436                        kind: ErrorKind::Foreign,
437                        location,
438                    });
439                }
440                let source = match occurrence.source() {
441                    ArtifactSource::Material(_) => ArtifactSource::Material(ordinal),
442                    source => source,
443                };
444                ordinal += 1;
445                Ok((
446                    source,
447                    Location {
448                        snapshot: mapping.snapshot,
449                        node: mapping.node(occurrence.location().node),
450                    },
451                    Placement::Cms(occurrence.placement()),
452                ))
453            })
454            .map_err(HistoryError::Projection)?;
455        for parsed in &self.values[..=revision] {
456            parsed
457                .append(&mut index, &mut ordinal, query)
458                .map_err(HistoryError::Index)?;
459        }
460        // EN319142-1 §6.3 Table1 requires a DSS at B-LT/B-LTA. Certificate
461        // duplication is discouraged, so that DSS may be empty when every
462        // used value is already incorporated in the actual CMS/token inputs.
463        // https://www.etsi.org/deliver/etsi_en/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf
464        let mut complete = self.owner.values[revision].is_present();
465        if !self
466            .source_complete(pool, revision, &index, query)
467            .map_err(HistoryError::Index)?
468        {
469            complete = false;
470        }
471        let prefix = pool.prepared().timestamps().len();
472        let tokens = Self::append_document_tokens(timestamps, mapping, prefix, &mut index, query)
473            .map_err(HistoryError::from)?;
474        let generation_revision = tokens.generation_revision;
475        if !tokens.complete {
476            complete = false;
477        }
478        if !self
479            .generation_covered(revision, generation_revision, &index, query)
480            .map_err(HistoryError::Index)?
481        {
482            complete = false;
483        }
484        if !self
485            .append_vri_tokens(timestamps, mapping, prefix, &mut index, query)
486            .map_err(HistoryError::from)?
487        {
488            complete = false;
489        }
490        let associations = super::vri::sources::associate(
491            self,
492            basic.binding().prepared().container,
493            pool,
494            timestamps,
495            provider,
496            query,
497            revision,
498        )
499        .await?;
500        if !associations.all_requirements_met() {
501            complete = false;
502        }
503        Ok(Unified {
504            session: signature.owner().value.session,
505            index,
506            root: mapping.root,
507            complete,
508            required: timestamps.requirements(),
509            revision,
510            associations,
511        })
512    }
513}