synta-python 0.2.5

Python extension module for the synta ASN.1 library
Documentation
//! Python wrappers for ASN.1 types

use std::str::FromStr;
use std::sync::OnceLock;

use pyo3::prelude::*;
use pyo3::types::{PyString, PyTuple};

use synta::ObjectIdentifier;

pub mod elements;
pub mod primitives;
pub mod strings;

pub use elements::{element_to_pyobject, PyRawElement, PyTaggedElement};
pub use primitives::{
    PyBitString, PyBoolean, PyGeneralizedTime, PyInteger, PyNull, PyOctetString, PyReal, PyUtcTime,
};
pub use strings::{
    PyBmpString, PyGeneralString, PyIA5String, PyNumericString, PyPrintableString, PyTeletexString,
    PyUniversalString, PyUtf8String, PyVisibleString,
};

/// Python wrapper for ASN.1 OBJECT IDENTIFIER.
///
/// Wraps a parsed ``synta::ObjectIdentifier`` value.  Instances are obtained
/// from certificate/CSR/CRL getters such as
/// :attr:`Certificate.signature_algorithm_oid`, from the
/// :class:`Decoder`, or constructed directly:
///
/// ```python
/// oid = synta.ObjectIdentifier("2.5.4.3")            # from dotted string
/// oid = synta.ObjectIdentifier.from_components([2, 5, 4, 3])
/// oid = synta.ObjectIdentifier.from_der_value(raw)   # from implicit-tag bytes
/// ```
#[pyclass(frozen, name = "ObjectIdentifier")]
#[derive(Debug, Clone)]
pub struct PyObjectIdentifier {
    pub(crate) inner: ObjectIdentifier,
    /// Lazily-cached dotted-decimal representation, e.g. `"2.5.4.3"`.
    /// Populated on first access by `__str__`, `__hash__`, or `__eq__`.
    dotted_cache: OnceLock<String>,
}

impl PyObjectIdentifier {
    /// Construct from a Rust [`ObjectIdentifier`] without going through Python.
    /// Used by `synta-python`'s decoder and type-mapping code.
    pub fn from_oid(inner: ObjectIdentifier) -> Self {
        Self {
            inner,
            dotted_cache: OnceLock::new(),
        }
    }
}

#[pymethods]
impl PyObjectIdentifier {
    /// Create a new OID from dotted-decimal string notation (e.g. ``"2.5.4.3"``).
    #[new]
    fn new(oid_str: &str) -> PyResult<Self> {
        let inner = ObjectIdentifier::from_str(oid_str)
            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid OID: {e:?}")))?;
        Ok(Self {
            inner,
            dotted_cache: OnceLock::new(),
        })
    }

    /// Create an OID from a list of integer arc components.
    #[staticmethod]
    fn from_components(components: Vec<u32>) -> PyResult<Self> {
        let inner = ObjectIdentifier::new(&components)
            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid OID: {e:?}")))?;
        Ok(Self {
            inner,
            dotted_cache: OnceLock::new(),
        })
    }

    /// Parse raw OID content bytes — the value bytes inside an OID TLV with
    /// the tag (``0x06``) and length already stripped.
    ///
    /// Use this after :meth:`Decoder.decode_implicit_tag` for
    /// ``registeredID [8] IMPLICIT OID`` in a GeneralName CHOICE:
    ///
    /// ```python
    ///     child = dec.decode_implicit_tag(8, "Context")
    ///     oid = synta.ObjectIdentifier.from_der_value(child.remaining_bytes())
    /// ```
    ///
    /// Raises :exc:`ValueError` if ``data`` is empty or invalid.
    #[staticmethod]
    fn from_der_value(data: &[u8]) -> PyResult<Self> {
        let inner = ObjectIdentifier::from_content_bytes(data).map_err(|e| {
            pyo3::exceptions::PyValueError::new_err(format!("Invalid OID content: {e:?}"))
        })?;
        Ok(Self {
            inner,
            dotted_cache: OnceLock::new(),
        })
    }

    /// Return the OID arc components as a tuple of integers.
    fn components<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
        PyTuple::new(py, self.inner.components())
    }

    fn __str__(&self) -> &str {
        self.dotted_cache.get_or_init(|| self.inner.to_string())
    }

    fn __repr__(&self) -> String {
        format!(
            "ObjectIdentifier('{}')",
            self.dotted_cache.get_or_init(|| self.inner.to_string())
        )
    }

    /// Equality with another :class:`ObjectIdentifier` or a dotted-decimal
    /// string.  Enables ``oid == "2.5.4.3"`` and ``"2.5.4.3" == oid``.
    fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
        if let Ok(other_oid) = other.extract::<PyRef<PyObjectIdentifier>>() {
            return self.inner == other_oid.inner;
        }
        if let Ok(s) = other.extract::<String>() {
            return self.dotted_cache.get_or_init(|| self.inner.to_string()) == &s;
        }
        false
    }

    /// Hash consistent with ``hash(str(oid))`` so that OID objects and their
    /// dotted-decimal string equivalents collide in the same set/dict bucket,
    /// enabling ``oid in {"2.5.4.3"}`` to work correctly.
    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
        PyString::new(py, self.dotted_cache.get_or_init(|| self.inner.to_string())).hash()
    }
}