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};
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        let work = (text.len() as u64)
30            .checked_mul(4)
31            .and_then(|work| work.checked_add(1))
32            .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, 0))?;
33        execution
34            .charge_work(work)
35            .map_err(|reason| Error::resource(reason, 0))?;
36        let root = validate(text)?;
37        let mut bytes = execution.vector();
38        let mut groups = execution.vector();
39        let mut offset = 2;
40        for (position, component) in text.split('.').enumerate() {
41            if position == 0 {
42                continue;
43            }
44            groups.clear();
45            groups
46                .try_push(0u8)
47                .map_err(|reason| Error::resource(reason, offset))?;
48            for digit in component.bytes() {
49                multiply(&mut groups, digit - b'0', execution, offset)?;
50            }
51            if position == 1 {
52                add_root(&mut groups, root * 40, execution, offset)?;
53            }
54            execution
55                .charge_work(groups.len() as u64)
56                .map_err(|reason| Error::resource(reason, offset))?;
57            for at in (0..groups.len()).rev() {
58                let byte = if at == 0 {
59                    groups[at]
60                } else {
61                    groups[at] | 0x80
62                };
63                bytes
64                    .try_push(byte)
65                    .map_err(|reason| Error::resource(reason, offset))?;
66            }
67            offset += component.len() as u64 + 1;
68        }
69        Ok(Self { bytes })
70    }
71
72    /// Copies an already validated identifier into the supplied storage domain.
73    /// The original encoding and source remain unchanged on resource refusal.
74    pub fn from_oid(value: Oid<'_>, execution: &Memory) -> core::result::Result<Self, MemoryError> {
75        let mut bytes = execution.vector();
76        bytes.try_extend_from_slice(value.as_bytes())?;
77        Ok(Self { bytes })
78    }
79
80    /// A constant-time borrowed view of the validated immutable encoding.
81    #[must_use]
82    pub fn as_oid(&self) -> Oid<'_> {
83        Oid(&self.bytes)
84    }
85}
86
87fn malformed(offset: usize) -> Error {
88    Error::new(ErrorCode::MalformedOid, offset as u64, Path::default())
89}
90
91fn validate(text: &str) -> Result<u8> {
92    // RFC4512 §1.4 supplies the lexical rule; X.690 §8.19.4 packs the
93    // first two ASN.1 arcs as 40*root + second, with second<40 for root<2.
94    // https://www.rfc-editor.org/rfc/rfc4512.html#section-1.4
95    let mut offset = 0;
96    let mut count = 0;
97    let mut root = 0;
98    for component in text.split('.') {
99        let bytes = component.as_bytes();
100        if bytes.is_empty() {
101            return Err(malformed(offset));
102        }
103        if bytes.len() > 1 && bytes[0] == b'0' {
104            return Err(malformed(offset));
105        }
106        for (at, byte) in bytes.iter().enumerate() {
107            if !byte.is_ascii_digit() {
108                return Err(malformed(offset + at));
109            }
110        }
111        if count == 0 {
112            if bytes.len() != 1 || bytes[0] > b'2' {
113                return Err(malformed(offset));
114            }
115            root = bytes[0] - b'0';
116        } else if count == 1
117            && root < 2
118            && (bytes.len() > 2 || (bytes.len() == 2 && bytes > &b"39"[..]))
119        {
120            return Err(malformed(offset));
121        }
122        count += 1;
123        offset += bytes.len() + 1;
124    }
125    if count < 2 {
126        return Err(malformed(text.len()));
127    }
128    Ok(root)
129}
130
131fn admit(groups: &OwnedVec<u8>, execution: &Memory, offset: u64) -> Result<()> {
132    let work = (groups.len() as u64)
133        .checked_mul(3)
134        .and_then(|work| work.checked_add(1))
135        .ok_or_else(|| Error::resource(MemoryError::WorkExceeded, offset))?;
136    execution
137        .charge_work(work)
138        .map_err(|reason| Error::resource(reason, offset))
139}
140
141fn multiply(groups: &mut OwnedVec<u8>, digit: u8, execution: &Memory, offset: u64) -> Result<()> {
142    admit(groups, execution, offset)?;
143    let mut carry = u16::from(digit);
144    for group in &mut **groups {
145        let current = u16::from(*group) * 10 + carry;
146        *group = u8::try_from(current & 0x7f).expect("seven-bit group");
147        carry = current >> 7;
148    }
149    while carry != 0 {
150        groups
151            .try_push(u8::try_from(carry & 0x7f).expect("seven-bit carry"))
152            .map_err(|reason| Error::resource(reason, offset))?;
153        carry >>= 7;
154    }
155    Ok(())
156}
157
158fn add_root(groups: &mut OwnedVec<u8>, root: u8, execution: &Memory, offset: u64) -> Result<()> {
159    admit(groups, execution, offset)?;
160    let mut carry = u16::from(root);
161    for group in &mut **groups {
162        if carry == 0 {
163            break;
164        }
165        let current = u16::from(*group) + carry;
166        *group = u8::try_from(current & 0x7f).expect("seven-bit group");
167        carry = current >> 7;
168    }
169    if carry != 0 {
170        groups
171            .try_push(u8::try_from(carry).expect("one-bit carry"))
172            .map_err(|reason| Error::resource(reason, offset))?;
173    }
174    Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::memory::MemoryAccount;
181    use alloc::sync::Arc;
182
183    fn memory(storage: usize, work: usize) -> Memory {
184        Memory::new(Arc::new(MemoryAccount::new(storage, work)))
185    }
186
187    #[test]
188    fn owned_decimal_oids_preserve_arbitrary_width_and_joint_root() {
189        // Values exceed u128 and force decimal multiplication into base128;
190        // the packed root offset must be added before encoding and undone by text.
191        for text in [
192            "0.0",
193            "1.39",
194            "2.0",
195            "2.18446744073709551616",
196            "1.2.10889035741470030830827987437816582766593",
197        ] {
198            let execution = memory(4096, 1_000_000);
199            let value = OwnedOid::parse(text, &execution).unwrap();
200            let rendered = value.as_oid().text(&execution).unwrap();
201            assert_eq!(rendered.as_str(&execution).unwrap(), text);
202            drop(rendered);
203            assert_eq!(execution.used(), value.bytes.capacity());
204            drop(value);
205            assert_eq!(execution.used(), 0);
206        }
207    }
208
209    #[test]
210    fn decimal_oid_syntax_refusal_has_no_retained_allocation() {
211        // Grammar/root failures are detected before constructing buffers;
212        // the original text offset remains available without allocation.
213        for (text, offset) in [
214            ("", 0),
215            ("1", 1),
216            ("1.", 2),
217            ("3.1", 0),
218            ("1.40", 2),
219            ("0.100", 2),
220            ("2.01", 2),
221            ("2.1..3", 4),
222            ("2.1x", 3),
223            (" 2.1", 0),
224        ] {
225            let execution = memory(0, 1024);
226            let error = OwnedOid::parse(text, &execution).unwrap_err();
227            assert_eq!(error.code(), ErrorCode::MalformedOid);
228            assert_eq!(error.offset(), offset);
229            assert_eq!(execution.used(), 0);
230        }
231    }
232
233    #[test]
234    fn owned_oid_refusal_releases_partial_conversion_and_copy_storage() {
235        // Work refusal during a wide-arc multiply releases both output and
236        // reusable scratch; a funded source survives rejected deep copies.
237        let execution = memory(4096, 350);
238        let error =
239            OwnedOid::parse("2.1844674407370955161618446744073709551616", &execution).unwrap_err();
240        assert_eq!(error.code(), ErrorCode::LimitExceeded(crate::Limit::Work));
241        assert_eq!(execution.used(), 0);
242        let storage = memory(128, 10_000);
243        let original = OwnedOid::parse("1.2.840.113549", &storage).unwrap();
244        let used = storage.used();
245        let refused = memory(0, 1000);
246        assert_eq!(
247            OwnedOid::from_oid(original.as_oid(), &refused).unwrap_err(),
248            MemoryError::LimitExceeded
249        );
250        assert_eq!(refused.used(), 0);
251        assert_eq!(storage.used(), used);
252        assert_eq!(
253            original.as_oid(),
254            Oid::from_bytes(&[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d])
255        );
256    }
257}