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::{BitString, Boolean, GeneralizedTime, Integer, OctetString, Real, UtcTime};
12
13/// Python wrapper for ASN.1 INTEGER
14#[pyclass(name = "Integer")]
15#[derive(Debug, Clone)]
16pub struct PyInteger {
17    pub(crate) inner: Integer,
18}
19
20#[pymethods]
21impl PyInteger {
22    /// Create a new Integer from a Python int
23    #[new]
24    fn new(value: i64) -> Self {
25        Self {
26            inner: Integer::from_i64(value),
27        }
28    }
29
30    /// Convert to Python int (i64)
31    fn to_int(&self) -> PyResult<i64> {
32        self.inner
33            .as_i64()
34            .map_err(|_| PyOverflowError::new_err("Integer too large for i64"))
35    }
36
37    /// Get the raw bytes (big-endian two's complement)
38    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
39        PyBytes::new(py, self.inner.as_bytes())
40    }
41
42    /// Create Integer from raw bytes
43    #[staticmethod]
44    fn from_bytes(bytes: &[u8]) -> Self {
45        Self {
46            inner: Integer::from_bytes(bytes),
47        }
48    }
49
50    /// Create Integer from an unsigned 64-bit value
51    #[staticmethod]
52    fn from_u64(value: u64) -> Self {
53        Self {
54            inner: Integer::from_u64(value),
55        }
56    }
57
58    /// Convert to Python int (i128) for larger integers
59    fn to_i128(&self) -> PyResult<i128> {
60        self.inner
61            .as_i128()
62            .map_err(|_| PyOverflowError::new_err("Integer too large for i128"))
63    }
64
65    fn __eq__(&self, other: &Self) -> bool {
66        // DER encodes integers with minimal bytes (no redundant leading zeros),
67        // so comparing the raw bytes is equivalent to comparing by value.
68        self.inner.as_bytes() == other.inner.as_bytes()
69    }
70
71    fn __hash__(&self) -> u64 {
72        let mut h = DefaultHasher::new();
73        self.inner.as_bytes().hash(&mut h);
74        h.finish()
75    }
76
77    fn __repr__(&self) -> PyResult<String> {
78        match self.inner.as_i64() {
79            Ok(val) => Ok(format!("Integer({})", val)),
80            Err(_) => Ok(format!("Integer(<{} bytes>)", self.inner.as_bytes().len())),
81        }
82    }
83
84    fn __str__(&self) -> PyResult<String> {
85        match self.inner.as_i64() {
86            Ok(val) => Ok(val.to_string()),
87            Err(_) => Ok(format!("<integer {} bytes>", self.inner.as_bytes().len())),
88        }
89    }
90}
91
92/// Python wrapper for ASN.1 OCTET STRING
93#[pyclass(name = "OctetString")]
94#[derive(Debug, Clone)]
95pub struct PyOctetString {
96    pub(crate) inner: OctetString,
97}
98
99#[pymethods]
100impl PyOctetString {
101    /// Create a new OctetString from bytes
102    #[new]
103    fn new(data: Vec<u8>) -> Self {
104        Self {
105            inner: OctetString::new(data),
106        }
107    }
108
109    /// Get the bytes as a Python bytes object
110    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
111        PyBytes::new(py, self.inner.as_bytes())
112    }
113
114    /// Get the length
115    fn __len__(&self) -> usize {
116        self.inner.as_bytes().len()
117    }
118
119    fn __eq__(&self, other: &Self) -> bool {
120        self.inner == other.inner
121    }
122
123    fn __repr__(&self) -> String {
124        format!("OctetString(<{} bytes>)", self.inner.as_bytes().len())
125    }
126}
127
128/// Python wrapper for ASN.1 BIT STRING
129#[pyclass(name = "BitString")]
130#[derive(Debug, Clone)]
131pub struct PyBitString {
132    pub(crate) inner: BitString,
133}
134
135#[pymethods]
136impl PyBitString {
137    /// Create a new BitString from bytes and unused bits count
138    #[new]
139    fn new(data: Vec<u8>, unused_bits: u8) -> PyResult<Self> {
140        if unused_bits > 7 {
141            return Err(PyValueError::new_err("unused_bits must be 0-7"));
142        }
143        let inner = BitString::new(data, unused_bits)
144            .map_err(|e| PyValueError::new_err(format!("Invalid BitString: {:?}", e)))?;
145        Ok(Self { inner })
146    }
147
148    /// Get the bytes
149    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
150        PyBytes::new(py, self.inner.as_bytes())
151    }
152
153    /// Get the number of unused bits in the last byte
154    fn unused_bits(&self) -> u8 {
155        self.inner.unused_bits()
156    }
157
158    /// Get the number of bits
159    fn bit_len(&self) -> usize {
160        self.inner.bit_len()
161    }
162
163    fn __len__(&self) -> usize {
164        self.inner.bit_len()
165    }
166
167    fn __eq__(&self, other: &Self) -> bool {
168        self.inner == other.inner
169    }
170
171    fn __repr__(&self) -> String {
172        format!(
173            "BitString(<{} bits, {} unused>)",
174            self.inner.bit_len(),
175            self.inner.unused_bits()
176        )
177    }
178}
179
180/// Python wrapper for ASN.1 BOOLEAN
181#[pyclass(name = "Boolean")]
182#[derive(Debug, Clone, Copy)]
183pub struct PyBoolean {
184    pub(crate) inner: Boolean,
185}
186
187#[pymethods]
188impl PyBoolean {
189    /// Create a new Boolean
190    #[new]
191    fn new(value: bool) -> Self {
192        Self {
193            inner: Boolean::new(value),
194        }
195    }
196
197    /// Get the boolean value
198    fn value(&self) -> bool {
199        self.inner.value()
200    }
201
202    fn __bool__(&self) -> bool {
203        self.inner.value()
204    }
205
206    fn __eq__(&self, other: &Self) -> bool {
207        self.inner.value() == other.inner.value()
208    }
209
210    fn __hash__(&self) -> isize {
211        // Matches Python's hash(True) == 1, hash(False) == 0.
212        self.inner.value() as isize
213    }
214
215    fn __repr__(&self) -> String {
216        format!("Boolean({})", self.inner.value())
217    }
218}
219
220/// Python wrapper for ASN.1 UTCTime
221#[pyclass(name = "UtcTime")]
222#[derive(Debug, Clone)]
223pub struct PyUtcTime {
224    pub(crate) inner: UtcTime,
225}
226
227#[pymethods]
228impl PyUtcTime {
229    /// Create a new UTCTime (year must be in 1950-2049 range)
230    #[new]
231    fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> PyResult<Self> {
232        let inner = UtcTime::new(year, month, day, hour, minute, second)
233            .map_err(|e| PyValueError::new_err(format!("Invalid UTCTime: {:?}", e)))?;
234        Ok(Self { inner })
235    }
236
237    #[getter]
238    fn year(&self) -> u16 {
239        self.inner.year
240    }
241
242    #[getter]
243    fn month(&self) -> u8 {
244        self.inner.month
245    }
246
247    #[getter]
248    fn day(&self) -> u8 {
249        self.inner.day
250    }
251
252    #[getter]
253    fn hour(&self) -> u8 {
254        self.inner.hour
255    }
256
257    #[getter]
258    fn minute(&self) -> u8 {
259        self.inner.minute
260    }
261
262    #[getter]
263    fn second(&self) -> u8 {
264        self.inner.second
265    }
266
267    fn __str__(&self) -> String {
268        self.inner.to_string()
269    }
270
271    fn __repr__(&self) -> String {
272        format!("UtcTime('{}')", self.inner)
273    }
274}
275
276impl std::fmt::Display for PyUtcTime {
277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278        write!(f, "{}", self.inner)
279    }
280}
281
282/// Python wrapper for ASN.1 GeneralizedTime
283#[pyclass(name = "GeneralizedTime")]
284#[derive(Debug, Clone)]
285pub struct PyGeneralizedTime {
286    pub(crate) inner: GeneralizedTime,
287}
288
289#[pymethods]
290impl PyGeneralizedTime {
291    /// Create a new GeneralizedTime
292    ///
293    /// Args:
294    ///     milliseconds: Optional fractional seconds in milliseconds (0-999)
295    #[new]
296    fn new(
297        year: u16,
298        month: u8,
299        day: u8,
300        hour: u8,
301        minute: u8,
302        second: u8,
303        milliseconds: Option<u16>,
304    ) -> PyResult<Self> {
305        let inner = GeneralizedTime::new(year, month, day, hour, minute, second, milliseconds)
306            .map_err(|e| PyValueError::new_err(format!("Invalid GeneralizedTime: {:?}", e)))?;
307        Ok(Self { inner })
308    }
309
310    #[getter]
311    fn year(&self) -> u16 {
312        self.inner.year
313    }
314
315    #[getter]
316    fn month(&self) -> u8 {
317        self.inner.month
318    }
319
320    #[getter]
321    fn day(&self) -> u8 {
322        self.inner.day
323    }
324
325    #[getter]
326    fn hour(&self) -> u8 {
327        self.inner.hour
328    }
329
330    #[getter]
331    fn minute(&self) -> u8 {
332        self.inner.minute
333    }
334
335    #[getter]
336    fn second(&self) -> u8 {
337        self.inner.second
338    }
339
340    #[getter]
341    fn milliseconds(&self) -> Option<u16> {
342        self.inner.milliseconds
343    }
344
345    fn __str__(&self) -> String {
346        self.inner.to_string()
347    }
348
349    fn __repr__(&self) -> String {
350        format!("GeneralizedTime('{}')", self.inner)
351    }
352}
353
354impl std::fmt::Display for PyGeneralizedTime {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        write!(f, "{}", self.inner)
357    }
358}
359
360/// Python wrapper for ASN.1 REAL
361#[pyclass(name = "Real")]
362#[derive(Debug, Clone, Copy)]
363pub struct PyReal {
364    pub(crate) inner: Real,
365}
366
367#[pymethods]
368impl PyReal {
369    /// Create a new Real from a Python float
370    #[new]
371    fn new(value: f64) -> Self {
372        Self {
373            inner: Real::new(value),
374        }
375    }
376
377    /// Get the f64 value
378    fn value(&self) -> f64 {
379        self.inner.value()
380    }
381
382    /// Return True if this is positive infinity
383    fn is_infinite(&self) -> bool {
384        self.inner.value().is_infinite()
385    }
386
387    /// Return True if this is NaN (not-a-number)
388    fn is_nan(&self) -> bool {
389        self.inner.value().is_nan()
390    }
391
392    /// Return True if the value is finite
393    fn is_finite(&self) -> bool {
394        self.inner.value().is_finite()
395    }
396
397    fn __float__(&self) -> f64 {
398        self.inner.value()
399    }
400
401    fn __repr__(&self) -> String {
402        format!("Real({})", self.inner.value())
403    }
404
405    fn __str__(&self) -> String {
406        self.inner.value().to_string()
407    }
408
409    fn __eq__(&self, other: &Self) -> bool {
410        // NaN != NaN by IEEE 754 — mirror Python float behaviour
411        self.inner.value() == other.inner.value()
412    }
413
414    fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
415        // Delegate to Python's float.__hash__ so that hash(Real(1.0)) == hash(1.0) == hash(1),
416        // matching the Python data model guarantee for numeric types.
417        self.inner.value().into_pyobject(py)?.hash()
418    }
419}
420
421/// Python wrapper for ASN.1 NULL
422#[pyclass(name = "Null")]
423#[derive(Debug, Clone, Copy)]
424pub struct PyNull;
425
426#[pymethods]
427impl PyNull {
428    #[new]
429    fn new() -> Self {
430        Self
431    }
432
433    fn __repr__(&self) -> String {
434        "Null()".to_string()
435    }
436
437    fn __eq__(&self, _other: &Self) -> bool {
438        true
439    }
440
441    fn __hash__(&self) -> isize {
442        // All Null values are equal; use a fixed hash consistent with __eq__.
443        0
444    }
445}