Skip to main content

structured_cms/
octets.rs

1//! Iterative BER OCTET STRING views with source spans and retained accounting.
2
3use crate::{Error, ErrorCode, Result};
4use structured_asn1::memory::{OwnedVec, Work};
5use structured_asn1::{Element, Parser, Reader, Span, Tag, universal};
6
7/// One original content range, excluding ASN.1 tag and length octets.
8#[derive(Debug, Clone, Copy)]
9pub struct Piece<'a> {
10    bytes: &'a [u8],
11    span: Span,
12}
13
14impl<'a> Piece<'a> {
15    /// The original content bytes; no join or re-encoding is performed.
16    #[must_use]
17    pub fn as_bytes(self) -> &'a [u8] {
18        self.bytes
19    }
20
21    /// The content's original document range.
22    #[must_use]
23    pub fn span(self) -> Span {
24        self.span
25    }
26}
27
28#[derive(Debug)]
29enum Storage<'a> {
30    One([Piece<'a>; 1]),
31    Many(OwnedVec<Piece<'a>>),
32}
33
34/// Borrowed logical octets with independently accounted segment metadata.
35///
36/// Primitive content stays inline and allocates no catalogue. Constructed BER
37/// content retains its original disjoint ranges, flattened only as metadata.
38/// The catalogue can outlive the producing reader; its bytes remain borrowed.
39#[derive(Debug)]
40pub struct Octets<'a> {
41    parser: &'a Parser,
42    outer_depth: u32,
43    carrier: Span,
44    contents: Span,
45    storage: Storage<'a>,
46    length: usize,
47    field: &'static str,
48}
49
50impl<'a> Octets<'a> {
51    /// Reads logical `IA5String` contents through the SAME iterative base-string
52    /// walker. BER constructed strings carry OCTET STRING fragments (X.690
53    /// §8.23.3); DER remains primitive. ASCII validation is admitted before every
54    /// content scan, and original fragment spans are preserved without joining.
55    /// <https://www.itu.int/rec/T-REC-X.690-202102-I/en>
56    pub fn read_ia5(
57        parent: &Reader<'a>,
58        element: &Element<'a>,
59        field: &'static str,
60    ) -> Result<Self> {
61        parent
62            .charge(1)
63            .map_err(|cause| Error::encoding(cause, field))?;
64        let value = Self::read_tagged(parent, element, universal::IA5_STRING, false, field)?;
65        let execution = parent.execution();
66        for piece in value.pieces() {
67            execution
68                .charge_work(1 + piece.as_bytes().len() as u64)
69                .map_err(|cause| Error::resource(cause, field, piece.span().start))?;
70            if !piece.as_bytes().is_ascii() {
71                return Err(Error::schema(
72                    ErrorCode::MalformedValue,
73                    field,
74                    piece.span().start,
75                ));
76            }
77        }
78        Ok(value)
79    }
80
81    /// Reads an OCTET STRING using the original parser's encoding and ceilings.
82    ///
83    /// X.690 §8.7 permits constructed BER strings composed of OCTET STRING
84    /// fragments. The iterative traversal preserves inherited depth and source
85    /// positions, admitting frame/catalogue growth before allocation. A DER
86    /// reader still refuses constructed forms under its own encoding contract.
87    /// <https://www.itu.int/rec/T-REC-X.690-202102-I/en>
88    pub fn read(parent: &Reader<'a>, element: &Element<'a>, field: &'static str) -> Result<Self> {
89        Self::read_tagged(parent, element, universal::OCTET_STRING, false, field)
90    }
91
92    /// Reads `[0] IMPLICIT OCTET STRING`, the CMS subject-key identifier.
93    ///
94    /// Only the outer tag is replaced; constructed children remain universal
95    /// OCTET STRINGs (X.690 §§8.7, 8.14). Both forms use the same segment walker.
96    pub(crate) fn read_identifier(
97        parent: &Reader<'a>,
98        element: &Element<'a>,
99        field: &'static str,
100    ) -> Result<Self> {
101        Self::read_tagged(parent, element, 0, true, field)
102    }
103
104    // Projection reuses BOTH the shared base-string grammar/validation and
105    // this range walker. Restricted strings have OCTET STRING fragments,
106    // rather than recursively repeating their outer string tag (X.6908.23.3).
107    pub(crate) fn projection_string(
108        parent: &structured_asn1::ScopedReader<'a, '_>,
109        element: &Element<'a>,
110        number: u32,
111        context: bool,
112        field: &'static str,
113    ) -> Result<Self> {
114        if !context {
115            structured_pkix::der::conforms(parent, element, field).map_err(Error::pkix)?;
116        }
117        Self::read_tagged(parent, element, number, context, field)
118    }
119
120    fn read_tagged(
121        parent: &structured_asn1::ScopedReader<'a, '_>,
122        element: &Element<'a>,
123        outer_number: u32,
124        context: bool,
125        field: &'static str,
126    ) -> Result<Self> {
127        let primitive = if context {
128            Tag::context(false, outer_number)
129        } else {
130            Tag::primitive(outer_number)
131        };
132        let constructed = if context {
133            Tag::context(true, outer_number)
134        } else {
135            Tag::constructed(outer_number)
136        };
137        if element.tag() == primitive {
138            return Ok(Self {
139                parser: parent.parser(),
140                outer_depth: parent.depth(),
141                carrier: element.span(),
142                contents: element.contents_span(),
143                storage: Storage::One([Piece {
144                    bytes: element.contents(),
145                    span: element.contents_span(),
146                }]),
147                length: element.contents().len(),
148                field,
149            });
150        }
151        if element.tag() != constructed {
152            return Err(Error::schema(
153                ErrorCode::UnexpectedTag,
154                field,
155                element.span().start,
156            ));
157        }
158        if parent.encoding().is_der() {
159            return Err(Error::schema(
160                ErrorCode::UnexpectedTag,
161                field,
162                element.span().start,
163            ));
164        }
165        let execution = parent.execution();
166        let mut pieces = execution.vector();
167        let mut frames = execution.vector();
168        frames
169            .try_push(
170                parent
171                    .enter(element)
172                    .map_err(|error| Error::encoding(error, field))?,
173            )
174            .map_err(|reason| Error::resource(reason, field, element.span().start))?;
175        let mut length = 0_usize;
176        while let Some(reader) = frames.last_mut() {
177            let Some(child) = reader
178                .next_element()
179                .map_err(|error| Error::encoding(error, field))?
180            else {
181                frames.pop();
182                continue;
183            };
184            // Only the OUTER tag changes: X.690 §8.23.3 defines restricted
185            // strings as [UNIVERSAL x] IMPLICIT OCTET STRING. Its §8.23.5.4
186            // example encodes constructed VisibleString with child tag 04,
187            // not another VisibleString. IA5String follows the same rule:
188            // 36 03 04 01 61 is valid; 36 03 16 01 61 is not.
189            // https://www.itu.int/rec/T-REC-X.690-202102-I/en
190            if child.tag() == Tag::primitive(universal::OCTET_STRING) {
191                length = length.checked_add(child.contents().len()).ok_or_else(|| {
192                    Error::schema(ErrorCode::LengthOverflow, field, child.span().start)
193                })?;
194                pieces
195                    .try_push(Piece {
196                        bytes: child.contents(),
197                        span: child.contents_span(),
198                    })
199                    .map_err(|reason| Error::resource(reason, field, child.span().start))?;
200            } else if child.tag() == Tag::constructed(universal::OCTET_STRING) {
201                let nested = reader
202                    .enter(&child)
203                    .map_err(|error| Error::encoding(error, field))?;
204                frames
205                    .try_push(nested)
206                    .map_err(|reason| Error::resource(reason, field, child.span().start))?;
207            } else {
208                return Err(Error::schema(
209                    ErrorCode::UnexpectedTag,
210                    field,
211                    child.span().start,
212                ));
213            }
214        }
215        Ok(Self {
216            parser: parent.parser(),
217            outer_depth: parent.depth(),
218            carrier: element.span(),
219            contents: element.contents_span(),
220            storage: Storage::Many(pieces.into_owned()),
221            length,
222            field,
223        })
224    }
225
226    /// The original primitive content ranges in logical order.
227    #[must_use]
228    pub fn pieces(&self) -> &[Piece<'a>] {
229        match &self.storage {
230            Storage::One(piece) => piece,
231            Storage::Many(pieces) => pieces,
232        }
233    }
234
235    /// The logical octet count, recorded during parsing without a later scan.
236    #[must_use]
237    pub fn len(&self) -> usize {
238        self.length
239    }
240
241    /// Whether the logical content is empty, including constructed empty input.
242    #[must_use]
243    pub fn is_empty(&self) -> bool {
244        self.length == 0
245    }
246
247    /// The original whole carrying element, including its tag and length.
248    #[must_use]
249    pub fn span(&self) -> Span {
250        self.carrier
251    }
252
253    pub(crate) fn content_span(&self) -> Span {
254        self.contents
255    }
256
257    pub(crate) fn outer_depth(&self) -> u32 {
258        self.outer_depth
259    }
260
261    pub(crate) fn parser(&self) -> &'a Parser {
262        self.parser
263    }
264
265    /// The borrowed bytes when the logical value occupies one original range.
266    #[must_use]
267    pub fn as_one(&self) -> Option<&'a [u8]> {
268        match self.pieces() {
269            [piece] => Some(piece.bytes),
270            _ => None,
271        }
272    }
273
274    /// Copies logical content into independently retained accounted storage.
275    ///
276    /// This is an explicit materialization; inspection can keep using borrowed
277    /// ranges, and an already owned result should be moved instead of copied.
278    /// The original producing session is required before any admission or scan.
279    /// Metadata traversal, exact capacity and each copy use both its local and
280    /// current aggregate ceilings. The result retains charge after reader drop.
281    /// `field` and `at` locate the actual enclosing operation, including an
282    /// empty constructed value. Refusal releases partial output, not the source.
283    pub fn copy_in(
284        &self,
285        parent: &Reader<'_>,
286        field: &'static str,
287        at: u64,
288    ) -> Result<OwnedVec<u8>> {
289        if !core::ptr::eq(self.parser, parent.parser()) {
290            return Err(Error::pkix(structured_pkix::Error::new(
291                structured_pkix::ErrorCode::ForeignReader,
292                field,
293                at,
294            )));
295        }
296        let execution = parent.execution();
297        let refusal = |reason| Error::resource(reason, field, at);
298        execution
299            .charge_work(self.pieces().len() as u64)
300            .map_err(refusal)?;
301        let mut output = execution.vector();
302        // The logical length was checked during the one structural traversal.
303        output.try_reserve_exact(self.length).map_err(refusal)?;
304        for piece in self.pieces() {
305            output.try_extend_from_slice(piece.bytes).map_err(refusal)?;
306        }
307        Ok(output.into_owned())
308    }
309
310    pub(crate) fn same_value(
311        &self,
312        other: &Octets<'_>,
313        parent: &Reader<'_>,
314        field: &'static str,
315        at: u64,
316    ) -> Result<bool> {
317        let execution = parent.execution();
318        execution
319            .charge_work(1)
320            .map_err(|reason| Error::resource(reason, field, at))?;
321        if self.length != other.length {
322            return Ok(false);
323        }
324        for amount in [self.pieces().len(), other.pieces().len(), self.length] {
325            execution
326                .charge_work(amount as u64)
327                .map_err(|reason| Error::resource(reason, field, at))?;
328        }
329        Ok(self
330            .pieces()
331            .iter()
332            .flat_map(|piece| piece.bytes.iter())
333            .eq(other.pieces().iter().flat_map(|piece| piece.bytes.iter())))
334    }
335}
336
337/// Why feeding attached ranges could not finish.
338#[derive(Debug)]
339pub enum SourceError {
340    /// Metadata traversal refused the current operation's resources.
341    Processing(Error),
342    /// The stream driver refused a chunk and retains its original cause.
343    Receiver(crate::stream::Refused),
344}
345
346impl crate::stream::Source for &Octets<'_> {
347    type Error = SourceError;
348
349    fn stream<W: Work>(
350        &mut self,
351        receiver: &mut impl crate::stream::Receiver,
352        execution: &W,
353    ) -> core::result::Result<(), Self::Error> {
354        for piece in self.pieces() {
355            execution.charge_work(1).map_err(|reason| {
356                SourceError::Processing(Error::resource(reason, self.field, piece.span.start))
357            })?;
358            receiver.write(piece.bytes).map_err(SourceError::Receiver)?;
359        }
360        Ok(())
361    }
362}