synta-python 0.3.0

Python extension module for the synta ASN.1 library
Documentation
//! Python wrappers for generic ASN.1 elements: TaggedElement, RawElement,
//! and the element_to_pyobject conversion helper.

use pyo3::prelude::*;
use pyo3::types::PyBytes;

use super::primitives::{
    PyBitString, PyBoolean, PyGeneralizedTime, PyInteger, PyNull, PyOctetString, PyReal, PyUtcTime,
};
use super::strings::{
    PyBmpString, PyGeneralString, PyIA5String, PyNumericString, PyPrintableString, PyTeletexString,
    PyUniversalString, PyUtf8String, PyVisibleString,
};
use super::PyObjectIdentifier;

/// Python representation of an explicitly-tagged ASN.1 element.
///
/// Returned by ``decode_any()`` when a context-specific, application, or private
/// tag wraps another element.  Callers can use ``isinstance(x, TaggedElement)``
/// to detect tagged values and access the inner element via ``.value``.
#[pyclass(name = "TaggedElement")]
#[derive(Debug)]
pub struct PyTaggedElement {
    pub(crate) tag_number: u32,
    pub(crate) tag_class: String,
    pub(crate) is_constructed: bool,
    pub(crate) value: Py<PyAny>,
}

#[pymethods]
impl PyTaggedElement {
    /// The tag number
    #[getter]
    fn tag_number(&self) -> u32 {
        self.tag_number
    }

    /// The tag class: "Universal", "Application", "Context", or "Private"
    #[getter]
    fn tag_class(&self) -> &str {
        &self.tag_class
    }

    /// Whether the tag is constructed (True) or primitive (False)
    #[getter]
    fn is_constructed(&self) -> bool {
        self.is_constructed
    }

    /// The wrapped inner element
    #[getter]
    fn value(&self, py: Python<'_>) -> Py<PyAny> {
        self.value.clone_ref(py)
    }

    fn __repr__(&self) -> String {
        format!(
            "TaggedElement(tag={}, class='{}', constructed={})",
            self.tag_number, self.tag_class, self.is_constructed
        )
    }
}

/// Python representation of a raw ASN.1 element with an unsupported or unknown tag.
///
/// Returned by ``decode_any()`` when an unrecognised universal tag is encountered
/// instead of raising an error, enabling forward-compatible parsing.
#[pyclass(name = "RawElement")]
#[derive(Debug, Clone)]
pub struct PyRawElement {
    /// Tag number
    pub(crate) tag_number: u32,
    /// Tag class as a string: "Universal", "Application", "Context", or "Private"
    pub(crate) tag_class: String,
    /// Whether the element is constructed (True) or primitive (False)
    pub(crate) is_constructed: bool,
    /// Raw content bytes (not including the outer tag or length)
    pub(crate) data: Vec<u8>,
}

#[pymethods]
impl PyRawElement {
    /// The tag number.
    #[getter]
    fn tag_number(&self) -> u32 {
        self.tag_number
    }

    /// The tag class: ``"Universal"``, ``"Application"``, ``"Context"``, or ``"Private"``.
    #[getter]
    fn tag_class(&self) -> &str {
        &self.tag_class
    }

    /// Whether the tag is constructed (``True``) or primitive (``False``).
    #[getter]
    fn is_constructed(&self) -> bool {
        self.is_constructed
    }

    /// Raw content bytes, not including the outer tag or length octets.
    #[getter]
    fn data<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.data)
    }

    fn __repr__(&self) -> String {
        format!(
            "RawElement(tag={}, class='{}', constructed={}, {} bytes)",
            self.tag_number,
            self.tag_class,
            self.is_constructed,
            self.data.len()
        )
    }
}

/// Map a `TagClass` to the string used by the Python encoder API.
///
/// The encoder's `encode_explicit_tag` accepts `"Context"`, `"Application"`, and
/// `"Private"`.  Using `{:?}` on `TagClass::ContextSpecific` would produce the
/// divergent string `"ContextSpecific"`, so we map explicitly here.
pub(crate) fn tag_class_str(class: synta::TagClass) -> &'static str {
    match class {
        synta::TagClass::Universal => "Universal",
        synta::TagClass::Application => "Application",
        synta::TagClass::ContextSpecific => "Context",
        synta::TagClass::Private => "Private",
    }
}

