Skip to main content

_synta/types/
mod.rs

1//! Python wrappers for ASN.1 types
2
3use std::str::FromStr;
4use std::sync::OnceLock;
5
6use pyo3::prelude::*;
7use pyo3::types::{PyString, PyTuple};
8
9use synta::ObjectIdentifier;
10
11pub mod elements;
12pub mod primitives;
13pub mod strings;
14
15pub use elements::{element_to_pyobject, PyRawElement, PyTaggedElement};
16pub use primitives::{
17    PyBitString, PyBoolean, PyGeneralizedTime, PyInteger, PyNull, PyOctetString, PyReal, PyUtcTime,
18};
19pub use strings::{
20    PyBmpString, PyGeneralString, PyIA5String, PyNumericString, PyPrintableString, PyTeletexString,
21    PyUniversalString, PyUtf8String, PyVisibleString,
22};
23
24/// Python wrapper for ASN.1 OBJECT IDENTIFIER.
25///
26/// Wraps a parsed ``synta::ObjectIdentifier`` value.  Instances are obtained
27/// from certificate/CSR/CRL getters such as
28/// :attr:`Certificate.signature_algorithm_oid`, from the
29/// :class:`Decoder`, or constructed directly:
30///
31/// ```python
32/// oid = synta.ObjectIdentifier("2.5.4.3")            # from dotted string
33/// oid = synta.ObjectIdentifier.from_components([2, 5, 4, 3])
34/// oid = synta.ObjectIdentifier.from_der_value(raw)   # from implicit-tag bytes
35/// ```
36#[pyclass(frozen, name = "ObjectIdentifier")]
37#[derive(Debug, Clone)]
38pub struct PyObjectIdentifier {
39    pub(crate) inner: ObjectIdentifier,
40    /// Lazily-cached dotted-decimal representation, e.g. `"2.5.4.3"`.
41    /// Populated on first access by `__str__`, `__hash__`, or `__eq__`.
42    dotted_cache: OnceLock<String>,
43}
44
45impl PyObjectIdentifier {
46    /// Construct from a Rust [`ObjectIdentifier`] without going through Python.
47    /// Used by `synta-python`'s decoder and type-mapping code.
48    pub fn from_oid(inner: ObjectIdentifier) -> Self {
49        Self {
50            inner,
51            dotted_cache: OnceLock::new(),
52        }
53    }
54}
55
56#[pymethods]
57impl PyObjectIdentifier {
58    /// Create a new OID from dotted-decimal string notation (e.g. ``"2.5.4.3"``).
59    #[new]
60    fn new(oid_str: &str) -> PyResult<Self> {
61        let inner = ObjectIdentifier::from_str(oid_str)
62            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid OID: {e:?}")))?;
63        Ok(Self {
64            inner,
65            dotted_cache: OnceLock::new(),
66        })
67    }
68
69    /// Create an OID from a list of integer arc components.
70    #[staticmethod]
71    fn from_components(components: Vec<u32>) -> PyResult<Self> {
72        let inner = ObjectIdentifier::new(&components)
73            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid OID: {e:?}")))?;
74        Ok(Self {
75            inner,
76            dotted_cache: OnceLock::new(),
77        })
78    }
79
80    /// Parse raw OID content bytes — the value bytes inside an OID TLV with
81    /// the tag (``0x06``) and length already stripped.
82    ///
83    /// Use this after :meth:`Decoder.decode_implicit_tag` for
84    /// ``registeredID [8] IMPLICIT OID`` in a GeneralName CHOICE:
85    ///
86    /// ```python
87    ///     child = dec.decode_implicit_tag(8, "Context")
88    ///     oid = synta.ObjectIdentifier.from_der_value(child.remaining_bytes())
89    /// ```
90    ///
91    /// Raises :exc:`ValueError` if ``data`` is empty or invalid.
92    #[staticmethod]
93    fn from_der_value(data: &[u8]) -> PyResult<Self> {
94        let inner = ObjectIdentifier::from_content_bytes(data).map_err(|e| {
95            pyo3::exceptions::PyValueError::new_err(format!("Invalid OID content: {e:?}"))
96        })?;
97        Ok(Self {
98            inner,
99            dotted_cache: OnceLock::new(),
100        })
101    }
102
103    /// Return the OID arc components as a tuple of integers.
104    fn components<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
105        PyTuple::new(py, self.inner.components())
106    }
107
108    fn __str__(&self) -> &str {
109        self.dotted_cache.get_or_init(|| self.inner.to_string())
110    }
111
112    fn __repr__(&self) -> String {
113        format!(
114            "ObjectIdentifier('{}')",
115            self.dotted_cache.get_or_init(|| self.inner.to_string())
116        )
117    }
118
119    /// Equality with another :class:`ObjectIdentifier` or a dotted-decimal
120    /// string.  Enables ``oid == "2.5.4.3"`` and ``"2.5.4.3" == oid``.
121    fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
122        if let Ok(other_oid) = other.extract::<PyRef<PyObjectIdentifier>>() {
123            return self.inner == other_oid.inner;
124        }
125        if let Ok(s) = other.extract::<String>() {
126            return self.dotted_cache.get_or_init(|| self.inner.to_string()) == &s;
127        }
128        false
129    }
130
131    /// Hash consistent with ``hash(str(oid))`` so that OID objects and their
132    /// dotted-decimal string equivalents collide in the same set/dict bucket,
133    /// enabling ``oid in {"2.5.4.3"}`` to work correctly.
134    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
135        PyString::new(py, self.dotted_cache.get_or_init(|| self.inner.to_string())).hash()
136    }
137}
138
139/// Python wrapper for ASN.1 RELATIVE-OID (tag 0x0D).
140///
141/// A RELATIVE-OID is a sequence of non-negative integer arc components with no
142/// restriction on the first two values (unlike OBJECT IDENTIFIER).  It is used
143/// in places where the first arcs of a containing OID are already implied by
144/// context — for example, `CosignerID` / `TrustAnchorID` in MTC
145/// (draft-ietf-plants-merkle-tree-certs).
146///
147/// Instances can be constructed from a dotted-decimal string or from a list of
148/// integer components:
149///
150/// ```python
151/// roid = synta.RelativeOid("1.3.6.1.4.1.44363.47.10.1")
152/// roid = synta.RelativeOid.from_components([1, 3, 6, 1, 4, 1, 44363, 47, 10, 1])
153/// roid = synta.RelativeOid.from_der(der_bytes)
154/// der  = roid.to_der()
155/// ```
156#[pyclass(frozen, name = "RelativeOid")]
157#[derive(Debug, Clone)]
158pub struct PyRelativeOid {
159    pub(crate) inner: synta::RelativeOid,
160    dotted_cache: OnceLock<String>,
161}
162
163impl PyRelativeOid {
164    /// Construct from a Rust [`synta::RelativeOid`] without going through Python.
165    pub fn from_roid(inner: synta::RelativeOid) -> Self {
166        Self {
167            inner,
168            dotted_cache: OnceLock::new(),
169        }
170    }
171}
172
173#[pymethods]
174impl PyRelativeOid {
175    /// Create a RELATIVE-OID from a dotted-decimal string (e.g. ``"1.3.6.1"``).
176    #[new]
177    fn new(s: &str) -> PyResult<Self> {
178        use std::str::FromStr;
179        let inner = synta::RelativeOid::from_str(s).map_err(|e| {
180            pyo3::exceptions::PyValueError::new_err(format!("Invalid RELATIVE-OID: {e:?}"))
181        })?;
182        Ok(Self {
183            inner,
184            dotted_cache: OnceLock::new(),
185        })
186    }
187
188    /// Create a RELATIVE-OID from a list of integer arc components.
189    #[staticmethod]
190    fn from_components(components: Vec<u32>) -> Self {
191        Self {
192            inner: synta::RelativeOid::new(&components),
193            dotted_cache: OnceLock::new(),
194        }
195    }
196
197    /// Parse DER-encoded RELATIVE-OID bytes (tag 0x0D already included).
198    #[staticmethod]
199    fn from_der(data: &[u8]) -> PyResult<Self> {
200        use synta::traits::Decode;
201        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
202        let inner = synta::RelativeOid::decode(&mut dec).map_err(|e| {
203            pyo3::exceptions::PyValueError::new_err(format!("Invalid RELATIVE-OID DER: {e:?}"))
204        })?;
205        Ok(Self {
206            inner,
207            dotted_cache: OnceLock::new(),
208        })
209    }
210
211    /// DER-encode this RELATIVE-OID (tag 0x0D + length + content bytes).
212    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<pyo3::Bound<'py, pyo3::types::PyBytes>> {
213        use synta::traits::Encode;
214        let mut enc = synta::Encoder::new(synta::Encoding::Der);
215        self.inner.encode(&mut enc).map_err(|e| {
216            pyo3::exceptions::PyValueError::new_err(format!("RELATIVE-OID encode failed: {e}"))
217        })?;
218        let bytes = enc.finish().map_err(|e| {
219            pyo3::exceptions::PyValueError::new_err(format!("RELATIVE-OID finish failed: {e}"))
220        })?;
221        Ok(pyo3::types::PyBytes::new(py, &bytes))
222    }
223
224    /// Return the arc components as a tuple of integers.
225    fn components<'py>(&self, py: Python<'py>) -> PyResult<pyo3::Bound<'py, PyTuple>> {
226        PyTuple::new(py, self.inner.components())
227    }
228
229    fn __str__(&self) -> &str {
230        self.dotted_cache.get_or_init(|| self.inner.to_string())
231    }
232
233    fn __repr__(&self) -> String {
234        format!(
235            "RelativeOid('{}')",
236            self.dotted_cache.get_or_init(|| self.inner.to_string())
237        )
238    }
239
240    fn __eq__(&self, other: &pyo3::Bound<'_, pyo3::PyAny>) -> bool {
241        if let Ok(other_roid) = other.extract::<pyo3::PyRef<PyRelativeOid>>() {
242            return self.inner == other_roid.inner;
243        }
244        if let Ok(s) = other.extract::<String>() {
245            return self.dotted_cache.get_or_init(|| self.inner.to_string()) == &s;
246        }
247        false
248    }
249
250    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
251        PyString::new(py, self.dotted_cache.get_or_init(|| self.inner.to_string())).hash()
252    }
253}