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