Skip to main content

structured_pades/
container.rs

1//! The actual decoded Contents owner and its carried DER CMS source context.
2
3use crate::document::Document;
4use crate::error::{Error, ErrorKind, ReadFailure, Syntax};
5use crate::model::admit;
6use crate::range::RangeSource;
7use crate::signature::Signature;
8use crate::source::Location;
9use structured_asn1::{
10    Reader,
11    memory::{Memory, OwnedVec, Work},
12};
13use structured_cms::{ContentInfo, SignedData};
14
15/// An ASN.1 or CMS diagnostic has coordinates in the decoded Contents value.
16/// Its physical enclosing string is retained separately, never added as if
17/// escape decoding were a one-to-one file-offset translation.
18#[derive(Debug)]
19pub enum ErrorOf<E> {
20    /// Original PDF acquisition/processing failure.
21    Pdf(ReadFailure<E>),
22    /// Original certificate/status cause with its actual PDF enclosure.
23    Pkix {
24        /// Original encoded signature or timestamp enclosure.
25        enclosure: Location,
26        /// Unchanged certificate/status processing cause.
27        cause: structured_pkix::Error,
28    },
29    /// Original ASN.1 decoding cause and honest physical enclosure.
30    Encoding {
31        /// Original encoded PDF string enclosure.
32        enclosure: Location,
33        /// Unchanged ASN.1 cause in decoded Contents coordinates.
34        cause: structured_asn1::Error,
35    },
36    /// Original CMS cause and honest physical enclosure.
37    Cms {
38        /// Original encoded PDF string enclosure.
39        enclosure: Location,
40        /// Unchanged CMS cause in decoded Contents coordinates.
41        cause: structured_cms::Error,
42    },
43}
44impl<E> ErrorOf<E> {
45    /// Terminal processing cannot be retained as an unreadable signature slot.
46    #[must_use]
47    pub fn is_terminal(&self) -> bool {
48        match self {
49            Self::Pdf(cause) => cause.is_terminal(),
50            Self::Encoding { cause, .. } => cause.is_resource(),
51            Self::Cms { cause, .. } => cause.is_terminal(),
52            Self::Pkix { cause, .. } => {
53                cause.is_resource() || cause.code() == structured_pkix::ErrorCode::ForeignReader
54            }
55        }
56    }
57}
58
59/// One original signature's decoded Contents, including the padding required
60/// by VRI naming. This owner keeps the producing PDF Session and signature
61/// membership; it cannot be constructed from replacement caller CMS bytes.
62pub struct Container<'a, 's, D, G> {
63    signature: Signature<'a, 's, D, G>,
64    bytes: OwnedVec<u8, Memory>,
65    der_length: usize,
66    session: &'s crate::Session,
67}
68
69impl<'a, 's, D: Copy + Eq, G: Copy + Eq> Container<'a, 's, D, G> {
70    pub(crate) fn require(
71        &self,
72        message: &SignedData<'_>,
73        parent: &Reader<'_>,
74    ) -> crate::Result<()> {
75        let location = self.signature.contents();
76        self.session
77            .query(&parent.execution())
78            .charge_work(1)
79            .map_err(|cause| Error {
80                kind: ErrorKind::Resource(cause),
81                location,
82            })?;
83        let span = message.span();
84        let start = usize::try_from(span.start).map_err(|_| Error {
85            kind: ErrorKind::Foreign,
86            location,
87        })?;
88        let end = start.checked_add(message.as_bytes().len()).ok_or(Error {
89            kind: ErrorKind::Foreign,
90            location,
91        })?;
92        if !self.session.owns(parent)
93            || self
94                .der()
95                .get(start..end)
96                .is_none_or(|bytes| !core::ptr::eq(bytes, message.as_bytes()))
97        {
98            return Err(Error {
99                kind: ErrorKind::Foreign,
100                location,
101            });
102        }
103        Ok(())
104    }
105    /// The exact original dictionary whose Contents this owner decoded.
106    #[must_use]
107    pub fn signature(&self) -> Signature<'a, 's, D, G> {
108        self.signature
109    }
110
111    /// Whole decoded Contents for VRI naming, including every padding octet.
112    #[must_use]
113    pub fn contents(&self) -> &[u8] {
114        &self.bytes
115    }
116
117    /// One exact DER `ContentInfo` extent, excluding checked zero padding.
118    #[must_use]
119    pub fn der(&self) -> &[u8] {
120        &self.bytes[..self.der_length]
121    }
122
123    /// Opens the original RFC3161 envelope without manufacturing another CMS
124    /// input or dropping the PDF producing session's depth/work boundary.
125    #[expect(
126        clippy::result_large_err,
127        reason = "an unchanged timestamp grammar error retains its actual PDF enclosure without allocating an error envelope"
128    )]
129    pub fn read_timestamp<'q, W: Work + 'q>(
130        &'q self,
131        operation: &W,
132    ) -> Result<
133        (structured_cms::timestamp::Token<'q>, Reader<'q>),
134        ErrorOf<core::convert::Infallible>,
135    > {
136        let enclosure = self.signature.contents();
137        let mut reader = self
138            .session
139            .carried(self.der(), operation)
140            .map_err(|cause| ErrorOf::Encoding { enclosure, cause })?;
141        let token = structured_cms::timestamp::Token::read_from(&mut reader)
142            .map_err(|cause| ErrorOf::Cms { enclosure, cause })?;
143        self.require(token.message(), &reader)
144            .map_err(|cause| ErrorOf::Pdf(cause.into()))?;
145        Ok((token, reader))
146    }
147
148    /// Opens the same carried document under a current operation. Root ASN.1
149    /// depth is zero because a PDF string is not an ASN.1 enclosing element.
150    #[expect(
151        clippy::result_large_err,
152        reason = "the original CMS diagnostic and encoded PDF enclosure must survive the scoped reading without a fallible error allocation"
153    )]
154    pub fn read<'q, W: Work + 'q>(
155        &'q self,
156        operation: &W,
157    ) -> Result<(SignedData<'q>, Reader<'q>), ErrorOf<core::convert::Infallible>> {
158        let enclosure = self.signature.contents();
159        let mut root = self
160            .session
161            .carried(self.der(), operation)
162            .map_err(|cause| ErrorOf::Encoding { enclosure, cause })?;
163        let (content, parent) =
164            ContentInfo::read(&mut root).map_err(|cause| ErrorOf::Cms { enclosure, cause })?;
165        let execution = parent.execution();
166        execution
167            .charge_work(content.kind().as_bytes().len() as u64)
168            .map_err(|cause| {
169                ErrorOf::Pdf(ReadFailure::Format(Error {
170                    kind: ErrorKind::Resource(cause),
171                    location: enclosure,
172                }))
173            })?;
174        if content.kind() != structured_cms::oid::SIGNED_DATA {
175            return Err(ErrorOf::Pdf(ReadFailure::Format(Error {
176                kind: ErrorKind::Syntax(Syntax::Container),
177                location: enclosure,
178            })));
179        }
180        let message = SignedData::read_element(&parent, content.content())
181            .map_err(|cause| ErrorOf::Cms { enclosure, cause })?;
182        Ok((message, parent))
183    }
184}
185
186impl<'s, S: RangeSource> Document<'s, S> {
187    /// Decodes the actual selected Contents and isolates its DER extent before
188    /// any crypto callback. EN319142-1 ยง5.2.1 requires a single DER `SignedData`;
189    /// unused signature capacity consists only of zero padding.
190    /// <https://www.etsi.org/deliver/etsi_EN/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf>
191    #[expect(
192        clippy::result_large_err,
193        reason = "the original source/CMS refusal retains its original coordinate system rather than being boxed or flattened"
194    )]
195    pub async fn container<'a, W: Work>(
196        &self,
197        signature: Signature<'a, 's, S::Document, S::Generation>,
198        operation: &W,
199    ) -> Result<Container<'a, 's, S::Document, S::Generation>, ErrorOf<S::Error>> {
200        let bytes = self
201            .contents(signature, operation)
202            .await
203            .map_err(ErrorOf::Pdf)?;
204        let enclosure = signature.contents();
205        let work = self.session.query(operation);
206        let mut reader = self
207            .session
208            .carried(&bytes, &work)
209            .map_err(|cause| ErrorOf::Encoding { enclosure, cause })?;
210        let element = reader
211            .next_element()
212            .map_err(|cause| ErrorOf::Encoding { enclosure, cause })?
213            .ok_or_else(|| {
214                ErrorOf::Pdf(ReadFailure::Format(Error {
215                    kind: ErrorKind::Syntax(Syntax::String),
216                    location: enclosure,
217                }))
218            })?;
219        let der_length = element.as_bytes().len();
220        admit(&work, enclosure, (bytes.len() - der_length) as u64)
221            .map_err(|cause| ErrorOf::Pdf(cause.into()))?;
222        if bytes[der_length..].iter().any(|&byte| byte != 0) {
223            return Err(ErrorOf::Pdf(
224                Error {
225                    kind: ErrorKind::Syntax(Syntax::String),
226                    location: enclosure,
227                }
228                .into(),
229            ));
230        }
231        drop(reader);
232        Ok(Container {
233            signature,
234            bytes,
235            der_length,
236            session: self.session,
237        })
238    }
239}