Skip to main content

_synta/types/
primitives.rs

1//! Python wrappers for primitive ASN.1 types: INTEGER, OCTET STRING, BIT STRING,
2//! BOOLEAN, UTCTime, GeneralizedTime, REAL, NULL.
3
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6
7use pyo3::exceptions::{PyOverflowError, PyValueError};
8use pyo3::prelude::*;
9use pyo3::types::PyBytes;
10
11use synta::{
12    BitString, Boolean, FromDer, GeneralizedTime, Integer, Null, OctetString, Real, ToDer, UtcTime,
13};
14
15use crate::error::SyntaErr;
16
17/// Python wrapper for ASN.1 INTEGER
18#[pyclass(name = "Integer")]
19#[derive(Debug, Clone)]
20pub struct PyInteger {
21    pub(crate) inner: Integer,
22}
23
24#[pymethods]
25impl PyInteger {
26    /// Create a new Integer from a Python int
27    #[new]
28    fn new(value: i64) -> Self {
29        Self {
30            inner: Integer::from_i64(value),
31        }
32    }
33
34    /// Convert to Python int (i64)
35    fn to_int(&self) -> PyResult<i64> {
36        self.inner
37            .as_i64()
38            .map_err(|_| PyOverflowError::new_err("Integer too large for i64"))
39    }
40
41    /// Get the raw bytes (big-endian two's complement)
42    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
43        PyBytes::new(py, self.inner.as_bytes())
44    }
45
46    /// Create Integer from raw bytes
47    #[staticmethod]
48    fn from_bytes(bytes: &[u8]) -> Self {
49        Self {
50            inner: Integer::from_bytes(bytes),
51        }
52    }
53
54    /// Create Integer from an unsigned 64-bit value
55    #[staticmethod]
56    fn from_u64(value: u64) -> Self {
57        Self {
58            inner: Integer::from_u64(value),
59        }
60    }
61
62    /// Return the DER encoding of this ``Integer``.
63    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
64        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
65    }
66
67    /// Parse a DER-encoded ``Integer``.
68    ///
69    /// :raises ValueError: if the bytes cannot be decoded.
70    #[staticmethod]
71    fn from_der(data: &[u8]) -> PyResult<Self> {
72        let inner = Integer::from_der(data).map_err(SyntaErr)?;
73        Ok(Self { inner })
74    }
75
76    /// Convert to Python int (i128) for larger integers
77    fn to_i128(&self) -> PyResult<i128> {
78        self.inner
79            .as_i128()
80            .map_err(|_| PyOverflowError::new_err("Integer too large for i128"))
81    }
82
83    fn __eq__(&self, other: &Self) -> bool {
84        // DER encodes integers with minimal bytes (no redundant leading zeros),
85        // so comparing the raw bytes is equivalent to comparing by value.
86        self.inner.as_bytes() == other.inner.as_bytes()
87    }
88
89    fn __hash__(&self) -> u64 {
90        let mut h = DefaultHasher::new();
91        self.inner.as_bytes().hash(&mut h);
92        h.finish()
93    }
94
95    fn __repr__(&self) -> PyResult<String> {
96        match self.inner.as_i64() {
97            Ok(val) => Ok(format!("Integer({})", val)),
98            Err(_) => Ok(format!("Integer(<{} bytes>)", self.inner.as_bytes().len())),
99        }
100    }
101
102    fn __str__(&self) -> PyResult<String> {
103        match self.inner.as_i64() {
104            Ok(val) => Ok(val.to_string()),
105            Err(_) => Ok(format!("<integer {} bytes>", self.inner.as_bytes().len())),
106        }
107    }
108}
109
110/// Python wrapper for ASN.1 OCTET STRING
111#[pyclass(name = "OctetString")]
112#[derive(Debug, Clone)]
113pub struct PyOctetString {
114    pub(crate) inner: OctetString,
115}
116
117#[pymethods]
118impl PyOctetString {
119    /// Create a new OctetString from bytes
120    #[new]
121    fn new(data: Vec<u8>) -> Self {
122        Self {
123            inner: OctetString::new(data),
124        }
125    }
126
127    /// Get the bytes as a Python bytes object
128    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
129        PyBytes::new(py, self.inner.as_bytes())
130    }
131
132    /// Get the length
133    fn __len__(&self) -> usize {
134        self.inner.as_bytes().len()
135    }
136
137    fn __eq__(&self, other: &Self) -> bool {
138        self.inner == other.inner
139    }
140
141    fn __repr__(&self) -> String {
142        format!("OctetString(<{} bytes>)", self.inner.as_bytes().len())
143    }
144
145    /// Return the DER encoding of this ``OctetString``.
146    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
147        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
148    }
149
150    /// Parse a DER-encoded ``OctetString``.
151    ///
152    /// :raises ValueError: if the bytes cannot be decoded.
153    #[staticmethod]
154    fn from_der(data: &[u8]) -> PyResult<Self> {
155        let inner = OctetString::from_der(data).map_err(SyntaErr)?;
156        Ok(Self { inner })
157    }
158}
159
160/// Python wrapper for ASN.1 BIT STRING
161#[pyclass(name = "BitString")]
162#[derive(Debug, Clone)]
163pub struct PyBitString {
164    pub(crate) inner: BitString,
165}
166
167#[pymethods]
168impl PyBitString {
169    /// Create a new BitString from bytes and unused bits count
170    #[new]
171    fn new(data: Vec<u8>, unused_bits: u8) -> PyResult<Self> {
172        if unused_bits > 7 {
173            return Err(PyValueError::new_err("unused_bits must be 0-7"));
174        }
175        let inner = BitString::new(data, unused_bits)
176            .map_err(|e| PyValueError::new_err(format!("Invalid BitString: {:?}", e)))?;
177        Ok(Self { inner })
178    }
179
180    /// Get the bytes
181    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
182        PyBytes::new(py, self.inner.as_bytes())
183    }
184
185    /// Get the number of unused bits in the last byte
186    fn unused_bits(&self) -> u8 {
187        self.inner.unused_bits()
188    }
189
190    /// Get the number of bits
191    fn bit_len(&self) -> usize {
192        self.inner.bit_len()
193    }
194
195    fn __len__(&self) -> usize {
196        self.inner.bit_len()
197    }
198
199    fn __eq__(&self, other: &Self) -> bool {
200        self.inner == other.inner
201    }
202
203    fn __repr__(&self) -> String {
204        format!(
205            "BitString(<{} bits, {} unused>)",
206            self.inner.bit_len(),
207            self.inner.unused_bits()
208        )
209    }
210
211    /// Return the DER encoding of this ``BitString``.
212    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
213        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
214    }
215
216    /// Parse a DER-encoded ``BitString``.
217    ///
218    /// :raises ValueError: if the bytes cannot be decoded.
219    #[staticmethod]
220    fn from_der(data: &[u8]) -> PyResult<Self> {
221        let inner = BitString::from_der(data).map_err(SyntaErr)?;
222        Ok(Self { inner })
223    }
224}
225
226/// Python wrapper for ASN.1 BOOLEAN
227#[pyclass(name = "Boolean")]
228#[derive(Debug, Clone, Copy)]
229pub struct PyBoolean {
230    pub(crate) inner: Boolean,
231}
232
233#[pymethods]
234impl PyBoolean {
235    /// Create a new Boolean
236    #[new]
237    fn new(value: bool) -> Self {
238        Self {
239            inner: Boolean::new(value),
240        }
241    }
242
243    /// Get the boolean value
244    fn value(&self) -> bool {
245        self.inner.value()
246    }
247
248    fn __bool__(&self) -> bool {
249        self.inner.value()
250    }
251
252    fn __eq__(&self, other: &Self) -> bool {
253        self.inner.value() == other.inner.value()
254    }
255
256    fn __hash__(&self) -> isize {
257        // Matches Python's hash(True) == 1, hash(False) == 0.
258        self.inner.value() as isize
259    }
260
261    fn __repr__(&self) -> String {
262        format!("Boolean({})", self.inner.value())
263    }
264
265    /// Return the DER encoding of this ``Boolean``.
266    #[allow(clippy::wrong_self_convention)]
267    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
268        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
269    }
270
271    /// Parse a DER-encoded ``Boolean``.
272    ///
273    /// :raises ValueError: if the bytes cannot be decoded.
274    #[staticmethod]
275    fn from_der(data: &[u8]) -> PyResult<Self> {
276        let inner = Boolean::from_der(data).map_err(SyntaErr)?;
277        Ok(Self { inner })
278    }
279}
280
281/// Python wrapper for ASN.1 UTCTime
282#[pyclass(name = "UtcTime")]
283#[derive(Debug, Clone)]
284pub struct PyUtcTime {
285    pub(crate) inner: UtcTime,
286}
287
288#[pymethods]
289impl PyUtcTime {
290    /// Create a new UTCTime (year must be in 1950-2049 range)
291    #[new]
292    fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> PyResult<Self> {
293        let inner = UtcTime::new(year, month, day, hour, minute, second)
294            .map_err(|e| PyValueError::new_err(format!("Invalid UTCTime: {:?}", e)))?;
295        Ok(Self { inner })
296    }
297
298    /// Four-digit year (1950–2049).
299    #[getter]
300    fn year(&self) -> u16 {
301        self.inner.year
302    }
303
304    /// Month (1–12).
305    #[getter]
306    fn month(&self) -> u8 {
307        self.inner.month
308    }
309
310    /// Day of month (1–31).
311    #[getter]
312    fn day(&self) -> u8 {
313        self.inner.day
314    }
315
316    /// Hour (0–23).
317    #[getter]
318    fn hour(&self) -> u8 {
319        self.inner.hour
320    }
321
322    /// Minute (0–59).
323    #[getter]
324    fn minute(&self) -> u8 {
325        self.inner.minute
326    }
327
328    /// Second (0–59).
329    #[getter]
330    fn second(&self) -> u8 {
331        self.inner.second
332    }
333
334    fn __str__(&self) -> String {
335        self.inner.to_string()
336    }
337
338    fn __repr__(&self) -> String {
339        format!("UtcTime('{}')", self.inner)
340    }
341
342    /// Return the DER encoding of this ``UtcTime``.
343    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
344        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
345    }
346
347    /// Parse a DER-encoded ``UtcTime``.
348    ///
349    /// :raises ValueError: if the bytes cannot be decoded.
350    #[staticmethod]
351    fn from_der(data: &[u8]) -> PyResult<Self> {
352        let inner = UtcTime::from_der(data).map_err(SyntaErr)?;
353        Ok(Self { inner })
354    }
355}
356
357impl std::fmt::Display for PyUtcTime {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        write!(f, "{}", self.inner)
360    }
361}
362
363/// Python wrapper for ASN.1 GeneralizedTime
364#[pyclass(name = "GeneralizedTime")]
365#[derive(Debug, Clone)]
366pub struct PyGeneralizedTime {
367    pub(crate) inner: GeneralizedTime,
368}
369
370#[pymethods]
371impl PyGeneralizedTime {
372    /// Create a new GeneralizedTime
373    ///
374    /// Args:
375    ///     milliseconds: Optional fractional seconds in milliseconds (0-999)
376    #[new]
377    fn new(
378        year: u16,
379        month: u8,
380        day: u8,
381        hour: u8,
382        minute: u8,
383        second: u8,
384        milliseconds: Option<u16>,
385    ) -> PyResult<Self> {
386        let inner = GeneralizedTime::new(year, month, day, hour, minute, second, milliseconds)
387            .map_err(|e| PyValueError::new_err(format!("Invalid GeneralizedTime: {:?}", e)))?;
388        Ok(Self { inner })
389    }
390
391    /// Four-digit year.
392    #[getter]
393    fn year(&self) -> u16 {
394        self.inner.year
395    }
396
397    /// Month (1–12).
398    #[getter]
399    fn month(&self) -> u8 {
400        self.inner.month
401    }
402
403    /// Day of month (1–31).
404    #[getter]
405    fn day(&self) -> u8 {
406        self.inner.day
407    }
408
409    /// Hour (0–23).
410    #[getter]
411    fn hour(&self) -> u8 {
412        self.inner.hour
413    }
414
415    /// Minute (0–59).
416    #[getter]
417    fn minute(&self) -> u8 {
418        self.inner.minute
419    }
420
421    /// Second (0–59).
422    #[getter]
423    fn second(&self) -> u8 {
424        self.inner.second
425    }
426
427    /// Fractional seconds expressed as whole milliseconds (0–999), or ``None``.
428    #[getter]
429    fn milliseconds(&self) -> Option<u16> {
430        self.inner.milliseconds
431    }
432
433    fn __str__(&self) -> String {
434        self.inner.to_string()
435    }
436
437    fn __repr__(&self) -> String {
438        format!("GeneralizedTime('{}')", self.inner)
439    }
440
441    /// Return the DER encoding of this ``GeneralizedTime``.
442    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
443        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
444    }
445
446    /// Parse a DER-encoded ``GeneralizedTime``.
447    ///
448    /// :raises ValueError: if the bytes cannot be decoded.
449    #[staticmethod]
450    fn from_der(data: &[u8]) -> PyResult<Self> {
451        let inner = GeneralizedTime::from_der(data).map_err(SyntaErr)?;
452        Ok(Self { inner })
453    }
454}
455
456impl std::fmt::Display for PyGeneralizedTime {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        write!(f, "{}", self.inner)
459    }
460}
461
462/// Python wrapper for ASN.1 REAL
463#[pyclass(name = "Real")]
464#[derive(Debug, Clone, Copy)]
465pub struct PyReal {
466    pub(crate) inner: Real,
467}
468
469#[pymethods]
470impl PyReal {
471    /// Create a new Real from a Python float
472    #[new]
473    fn new(value: f64) -> Self {
474        Self {
475            inner: Real::new(value),
476        }
477    }
478
479    /// Get the f64 value
480    fn value(&self) -> f64 {
481        self.inner.value()
482    }
483
484    /// Return True if this is positive infinity
485    fn is_infinite(&self) -> bool {
486        self.inner.value().is_infinite()
487    }
488
489    /// Return True if this is NaN (not-a-number)
490    fn is_nan(&self) -> bool {
491        self.inner.value().is_nan()
492    }
493
494    /// Return True if the value is finite
495    fn is_finite(&self) -> bool {
496        self.inner.value().is_finite()
497    }
498
499    fn __float__(&self) -> f64 {
500        self.inner.value()
501    }
502
503    fn __repr__(&self) -> String {
504        format!("Real({})", self.inner.value())
505    }
506
507    fn __str__(&self) -> String {
508        self.inner.value().to_string()
509    }
510
511    fn __eq__(&self, other: &Self) -> bool {
512        // NaN != NaN by IEEE 754 — mirror Python float behaviour
513        self.inner.value() == other.inner.value()
514    }
515
516    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
517        // Delegate to Python's float.__hash__ so that hash(Real(1.0)) == hash(1.0) == hash(1),
518        // matching the Python data model guarantee for numeric types.
519        self.inner.value().into_pyobject(py)?.hash()
520    }
521
522    /// Return the DER encoding of this ``Real``.
523    #[allow(clippy::wrong_self_convention)]
524    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
525        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
526    }
527
528    /// Parse a DER-encoded ``Real``.
529    ///
530    /// :raises ValueError: if the bytes cannot be decoded.
531    #[staticmethod]
532    fn from_der(data: &[u8]) -> PyResult<Self> {
533        let inner = Real::from_der(data).map_err(SyntaErr)?;
534        Ok(Self { inner })
535    }
536}
537
538/// Python wrapper for ASN.1 NULL
539#[pyclass(name = "Null")]
540#[derive(Debug, Clone, Copy)]
541pub struct PyNull;
542
543#[pymethods]
544impl PyNull {
545    /// Create a ``Null`` value.
546    #[new]
547    fn new() -> Self {
548        Self
549    }
550
551    fn __repr__(&self) -> String {
552        "Null()".to_string()
553    }
554
555    fn __eq__(&self, _other: &Self) -> bool {
556        true
557    }
558
559    fn __hash__(&self) -> isize {
560        // All Null values are equal; use a fixed hash consistent with __eq__.
561        0
562    }
563
564    /// Return the DER encoding of this ``Null``.
565    #[allow(clippy::wrong_self_convention)]
566    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
567        Ok(PyBytes::new(py, &Null.to_der().map_err(SyntaErr)?))
568    }
569
570    /// Parse a DER-encoded ``Null``.
571    ///
572    /// :raises ValueError: if the bytes cannot be decoded.
573    #[staticmethod]
574    fn from_der(data: &[u8]) -> PyResult<Self> {
575        Null::from_der(data).map_err(SyntaErr)?;
576        Ok(Self)
577    }
578}