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}