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// The original XML enclosure of a decoded binary source is a common source
150// location; `Located` and every binary error carry it.
151pub use structured_ltv::source::Location;
152
153/// A retained result alongside its original XML/binary-source enclosure.
154/// Any ASN.1 offsets inside the value remain decoded-source coordinates.
155#[derive(Debug)]
156pub struct Located<T, N, D, G> {
157    /// Original document generation and encapsulated-value node.
158    pub location: Location<N, D, G>,
159    /// Retained scoped result; it cannot borrow the temporary decoded source.
160    pub value: T,
161}
162
163/// A result carrying this XML owner's stable source identity types.
164pub type LocatedValue<T, X> =
165    Located<T, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
166
167/// Moves an owned decoded source out together with its exact XML enclosure.
168///
169/// This performs no ASN.1 parse or codec substitution. The caller's material
170/// schema must still select and validate its actual PKI format. No buffer copy,
171/// allocation or reference-count increment is introduced by this wrapper.
172pub fn owned<X: OwnedBinaryXml, W: Work>(
173    xml: &X,
174    location: Location<X::Id, X::Document, X::Generation>,
175    execution: &W,
176) -> AccessResult<LocatedValue<OwnedVec<u8>, X>, X> {
177    let value = Access::new(xml, location.snapshot, execution)
178        .query(location.node, |xml, snapshot, work| {
179            xml.decode_owned(snapshot, location.node, work)
180        })?;
181    Ok(Located { location, value })
182}
183
184/// Retains a source for simultaneous material checking without a second decode.
185/// Original XML identity accompanies every source in a multi-source catalogue.
186/// Like [`owned`], this does not itself establish the material's PKI schema,
187/// authenticity, completeness or trust.
188pub fn owned_source<X: OwnedBinaryXml>(
189    xml: &X,
190    location: Location<X::Id, X::Document, X::Generation>,
191    operation: &Reader<'_>,
192) -> AccessResult<LocatedValue<OwnedDecoded, X>, X> {
193    let execution = operation.execution();
194    let value = Access::new(xml, location.snapshot, &execution)
195        .query(location.node, |xml, snapshot, _| {
196            xml.decode_binary_owned(snapshot, location.node, operation)
197        })?;
198    Ok(Located { location, value })
199}
200
201/// Refusal preserving both the XML enclosure and the unchanged inner cause.
202#[derive(Debug)]
203pub enum Error<E, C, N, D, G> {
204    /// The original Encoding attribute does not identify a specified PKI codec.
205    UnknownEncoding {
206        /// Original encapsulated-value element.
207        location: Location<N, D, G>,
208    },
209    /// A specified codec has no decoder in this ASN.1 processing capability.
210    /// This is unsupported processing, not malformed XML or a digest mismatch.
211    UnsupportedEncoding {
212        /// Original encapsulated-value element.
213        location: Location<N, D, G>,
214        /// Original specified codec.
215        encoding: PkiEncoding,
216    },
217    /// Common Reader construction refused before the inspection visitor.
218    ReaderCreation {
219        /// Original encapsulated-value element.
220        location: Location<N, D, G>,
221        /// Original ASN.1 processing/resource error in decoded-source coordinates.
222        cause: structured_asn1::Error,
223    },
224    /// XML access, decoding, generation or resource refusal.
225    Access {
226        /// Original XML enclosure, not a fabricated binary byte range.
227        location: Location<N, D, G>,
228        /// Original access/owner cause, including pre-visitor resource errors.
229        cause: AccessError<E, N>,
230    },
231    /// Scoped ASN.1 inspection/check callback refused.
232    Callback {
233        /// Inner offsets refer to the decoded source at this XML node.
234        location: Location<N, D, G>,
235        /// Unchanged cause; retained maps keep their own inner source contexts.
236        cause: C,
237    },
238}
239
240impl<E, C, N, D, G> Error<E, C, N, D, G> {
241    // Flatten nested scoped operations without relabelling XML locations or
242    // allocating/cloning while already handling an opaque callback refusal.
243    /// Maps only the scoped callback cause. Original XML/source locations,
244    /// resource refusals and access/generation classifications remain intact.
245    pub fn map_callback<T>(self, map: impl FnOnce(C) -> T) -> Error<E, T, N, D, G> {
246        match self {
247            Self::UnknownEncoding { location } => Error::UnknownEncoding { location },
248            Self::UnsupportedEncoding { location, encoding } => {
249                Error::UnsupportedEncoding { location, encoding }
250            }
251            Self::ReaderCreation { location, cause } => Error::ReaderCreation { location, cause },
252            Self::Access { location, cause } => Error::Access { location, cause },
253            Self::Callback { location, cause } => Error::Callback {
254                location,
255                cause: map(cause),
256            },
257        }
258    }
259}
260
261/// Encoding identifiers defined by EN319132-1 §5.1.3; absence specifically means DER.
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum PkiEncoding {
264    /// ASN.1 distinguished encoding.
265    Der,
266    /// ASN.1 basic encoding.
267    Ber,
268    /// ASN.1 canonical encoding, distinct from merely accepting BER syntax.
269    Cer,
270    /// ASN.1 packed encoding.
271    Per,
272    /// ASN.1 XML encoding, distinct from the outer base64 XML carrier.
273    Xer,
274}
275
276/// Original XML declaration, distinct from the caller-selected parser profile.
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum EncodingDeclaration {
279    /// No Encoding attribute: EN319132-1 §5.1.3 declares DER by absence.
280    Absent,
281    /// The original recognized Encoding attribute, without changing XML.
282    Explicit(PkiEncoding),
283}
284
285impl EncodingDeclaration {
286    /// Effective declared encoding, including the specified wire default.
287    #[must_use]
288    pub fn encoding(self) -> PkiEncoding {
289        match self {
290            Self::Absent => PkiEncoding::Der,
291            Self::Explicit(encoding) => encoding,
292        }
293    }
294}
295
296pub(crate) fn pki_encoding<X: Xml, W: Work>(
297    access: &Access<'_, X, W>,
298    node: X::Id,
299) -> core::result::Result<Option<EncodingDeclaration>, AccessError<X::Error, X::Id>> {
300    access
301        .attribute(
302            node,
303            Name {
304                namespace: "",
305                local: "Encoding",
306            },
307            |value| {
308                // EN319132-1 §5.1.3 specifies DER for the absent attribute; this is
309                // a wire default, never an implicit permissive parser/policy fallback.
310                // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
311                let Some(value) = value else {
312                    return Ok(Some(EncodingDeclaration::Absent));
313                };
314                let value = access.trim_xml_whitespace(value)?;
315                for (uri, encoding) in [
316                    ("http://uri.etsi.org/01903/v1.2.2#DER", PkiEncoding::Der),
317                    ("http://uri.etsi.org/01903/v1.2.2#BER", PkiEncoding::Ber),
318                    ("http://uri.etsi.org/01903/v1.2.2#CER", PkiEncoding::Cer),
319                    ("http://uri.etsi.org/01903/v1.2.2#PER", PkiEncoding::Per),
320                    ("http://uri.etsi.org/01903/v1.2.2#XER", PkiEncoding::Xer),
321                ] {
322                    if access.equal(value, uri)? {
323                        return Ok(Some(EncodingDeclaration::Explicit(encoding)));
324                    }
325                }
326                Ok(None)
327            },
328        )?
329        .map_err(|cause| AccessError::Resource { node, cause })
330}
331
332/// Scoped binary result using this XML owner's source identity types.
333pub type BinaryResult<T, X, C> = core::result::Result<
334    T,
335    Error<<X as Xml>::Error, C, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
336>;
337
338/// Visits an original decoded octet value without inventing an ASN.1 enclosure.
339/// The result cannot borrow the temporary decoded buffer.
340///
341/// ```compile_fail
342/// use structured_asn1::memory::Work;
343/// use structured_xades::binary::{self, BinaryResult, BinaryXml, Location};
344/// fn escape<'a, X: BinaryXml, W: Work>(xml: &X,
345///     location: Location<X::Id, X::Document, X::Generation>, work: &W)
346///     -> BinaryResult<&'a [u8], X, ()>
347/// {
348///     binary::with_octets(xml, location, work, |bytes| Ok(bytes))
349/// }
350/// ```
351pub fn with_octets<X: BinaryXml, W: Work, R, C>(
352    xml: &X,
353    location: Location<X::Id, X::Document, X::Generation>,
354    execution: &W,
355    visitor: impl for<'source> FnOnce(&'source [u8]) -> Result<R, C>,
356) -> BinaryResult<R, X, C> {
357    Access::new(xml, location.snapshot, execution)
358        .query(location.node, |xml, snapshot, work| {
359            xml.with_octets(snapshot, location.node, work, visitor)
360        })
361        .map_err(|cause| Error::Access { location, cause })?
362        .map_err(|cause| Error::Callback { location, cause })
363}
364
365/// Visits one original decoded source with snapshot checks around the callback.
366pub fn with_reader<X: BinaryXml, R, C>(
367    xml: &X,
368    location: Location<X::Id, X::Document, X::Generation>,
369    operation: &Reader<'_>,
370    visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> core::result::Result<R, C>,
371) -> BinaryResult<R, X, C> {
372    let execution = operation.execution();
373    let access = Access::new(xml, location.snapshot, &execution);
374    let encoding = selected_encoding(&access, location)
375        .map_err(|error| error.map_callback(|never| match never {}))?;
376    with_reader_as(xml, location, operation, encoding, visitor)
377}
378
379pub(crate) fn selected_encoding<X: Xml, W: Work>(
380    access: &Access<'_, X, W>,
381    location: Location<X::Id, X::Document, X::Generation>,
382) -> BinaryResult<Encoding, X, core::convert::Infallible> {
383    let specified = pki_encoding(access, location.node)
384        .map_err(|cause| Error::Access { location, cause })?
385        .ok_or(Error::UnknownEncoding { location })?;
386    let encoding = match specified.encoding() {
387        PkiEncoding::Der => Encoding::Der,
388        PkiEncoding::Ber => Encoding::Ber,
389        other => {
390            return Err(Error::UnsupportedEncoding {
391                location,
392                encoding: other,
393            });
394        }
395    };
396    Ok(encoding)
397}
398
399/// Visits a schema-fixed DER value, such as `IssuerSerialV2` (§5.2.2).
400/// An unrelated XML attribute cannot make this ASN.1 value permissive BER.
401/// Outer XML schema/attribute validation remains a separate obligation.
402pub fn with_der_reader<X: BinaryXml, R, C>(
403    xml: &X,
404    location: Location<X::Id, X::Document, X::Generation>,
405    operation: &Reader<'_>,
406    visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
407) -> BinaryResult<R, X, C> {
408    with_reader_as(xml, location, operation, Encoding::Der, visitor)
409}
410
411fn with_reader_as<X: BinaryXml, R, C>(
412    xml: &X,
413    location: Location<X::Id, X::Document, X::Generation>,
414    operation: &Reader<'_>,
415    encoding: Encoding,
416    visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
417) -> BinaryResult<R, X, C> {
418    let execution = operation.execution();
419    let result = Access::new(xml, location.snapshot, &execution)
420        .query(location.node, |xml, snapshot, _| {
421            xml.with_binary(snapshot, location.node, operation, |source| {
422                let mut reader = source
423                    .parser
424                    .read_query_in(operation, source.bytes, source.base, encoding, source.depth)
425                    .map_err(ScopedError::Processing)?;
426                visitor(&mut reader).map_err(ScopedError::Callback)
427            })
428        })
429        .map_err(|cause| Error::Access { location, cause })?;
430    result.map_err(|cause| match cause {
431        ScopedError::Processing(cause) => Error::ReaderCreation { location, cause },
432        ScopedError::Callback(cause) => Error::Callback { location, cause },
433    })
434}
435
436enum ScopedError<C> {
437    Processing(structured_asn1::Error),
438    Callback(C),
439}
440
441/// Scoped token refusal, distinct from XML decoding and callback failures.
442#[derive(Debug)]
443pub enum TokenError<C> {
444    /// Original CMS envelope processing error in decoded-source coordinates.
445    Processing(structured_cms::Error),
446    /// Original inspection or complete timestamp-check refusal.
447    Callback(C),
448}
449
450/// Reads the carried RFC3161 CMS token through its existing common kernel.
451///
452/// The visitor can inspect retained faults or run [`Token::check`] against the
453/// selected TSA certificate and original covered input. Reading alone establishes
454/// neither timestamp mathematics nor authority/trust. Errors from a complete
455/// check, including its retained inner `SourceMap`s, keep the binary/XML enclosure.
456/// No new grammar, encoded copy or replacement default-budget parser is used.
457pub fn with_token<X: BinaryXml, R, C>(
458    xml: &X,
459    location: Location<X::Id, X::Document, X::Generation>,
460    operation: &Reader<'_>,
461    visitor: impl for<'source> FnOnce(Token<'source>, &Reader<'source>) -> core::result::Result<R, C>,
462) -> BinaryResult<R, X, TokenError<C>> {
463    with_reader(xml, location, operation, |reader| {
464        let token = Token::read_from(reader).map_err(TokenError::Processing)?;
465        visitor(token, reader).map_err(TokenError::Callback)
466    })
467}