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
276fn pki_encoding<X: Xml, W: Work>(
277 access: &Access<'_, X, W>,
278 node: X::Id,
279) -> core::result::Result<Option<PkiEncoding>, AccessError<X::Error, X::Id>> {
280 access
281 .attribute(
282 node,
283 Name {
284 namespace: "",
285 local: "Encoding",
286 },
287 |value| {
288 // EN319132-1 §5.1.3 specifies DER for the absent attribute; this is
289 // a wire default, never an implicit permissive parser/policy fallback.
290 // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
291 let Some(value) = value else {
292 return Ok(Some(PkiEncoding::Der));
293 };
294 let value = access.trim_xml_whitespace(value)?;
295 for (uri, encoding) in [
296 ("http://uri.etsi.org/01903/v1.2.2#DER", PkiEncoding::Der),
297 ("http://uri.etsi.org/01903/v1.2.2#BER", PkiEncoding::Ber),
298 ("http://uri.etsi.org/01903/v1.2.2#CER", PkiEncoding::Cer),
299 ("http://uri.etsi.org/01903/v1.2.2#PER", PkiEncoding::Per),
300 ("http://uri.etsi.org/01903/v1.2.2#XER", PkiEncoding::Xer),
301 ] {
302 if access.equal(value, uri)? {
303 return Ok(Some(encoding));
304 }
305 }
306 Ok(None)
307 },
308 )?
309 .map_err(|cause| AccessError::Resource { node, cause })
310}
311
312/// Scoped binary result using this XML owner's source identity types.
313pub type BinaryResult<T, X, C> = core::result::Result<
314 T,
315 Error<<X as Xml>::Error, C, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
316>;
317
318/// Visits an original decoded octet value without inventing an ASN.1 enclosure.
319/// The result cannot borrow the temporary decoded buffer.
320///
321/// ```compile_fail
322/// use structured_asn1::memory::Work;
323/// use structured_xades::binary::{self, BinaryResult, BinaryXml, Location};
324/// fn escape<'a, X: BinaryXml, W: Work>(xml: &X,
325/// location: Location<X::Id, X::Document, X::Generation>, work: &W)
326/// -> BinaryResult<&'a [u8], X, ()>
327/// {
328/// binary::with_octets(xml, location, work, |bytes| Ok(bytes))
329/// }
330/// ```
331pub fn with_octets<X: BinaryXml, W: Work, R, C>(
332 xml: &X,
333 location: Location<X::Id, X::Document, X::Generation>,
334 execution: &W,
335 visitor: impl for<'source> FnOnce(&'source [u8]) -> Result<R, C>,
336) -> BinaryResult<R, X, C> {
337 Access::new(xml, location.snapshot, execution)
338 .query(location.node, |xml, snapshot, work| {
339 xml.with_octets(snapshot, location.node, work, visitor)
340 })
341 .map_err(|cause| Error::Access { location, cause })?
342 .map_err(|cause| Error::Callback { location, cause })
343}
344
345/// Visits one original decoded source with snapshot checks around the callback.
346pub fn with_reader<X: BinaryXml, R, C>(
347 xml: &X,
348 location: Location<X::Id, X::Document, X::Generation>,
349 operation: &Reader<'_>,
350 visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> core::result::Result<R, C>,
351) -> BinaryResult<R, X, C> {
352 let execution = operation.execution();
353 let access = Access::new(xml, location.snapshot, &execution);
354 let encoding = selected_encoding(&access, location)
355 .map_err(|error| error.map_callback(|never| match never {}))?;
356 with_reader_as(xml, location, operation, encoding, visitor)
357}
358
359pub(crate) fn selected_encoding<X: Xml, W: Work>(
360 access: &Access<'_, X, W>,
361 location: Location<X::Id, X::Document, X::Generation>,
362) -> BinaryResult<Encoding, X, core::convert::Infallible> {
363 let specified = pki_encoding(access, location.node)
364 .map_err(|cause| Error::Access { location, cause })?
365 .ok_or(Error::UnknownEncoding { location })?;
366 let encoding = match specified {
367 PkiEncoding::Der => Encoding::Der,
368 PkiEncoding::Ber => Encoding::Ber,
369 other => {
370 return Err(Error::UnsupportedEncoding {
371 location,
372 encoding: other,
373 });
374 }
375 };
376 Ok(encoding)
377}
378
379/// Visits a schema-fixed DER value, such as `IssuerSerialV2` (§5.2.2).
380/// An unrelated XML attribute cannot make this ASN.1 value permissive BER.
381/// Outer XML schema/attribute validation remains a separate obligation.
382pub fn with_der_reader<X: BinaryXml, R, C>(
383 xml: &X,
384 location: Location<X::Id, X::Document, X::Generation>,
385 operation: &Reader<'_>,
386 visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
387) -> BinaryResult<R, X, C> {
388 with_reader_as(xml, location, operation, Encoding::Der, visitor)
389}
390
391fn with_reader_as<X: BinaryXml, R, C>(
392 xml: &X,
393 location: Location<X::Id, X::Document, X::Generation>,
394 operation: &Reader<'_>,
395 encoding: Encoding,
396 visitor: impl for<'source> FnOnce(&mut Reader<'source>) -> Result<R, C>,
397) -> BinaryResult<R, X, C> {
398 let execution = operation.execution();
399 let result = Access::new(xml, location.snapshot, &execution)
400 .query(location.node, |xml, snapshot, _| {
401 xml.with_binary(snapshot, location.node, operation, |source| {
402 let mut reader = source
403 .parser
404 .read_query_in(operation, source.bytes, source.base, encoding, source.depth)
405 .map_err(ScopedError::Processing)?;
406 visitor(&mut reader).map_err(ScopedError::Callback)
407 })
408 })
409 .map_err(|cause| Error::Access { location, cause })?;
410 result.map_err(|cause| match cause {
411 ScopedError::Processing(cause) => Error::ReaderCreation { location, cause },
412 ScopedError::Callback(cause) => Error::Callback { location, cause },
413 })
414}
415
416enum ScopedError<C> {
417 Processing(structured_asn1::Error),
418 Callback(C),
419}
420
421/// Scoped token refusal, distinct from XML decoding and callback failures.
422#[derive(Debug)]
423pub enum TokenError<C> {
424 /// Original CMS envelope processing error in decoded-source coordinates.
425 Processing(structured_cms::Error),
426 /// Original inspection or complete timestamp-check refusal.
427 Callback(C),
428}
429
430/// Reads the carried RFC3161 CMS token through its existing common kernel.
431///
432/// The visitor can inspect retained faults or run [`Token::check`] against the
433/// selected TSA certificate and original covered input. Reading alone establishes
434/// neither timestamp mathematics nor authority/trust. Errors from a complete
435/// check, including its retained inner `SourceMap`s, keep the binary/XML enclosure.
436/// No new grammar, encoded copy or replacement default-budget parser is used.
437pub fn with_token<X: BinaryXml, R, C>(
438 xml: &X,
439 location: Location<X::Id, X::Document, X::Generation>,
440 operation: &Reader<'_>,
441 visitor: impl for<'source> FnOnce(Token<'source>, &Reader<'source>) -> core::result::Result<R, C>,
442) -> BinaryResult<R, X, TokenError<C>> {
443 with_reader(xml, location, operation, |reader| {
444 let token = Token::read_from(reader).map_err(TokenError::Processing)?;
445 visitor(token, reader).map_err(TokenError::Callback)
446 })
447}