Skip to main content

structured_pades/timestamp/
sources.rs

1//! Complete original DTS sources and once-read carried validation material.
2
3use super::{Node, Requirements};
4use crate::{
5    container::{Container, ErrorOf},
6    document::Document,
7    error::{Error, ErrorKind},
8    model::admit,
9    range::RangeSource,
10    source::Location,
11};
12use structured_asn1::memory::{Memory, OwnedVec, Work};
13use structured_cms::{signed::revocation, timestamp::Token};
14
15/// Completed original syntax/unavailable-input finding. Processing refusals
16/// are returned by the operation rather than retained as malformed material.
17#[derive(Debug)]
18pub enum Fault {
19    /// Original PDF dictionary or value failure.
20    Pdf(Error),
21    /// Original carried ASN.1 coordinates within this physical enclosure.
22    Encoding {
23        /// Honest PDF enclosure of the decoded Contents.
24        enclosure: Location,
25        /// Original located parser cause.
26        cause: structured_asn1::Error,
27    },
28    /// Original carried CMS coordinates within this physical enclosure.
29    Cms {
30        /// Honest PDF enclosure of the decoded Contents.
31        enclosure: Location,
32        /// Original located CMS cause.
33        cause: structured_cms::Error,
34    },
35}
36
37/// One actual source occurrence, including unsuccessful/unreadable members.
38pub enum Slot<'a, 's, D, G> {
39    /// Original decoded Contents and its immutable source association.
40    Value(Container<'a, 's, D, G>),
41    /// Original source-shaped occurrence could not provide such a value.
42    Unreadable(Fault),
43}
44/// Complete source set derived from the same original timestamp obligations.
45pub struct Sources<'r, 'a, 's, D, G> {
46    pub(super) required: &'r Requirements<'a, 's, D, G>,
47    pub(super) slots: OwnedVec<Slot<'a, 's, D, G>, Memory>,
48}
49
50/// One original carried revocation and its complete first-read result.
51pub struct Revocation<'d> {
52    pub(crate) choice: usize,
53    element: structured_asn1::Element<'d>,
54    pub(crate) value: Result<revocation::Reading<'d>, revocation::Error>,
55}
56pub(crate) enum RevocationError {
57    Pdf(Error),
58    Cms(structured_cms::Error),
59    Pkix(structured_pkix::Error),
60}
61pub(crate) fn read_revocations<'d>(
62    token: &Token<'d>,
63    parent: &structured_asn1::Reader<'d>,
64    work: &impl Work,
65    location: Location,
66) -> Result<OwnedVec<Revocation<'d>>, RevocationError> {
67    let mut values = work.vector();
68    for (choice, element) in token.message().revocations().iter().enumerate() {
69        admit(work, location, 1).map_err(RevocationError::Pdf)?;
70        let reader = token
71            .message()
72            .revocation_reader_in(parent, choice)
73            .map_err(RevocationError::Cms)?
74            .expect("bounded original choice");
75        let value = revocation::read(&reader, element);
76        if let Err(cause) = &value
77            && cause.is_terminal()
78        {
79            return Err(match value {
80                Err(revocation::Error::Cms(cause)) => RevocationError::Cms(cause),
81                Err(revocation::Error::Pkix(cause)) => RevocationError::Pkix(cause),
82                Ok(_) => unreachable!("failed original reading"),
83            });
84        }
85        values
86            .try_push(Revocation {
87                choice,
88                element: *element,
89                value,
90            })
91            .map_err(|cause| {
92                RevocationError::Pdf(Error {
93                    kind: ErrorKind::Resource(cause),
94                    location,
95                })
96            })?;
97    }
98    Ok(values.into_owned())
99}
100impl<'d> Revocation<'d> {
101    /// Original choice ordinal in this timestamp `SignedData`.
102    #[must_use]
103    pub fn choice(&self) -> usize {
104        self.choice
105    }
106    /// Untouched whole original choice in its carried CMS coordinate domain.
107    #[must_use]
108    pub fn element(&self) -> structured_asn1::Element<'d> {
109        self.element
110    }
111    /// Actual first-read model or the original known-format refusal.
112    pub fn reading(&self) -> Result<&revocation::Reading<'d>, &revocation::Error> {
113        self.value.as_ref()
114    }
115}
116#[expect(
117    clippy::large_enum_variant,
118    reason = "the funded original-order catalogue stores parsed tokens in place without a separate allocation for every successful occurrence"
119)]
120pub(crate) enum Model<'d> {
121    Known {
122        token: Token<'d>,
123        revocations: OwnedVec<Revocation<'d>, Memory>,
124    },
125    Unreadable(Fault),
126    Source,
127}
128/// Original token models borrow retained Contents; their material is parsed
129/// once, independently of timestamp arithmetic, imprint, authority or trust.
130pub struct Models<'v, 'r, 'a, 's, D, G> {
131    pub(crate) sources: &'v Sources<'r, 'a, 's, D, G>,
132    pub(crate) values: OwnedVec<Model<'v>, Memory>,
133}
134
135impl<'s, S: RangeSource> Document<'s, S> {
136    /// Reads ALL original PDF DTS occurrences without success-based filtering.
137    /// Unreadable syntax remains an entry; source/allocator/work refusal aborts
138    /// and releases the entire partial result.
139    ///
140    /// # Panics
141    /// Panics if an internally unreadable source slot loses its retained failure.
142    #[expect(
143        clippy::result_large_err,
144        reason = "original unreadable/source failures retain their encoded PDF enclosure while resource refusal cannot allocate an error box"
145    )]
146    pub async fn timestamp_sources<'r, 'a, W: Work>(
147        &self,
148        required: &'r Requirements<'a, 's, S::Document, S::Generation>,
149        operation: &W,
150    ) -> Result<Sources<'r, 'a, 's, S::Document, S::Generation>, ErrorOf<S::Error>> {
151        let root = required.root;
152        let location = root.location();
153        let work = self.session.query(operation);
154        self.check(&work, location)
155            .map_err(|cause| ErrorOf::Pdf(cause.into()))?;
156        if !core::ptr::eq(root.owner().value.session, self.session)
157            || root.owner().value.stamp != self.stamp
158        {
159            return Err(ErrorOf::Pdf(
160                Error {
161                    kind: ErrorKind::Foreign,
162                    location,
163                }
164                .into(),
165            ));
166        }
167        let mut slots = work.vector();
168        for original in required.document_timestamps() {
169            admit(&work, original.location(), 1).map_err(|cause| ErrorOf::Pdf(cause.into()))?;
170            let value = if let Some(signature) = original.signature() {
171                match self.container(signature, &work).await {
172                    Ok(value) => Slot::Value(value),
173                    Err(ErrorOf::Pdf(crate::error::ReadFailure::Format(cause)))
174                        if matches!(
175                            cause.kind,
176                            ErrorKind::Syntax(_)
177                                | ErrorKind::Ambiguous(_)
178                                | ErrorKind::Unsupported(_)
179                        ) =>
180                    {
181                        Slot::Unreadable(Fault::Pdf(cause))
182                    }
183                    Err(ErrorOf::Encoding { enclosure, cause }) if !cause.is_resource() => {
184                        Slot::Unreadable(Fault::Encoding { enclosure, cause })
185                    }
186                    Err(ErrorOf::Cms { enclosure, cause }) if !cause.is_terminal() => {
187                        Slot::Unreadable(Fault::Cms { enclosure, cause })
188                    }
189                    Err(cause) => return Err(cause),
190                }
191            } else {
192                Slot::Unreadable(Fault::Pdf(
193                    original
194                        .failure()
195                        .expect("unreadable original source")
196                        .cause,
197                ))
198            };
199            slots.try_push(value).map_err(|cause| {
200                ErrorOf::Pdf(
201                    Error {
202                        kind: ErrorKind::Resource(cause),
203                        location: original.location(),
204                    }
205                    .into(),
206                )
207            })?;
208        }
209        self.check(&work, location)
210            .map_err(|cause| ErrorOf::Pdf(cause.into()))?;
211        Ok(Sources {
212            required,
213            slots: slots.into_owned(),
214        })
215    }
216}
217
218impl<'r, 'a, 's, D: Copy + Eq, G: Copy + Eq> Sources<'r, 'a, 's, D, G> {
219    /// Count includes every original unreadable occurrence.
220    #[must_use]
221    pub fn len(&self) -> usize {
222        self.slots.len()
223    }
224    /// Whether the complete source census contained no PDF document timestamps.
225    #[must_use]
226    pub fn is_empty(&self) -> bool {
227        self.slots.is_empty()
228    }
229    /// Actual original source under current/original metadata admission.
230    pub fn get(
231        &self,
232        ordinal: usize,
233        operation: &impl Work,
234    ) -> crate::Result<Option<&Slot<'a, 's, D, G>>> {
235        let work = self.required.root.owner().value.session.query(operation);
236        admit(&work, self.required.root.location(), 1)?;
237        Ok(self.slots.get(ordinal))
238    }
239    /// Reads each envelope and each carried revocation exactly once, retaining
240    /// known malformed/unknown choices without a private format dispatcher.
241    #[expect(
242        clippy::result_large_err,
243        reason = "envelope and revocation parsing preserves original diagnostic storage and PDF coordinates without allocation after failure"
244    )]
245    pub fn parse<'v>(
246        &'v self,
247        operation: &'v impl Work,
248    ) -> Result<Models<'v, 'r, 'a, 's, D, G>, ErrorOf<core::convert::Infallible>> {
249        let work = self.required.root.owner().value.session.query(operation);
250        let location = self.required.root.location();
251        let mut values = work.vector();
252        for slot in &self.slots {
253            admit(&work, location, 1).map_err(|cause| ErrorOf::Pdf(cause.into()))?;
254            let value = match slot {
255                Slot::Unreadable(_) => Model::Source,
256                Slot::Value(container) => match container.read_timestamp(operation) {
257                    Ok((token, parent)) => {
258                        let enclosure = container.signature().contents();
259                        let revocations = read_revocations(&token, &parent, &work, enclosure)
260                            .map_err(|cause| match cause {
261                                RevocationError::Pdf(cause) => ErrorOf::Pdf(cause.into()),
262                                RevocationError::Cms(cause) => ErrorOf::Cms { enclosure, cause },
263                                RevocationError::Pkix(cause) => ErrorOf::Pkix { enclosure, cause },
264                            })?;
265                        Model::Known { token, revocations }
266                    }
267                    Err(ErrorOf::Encoding { enclosure, cause }) if !cause.is_resource() => {
268                        Model::Unreadable(Fault::Encoding { enclosure, cause })
269                    }
270                    Err(ErrorOf::Cms { enclosure, cause }) if !cause.is_terminal() => {
271                        Model::Unreadable(Fault::Cms { enclosure, cause })
272                    }
273                    Err(cause) => return Err(cause),
274                },
275            };
276            values.try_push(value).map_err(|cause| {
277                ErrorOf::Pdf(
278                    Error {
279                        kind: ErrorKind::Resource(cause),
280                        location,
281                    }
282                    .into(),
283                )
284            })?;
285        }
286        Ok(Models {
287            sources: self,
288            values: values.into_owned(),
289        })
290    }
291}
292
293impl<'d, D: Copy + Eq, G: Copy + Eq> Models<'d, '_, '_, '_, D, G> {
294    /// Reopens the already parsed envelope's original source context under
295    /// this operation. It performs no token or certificate reparse.
296    #[expect(
297        clippy::result_large_err,
298        reason = "reopened scoped readers propagate the same typed PDF, ASN.1 and CMS causes as the retained original source"
299    )]
300    pub fn reader<'q, W: Work + 'q>(
301        &'q self,
302        ordinal: usize,
303        operation: &W,
304    ) -> Result<Option<structured_asn1::Reader<'q>>, ErrorOf<core::convert::Infallible>> {
305        let root = self.sources.required.root;
306        let work = root.owner().value.session.query(operation);
307        admit(&work, root.location(), 1).map_err(|cause| ErrorOf::Pdf(cause.into()))?;
308        if !matches!(self.values.get(ordinal), Some(Model::Known { .. })) {
309            return Ok(None);
310        }
311        let Slot::Value(container) = &self.sources.slots[ordinal] else {
312            unreachable!("a parsed token has an original Contents owner")
313        };
314        root.owner()
315            .value
316            .session
317            .carried(container.der(), operation)
318            .map(Some)
319            .map_err(|cause| ErrorOf::Encoding {
320                enclosure: container.signature().contents(),
321                cause,
322            })
323    }
324    /// Visits every carried choice with current/original admission before
325    /// access. The first refusal is returned once and permanently fuses it.
326    pub fn revocations<'q>(
327        &'q self,
328        ordinal: usize,
329        operation: &'q impl Work,
330    ) -> crate::Result<Option<impl Iterator<Item = crate::Result<&'q Revocation<'d>>> + 'q>> {
331        let work = self
332            .sources
333            .required
334            .root
335            .owner()
336            .value
337            .session
338            .query(operation);
339        let location = self.sources.required.root.location();
340        admit(&work, location, 1)?;
341        let Some(Model::Known { revocations, .. }) = self.values.get(ordinal) else {
342            return Ok(None);
343        };
344        let mut next = if revocations.is_empty() {
345            None
346        } else {
347            Some(0)
348        };
349        Ok(Some(core::iter::from_fn(move || {
350            let ordinal = next.take()?;
351            if let Err(cause) = admit(&work, location, 1) {
352                return Some(Err(cause));
353            }
354            let value = &revocations[ordinal];
355            if ordinal + 1 < revocations.len() {
356                next = Some(ordinal + 1);
357            }
358            Some(Ok(value))
359        })))
360    }
361    /// Original parsed token only; absence retains an explicit failed source.
362    pub fn token(
363        &self,
364        ordinal: usize,
365        operation: &impl Work,
366    ) -> crate::Result<Option<&Token<'_>>> {
367        let work = self
368            .sources
369            .required
370            .root
371            .owner()
372            .value
373            .session
374            .query(operation);
375        admit(&work, self.sources.required.root.location(), 1)?;
376        Ok(match self.values.get(ordinal) {
377            Some(Model::Known { token, .. }) => Some(token),
378            _ => None,
379        })
380    }
381    /// Original source/parser failure for this exact occurrence, when present.
382    pub fn failure(&self, ordinal: usize, operation: &impl Work) -> crate::Result<Option<&Fault>> {
383        let work = self
384            .sources
385            .required
386            .root
387            .owner()
388            .value
389            .session
390            .query(operation);
391        admit(&work, self.sources.required.root.location(), 1)?;
392        Ok(match self.values.get(ordinal) {
393            Some(Model::Unreadable(cause)) => Some(cause),
394            Some(Model::Source) => match &self.sources.slots[ordinal] {
395                Slot::Unreadable(cause) => Some(cause),
396                Slot::Value(_) => unreachable!("unreadable source slot"),
397            },
398            Some(Model::Known { .. }) | None => None,
399        })
400    }
401    pub(crate) fn root(&self) -> Node {
402        Node::of(self.sources.required.root.location())
403    }
404    /// Selected historical revision, or the full current source window.
405    #[must_use]
406    pub fn revision(&self) -> Option<usize> {
407        self.sources.required.revision()
408    }
409    pub(crate) fn vri_timestamps(&self) -> &[super::VriTimestamp] {
410        self.sources.required.vri_timestamps()
411    }
412    pub(crate) fn total_pdf(&self) -> usize {
413        self.sources.required.total_pdf()
414    }
415    pub(crate) fn snapshot(&self) -> structured_ltv::source::Snapshot<D, G> {
416        self.sources.required.root.owner().value.stamp.snapshot
417    }
418    /// Actual original PDF dictionary location at this source ordinal.
419    #[must_use]
420    pub fn location(&self, ordinal: usize) -> Option<Location> {
421        self.sources
422            .required
423            .document_timestamps()
424            .get(ordinal)
425            .map(super::DocumentTimestamp::location)
426    }
427    pub(crate) fn contents(&self, ordinal: usize) -> Option<Location> {
428        match &self.sources.slots[ordinal] {
429            Slot::Value(value) => Some(value.signature().contents()),
430            Slot::Unreadable(_) => None,
431        }
432    }
433    /// Same complete original root/counter/PDF obligations used for this pool.
434    #[must_use]
435    pub fn requirements(
436        &self,
437    ) -> &structured_ltv::timestamp::TimestampRequirements<Node, D, G, super::Pades> {
438        self.sources.required.all()
439    }
440}