Skip to main content

structured_asn1/value/
owned.rs

1//! Accounted dotted-decimal parsing with validated immutable OID storage.
2
3use super::Oid;
4use crate::error::{Error, ErrorCode, Path, Result};
5use crate::memory::{Memory, MemoryError, OwnedVec, Work};
6
7/// An independently owned object identifier with retained capacity accounting.
8///
9/// Its immutable storage is validated at construction. Borrowing its OID view
10/// does not rescan the encoded arcs; callers admit subsequent comparisons or
11/// traversal in the execution domain of the current operation.
12#[derive(Debug)]
13pub struct OwnedOid {
14    bytes: OwnedVec<u8>,
15}
16
17impl OwnedOid {
18    /// Parses strict dotted-decimal numericOID text into owned encoded arcs.
19    ///
20    /// Uses RFC4512 §1.4's decimal-component grammar (at least two components,
21    /// no padded decimal component) and ASN.1's first/second arc restrictions.
22    /// Arc width is unrestricted by machine integers. Work, scratch, copying
23    /// and capacity are admitted before execution; the result keeps its charge.
24    ///
25    /// # Errors
26    /// Returns `MalformedOid` at the offending text offset, or a distinct
27    /// resource error before unfunded conversion/allocation.
28    pub fn parse(text: &str, execution: &Memory) -> Result<Self> {
29        Self::parse_in(text, execution)
30    }
31
32    /// Parses using the current operation's work and storage admission.
33    /// The returned allocation retains its original account, not a borrowed
34    /// parser/execution scope. Grammar and conversion share [`Self::parse`]'s
35    /// kernel; aggregate and local refusal remain distinct resource errors.
36    pub fn parse_in<W: Work>(text: &str, execution: &W) -> Result<Self> {
37        let work = u64::try_from(text.len())
38            .map_err(|_| Error::resource(MemoryError::WorkExceeded, 0))?
39            .checked_mul(4)
40            .and_then(|work| work.checked_add(1))
41            .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, 0))?;
42        execution
43            .charge_work(work)
44            .map_err(|reason| Error::resource(reason, 0))?;
45        let root = validate(text)?;
46        let mut bytes = execution.vector();
47        let mut groups = execution.vector();
48        let mut offset = 2;
49        for (position, component) in text.split('.').enumerate() {
50            if position == 0 {
51                continue;
52            }
53            groups.clear();
54            groups
55                .try_push(0u8)
56                .map_err(|reason| Error::resource(reason, offset))?;
57            for digit in component.bytes() {
58                multiply(&mut groups, digit - b'0', execution, offset)?;
59            }
60            if position == 1 {
61                add_root(&mut groups, root * 40, execution, offset)?;
62            }
63            execution
64                .charge_work(groups.len() as u64)
65                .map_err(|reason| Error::resource(reason, offset))?;
66            for at in (0..groups.len()).rev() {
67                let byte = if at == 0 {
68                    groups[at]
69                } else {
70                    groups[at] | 0x80
71                };
72                bytes
73                    .try_push(byte)
74                    .map_err(|reason| Error::resource(reason, offset))?;
75            }
76            offset += component.len() as u64 + 1;
77        }
78        Ok(Self {
79            bytes: bytes.into_owned(),
80        })
81    }
82
83    /// Copies an already validated identifier into the supplied storage domain.
84    /// The original encoding and source remain unchanged on resource refusal.
85    pub fn from_oid(value: Oid<'_>, execution: &Memory) -> core::result::Result<Self, MemoryError> {
86        Self::from_oid_in(value, execution)
87    }
88
89    /// Retains validated arcs after funding copying/capacity in current work.
90    /// The resulting owner keeps its allocation but no borrowed parser budget.
91    pub fn from_oid_in<W: Work>(
92        value: Oid<'_>,
93        execution: &W,
94    ) -> core::result::Result<Self, MemoryError> {
95        let mut bytes = execution.vector();
96        bytes.try_extend_from_slice(value.as_bytes())?;
97        Ok(Self {
98            bytes: bytes.into_owned(),
99        })
100    }
101
102    /// A constant-time borrowed view of the validated immutable encoding.
103    #[must_use]
104    pub fn as_oid(&self) -> Oid<'_> {
105        Oid(&self.bytes)
106    }
107}
108
109fn malformed(offset: usize) -> Error {
110    Error::new(ErrorCode::MalformedOid, offset as u64, Path::default())
111}
112
113fn validate(text: &str) -> Result<u8> {
114    // RFC4512 §1.4 supplies the lexical rule; X.690 §8.19.4 packs the
115    // first two ASN.1 arcs as 40*root + second, with second<40 for root<2.
116    // https://www.rfc-editor.org/rfc/rfc4512.html#section-1.4
117    let mut offset = 0;
118    let mut count = 0;
119    let mut root = 0;
120    for component in text.split('.') {
121        let bytes = component.as_bytes();
122        if bytes.is_empty() {
123            return Err(malformed(offset));
124        }
125        if bytes.len() > 1 && bytes[0] == b'0' {
126            return Err(malformed(offset));
127        }
128        for (at, byte) in bytes.iter().enumerate() {
129            if !byte.is_ascii_digit() {
130                return Err(malformed(offset + at));
131            }
132        }
133        if count == 0 {
134            if bytes.len() != 1 || bytes[0] > b'2' {
135                return Err(malformed(offset));
136            }
137            root = bytes[0] - b'0';
138        } else if count == 1
139            && root < 2
140            && (bytes.len() > 2 || (bytes.len() == 2 && bytes > &b"39"[..]))
141        {
142            return Err(malformed(offset));
143        }
144        count += 1;
145        offset += bytes.len() + 1;
146    }
147    if count < 2 {
148        return Err(malformed(text.len()));
149    }
150    Ok(root)
151}
152
153fn admit<W: Work>(groups: &OwnedVec<u8, W>, execution: &W, offset: u64) -> Result<()> {
154    let work = (groups.len() as u64)
155        .checked_mul(3)
156        .and_then(|work| work.checked_add(1))
157        .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, offset))?;
158    execution
159        .charge_work(work)
160        .map_err(|reason| Error::resource(reason, offset))
161}
162
163fn multiply<W: Work>(
164    groups: &mut OwnedVec<u8, W>,
165    digit: u8,
166    execution: &W,
167    offset: u64,
168) -> Result<()> {
169    admit(groups, execution, offset)?;
170    let mut carry = u16::from(digit);
171    for group in &mut **groups {
172        let current = u16::from(*group) * 10 + carry;
173        *group = u8::try_from(current & 0x7f).expect("seven-bit group");
174        carry = current >> 7;
175    }
176    while carry != 0 {
177        groups
178            .try_push(u8::try_from(carry & 0x7f).expect("seven-bit carry"))
179            .map_err(|reason| Error::resource(reason, offset))?;
180        carry >>= 7;
181    }
182    Ok(())
183}
184
185fn add_root<W: Work>(
186    groups: &mut OwnedVec<u8, W>,
187    root: u8,
188    execution: &W,
189    offset: u64,
190) -> Result<()> {
191    admit(groups, execution, offset)?;
192    let mut carry = u16::from(root);
193    for group in &mut **groups {
194        if carry == 0 {
195            break;
196        }
197        let current = u16::from(*group) + carry;
198        *group = u8::try_from(current & 0x7f).expect("seven-bit group");
199        carry = current >> 7;
200    }
201    if carry != 0 {
202        groups
203            .try_push(u8::try_from(carry).expect("one-bit carry"))
204            .map_err(|reason| Error::resource(reason, offset))?;
205    }
206    Ok(())
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use crate::memory::MemoryAccount;
213    use alloc::sync::Arc;
214
215    fn memory(storage: usize, work: usize) -> Memory {
216        Memory::new(Arc::new(MemoryAccount::new(storage, work)))
217    }
218
219    #[test]
220    fn decimal_conversion_uses_current_local_work_and_retains_only_its_allocation() {
221        // Wide decimal conversion must not escape a producing local ceiling
222        // through Memory. Returning the owner cannot retain the borrowed Reader.
223        use crate::{Encoding, Limits, Parser};
224        let account = memory(4096, 1_000_000);
225        let text = "2.1844674407370955161618446744073709551616";
226        {
227            let parser = Parser::new(
228                Encoding::Der,
229                Limits {
230                    max_work: 350,
231                    ..Limits::default()
232                },
233                account.clone(),
234            );
235            let reader = parser.read(&[], 700).unwrap();
236            let error = OwnedOid::parse_in(text, &reader.execution()).unwrap_err();
237            assert_eq!(error.code(), ErrorCode::LimitExceeded(crate::Limit::Work));
238            assert_eq!(account.used(), 0);
239        }
240        let value = {
241            let parser = Parser::der(account.clone());
242            let reader = parser.read(&[], 700).unwrap();
243            OwnedOid::parse_in(text, &reader.execution()).unwrap()
244        };
245        assert_eq!(account.used(), value.bytes.capacity());
246        assert!(account.used() > 0);
247        let aggregate = memory(4096, 350);
248        let parser = Parser::der(aggregate.clone());
249        let reader = parser.read(&[], 900).unwrap();
250        let error = OwnedOid::parse_in(text, &reader.execution()).unwrap_err();
251        assert_eq!(error.code(), ErrorCode::LimitExceeded(crate::Limit::Work));
252        assert_eq!(aggregate.used(), 0);
253        drop(value);
254        assert_eq!(account.used(), 0);
255    }
256
257    #[test]
258    fn owned_decimal_oids_preserve_arbitrary_width_and_joint_root() {
259        // Values exceed u128 and force decimal multiplication into base128;
260        // the packed root offset must be added before encoding and undone by text.
261        for text in [
262            "0.0",
263            "1.39",
264            "2.0",
265            "2.18446744073709551616",
266            "1.2.10889035741470030830827987437816582766593",
267        ] {
268            let execution = memory(4096, 1_000_000);
269            let value = OwnedOid::parse(text, &execution).unwrap();
270            let rendered = value.as_oid().text(&execution).unwrap();
271            assert_eq!(rendered.as_str(&execution).unwrap(), text);
272            drop(rendered);
273            assert_eq!(execution.used(), value.bytes.capacity());
274            drop(value);
275            assert_eq!(execution.used(), 0);
276        }
277    }
278
279    #[test]
280    fn decimal_oid_syntax_refusal_has_no_retained_allocation() {
281        // Grammar/root failures are detected before constructing buffers;
282        // the original text offset remains available without allocation.
283        for (text, offset) in [
284            ("", 0),
285            ("1", 1),
286            ("1.", 2),
287            ("3.1", 0),
288            ("1.40", 2),
289            ("0.100", 2),
290            ("2.01", 2),
291            ("2.1..3", 4),
292            ("2.1x", 3),
293            (" 2.1", 0),
294        ] {
295            let execution = memory(0, 1024);
296            let error = OwnedOid::parse(text, &execution).unwrap_err();
297            assert_eq!(error.code(), ErrorCode::MalformedOid);
298            assert_eq!(error.offset(), offset);
299            assert_eq!(execution.used(), 0);
300        }
301    }
302
303    #[test]
304    fn owned_oid_refusal_releases_partial_conversion_and_copy_storage() {
305        // Work refusal during a wide-arc multiply releases both output and
306        // reusable scratch; a funded source survives rejected deep copies.
307        let execution = memory(4096, 350);
308        let error =
309            OwnedOid::parse("2.1844674407370955161618446744073709551616", &execution).unwrap_err();
310        assert_eq!(error.code(), ErrorCode::LimitExceeded(crate::Limit::Work));
311        assert_eq!(execution.used(), 0);
312        let storage = memory(128, 10_000);
313        let original = OwnedOid::parse("1.2.840.113549", &storage).unwrap();
314        let used = storage.used();
315        let refused = memory(0, 1000);
316        assert_eq!(
317            OwnedOid::from_oid(original.as_oid(), &refused).unwrap_err(),
318            MemoryError::LimitExceeded
319        );
320        assert_eq!(refused.used(), 0);
321        assert_eq!(storage.used(), used);
322        assert_eq!(
323            original.as_oid(),
324            Oid::from_bytes(&[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d])
325        );
326    }
327}