Skip to main content

_synta/types/
elements.rs

1//! Python wrappers for generic ASN.1 elements: TaggedElement, RawElement,
2//! and the element_to_pyobject conversion helper.
3
4use pyo3::prelude::*;
5use pyo3::types::PyBytes;
6
7use super::primitives::{
8    PyBitString, PyBoolean, PyGeneralizedTime, PyInteger, PyNull, PyOctetString, PyReal, PyUtcTime,
9};
10use super::strings::{
11    PyBmpString, PyGeneralString, PyIA5String, PyNumericString, PyPrintableString, PyTeletexString,
12    PyUniversalString, PyUtf8String, PyVisibleString,
13};
14use super::PyObjectIdentifier;
15
16/// Python representation of an explicitly-tagged ASN.1 element.
17///
18/// Returned by ``decode_any()`` when a context-specific, application, or private
19/// tag wraps another element.  Callers can use ``isinstance(x, TaggedElement)``
20/// to detect tagged values and access the inner element via ``.value``.
21#[pyclass(name = "TaggedElement")]
22#[derive(Debug)]
23pub struct PyTaggedElement {
24    pub(crate) tag_number: u32,
25    pub(crate) tag_class: String,
26    pub(crate) is_constructed: bool,
27    pub(crate) value: Py<PyAny>,
28}
29
30#[pymethods]
31impl PyTaggedElement {
32    /// The tag number
33    #[getter]
34    fn tag_number(&self) -> u32 {
35        self.tag_number
36    }
37
38    /// The tag class: "Universal", "Application", "Context", or "Private"
39    #[getter]
40    fn tag_class(&self) -> &str {
41        &self.tag_class
42    }
43
44    /// Whether the tag is constructed (True) or primitive (False)
45    #[getter]
46    fn is_constructed(&self) -> bool {
47        self.is_constructed
48    }
49
50    /// The wrapped inner element
51    #[getter]
52    fn value(&self, py: Python<'_>) -> Py<PyAny> {
53        self.value.clone_ref(py)
54    }
55
56    fn __repr__(&self) -> String {
57        format!(
58            "TaggedElement(tag={}, class='{}', constructed={})",
59            self.tag_number, self.tag_class, self.is_constructed
60        )
61    }
62}
63
64/// Python representation of a raw ASN.1 element with an unsupported or unknown tag.
65///
66/// Returned by ``decode_any()`` when an unrecognised universal tag is encountered
67/// instead of raising an error, enabling forward-compatible parsing.
68#[pyclass(name = "RawElement")]
69#[derive(Debug, Clone)]
70pub struct PyRawElement {
71    /// Tag number
72    pub(crate) tag_number: u32,
73    /// Tag class as a string: "Universal", "Application", "Context", or "Private"
74    pub(crate) tag_class: String,
75    /// Whether the element is constructed (True) or primitive (False)
76    pub(crate) is_constructed: bool,
77    /// Raw content bytes (not including the outer tag or length)
78    pub(crate) data: Vec<u8>,
79}
80
81#[pymethods]
82impl PyRawElement {
83    /// The tag number.
84    #[getter]
85    fn tag_number(&self) -> u32 {
86        self.tag_number
87    }
88
89    /// The tag class: ``"Universal"``, ``"Application"``, ``"Context"``, or ``"Private"``.
90    #[getter]
91    fn tag_class(&self) -> &str {
92        &self.tag_class
93    }
94
95    /// Whether the tag is constructed (``True``) or primitive (``False``).
96    #[getter]
97    fn is_constructed(&self) -> bool {
98        self.is_constructed
99    }
100
101    /// Raw content bytes, not including the outer tag or length octets.
102    #[getter]
103    fn data<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
104        PyBytes::new(py, &self.data)
105    }
106
107    fn __repr__(&self) -> String {
108        format!(
109            "RawElement(tag={}, class='{}', constructed={}, {} bytes)",
110            self.tag_number,
111            self.tag_class,
112            self.is_constructed,
113            self.data.len()
114        )
115    }
116}
117
118/// Map a `TagClass` to the string used by the Python encoder API.
119///
120/// The encoder's `encode_explicit_tag` accepts `"Context"`, `"Application"`, and
121/// `"Private"`.  Using `{:?}` on `TagClass::ContextSpecific` would produce the
122/// divergent string `"ContextSpecific"`, so we map explicitly here.
123pub(crate) fn tag_class_str(class: synta::TagClass) -> &'static str {
124    match class {
125        synta::TagClass::Universal => "Universal",
126        synta::TagClass::Application => "Application",
127        synta::TagClass::ContextSpecific => "Context",
128        synta::TagClass::Private => "Private",
129    }
130}
131
132/// Convert an ASN.1 Element to a Python object.
133///
134/// Sequences and Sets are returned as Python lists.
135/// Tagged elements are returned as `TaggedElement` objects.
136pub fn element_to_pyobject(element: synta::Element<'_>, py: Python) -> PyResult<Py<PyAny>> {
137    use synta::Element;
138    match element {
139        Element::Boolean(v) => Py::new(py, PyBoolean { inner: v }).map(|p| p.into_any()),
140        Element::Integer(v) => Py::new(py, PyInteger { inner: v }).map(|p| p.into_any()),
141        Element::BitString(v) => Py::new(
142            py,
143            PyBitString {
144                inner: v.to_owned(),
145            },
146        )
147        .map(|p| p.into_any()),
148        Element::OctetString(v) => Py::new(
149            py,
150            PyOctetString {
151                inner: v.to_owned(),
152            },
153        )
154        .map(|p| p.into_any()),
155        Element::Null(_) => Py::new(py, PyNull).map(|p| p.into_any()),
156        Element::Real(v) => Py::new(py, PyReal { inner: v }).map(|p| p.into_any()),
157        Element::ObjectIdentifier(v) => {
158            Py::new(py, PyObjectIdentifier::from_oid(v)).map(|p| p.into_any())
159        }
160        Element::Utf8String(v) => Py::new(
161            py,
162            PyUtf8String {
163                inner: v.to_owned(),
164            },
165        )
166        .map(|p| p.into_any()),
167        Element::PrintableString(v) => Py::new(
168            py,
169            PyPrintableString {
170                inner: v.to_owned(),
171            },
172        )
173        .map(|p| p.into_any()),
174        Element::IA5String(v) => Py::new(
175            py,
176            PyIA5String {
177                inner: v.to_owned(),
178            },
179        )
180        .map(|p| p.into_any()),
181        Element::UtcTime(v) => Py::new(py, PyUtcTime { inner: v }).map(|p| p.into_any()),
182        Element::GeneralizedTime(v) => {
183            Py::new(py, PyGeneralizedTime { inner: v }).map(|p| p.into_any())
184        }
185        Element::Sequence(seq) => {
186            let list = pyo3::types::PyList::empty(py);
187            for elem in seq.into_elements().map_err(|e| {
188                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
189            })? {
190                list.append(element_to_pyobject(elem, py)?)?;
191            }
192            Ok(list.into_any().unbind())
193        }
194        Element::Set(set) => {
195            let list = pyo3::types::PyList::empty(py);
196            for elem in set.into_elements().map_err(|e| {
197                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
198            })? {
199                list.append(element_to_pyobject(elem, py)?)?;
200            }
201            Ok(list.into_any().unbind())
202        }
203        Element::Tagged(tag, inner) => {
204            let value = element_to_pyobject(*inner, py)?;
205            Py::new(
206                py,
207                PyTaggedElement {
208                    tag_number: tag.number(),
209                    tag_class: tag_class_str(tag.class()).to_string(),
210                    is_constructed: tag.is_constructed(),
211                    value,
212                },
213            )
214            .map(|p| p.into_any())
215        }
216        Element::NumericString(v) => {
217            Py::new(py, PyNumericString { inner: v }).map(|p| p.into_any())
218        }
219        Element::TeletexString(v) => {
220            Py::new(py, PyTeletexString { inner: v }).map(|p| p.into_any())
221        }
222        Element::VisibleString(v) => {
223            Py::new(py, PyVisibleString { inner: v }).map(|p| p.into_any())
224        }
225        Element::GeneralString(v) => {
226            Py::new(py, PyGeneralString { inner: v }).map(|p| p.into_any())
227        }
228        Element::UniversalString(v) => {
229            Py::new(py, PyUniversalString { inner: v }).map(|p| p.into_any())
230        }
231        Element::BmpString(v) => Py::new(py, PyBmpString { inner: v }).map(|p| p.into_any()),
232        Element::Raw(tag, bytes) => {
233            let raw = PyRawElement {
234                tag_number: tag.number(),
235                tag_class: tag_class_str(tag.class()).to_string(),
236                is_constructed: tag.is_constructed(),
237                data: bytes.to_vec(),
238            };
239            Py::new(py, raw).map(|p| p.into_any())
240        }
241    }
242}