Skip to main content

_synta/
types.rs

1//! Python wrappers for ASN.1 types
2
3use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::str::FromStr;
6use std::sync::OnceLock;
7
8use pyo3::exceptions::{PyOverflowError, PyValueError};
9use pyo3::prelude::*;
10use pyo3::types::{PyBytes, PyString, PyTuple};
11
12use synta::{
13    BitString, BmpString, Boolean, GeneralString, GeneralizedTime, IA5String, Integer,
14    NumericString, ObjectIdentifier, OctetString, PrintableString, Real, TeletexString,
15    UniversalString, UtcTime, Utf8String, VisibleString,
16};
17
18/// Python wrapper for ASN.1 OBJECT IDENTIFIER.
19///
20/// Wraps a parsed ``synta::ObjectIdentifier`` value.  Instances are obtained
21/// from certificate/CSR/CRL getters such as
22/// :attr:`Certificate.signature_algorithm_oid`, from the
23/// :class:`Decoder`, or constructed directly:
24///
25/// ```python
26/// oid = synta.ObjectIdentifier("2.5.4.3")            # from dotted string
27/// oid = synta.ObjectIdentifier.from_components([2, 5, 4, 3])
28/// oid = synta.ObjectIdentifier.from_der_value(raw)   # from implicit-tag bytes
29/// ```
30#[pyclass(frozen, name = "ObjectIdentifier")]
31#[derive(Debug, Clone)]
32pub struct PyObjectIdentifier {
33    pub(crate) inner: ObjectIdentifier,
34    /// Lazily-cached dotted-decimal representation, e.g. `"2.5.4.3"`.
35    /// Populated on first access by `__str__`, `__hash__`, or `__eq__`.
36    dotted_cache: OnceLock<String>,
37}
38
39impl PyObjectIdentifier {
40    /// Construct from a Rust [`ObjectIdentifier`] without going through Python.
41    /// Used by `synta-python`'s decoder and type-mapping code.
42    pub fn from_oid(inner: ObjectIdentifier) -> Self {
43        Self {
44            inner,
45            dotted_cache: OnceLock::new(),
46        }
47    }
48}
49
50#[pymethods]
51impl PyObjectIdentifier {
52    /// Create a new OID from dotted-decimal string notation (e.g. ``"2.5.4.3"``).
53    #[new]
54    fn new(oid_str: &str) -> PyResult<Self> {
55        let inner = ObjectIdentifier::from_str(oid_str)
56            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid OID: {e:?}")))?;
57        Ok(Self {
58            inner,
59            dotted_cache: OnceLock::new(),
60        })
61    }
62
63    /// Create an OID from a list of integer arc components.
64    #[staticmethod]
65    fn from_components(components: Vec<u32>) -> PyResult<Self> {
66        let inner = ObjectIdentifier::new(&components)
67            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid OID: {e:?}")))?;
68        Ok(Self {
69            inner,
70            dotted_cache: OnceLock::new(),
71        })
72    }
73
74    /// Parse raw OID content bytes — the value bytes inside an OID TLV with
75    /// the tag (``0x06``) and length already stripped.
76    ///
77    /// Use this after :meth:`Decoder.decode_implicit_tag` for
78    /// ``registeredID [8] IMPLICIT OID`` in a GeneralName CHOICE:
79    ///
80    /// ```python
81    ///     child = dec.decode_implicit_tag(8, "Context")
82    ///     oid = synta.ObjectIdentifier.from_der_value(child.remaining_bytes())
83    /// ```
84    ///
85    /// Raises :exc:`ValueError` if ``data`` is empty or invalid.
86    #[staticmethod]
87    fn from_der_value(data: &[u8]) -> PyResult<Self> {
88        let inner = ObjectIdentifier::from_content_bytes(data).map_err(|e| {
89            pyo3::exceptions::PyValueError::new_err(format!("Invalid OID content: {e:?}"))
90        })?;
91        Ok(Self {
92            inner,
93            dotted_cache: OnceLock::new(),
94        })
95    }
96
97    /// Return the OID arc components as a tuple of integers.
98    fn components<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
99        PyTuple::new(py, self.inner.components())
100    }
101
102    fn __str__(&self) -> &str {
103        self.dotted_cache.get_or_init(|| self.inner.to_string())
104    }
105
106    fn __repr__(&self) -> String {
107        format!(
108            "ObjectIdentifier('{}')",
109            self.dotted_cache.get_or_init(|| self.inner.to_string())
110        )
111    }
112
113    /// Equality with another :class:`ObjectIdentifier` or a dotted-decimal
114    /// string.  Enables ``oid == "2.5.4.3"`` and ``"2.5.4.3" == oid``.
115    fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
116        if let Ok(other_oid) = other.extract::<PyRef<PyObjectIdentifier>>() {
117            return self.inner == other_oid.inner;
118        }
119        if let Ok(s) = other.extract::<String>() {
120            return self.dotted_cache.get_or_init(|| self.inner.to_string()) == &s;
121        }
122        false
123    }
124
125    /// Hash consistent with ``hash(str(oid))`` so that OID objects and their
126    /// dotted-decimal string equivalents collide in the same set/dict bucket,
127    /// enabling ``oid in {"2.5.4.3"}`` to work correctly.
128    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
129        PyString::new(py, self.dotted_cache.get_or_init(|| self.inner.to_string())).hash()
130    }
131}
132
133/// Python wrapper for ASN.1 INTEGER
134#[pyclass(name = "Integer")]
135#[derive(Debug, Clone)]
136pub struct PyInteger {
137    pub(crate) inner: Integer,
138}
139
140#[pymethods]
141impl PyInteger {
142    /// Create a new Integer from a Python int
143    #[new]
144    fn new(value: i64) -> Self {
145        Self {
146            inner: Integer::from_i64(value),
147        }
148    }
149
150    /// Convert to Python int (i64)
151    fn to_int(&self) -> PyResult<i64> {
152        self.inner
153            .as_i64()
154            .map_err(|_| PyOverflowError::new_err("Integer too large for i64"))
155    }
156
157    /// Get the raw bytes (big-endian two's complement)
158    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
159        PyBytes::new(py, self.inner.as_bytes())
160    }
161
162    /// Create Integer from raw bytes
163    #[staticmethod]
164    fn from_bytes(bytes: &[u8]) -> Self {
165        Self {
166            inner: Integer::from_bytes(bytes),
167        }
168    }
169
170    /// Create Integer from an unsigned 64-bit value
171    #[staticmethod]
172    fn from_u64(value: u64) -> Self {
173        Self {
174            inner: Integer::from_u64(value),
175        }
176    }
177
178    /// Convert to Python int (i128) for larger integers
179    fn to_i128(&self) -> PyResult<i128> {
180        self.inner
181            .as_i128()
182            .map_err(|_| PyOverflowError::new_err("Integer too large for i128"))
183    }
184
185    fn __eq__(&self, other: &Self) -> bool {
186        // DER encodes integers with minimal bytes (no redundant leading zeros),
187        // so comparing the raw bytes is equivalent to comparing by value.
188        self.inner.as_bytes() == other.inner.as_bytes()
189    }
190
191    fn __hash__(&self) -> u64 {
192        let mut h = DefaultHasher::new();
193        self.inner.as_bytes().hash(&mut h);
194        h.finish()
195    }
196
197    fn __repr__(&self) -> PyResult<String> {
198        match self.inner.as_i64() {
199            Ok(val) => Ok(format!("Integer({})", val)),
200            Err(_) => Ok(format!("Integer(<{} bytes>)", self.inner.as_bytes().len())),
201        }
202    }
203
204    fn __str__(&self) -> PyResult<String> {
205        match self.inner.as_i64() {
206            Ok(val) => Ok(val.to_string()),
207            Err(_) => Ok(format!("<integer {} bytes>", self.inner.as_bytes().len())),
208        }
209    }
210}
211
212/// Python wrapper for ASN.1 OCTET STRING
213#[pyclass(name = "OctetString")]
214#[derive(Debug, Clone)]
215pub struct PyOctetString {
216    pub(crate) inner: OctetString,
217}
218
219#[pymethods]
220impl PyOctetString {
221    /// Create a new OctetString from bytes
222    #[new]
223    fn new(data: Vec<u8>) -> Self {
224        Self {
225            inner: OctetString::new(data),
226        }
227    }
228
229    /// Get the bytes as a Python bytes object
230    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
231        PyBytes::new(py, self.inner.as_bytes())
232    }
233
234    /// Get the length
235    fn __len__(&self) -> usize {
236        self.inner.as_bytes().len()
237    }
238
239    fn __eq__(&self, other: &Self) -> bool {
240        self.inner == other.inner
241    }
242
243    fn __repr__(&self) -> String {
244        format!("OctetString(<{} bytes>)", self.inner.as_bytes().len())
245    }
246}
247
248/// Python wrapper for ASN.1 BIT STRING
249#[pyclass(name = "BitString")]
250#[derive(Debug, Clone)]
251pub struct PyBitString {
252    pub(crate) inner: BitString,
253}
254
255#[pymethods]
256impl PyBitString {
257    /// Create a new BitString from bytes and unused bits count
258    #[new]
259    fn new(data: Vec<u8>, unused_bits: u8) -> PyResult<Self> {
260        if unused_bits > 7 {
261            return Err(PyValueError::new_err("unused_bits must be 0-7"));
262        }
263        let inner = BitString::new(data, unused_bits)
264            .map_err(|e| PyValueError::new_err(format!("Invalid BitString: {:?}", e)))?;
265        Ok(Self { inner })
266    }
267
268    /// Get the bytes
269    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
270        PyBytes::new(py, self.inner.as_bytes())
271    }
272
273    /// Get the number of unused bits in the last byte
274    fn unused_bits(&self) -> u8 {
275        self.inner.unused_bits()
276    }
277
278    /// Get the number of bits
279    fn bit_len(&self) -> usize {
280        self.inner.bit_len()
281    }
282
283    fn __len__(&self) -> usize {
284        self.inner.bit_len()
285    }
286
287    fn __eq__(&self, other: &Self) -> bool {
288        self.inner == other.inner
289    }
290
291    fn __repr__(&self) -> String {
292        format!(
293            "BitString(<{} bits, {} unused>)",
294            self.inner.bit_len(),
295            self.inner.unused_bits()
296        )
297    }
298}
299
300/// Python wrapper for ASN.1 BOOLEAN
301#[pyclass(name = "Boolean")]
302#[derive(Debug, Clone, Copy)]
303pub struct PyBoolean {
304    pub(crate) inner: Boolean,
305}
306
307#[pymethods]
308impl PyBoolean {
309    /// Create a new Boolean
310    #[new]
311    fn new(value: bool) -> Self {
312        Self {
313            inner: Boolean::new(value),
314        }
315    }
316
317    /// Get the boolean value
318    fn value(&self) -> bool {
319        self.inner.value()
320    }
321
322    fn __bool__(&self) -> bool {
323        self.inner.value()
324    }
325
326    fn __eq__(&self, other: &Self) -> bool {
327        self.inner.value() == other.inner.value()
328    }
329
330    fn __hash__(&self) -> isize {
331        // Matches Python's hash(True) == 1, hash(False) == 0.
332        self.inner.value() as isize
333    }
334
335    fn __repr__(&self) -> String {
336        format!("Boolean({})", self.inner.value())
337    }
338}
339
340/// Python wrapper for ASN.1 UTCTime
341#[pyclass(name = "UtcTime")]
342#[derive(Debug, Clone)]
343pub struct PyUtcTime {
344    pub(crate) inner: UtcTime,
345}
346
347#[pymethods]
348impl PyUtcTime {
349    /// Create a new UTCTime (year must be in 1950-2049 range)
350    #[new]
351    fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> PyResult<Self> {
352        let inner = UtcTime::new(year, month, day, hour, minute, second)
353            .map_err(|e| PyValueError::new_err(format!("Invalid UTCTime: {:?}", e)))?;
354        Ok(Self { inner })
355    }
356
357    #[getter]
358    fn year(&self) -> u16 {
359        self.inner.year
360    }
361
362    #[getter]
363    fn month(&self) -> u8 {
364        self.inner.month
365    }
366
367    #[getter]
368    fn day(&self) -> u8 {
369        self.inner.day
370    }
371
372    #[getter]
373    fn hour(&self) -> u8 {
374        self.inner.hour
375    }
376
377    #[getter]
378    fn minute(&self) -> u8 {
379        self.inner.minute
380    }
381
382    #[getter]
383    fn second(&self) -> u8 {
384        self.inner.second
385    }
386
387    fn __str__(&self) -> String {
388        self.inner.to_string()
389    }
390
391    fn __repr__(&self) -> String {
392        format!("UtcTime('{}')", self.inner)
393    }
394}
395
396impl std::fmt::Display for PyUtcTime {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        write!(f, "{}", self.inner)
399    }
400}
401
402/// Python wrapper for ASN.1 GeneralizedTime
403#[pyclass(name = "GeneralizedTime")]
404#[derive(Debug, Clone)]
405pub struct PyGeneralizedTime {
406    pub(crate) inner: GeneralizedTime,
407}
408
409#[pymethods]
410impl PyGeneralizedTime {
411    /// Create a new GeneralizedTime
412    ///
413    /// Args:
414    ///     milliseconds: Optional fractional seconds in milliseconds (0-999)
415    #[new]
416    fn new(
417        year: u16,
418        month: u8,
419        day: u8,
420        hour: u8,
421        minute: u8,
422        second: u8,
423        milliseconds: Option<u16>,
424    ) -> PyResult<Self> {
425        let inner = GeneralizedTime::new(year, month, day, hour, minute, second, milliseconds)
426            .map_err(|e| PyValueError::new_err(format!("Invalid GeneralizedTime: {:?}", e)))?;
427        Ok(Self { inner })
428    }
429
430    #[getter]
431    fn year(&self) -> u16 {
432        self.inner.year
433    }
434
435    #[getter]
436    fn month(&self) -> u8 {
437        self.inner.month
438    }
439
440    #[getter]
441    fn day(&self) -> u8 {
442        self.inner.day
443    }
444
445    #[getter]
446    fn hour(&self) -> u8 {
447        self.inner.hour
448    }
449
450    #[getter]
451    fn minute(&self) -> u8 {
452        self.inner.minute
453    }
454
455    #[getter]
456    fn second(&self) -> u8 {
457        self.inner.second
458    }
459
460    #[getter]
461    fn milliseconds(&self) -> Option<u16> {
462        self.inner.milliseconds
463    }
464
465    fn __str__(&self) -> String {
466        self.inner.to_string()
467    }
468
469    fn __repr__(&self) -> String {
470        format!("GeneralizedTime('{}')", self.inner)
471    }
472}
473
474impl std::fmt::Display for PyGeneralizedTime {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        write!(f, "{}", self.inner)
477    }
478}
479
480/// Python wrapper for ASN.1 REAL
481#[pyclass(name = "Real")]
482#[derive(Debug, Clone, Copy)]
483pub struct PyReal {
484    pub(crate) inner: Real,
485}
486
487#[pymethods]
488impl PyReal {
489    /// Create a new Real from a Python float
490    #[new]
491    fn new(value: f64) -> Self {
492        Self {
493            inner: Real::new(value),
494        }
495    }
496
497    /// Get the f64 value
498    fn value(&self) -> f64 {
499        self.inner.value()
500    }
501
502    /// Return True if this is positive infinity
503    fn is_infinite(&self) -> bool {
504        self.inner.value().is_infinite()
505    }
506
507    /// Return True if this is NaN (not-a-number)
508    fn is_nan(&self) -> bool {
509        self.inner.value().is_nan()
510    }
511
512    /// Return True if the value is finite
513    fn is_finite(&self) -> bool {
514        self.inner.value().is_finite()
515    }
516
517    fn __float__(&self) -> f64 {
518        self.inner.value()
519    }
520
521    fn __repr__(&self) -> String {
522        format!("Real({})", self.inner.value())
523    }
524
525    fn __str__(&self) -> String {
526        self.inner.value().to_string()
527    }
528
529    fn __eq__(&self, other: &Self) -> bool {
530        // NaN != NaN by IEEE 754 — mirror Python float behaviour
531        self.inner.value() == other.inner.value()
532    }
533
534    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
535        // Delegate to Python's float.__hash__ so that hash(Real(1.0)) == hash(1.0) == hash(1),
536        // matching the Python data model guarantee for numeric types.
537        self.inner.value().into_pyobject(py)?.hash()
538    }
539}
540
541/// Python wrapper for ASN.1 NULL
542#[pyclass(name = "Null")]
543#[derive(Debug, Clone, Copy)]
544pub struct PyNull;
545
546#[pymethods]
547impl PyNull {
548    #[new]
549    fn new() -> Self {
550        Self
551    }
552
553    fn __repr__(&self) -> String {
554        "Null()".to_string()
555    }
556
557    fn __eq__(&self, _other: &Self) -> bool {
558        true
559    }
560
561    fn __hash__(&self) -> isize {
562        // All Null values are equal; use a fixed hash consistent with __eq__.
563        0
564    }
565}
566
567/// Python wrapper for ASN.1 UTF8String
568#[pyclass(name = "Utf8String")]
569#[derive(Debug, Clone)]
570pub struct PyUtf8String {
571    pub(crate) inner: Utf8String,
572}
573
574#[pymethods]
575impl PyUtf8String {
576    /// Create a new UTF8String from a Python str
577    #[new]
578    fn new(value: &str) -> Self {
579        Self {
580            inner: Utf8String::new(value.to_string()),
581        }
582    }
583
584    /// Get the string value
585    fn as_str(&self) -> &str {
586        self.inner.as_str()
587    }
588
589    fn __str__(&self) -> &str {
590        self.inner.as_str()
591    }
592
593    fn __repr__(&self) -> String {
594        format!("Utf8String('{}')", self.inner.as_str())
595    }
596
597    fn __eq__(&self, other: &Self) -> bool {
598        self.inner == other.inner
599    }
600
601    fn __len__(&self) -> usize {
602        self.inner.as_str().chars().count()
603    }
604}
605
606/// Python wrapper for ASN.1 PrintableString
607///
608/// PrintableString is a subset of ASCII containing A-Z, a-z, 0-9,
609/// and the characters: ' ( ) + , - . / : = ?
610#[pyclass(name = "PrintableString")]
611#[derive(Debug, Clone)]
612pub struct PyPrintableString {
613    pub(crate) inner: PrintableString,
614}
615
616#[pymethods]
617impl PyPrintableString {
618    /// Create a new PrintableString
619    ///
620    /// Raises ValueError if the string contains characters outside the PrintableString charset.
621    #[new]
622    fn new(value: &str) -> PyResult<Self> {
623        let inner = PrintableString::new(value.to_string())
624            .map_err(|e| PyValueError::new_err(format!("Invalid PrintableString: {:?}", e)))?;
625        Ok(Self { inner })
626    }
627
628    /// Get the string value
629    fn as_str(&self) -> &str {
630        self.inner.as_str()
631    }
632
633    fn __str__(&self) -> &str {
634        self.inner.as_str()
635    }
636
637    fn __repr__(&self) -> String {
638        format!("PrintableString('{}')", self.inner.as_str())
639    }
640
641    fn __eq__(&self, other: &Self) -> bool {
642        self.inner == other.inner
643    }
644
645    fn __len__(&self) -> usize {
646        self.inner.as_str().chars().count()
647    }
648}
649
650/// Python wrapper for ASN.1 IA5String (International Alphabet 5 = ASCII)
651#[pyclass(name = "IA5String")]
652#[derive(Debug, Clone)]
653pub struct PyIA5String {
654    pub(crate) inner: IA5String,
655}
656
657#[pymethods]
658impl PyIA5String {
659    /// Create a new IA5String
660    ///
661    /// Raises ValueError if the string contains non-ASCII characters.
662    #[new]
663    fn new(value: &str) -> PyResult<Self> {
664        let inner = IA5String::new(value.to_string())
665            .map_err(|e| PyValueError::new_err(format!("Invalid IA5String: {:?}", e)))?;
666        Ok(Self { inner })
667    }
668
669    /// Get the string value
670    fn as_str(&self) -> &str {
671        self.inner.as_str()
672    }
673
674    fn __str__(&self) -> &str {
675        self.inner.as_str()
676    }
677
678    fn __repr__(&self) -> String {
679        format!("IA5String('{}')", self.inner.as_str())
680    }
681
682    fn __eq__(&self, other: &Self) -> bool {
683        self.inner == other.inner
684    }
685
686    fn __len__(&self) -> usize {
687        self.inner.as_str().chars().count()
688    }
689}
690
691/// Python wrapper for ASN.1 NumericString (tag 18) — digits and space
692#[pyclass(name = "NumericString")]
693#[derive(Debug, Clone)]
694pub struct PyNumericString {
695    pub(crate) inner: NumericString,
696}
697
698#[pymethods]
699impl PyNumericString {
700    #[new]
701    fn new(value: &str) -> PyResult<Self> {
702        let inner = NumericString::new(value.to_string())
703            .map_err(|e| PyValueError::new_err(format!("Invalid NumericString: {:?}", e)))?;
704        Ok(Self { inner })
705    }
706
707    fn as_str(&self) -> &str {
708        self.inner.as_str()
709    }
710
711    fn __str__(&self) -> &str {
712        self.inner.as_str()
713    }
714
715    fn __repr__(&self) -> String {
716        format!("NumericString('{}')", self.inner.as_str())
717    }
718
719    fn __eq__(&self, other: &Self) -> bool {
720        self.inner == other.inner
721    }
722
723    fn __len__(&self) -> usize {
724        self.inner.as_str().chars().count()
725    }
726}
727
728/// Python wrapper for ASN.1 TeletexString / T61String (tag 20) — arbitrary 8-bit bytes
729#[pyclass(name = "TeletexString")]
730#[derive(Debug, Clone)]
731pub struct PyTeletexString {
732    pub(crate) inner: TeletexString,
733}
734
735#[pymethods]
736impl PyTeletexString {
737    /// Create from raw bytes.
738    #[new]
739    fn new(data: Vec<u8>) -> Self {
740        Self {
741            inner: TeletexString::new(data),
742        }
743    }
744
745    /// Create from a Python str, encoding as Latin-1.
746    ///
747    /// Raises ValueError if any character is outside the Latin-1 range (U+0000..=U+00FF).
748    #[staticmethod]
749    fn from_latin1(value: &str) -> PyResult<Self> {
750        TeletexString::from_latin1(value)
751            .map(|inner| Self { inner })
752            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
753    }
754
755    /// Create from a Python str, encoding as Latin-1.
756    ///
757    /// Raises ValueError if any character is outside the Latin-1 range (U+0000..=U+00FF).
758    #[staticmethod]
759    fn from_str(value: &str) -> PyResult<Self> {
760        TeletexString::from_latin1(value)
761            .map(|inner| Self { inner })
762            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
763    }
764
765    /// Return the raw bytes.
766    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
767        PyBytes::new(py, self.inner.as_bytes())
768    }
769
770    /// Decode as a Latin-1 string.
771    fn as_str(&self) -> String {
772        self.inner.as_latin1_string()
773    }
774
775    fn __str__(&self) -> String {
776        self.inner.as_latin1_string()
777    }
778
779    fn __repr__(&self) -> String {
780        format!("TeletexString(<{} bytes>)", self.inner.as_bytes().len())
781    }
782
783    fn __len__(&self) -> usize {
784        self.inner.as_bytes().len()
785    }
786
787    fn __eq__(&self, other: &Self) -> bool {
788        self.inner == other.inner
789    }
790}
791
792/// Python wrapper for ASN.1 VisibleString (tag 26) — printable ASCII
793#[pyclass(name = "VisibleString")]
794#[derive(Debug, Clone)]
795pub struct PyVisibleString {
796    pub(crate) inner: VisibleString,
797}
798
799#[pymethods]
800impl PyVisibleString {
801    #[new]
802    fn new(value: &str) -> PyResult<Self> {
803        let inner = VisibleString::new(value.to_string())
804            .map_err(|e| PyValueError::new_err(format!("Invalid VisibleString: {:?}", e)))?;
805        Ok(Self { inner })
806    }
807
808    fn as_str(&self) -> &str {
809        self.inner.as_str()
810    }
811
812    fn __str__(&self) -> &str {
813        self.inner.as_str()
814    }
815
816    fn __repr__(&self) -> String {
817        format!("VisibleString('{}')", self.inner.as_str())
818    }
819
820    fn __eq__(&self, other: &Self) -> bool {
821        self.inner == other.inner
822    }
823
824    fn __len__(&self) -> usize {
825        self.inner.as_str().len()
826    }
827}
828
829/// Python wrapper for ASN.1 GeneralString (tag 27) — 8-bit bytes, treated as Latin-1
830#[pyclass(name = "GeneralString")]
831#[derive(Debug, Clone)]
832pub struct PyGeneralString {
833    pub(crate) inner: GeneralString,
834}
835
836#[pymethods]
837impl PyGeneralString {
838    /// Create from raw bytes.
839    #[new]
840    fn new(data: Vec<u8>) -> Self {
841        Self {
842            inner: GeneralString::new(data),
843        }
844    }
845
846    /// Create from a Python str, encoding as ASCII.
847    ///
848    /// Raises ValueError if any character is outside the ASCII range (U+0000..=U+007F).
849    #[staticmethod]
850    fn from_ascii(value: &str) -> PyResult<Self> {
851        GeneralString::from_ascii(value)
852            .map(|inner| Self { inner })
853            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
854    }
855
856    /// Create from a Python str, encoding as ASCII.
857    ///
858    /// Raises ValueError if any character is outside the ASCII range (U+0000..=U+007F).
859    #[staticmethod]
860    fn from_str(value: &str) -> PyResult<Self> {
861        GeneralString::from_ascii(value)
862            .map(|inner| Self { inner })
863            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
864    }
865
866    /// Return the raw bytes.
867    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
868        PyBytes::new(py, self.inner.as_bytes())
869    }
870
871    /// Decode as a Latin-1 string.
872    fn as_str(&self) -> String {
873        self.inner.as_latin1_string()
874    }
875
876    fn __str__(&self) -> String {
877        self.inner.as_latin1_string()
878    }
879
880    fn __repr__(&self) -> String {
881        format!("GeneralString(<{} bytes>)", self.inner.as_bytes().len())
882    }
883
884    fn __len__(&self) -> usize {
885        self.inner.as_bytes().len()
886    }
887
888    fn __eq__(&self, other: &Self) -> bool {
889        self.inner == other.inner
890    }
891}
892
893/// Python wrapper for ASN.1 UniversalString (tag 28) — UCS-4 big-endian
894#[pyclass(name = "UniversalString")]
895#[derive(Debug, Clone)]
896pub struct PyUniversalString {
897    pub(crate) inner: UniversalString,
898}
899
900#[pymethods]
901impl PyUniversalString {
902    /// Create from a Python str.
903    #[new]
904    fn new(value: &str) -> Self {
905        Self {
906            inner: UniversalString::new(value.to_string()),
907        }
908    }
909
910    /// Create from raw UCS-4 big-endian bytes.
911    #[staticmethod]
912    fn from_bytes(data: &[u8]) -> PyResult<Self> {
913        let inner = UniversalString::from_ucs4_be(data)
914            .map_err(|e| PyValueError::new_err(format!("Invalid UniversalString: {:?}", e)))?;
915        Ok(Self { inner })
916    }
917
918    fn as_str(&self) -> &str {
919        self.inner.as_str()
920    }
921
922    fn __str__(&self) -> &str {
923        self.inner.as_str()
924    }
925
926    fn __repr__(&self) -> String {
927        format!("UniversalString('{}')", self.inner.as_str())
928    }
929
930    fn __eq__(&self, other: &Self) -> bool {
931        self.inner == other.inner
932    }
933
934    fn __len__(&self) -> usize {
935        self.inner.as_str().chars().count()
936    }
937}
938
939/// Python wrapper for ASN.1 BMPString (tag 30) — UCS-2 big-endian (BMP only)
940#[pyclass(name = "BmpString")]
941#[derive(Debug, Clone)]
942pub struct PyBmpString {
943    pub(crate) inner: BmpString,
944}
945
946#[pymethods]
947impl PyBmpString {
948    /// Create from a Python str (must be BMP-only, i.e. no code points > U+FFFF).
949    #[new]
950    fn new(value: &str) -> PyResult<Self> {
951        let inner = BmpString::new(value.to_string())
952            .map_err(|e| PyValueError::new_err(format!("Invalid BMPString: {:?}", e)))?;
953        Ok(Self { inner })
954    }
955
956    /// Create from raw UCS-2 big-endian bytes.
957    #[staticmethod]
958    fn from_bytes(data: &[u8]) -> PyResult<Self> {
959        let inner = BmpString::from_ucs2_be(data)
960            .map_err(|e| PyValueError::new_err(format!("Invalid BMPString: {:?}", e)))?;
961        Ok(Self { inner })
962    }
963
964    fn as_str(&self) -> &str {
965        self.inner.as_str()
966    }
967
968    fn __str__(&self) -> &str {
969        self.inner.as_str()
970    }
971
972    fn __repr__(&self) -> String {
973        format!("BmpString('{}')", self.inner.as_str())
974    }
975
976    fn __eq__(&self, other: &Self) -> bool {
977        self.inner == other.inner
978    }
979
980    fn __len__(&self) -> usize {
981        self.inner.as_str().chars().count()
982    }
983}
984
985/// Python representation of an explicitly-tagged ASN.1 element.
986///
987/// Returned by ``decode_any()`` when a context-specific, application, or private
988/// tag wraps another element.  Callers can use ``isinstance(x, TaggedElement)``
989/// to detect tagged values and access the inner element via ``.value``.
990#[pyclass(name = "TaggedElement")]
991#[derive(Debug)]
992pub struct PyTaggedElement {
993    pub(crate) tag_number: u32,
994    pub(crate) tag_class: String,
995    pub(crate) is_constructed: bool,
996    pub(crate) value: Py<PyAny>,
997}
998
999#[pymethods]
1000impl PyTaggedElement {
1001    /// The tag number
1002    #[getter]
1003    fn tag_number(&self) -> u32 {
1004        self.tag_number
1005    }
1006
1007    /// The tag class: "Universal", "Application", "Context", or "Private"
1008    #[getter]
1009    fn tag_class(&self) -> &str {
1010        &self.tag_class
1011    }
1012
1013    /// Whether the tag is constructed (True) or primitive (False)
1014    #[getter]
1015    fn is_constructed(&self) -> bool {
1016        self.is_constructed
1017    }
1018
1019    /// The wrapped inner element
1020    #[getter]
1021    fn value(&self, py: Python<'_>) -> Py<PyAny> {
1022        self.value.clone_ref(py)
1023    }
1024
1025    fn __repr__(&self) -> String {
1026        format!(
1027            "TaggedElement(tag={}, class='{}', constructed={})",
1028            self.tag_number, self.tag_class, self.is_constructed
1029        )
1030    }
1031}
1032
1033/// Python representation of a raw ASN.1 element with an unsupported or unknown tag.
1034///
1035/// Returned by ``decode_any()`` when an unrecognised universal tag is encountered
1036/// instead of raising an error, enabling forward-compatible parsing.
1037#[pyclass(name = "RawElement")]
1038#[derive(Debug, Clone)]
1039pub struct PyRawElement {
1040    /// Tag number
1041    pub(crate) tag_number: u32,
1042    /// Tag class as a string: "Universal", "Application", "Context", or "Private"
1043    pub(crate) tag_class: String,
1044    /// Whether the element is constructed (True) or primitive (False)
1045    pub(crate) is_constructed: bool,
1046    /// Raw content bytes (not including the outer tag or length)
1047    pub(crate) data: Vec<u8>,
1048}
1049
1050#[pymethods]
1051impl PyRawElement {
1052    #[getter]
1053    fn tag_number(&self) -> u32 {
1054        self.tag_number
1055    }
1056
1057    #[getter]
1058    fn tag_class(&self) -> &str {
1059        &self.tag_class
1060    }
1061
1062    #[getter]
1063    fn is_constructed(&self) -> bool {
1064        self.is_constructed
1065    }
1066
1067    #[getter]
1068    fn data<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1069        PyBytes::new(py, &self.data)
1070    }
1071
1072    fn __repr__(&self) -> String {
1073        format!(
1074            "RawElement(tag={}, class='{}', constructed={}, {} bytes)",
1075            self.tag_number,
1076            self.tag_class,
1077            self.is_constructed,
1078            self.data.len()
1079        )
1080    }
1081}
1082
1083/// Map a `TagClass` to the string used by the Python encoder API.
1084///
1085/// The encoder's `encode_explicit_tag` accepts `"Context"`, `"Application"`, and
1086/// `"Private"`.  Using `{:?}` on `TagClass::ContextSpecific` would produce the
1087/// divergent string `"ContextSpecific"`, so we map explicitly here.
1088fn tag_class_str(class: synta::TagClass) -> &'static str {
1089    match class {
1090        synta::TagClass::Universal => "Universal",
1091        synta::TagClass::Application => "Application",
1092        synta::TagClass::ContextSpecific => "Context",
1093        synta::TagClass::Private => "Private",
1094    }
1095}
1096
1097/// Convert an ASN.1 Element to a Python object.
1098///
1099/// Sequences and Sets are returned as Python lists.
1100/// Tagged elements are returned as `TaggedElement` objects.
1101pub(crate) fn element_to_pyobject(element: synta::Element<'_>, py: Python) -> PyResult<Py<PyAny>> {
1102    use synta::Element;
1103    match element {
1104        Element::Boolean(v) => Py::new(py, PyBoolean { inner: v }).map(|p| p.into_any()),
1105        Element::Integer(v) => Py::new(py, PyInteger { inner: v }).map(|p| p.into_any()),
1106        Element::BitString(v) => Py::new(
1107            py,
1108            PyBitString {
1109                inner: v.to_owned(),
1110            },
1111        )
1112        .map(|p| p.into_any()),
1113        Element::OctetString(v) => Py::new(
1114            py,
1115            PyOctetString {
1116                inner: v.to_owned(),
1117            },
1118        )
1119        .map(|p| p.into_any()),
1120        Element::Null(_) => Py::new(py, PyNull).map(|p| p.into_any()),
1121        Element::Real(v) => Py::new(py, PyReal { inner: v }).map(|p| p.into_any()),
1122        Element::ObjectIdentifier(v) => {
1123            Py::new(py, PyObjectIdentifier::from_oid(v)).map(|p| p.into_any())
1124        }
1125        Element::Utf8String(v) => Py::new(
1126            py,
1127            PyUtf8String {
1128                inner: v.to_owned(),
1129            },
1130        )
1131        .map(|p| p.into_any()),
1132        Element::PrintableString(v) => Py::new(
1133            py,
1134            PyPrintableString {
1135                inner: v.to_owned(),
1136            },
1137        )
1138        .map(|p| p.into_any()),
1139        Element::IA5String(v) => Py::new(
1140            py,
1141            PyIA5String {
1142                inner: v.to_owned(),
1143            },
1144        )
1145        .map(|p| p.into_any()),
1146        Element::UtcTime(v) => Py::new(py, PyUtcTime { inner: v }).map(|p| p.into_any()),
1147        Element::GeneralizedTime(v) => {
1148            Py::new(py, PyGeneralizedTime { inner: v }).map(|p| p.into_any())
1149        }
1150        Element::Sequence(seq) => {
1151            let list = pyo3::types::PyList::empty(py);
1152            for elem in seq.into_elements().map_err(|e| {
1153                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
1154            })? {
1155                list.append(element_to_pyobject(elem, py)?)?;
1156            }
1157            Ok(list.into_any().unbind())
1158        }
1159        Element::Set(set) => {
1160            let list = pyo3::types::PyList::empty(py);
1161            for elem in set.into_elements().map_err(|e| {
1162                pyo3::exceptions::PyValueError::new_err(format!("ASN.1 decode error: {}", e))
1163            })? {
1164                list.append(element_to_pyobject(elem, py)?)?;
1165            }
1166            Ok(list.into_any().unbind())
1167        }
1168        Element::Tagged(tag, inner) => {
1169            let value = element_to_pyobject(*inner, py)?;
1170            Py::new(
1171                py,
1172                PyTaggedElement {
1173                    tag_number: tag.number(),
1174                    tag_class: tag_class_str(tag.class()).to_string(),
1175                    is_constructed: tag.is_constructed(),
1176                    value,
1177                },
1178            )
1179            .map(|p| p.into_any())
1180        }
1181        Element::NumericString(v) => {
1182            Py::new(py, PyNumericString { inner: v }).map(|p| p.into_any())
1183        }
1184        Element::TeletexString(v) => {
1185            Py::new(py, PyTeletexString { inner: v }).map(|p| p.into_any())
1186        }
1187        Element::VisibleString(v) => {
1188            Py::new(py, PyVisibleString { inner: v }).map(|p| p.into_any())
1189        }
1190        Element::GeneralString(v) => {
1191            Py::new(py, PyGeneralString { inner: v }).map(|p| p.into_any())
1192        }
1193        Element::UniversalString(v) => {
1194            Py::new(py, PyUniversalString { inner: v }).map(|p| p.into_any())
1195        }
1196        Element::BmpString(v) => Py::new(py, PyBmpString { inner: v }).map(|p| p.into_any()),
1197        Element::Raw(tag, bytes) => {
1198            let raw = PyRawElement {
1199                tag_number: tag.number(),
1200                tag_class: tag_class_str(tag.class()).to_string(),
1201                is_constructed: tag.is_constructed(),
1202                data: bytes.to_vec(),
1203            };
1204            Py::new(py, raw).map(|p| p.into_any())
1205        }
1206    }
1207}