Skip to main content

structured_xades/
binary.rs

1//! Scoped ASN.1 carried by original XML nodes, without retaining decoded buffers.
2
3use crate::xml::{Access, AccessError, AccessResult, Name, Snapshot, Xml};
4use structured_asn1::{
5    Encoding, Parser, Reader,
6    memory::{OwnedVec, Work},
7};
8use structured_cms::timestamp::Token;
9
10/// XML-owned lexical decoding of an encapsulated binary value.
11///
12/// Common processing selects the original `Encoding` attribute/default; the
13/// owner validates the XML lexical form,
14/// including children and whitespace, before invoking the visitor. It admits
15/// traversal, decoded capacity and copies before doing that work. Existing XML
16/// base64 processing must be reused rather than reparsing serialized XML.
17///
18/// The owner supplies the actual decoded buffer and its producing Parser inside
19/// the scope. Common processing constructs the Reader with the selected encoding
20/// through [`Parser::read_query_in`]. Both original and current operation
21/// ceilings are therefore preserved by construction, rather than trusting an
22/// adapter to return a Reader with the correct budget. ASN.1 depth is not XML depth.
23///
24/// Decoded offsets identify this binary source, never physical XML bytes. The
25/// original XML node and generation accompany every returned common error.
26pub trait BinaryXml: Xml {
27    /// Visits decoded octets that are not themselves an ASN.1 value, such as a
28    /// `CertDigest` `DigestValue`. No ASN.1 depth gate applies to these octets.
29    /// Both entry points reuse one owner lexical/decoding kernel and admit its
30    /// complete work/capacity before scanning or decoding. No implicit I/O.
31    fn with_octets<R, C, W: Work>(
32        &self,
33        snapshot: Snapshot<Self::Document, Self::Generation>,
34        node: Self::Id,
35        execution: &W,
36        visitor: impl for<'source> FnOnce(&'source [u8]) -> Result<R, C>,
37    ) -> Result<Result<R, C>, Self::Error>;
38
39    /// Visits the actual decoded source under the current operation.
40    ///
41    /// The result cannot borrow the temporary buffer or parser. Independent
42    /// retained results must retain their allocation charges after scope exit.
43    /// The owner must not swallow a visitor refusal or call the visitor twice.
44    fn with_binary<R, C>(
45        &self,
46        snapshot: Snapshot<Self::Document, Self::Generation>,
47        node: Self::Id,
48        operation: &Reader<'_>,
49        visitor: impl for<'source> FnOnce(Decoded<'source>) -> Result<R, C>,
50    ) -> Result<Result<R, C>, Self::Error>;
51}
52
53/// Independent decoded ownership for simultaneous certificate/status checking.
54///
55/// The same owner lexical/decode kernel serves scoped and owned entry points.
56/// Decode directly into admitted caller storage, then move its ownership; do
57/// not decode into an ordinary Vec and copy it a second time. Every allocation,
58/// initialization, lexical pass and decode is admitted before execution. A moved
59/// existing buffer retains its original storage account; new processing work is
60/// charged to the current operation. Returning owned data does not qualify an
61/// implementation's transitive accounting without testing its actual kernel.
62pub trait OwnedBinaryXml: BinaryXml {
63    /// Returns the original decoded octets with independent retained capacity.
64    fn decode_owned<W: Work>(
65        &self,
66        snapshot: Snapshot<Self::Document, Self::Generation>,
67        node: Self::Id,
68        execution: &W,
69    ) -> Result<OwnedVec<u8>, Self::Error>;
70
71    /// Moves an ASN.1 source together with its producing session. A cached
72    /// source keeps its existing cumulative ceilings; the adapter must not
73    /// replace that session merely to start a new query with a clean budget.
74    /// The current operation funds decoding and subsequent query execution.
75    fn decode_binary_owned(
76        &self,
77        snapshot: Snapshot<Self::Document, Self::Generation>,
78        node: Self::Id,
79        operation: &Reader<'_>,
80    ) -> Result<OwnedDecoded, Self::Error>;
81}
82
83/// Independently owned decoded bytes and their actual producing session.
84///
85/// This has no internal borrowed pointers. Move sources into their final
86/// accounted catalogue before borrowing parsed certificates or status values.
87/// A later borrow cannot outlive the source or coexist with moving it. Storage
88/// remains charged to the bytes' original account; a new query pays current work.
89///
90/// ```compile_fail
91/// use structured_asn1::{Encoding, Reader};
92/// use structured_xades::binary::OwnedDecoded;
93/// fn escape<'a>(source: OwnedDecoded, operation: &Reader<'a>) -> Reader<'a> {
94///     source.reader_in(operation, Encoding::Der).unwrap()
95/// }
96/// ```
97#[derive(Debug)]
98pub struct OwnedDecoded {
99    /// Actual producing session, including work already consumed on this source.
100    pub parser: Parser,
101    /// Original decoded allocation, transferred without re-encoding or copying.
102    pub bytes: OwnedVec<u8>,
103    /// Offset of this window in the decoded document, never a physical XML offset.
104    pub base: u64,
105    /// Actual source-reader ASN.1 depth, independent of XML depth; fresh roots use zero.
106    pub depth: u32,
107}
108
109impl OwnedDecoded {
110    /// Borrows the source without allocation, account cloning or a new Parser.
111    #[must_use]
112    pub fn view(&self) -> Decoded<'_> {
113        Decoded {
114            parser: &self.parser,
115            bytes: &self.bytes,
116            base: self.base,
117            depth: self.depth,
118        }
119    }
120
121    /// Opens the original window under both source and current-operation limits.
122    /// The material schema selects its encoding; this never infers a permissive
123    /// codec from malformed bytes. Existing source work is not reset or credited.
124    pub fn reader_in<'source>(
125        &'source self,
126        operation: &Reader<'source>,
127        encoding: Encoding,
128    ) -> structured_asn1::Result<Reader<'source>> {
129        self.parser
130            .read_query_in(operation, &self.bytes, self.base, encoding, self.depth)
131    }
132}
133
134/// The actual scoped decoded source and its original ASN.1 provenance.
135/// These borrows cannot escape the higher-ranked visitor. A fresh decoded
136/// document has base zero and reader depth zero; neither is a physical XML offset.
137#[derive(Debug, Clone, Copy)]
138pub struct Decoded<'source> {
139    /// Producing session for this actual source, with explicit owner limits.
140    pub parser: &'source Parser,
141    /// Original decoded source window, without re-encoding or an extra copy.
142    pub bytes: &'source [u8],
143    /// Source-window offset in this decoded document.
144    pub base: u64,
145    /// Actual source-reader ASN.1 depth, independent of XML depth; fresh roots use zero.
146    pub depth: u32,
147}
148
149/// Original XML enclosure of a distinct decoded binary source.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct Location<N, D, G> {
152    /// Exact original XML document and generation.
153    pub snapshot: Snapshot<D, G>,
154    /// Original encapsulated-value element.
155    pub node: N,
156}
157
158/// A retained result alongside its original XML/binary-source enclosure.
159/// Any ASN.1 offsets inside the value remain decoded-source coordinates.
160#[derive(Debug)]
161pub struct Located<T, N, D, G> {
162    /// Original document generation and encapsulated-value node.
163    pub location: Location<N, D, G>,
164    /// Retained scoped result; it cannot borrow the temporary decoded source.
165    pub value: T,
166}
167
168/// A result carrying this XML owner's stable source identity types.
169pub type LocatedValue<T, X> =
170    Located<T, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
171
172/// Moves an owned decoded source out together with its exact XML enclosure.
173///
174/// This performs no ASN.1 parse or codec substitution. The caller's material
175/// schema must still select and validate its actual PKI format. No buffer copy,
176/// allocation or reference-count increment is introduced by this wrapper.
177pub fn owned<X: OwnedBinaryXml, W: Work>(
178    xml: &X,
179    location: Location<X::Id, X::Document, X::Generation>,
180    execution: &W,
181) -> AccessResult<LocatedValue<OwnedVec<u8>, X>, X> {
182    let value = Access::new(xml, location.snapshot, execution)
183        .query(location.node, |xml, snapshot, work| {
184            xml.decode_owned(snapshot, location.node, work)
185        })?;
186    Ok(Located { location, value })
187}
188
189/// Retains a source for simultaneous material checking without a second decode.
190/// Original XML identity accompanies every source in a multi-source catalogue.
191/// Like [`owned`], this does not itself establish the material's PKI schema,
192/// authenticity, completeness or trust.
193pub fn owned_source<X: OwnedBinaryXml>(
194    xml: &X,
195    location: Location<X::Id, X::Document, X::Generation>,
196    operation: &Reader<'_>,
197) -> AccessResult<LocatedValue<OwnedDecoded, X>, X> {
198    let execution = operation.execution();
199    let value = Access::new(xml, location.snapshot, &execution)
200        .query(location.node, |xml, snapshot, _| {
201            xml.decode_binary_owned(snapshot, location.node, operation)
202        })?;
203    Ok(Located { location, value })
204}
205
206/// Refusal preserving both the XML enclosure and the unchanged inner cause.
207#[derive(Debug)]
208pub enum Error<E, C, N, D, G> {
209    /// The original Encoding attribute does not identify a specified PKI codec.
210    UnknownEncoding {
211        /// Original encapsulated-value element.
212        location: Location<N, D, G>,
213    },
214    /// A specified codec has no decoder in this ASN.1 processing capability.
215    /// This is unsupported processing, not malformed XML or a digest mismatch.
216    UnsupportedEncoding {
217        /// Original encapsulated-value element.
218        location: Location<N, D, G>,
219        /// Original specified codec.
220        encoding: PkiEncoding,
221    },
222    /// Common Reader construction refused before the inspection visitor.
223    ReaderCreation {
224        /// Original encapsulated-value element.
225        location: Location<N, D, G>,
226        /// Original ASN.1 processing/resource error in decoded-source coordinates.
227        cause: structured_asn1::Error,
228    },
229    /// XML access, decoding, generation or resource refusal.
230    Access {
231        /// Original XML enclosure, not a fabricated binary byte range.
232        location: Location<N, D, G>,
233        /// Original access/owner cause, including pre-visitor resource errors.
234        cause: AccessError<E, N>,
235    },
236    /// Scoped ASN.1 inspection/check callback refused.
237    Callback {
238        /// Inner offsets refer to the decoded source at this XML node.
239        location: Location<N, D, G>,
240        /// Unchanged cause; retained maps keep their own inner source contexts.
241        cause: C,
242    },
243}
244
245impl<E, C, N, D, G> Error<E, C, N, D, G> {
246    // Flatten nested scoped operations without relabelling XML locations or
247    // allocating/cloning while already handling an opaque callback refusal.
248    pub(crate) fn map_callback<T>(self, map: impl FnOnce(C) -> T) -> Error<E, T, N, D, G> {
249        match self {
250            Self::UnknownEncoding { location } => Error::UnknownEncoding { location },
251            Self::UnsupportedEncoding { location, encoding } => {
252                Error::UnsupportedEncoding { location, encoding }
253            }
254            Self::ReaderCreation { location, cause } => Error::ReaderCreation { location, cause },
255            Self::Access { location, cause } => Error::Access { location, cause },
256            Self::Callback { location, cause } => Error::Callback {
257                location,
258                cause: map(cause),
259            },
260        }
261    }
262}
263
264/// Encoding identifiers defined by EN319132-1 §5.1.3; absence specifically means DER.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum PkiEncoding {
267    /// ASN.1 distinguished encoding.
268    Der,
269    /// ASN.1 basic encoding.
270    Ber,
271    /// ASN.1 canonical encoding, distinct from merely accepting BER syntax.
272    Cer,
273    /// ASN.1 packed encoding.
274    Per,
275    /// ASN.1 XML encoding, distinct from the outer base64 XML carrier.
276    Xer,
277}
278
279fn pki_encoding<X: Xml, W: Work>(
280    access: &Access<'_, X, W>,
281    node: X::Id,
282) -> core::result::Result<Option<PkiEncoding>, AccessError<X::Error, X::Id>> {
283    access
284        .attribute(
285            node,
286            Name {
287                namespace: "",
288                local: "Encoding",
289            },
290            |value| {
291                // EN319132-1 §5.1.3 specifies DER for the absent attribute; this is
292                // a wire default, never an implicit permissive parser/policy fallback.
293                // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
294                let Some(value) = value else {
295                    return Ok(Some(PkiEncoding::Der));
296                };
297                let value = access.trim_xml_whitespace(value)?;
298                for (uri, encoding) in [
299                    ("http://uri.etsi.org/01903/v1.2.2#DER", PkiEncoding::Der),
300                    ("http://uri.etsi.org/01903/v1.2.2#BER", PkiEncoding::Ber),
301                    ("http://uri.etsi.org/01903/v1.2.2#CER", PkiEncoding::Cer),
302                    ("http://uri.etsi.org/01903/v1.2.2#PER", PkiEncoding::Per),
303                    ("http://uri.etsi.org/01903/v1.2.2#XER", PkiEncoding::Xer),
304                ] {
305                    if access.equal(value, uri)? {
306                        return Ok(Some(encoding));
307                    }
308                }
309                Ok(None)
310            },
311        )?
312        .map_err(|cause| AccessError::Resource { node, cause })
313}
314
315/// Scoped binary result using this XML owner's source identity types.
316pub type BinaryResult<T, X, C> = core::result::Result<
317    T,
318    Error<<X as Xml>::Error, C, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
319>;
320
321/// Visits an original decoded octet value without inventing an ASN.1 enclosure.
322/// The result cannot borrow the temporary decoded buffer.
323///
324/// ```compile_fail
325/// use structured_asn1::memory::Work;
326/// use structured_xades::binary::{self, BinaryResult, BinaryXml, Location};
327/// fn escape<'a, X: BinaryXml, W: Work>(xml: &X,
328///     location: Location<X::Id, X::Document, X::Generation>, work: &W)
329///     -> BinaryResult<&'a [u8], X, ()>
330/// {
331///     binary::with_octets(xml, location, work, |bytes| Ok(bytes))
332/// }
333/// ```
334pub fn with_octets<X: BinaryXml, W: Work, R, C>(
335    xml: &X,
336    location: Location<X::Id, X::Document, X::Generation>,
337    execution: &W,
338    visitor: impl for<'source> FnOnce(&'source [u8]) -> Result<R, C>,
339) -> BinaryResult<R, X, C> {
340    Access::new(xml, location.snapshot, execution)
341        .query(location.node, |xml, snapshot, work| {
342            xml.with_octets(snapshot, location.node, work, visitor)
343        })
344        .map_err(|cause| Error::Access { location, cause })?
345        .map_err(|cause| Error::Callback { location, cause })
346}
347
348/// Visits one original decoded source with snapshot checks around the callback.
349pub fn with_reader<X: BinaryXml, R, C>(
350    xml: &X,
351    location: Location<X::Id, X::Document, X::Generation>,
352    operation: &Reader<'_>,
353    visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> core::result::Result<R, C>,
354) -> BinaryResult<R, X, C> {
355    let execution = operation.execution();
356    let access = Access::new(xml, location.snapshot, &execution);
357    let encoding = selected_encoding(&access, location)
358        .map_err(|error| error.map_callback(|never| match never {}))?;
359    with_reader_as(xml, location, operation, encoding, visitor)
360}
361
362pub(crate) fn selected_encoding<X: Xml, W: Work>(
363    access: &Access<'_, X, W>,
364    location: Location<X::Id, X::Document, X::Generation>,
365) -> BinaryResult<Encoding, X, core::convert::Infallible> {
366    let specified = pki_encoding(access, location.node)
367        .map_err(|cause| Error::Access { location, cause })?
368        .ok_or(Error::UnknownEncoding { location })?;
369    let encoding = match specified {
370        PkiEncoding::Der => Encoding::Der,
371        PkiEncoding::Ber => Encoding::Ber,
372        other => {
373            return Err(Error::UnsupportedEncoding {
374                location,
375                encoding: other,
376            });
377        }
378    };
379    Ok(encoding)
380}
381
382/// Visits a schema-fixed DER value, such as `IssuerSerialV2` (§5.2.2).
383/// An unrelated XML attribute cannot make this ASN.1 value permissive BER.
384/// Outer XML schema/attribute validation remains a separate obligation.
385pub fn with_der_reader<X: BinaryXml, R, C>(
386    xml: &X,
387    location: Location<X::Id, X::Document, X::Generation>,
388    operation: &Reader<'_>,
389    visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
390) -> BinaryResult<R, X, C> {
391    with_reader_as(xml, location, operation, Encoding::Der, visitor)
392}
393
394fn with_reader_as<X: BinaryXml, R, C>(
395    xml: &X,
396    location: Location<X::Id, X::Document, X::Generation>,
397    operation: &Reader<'_>,
398    encoding: Encoding,
399    visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
400) -> BinaryResult<R, X, C> {
401    let execution = operation.execution();
402    let result = Access::new(xml, location.snapshot, &execution)
403        .query(location.node, |xml, snapshot, _| {
404            xml.with_binary(snapshot, location.node, operation, |source| {
405                let mut reader = source
406                    .parser
407                    .read_query_in(operation, source.bytes, source.base, encoding, source.depth)
408                    .map_err(ScopedError::Processing)?;
409                visitor(&mut reader).map_err(ScopedError::Callback)
410            })
411        })
412        .map_err(|cause| Error::Access { location, cause })?;
413    result.map_err(|cause| match cause {
414        ScopedError::Processing(cause) => Error::ReaderCreation { location, cause },
415        ScopedError::Callback(cause) => Error::Callback { location, cause },
416    })
417}
418
419enum ScopedError<C> {
420    Processing(structured_asn1::Error),
421    Callback(C),
422}
423
424/// Scoped token refusal, distinct from XML decoding and callback failures.
425#[derive(Debug)]
426pub enum TokenError<C> {
427    /// Original CMS envelope processing error in decoded-source coordinates.
428    Processing(structured_cms::Error),
429    /// Original inspection or complete timestamp-check refusal.
430    Callback(C),
431}
432
433/// Reads the carried RFC3161 CMS token through its existing common kernel.
434///
435/// The visitor can inspect retained faults or run [`Token::check`] against the
436/// selected TSA certificate and original covered input. Reading alone establishes
437/// neither timestamp mathematics nor authority/trust. Errors from a complete
438/// check, including its retained inner `SourceMap`s, keep the binary/XML enclosure.
439/// No new grammar, encoded copy or replacement default-budget parser is used.
440pub fn with_token<X: BinaryXml, R, C>(
441    xml: &X,
442    location: Location<X::Id, X::Document, X::Generation>,
443    operation: &Reader<'_>,
444    visitor: impl for<'source> FnOnce(Token<'source>, &Reader<'source>) -> core::result::Result<R, C>,
445) -> BinaryResult<R, X, TokenError<C>> {
446    with_reader(xml, location, operation, |reader| {
447        let token = Token::read_from(reader).map_err(TokenError::Processing)?;
448        visitor(token, reader).map_err(TokenError::Callback)
449    })
450}