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    #[getter]
84    fn tag_number(&self) -> u32 {
85        self.tag_number
86    }
87
88    #[getter]
89    fn tag_class(&self) -> &str {
90        &self.tag_class
91    }
92
93    #[getter]
94    fn is_constructed(&self) -> bool {
95        self.is_constructed
96    }
97
98    #[getter]
99    fn data<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
100        PyBytes::new(py, &self.data)
101    }
102
103    fn __repr__(&self) -> String {
104        format!(
105            "RawElement(tag={}, class='{}', constructed={}, {} bytes)",
106            self.tag_number,
107            self.tag_class,
108            self.is_constructed,
109            self.data.len()
110        )
111    }
112}
113
114/// Map a `TagClass` to the string used by the Python encoder API.
115///
116/// The encoder's `encode_explicit_tag` accepts `"Context"`, `"Application"`, and
117/// `"Private"`.  Using `{:?}` on `TagClass::ContextSpecific` would produce the
118/// divergent string `"ContextSpecific"`, so we map explicitly here.
119pub(crate) fn tag_class_str(class: synta::TagClass) -> &'static str {
120    match class {
121        synta::TagClass::Universal => "Universal",
122        synta::TagClass::Application => "Application",
123        synta::TagClass::ContextSpecific => "Context",
124        synta::TagClass::Private => "Private",
125    }
126}
127
128/// Convert an ASN.1 Element to a Python object.
129///
130/// Sequences and Sets are returned as Python lists.
131/// Tagged elements are returned as `TaggedElement` objects.
132pub fn element_to_pyobject(element: synta::Element<'_>, py: Python) -> PyResult<Py<PyAny>> {
133    use synta::Element;
134    match element {
135        Element::Boolean(v) => Py::new(py, PyBoolean { inner: v }).map(|p| p.into_any()),
136        Element::Integer(v) => Py::new(py, PyInteger { inner: v }).map(|p| p.into_any()),
137        Element::BitString(v) => Py::new(
138            py,
139            PyBitString {
140                inner: v.to_owned(),
141            },
142        )
143        .map(|p| p.into_any()),
144        Element::OctetString(v) => Py::new(
145            py,
146            PyOctetString {
147                inner: v.to_owned(),
148            },
149        )
150        .map(|p| p.into_any()),
151        Element::Null(_) => Py::new(py, PyNull).map(|p| p.into_any()),
152        Element::Real(v) => Py::new(py, PyReal { inner: v }).map(|p| p.into_any()),
153        Element::ObjectIdentifier(v) => {
154            Py::new(py, PyObjectIdentifier::from_oid(v)).map(|p| p.into_any())
155        }
156        Element::Utf8String(v) => Py::new(
157            py,
158            PyUtf8String {
159                inner: v.to_owned(),
160            },
161        )
162        .map(|p| p.into_any()),
163        Element::PrintableString(v) => Py::new(
164            py,
165            PyPrintableString {
166                inner: v.to_owned(),
167            },
168        )
169        .map(|p| p.into_any()),
170        Element::IA5String(v) => Py::new(
171            py,
172            PyIA5String {
173                inner: v.to_owned(),
174            },
175        )
176        .map(|p| p.into_any()),
177        Element::UtcTime(v) => Py::new(py, PyUtcTime { inner: v }).map(|p| p.into_any()),
178        Element::GeneralizedTime(v) => {
179            Py::new(py, PyGeneralizedTime { inner: v }).map(|p| p.into_any())
180        }
181        Element::Sequence(seq) => {
182            let list = pyo3::types::PyList::empty(py);
183            for elem in seq.into_elements().map_err(|e| {
184                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
185            })? {
186                list.append(element_to_pyobject(elem, py)?)?;
187            }
188            Ok(list.into_any().unbind())
189        }
190        Element::Set(set) => {
191            let list = pyo3::types::PyList::empty(py);
192            for elem in set.into_elements().map_err(|e| {
193                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
194            })? {
195                list.append(element_to_pyobject(elem, py)?)?;
196            }
197            Ok(list.into_any().unbind())
198        }
199        Element::Tagged(tag, inner) => {
200            let value = element_to_pyobject(*inner, py)?;
201            Py::new(
202                py,
203                PyTaggedElement {
204                    tag_number: tag.number(),
205                    tag_class: tag_class_str(tag.class()).to_string(),
206                    is_constructed: tag.is_constructed(),
207                    value,
208                },
209            )
210            .map(|p| p.into_any())
211        }
212        Element::NumericString(v) => {
213            Py::new(py, PyNumericString { inner: v }).map(|p| p.into_any())
214        }
215        Element::TeletexString(v) => {
216            Py::new(py, PyTeletexString { inner: v }).map(|p| p.into_any())
217        }
218        Element::VisibleString(v) => {
219            Py::new(py, PyVisibleString { inner: v }).map(|p| p.into_any())
220        }
221        Element::GeneralString(v) => {
222            Py::new(py, PyGeneralString { inner: v }).map(|p| p.into_any())
223        }
224        Element::UniversalString(v) => {
225            Py::new(py, PyUniversalString { inner: v }).map(|p| p.into_any())
226        }
227        Element::BmpString(v) => Py::new(py, PyBmpString { inner: v }).map(|p| p.into_any()),
228        Element::Raw(tag, bytes) => {
229            let raw = PyRawElement {
230                tag_number: tag.number(),
231                tag_class: tag_class_str(tag.class()).to_string(),
232                is_constructed: tag.is_constructed(),
233                data: bytes.to_vec(),
234            };
235            Py::new(py, raw).map(|p| p.into_any())
236        }
237    }
238}