/// Convert an ASN.1 Element to a Python object.
///
/// Sequences and Sets are returned as Python lists.
/// Tagged elements are returned as `TaggedElement` objects.
pub fn element_to_pyobject(element: synta::Element<'_>, py: Python) -> PyResult<Py<PyAny>> {
    use synta::Element;
    match element {
        Element::Boolean(v) => Py::new(py, PyBoolean { inner: v }).map(|p| p.into_any()),
        Element::Integer(v) => Py::new(py, PyInteger { inner: v }).map(|p| p.into_any()),
        Element::BitString(v) => Py::new(
            py,
            PyBitString {
                inner: v.to_owned(),
            },
        )
        .map(|p| p.into_any()),
        Element::OctetString(v) => Py::new(
            py,
            PyOctetString {
                inner: v.to_owned(),
            },
        )
        .map(|p| p.into_any()),
        Element::Null(_) => Py::new(py, PyNull).map(|p| p.into_any()),
        Element::Real(v) => Py::new(py, PyReal { inner: v }).map(|p| p.into_any()),
        Element::ObjectIdentifier(v) => {
            Py::new(py, PyObjectIdentifier::from_oid(v)).map(|p| p.into_any())
        }
        Element::Utf8String(v) => Py::new(
            py,
            PyUtf8String {
                inner: v.to_owned(),
            },
        )
        .map(|p| p.into_any()),
        Element::PrintableString(v) => Py::new(
            py,
            PyPrintableString {
                inner: v.to_owned(),
            },
        )
        .map(|p| p.into_any()),
        Element::IA5String(v) => Py::new(
            py,
            PyIA5String {
                inner: v.to_owned(),
            },
        )
        .map(|p| p.into_any()),
        Element::UtcTime(v) => Py::new(py, PyUtcTime { inner: v }).map(|p| p.into_any()),
        Element::GeneralizedTime(v) => {
            Py::new(py, PyGeneralizedTime { inner: v }).map(|p| p.into_any())
        }
        Element::Sequence(seq) => {
            let list = pyo3::types::PyList::empty(py);
            for elem in seq.into_elements().map_err(|e| {
                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
            })? {
                list.append(element_to_pyobject(elem, py)?)?;
            }
            Ok(list.into_any().unbind())
        }
        Element::Set(set) => {
            let list = pyo3::types::PyList::empty(py);
            for elem in set.into_elements().map_err(|e| {
                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
            })? {
                list.append(element_to_pyobject(elem, py)?)?;
            }
            Ok(list.into_any().unbind())
        }
        Element::Tagged(tag, inner) => {
            let value = element_to_pyobject(*inner, py)?;
            Py::new(
                py,
                PyTaggedElement {
                    tag_number: tag.number(),
                    tag_class: tag_class_str(tag.class()).to_string(),
                    is_constructed: tag.is_constructed(),
                    value,
                },
            )
            .map(|p| p.into_any())
        }
        Element::NumericString(v) => {
            Py::new(py, PyNumericString { inner: v }).map(|p| p.into_any())
        }
        Element::TeletexString(v) => {
            Py::new(py, PyTeletexString { inner: v }).map(|p| p.into_any())
        }
        Element::VisibleString(v) => {
            Py::new(py, PyVisibleString { inner: v }).map(|p| p.into_any())
        }
        Element::GeneralString(v) => {
            Py::new(py, PyGeneralString { inner: v }).map(|p| p.into_any())
        }
        Element::UniversalString(v) => {
            Py::new(py, PyUniversalString { inner: v }).map(|p| p.into_any())
        }
        Element::BmpString(v) => Py::new(py, PyBmpString { inner: v }).map(|p| p.into_any()),
        Element::Raw(tag, bytes) => {
            let raw = PyRawElement {
                tag_number: tag.number(),
                tag_class: tag_class_str(tag.class()).to_string(),
                is_constructed: tag.is_constructed(),
                data: bytes.to_vec(),
            };
            Py::new(py, raw).map(|p| p.into_any())
        }
    }
}