Skip to main content

_synta/types/
strings.rs

1//! Python wrappers for ASN.1 string types: UTF8String, PrintableString, IA5String,
2//! NumericString, TeletexString, VisibleString, GeneralString, UniversalString, BMPString.
3
4use pyo3::exceptions::PyValueError;
5use pyo3::prelude::*;
6use pyo3::types::PyBytes;
7
8use synta::{
9    BmpString, FromDer, GeneralString, IA5String, NumericString, PrintableString, TeletexString,
10    ToDer, UniversalString, Utf8String, VisibleString,
11};
12
13use crate::error::SyntaErr;
14
15/// Python wrapper for ASN.1 UTF8String
16#[pyclass(name = "Utf8String")]
17#[derive(Debug, Clone)]
18pub struct PyUtf8String {
19    pub(crate) inner: Utf8String,
20}
21
22#[pymethods]
23impl PyUtf8String {
24    /// Create a new UTF8String from a Python str
25    #[new]
26    fn new(value: &str) -> Self {
27        Self {
28            inner: Utf8String::new(value.to_string()),
29        }
30    }
31
32    /// Get the string value
33    fn as_str(&self) -> &str {
34        self.inner.as_str()
35    }
36
37    fn __str__(&self) -> &str {
38        self.inner.as_str()
39    }
40
41    fn __repr__(&self) -> String {
42        format!("Utf8String('{}')", self.inner.as_str())
43    }
44
45    fn __eq__(&self, other: &Self) -> bool {
46        self.inner == other.inner
47    }
48
49    fn __len__(&self) -> usize {
50        self.inner.as_str().chars().count()
51    }
52
53    /// Return the DER encoding of this ``Utf8String``.
54    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
55        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
56    }
57
58    /// Parse a DER-encoded ``Utf8String``.
59    ///
60    /// :raises ValueError: if the bytes cannot be decoded.
61    #[staticmethod]
62    fn from_der(data: &[u8]) -> PyResult<Self> {
63        let inner = Utf8String::from_der(data).map_err(SyntaErr)?;
64        Ok(Self { inner })
65    }
66}
67
68/// Python wrapper for ASN.1 PrintableString
69///
70/// PrintableString is a subset of ASCII containing A-Z, a-z, 0-9,
71/// and the characters: ' ( ) + , - . / : = ?
72#[pyclass(name = "PrintableString")]
73#[derive(Debug, Clone)]
74pub struct PyPrintableString {
75    pub(crate) inner: PrintableString,
76}
77
78#[pymethods]
79impl PyPrintableString {
80    /// Create a new PrintableString
81    ///
82    /// Raises ValueError if the string contains characters outside the PrintableString charset.
83    #[new]
84    fn new(value: &str) -> PyResult<Self> {
85        let inner = PrintableString::new(value.to_string())
86            .map_err(|e| PyValueError::new_err(format!("Invalid PrintableString: {:?}", e)))?;
87        Ok(Self { inner })
88    }
89
90    /// Get the string value
91    fn as_str(&self) -> &str {
92        self.inner.as_str()
93    }
94
95    fn __str__(&self) -> &str {
96        self.inner.as_str()
97    }
98
99    fn __repr__(&self) -> String {
100        format!("PrintableString('{}')", self.inner.as_str())
101    }
102
103    fn __eq__(&self, other: &Self) -> bool {
104        self.inner == other.inner
105    }
106
107    fn __len__(&self) -> usize {
108        self.inner.as_str().chars().count()
109    }
110
111    /// Return the DER encoding of this ``PrintableString``.
112    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
113        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
114    }
115
116    /// Parse a DER-encoded ``PrintableString``.
117    ///
118    /// :raises ValueError: if the bytes cannot be decoded.
119    #[staticmethod]
120    fn from_der(data: &[u8]) -> PyResult<Self> {
121        let inner = PrintableString::from_der(data).map_err(SyntaErr)?;
122        Ok(Self { inner })
123    }
124}
125
126/// Python wrapper for ASN.1 IA5String (International Alphabet 5 = ASCII)
127#[pyclass(name = "IA5String")]
128#[derive(Debug, Clone)]
129pub struct PyIA5String {
130    pub(crate) inner: IA5String,
131}
132
133#[pymethods]
134impl PyIA5String {
135    /// Create a new IA5String
136    ///
137    /// Raises ValueError if the string contains non-ASCII characters.
138    #[new]
139    fn new(value: &str) -> PyResult<Self> {
140        let inner = IA5String::new(value.to_string())
141            .map_err(|e| PyValueError::new_err(format!("Invalid IA5String: {:?}", e)))?;
142        Ok(Self { inner })
143    }
144
145    /// Get the string value
146    fn as_str(&self) -> &str {
147        self.inner.as_str()
148    }
149
150    fn __str__(&self) -> &str {
151        self.inner.as_str()
152    }
153
154    fn __repr__(&self) -> String {
155        format!("IA5String('{}')", self.inner.as_str())
156    }
157
158    fn __eq__(&self, other: &Self) -> bool {
159        self.inner == other.inner
160    }
161
162    fn __len__(&self) -> usize {
163        self.inner.as_str().chars().count()
164    }
165
166    /// Return the DER encoding of this ``IA5String``.
167    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
168        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
169    }
170
171    /// Parse a DER-encoded ``IA5String``.
172    ///
173    /// :raises ValueError: if the bytes cannot be decoded.
174    #[staticmethod]
175    fn from_der(data: &[u8]) -> PyResult<Self> {
176        let inner = IA5String::from_der(data).map_err(SyntaErr)?;
177        Ok(Self { inner })
178    }
179}
180
181/// Python wrapper for ASN.1 NumericString (tag 18) — digits and space
182#[pyclass(name = "NumericString")]
183#[derive(Debug, Clone)]
184pub struct PyNumericString {
185    pub(crate) inner: NumericString,
186}
187
188#[pymethods]
189impl PyNumericString {
190    /// Create a ``NumericString`` from *value*.
191    ///
192    /// :raises ValueError: if *value* contains characters other than digits (0–9) and space.
193    #[new]
194    fn new(value: &str) -> PyResult<Self> {
195        let inner = NumericString::new(value.to_string())
196            .map_err(|e| PyValueError::new_err(format!("Invalid NumericString: {:?}", e)))?;
197        Ok(Self { inner })
198    }
199
200    fn as_str(&self) -> &str {
201        self.inner.as_str()
202    }
203
204    fn __str__(&self) -> &str {
205        self.inner.as_str()
206    }
207
208    fn __repr__(&self) -> String {
209        format!("NumericString('{}')", self.inner.as_str())
210    }
211
212    fn __eq__(&self, other: &Self) -> bool {
213        self.inner == other.inner
214    }
215
216    fn __len__(&self) -> usize {
217        self.inner.as_str().chars().count()
218    }
219
220    /// Return the DER encoding of this ``NumericString``.
221    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
222        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
223    }
224
225    /// Parse a DER-encoded ``NumericString``.
226    ///
227    /// :raises ValueError: if the bytes cannot be decoded.
228    #[staticmethod]
229    fn from_der(data: &[u8]) -> PyResult<Self> {
230        let inner = NumericString::from_der(data).map_err(SyntaErr)?;
231        Ok(Self { inner })
232    }
233}
234
235/// Python wrapper for ASN.1 TeletexString / T61String (tag 20) — arbitrary 8-bit bytes
236#[pyclass(name = "TeletexString")]
237#[derive(Debug, Clone)]
238pub struct PyTeletexString {
239    pub(crate) inner: TeletexString,
240}
241
242#[pymethods]
243impl PyTeletexString {
244    /// Create from raw bytes.
245    #[new]
246    fn new(data: Vec<u8>) -> Self {
247        Self {
248            inner: TeletexString::new(data),
249        }
250    }
251
252    /// Create from a Python str, encoding as Latin-1.
253    ///
254    /// Raises ValueError if any character is outside the Latin-1 range (U+0000..=U+00FF).
255    #[staticmethod]
256    fn from_latin1(value: &str) -> PyResult<Self> {
257        TeletexString::from_latin1(value)
258            .map(|inner| Self { inner })
259            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
260    }
261
262    /// Create from a Python str, encoding as Latin-1.
263    ///
264    /// Raises ValueError if any character is outside the Latin-1 range (U+0000..=U+00FF).
265    #[staticmethod]
266    fn from_str(value: &str) -> PyResult<Self> {
267        TeletexString::from_latin1(value)
268            .map(|inner| Self { inner })
269            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
270    }
271
272    /// Return the raw bytes.
273    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
274        PyBytes::new(py, self.inner.as_bytes())
275    }
276
277    /// Decode as a Latin-1 string.
278    fn as_str(&self) -> String {
279        self.inner.as_latin1_string()
280    }
281
282    fn __str__(&self) -> String {
283        self.inner.as_latin1_string()
284    }
285
286    fn __repr__(&self) -> String {
287        format!("TeletexString(<{} bytes>)", self.inner.as_bytes().len())
288    }
289
290    fn __len__(&self) -> usize {
291        self.inner.as_bytes().len()
292    }
293
294    fn __eq__(&self, other: &Self) -> bool {
295        self.inner == other.inner
296    }
297
298    /// Return the DER encoding of this ``TeletexString``.
299    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
300        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
301    }
302
303    /// Parse a DER-encoded ``TeletexString``.
304    ///
305    /// :raises ValueError: if the bytes cannot be decoded.
306    #[staticmethod]
307    fn from_der(data: &[u8]) -> PyResult<Self> {
308        let inner = TeletexString::from_der(data).map_err(SyntaErr)?;
309        Ok(Self { inner })
310    }
311}
312
313/// Python wrapper for ASN.1 VisibleString (tag 26) — printable ASCII
314#[pyclass(name = "VisibleString")]
315#[derive(Debug, Clone)]
316pub struct PyVisibleString {
317    pub(crate) inner: VisibleString,
318}
319
320#[pymethods]
321impl PyVisibleString {
322    /// Create a ``VisibleString`` from *value*.
323    ///
324    /// :raises ValueError: if *value* contains characters outside the printable ASCII range
325    ///     (U+0020–U+007E).
326    #[new]
327    fn new(value: &str) -> PyResult<Self> {
328        let inner = VisibleString::new(value.to_string())
329            .map_err(|e| PyValueError::new_err(format!("Invalid VisibleString: {:?}", e)))?;
330        Ok(Self { inner })
331    }
332
333    fn as_str(&self) -> &str {
334        self.inner.as_str()
335    }
336
337    fn __str__(&self) -> &str {
338        self.inner.as_str()
339    }
340
341    fn __repr__(&self) -> String {
342        format!("VisibleString('{}')", self.inner.as_str())
343    }
344
345    fn __eq__(&self, other: &Self) -> bool {
346        self.inner == other.inner
347    }
348
349    fn __len__(&self) -> usize {
350        self.inner.as_str().len()
351    }
352
353    /// Return the DER encoding of this ``VisibleString``.
354    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
355        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
356    }
357
358    /// Parse a DER-encoded ``VisibleString``.
359    ///
360    /// :raises ValueError: if the bytes cannot be decoded.
361    #[staticmethod]
362    fn from_der(data: &[u8]) -> PyResult<Self> {
363        let inner = VisibleString::from_der(data).map_err(SyntaErr)?;
364        Ok(Self { inner })
365    }
366}
367
368/// Python wrapper for ASN.1 GeneralString (tag 27) — 8-bit bytes, treated as Latin-1
369#[pyclass(name = "GeneralString")]
370#[derive(Debug, Clone)]
371pub struct PyGeneralString {
372    pub(crate) inner: GeneralString,
373}
374
375#[pymethods]
376impl PyGeneralString {
377    /// Create from raw bytes.
378    #[new]
379    fn new(data: Vec<u8>) -> Self {
380        Self {
381            inner: GeneralString::new(data),
382        }
383    }
384
385    /// Create from a Python str, encoding as ASCII.
386    ///
387    /// Raises ValueError if any character is outside the ASCII range (U+0000..=U+007F).
388    #[staticmethod]
389    fn from_ascii(value: &str) -> PyResult<Self> {
390        GeneralString::from_ascii(value)
391            .map(|inner| Self { inner })
392            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
393    }
394
395    /// Create from a Python str, encoding as ASCII.
396    ///
397    /// Raises ValueError if any character is outside the ASCII range (U+0000..=U+007F).
398    #[staticmethod]
399    fn from_str(value: &str) -> PyResult<Self> {
400        GeneralString::from_ascii(value)
401            .map(|inner| Self { inner })
402            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
403    }
404
405    /// Return the raw bytes.
406    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
407        PyBytes::new(py, self.inner.as_bytes())
408    }
409
410    /// Decode as a Latin-1 string.
411    fn as_str(&self) -> String {
412        self.inner.as_latin1_string()
413    }
414
415    fn __str__(&self) -> String {
416        self.inner.as_latin1_string()
417    }
418
419    fn __repr__(&self) -> String {
420        format!("GeneralString(<{} bytes>)", self.inner.as_bytes().len())
421    }
422
423    fn __len__(&self) -> usize {
424        self.inner.as_bytes().len()
425    }
426
427    fn __eq__(&self, other: &Self) -> bool {
428        self.inner == other.inner
429    }
430
431    /// Return the DER encoding of this ``GeneralString``.
432    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
433        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
434    }
435
436    /// Parse a DER-encoded ``GeneralString``.
437    ///
438    /// :raises ValueError: if the bytes cannot be decoded.
439    #[staticmethod]
440    fn from_der(data: &[u8]) -> PyResult<Self> {
441        let inner = GeneralString::from_der(data).map_err(SyntaErr)?;
442        Ok(Self { inner })
443    }
444}
445
446/// Python wrapper for ASN.1 UniversalString (tag 28) — UCS-4 big-endian
447#[pyclass(name = "UniversalString")]
448#[derive(Debug, Clone)]
449pub struct PyUniversalString {
450    pub(crate) inner: UniversalString,
451}
452
453#[pymethods]
454impl PyUniversalString {
455    /// Create from a Python str.
456    #[new]
457    fn new(value: &str) -> Self {
458        Self {
459            inner: UniversalString::new(value.to_string()),
460        }
461    }
462
463    /// Create from raw UCS-4 big-endian bytes.
464    #[staticmethod]
465    fn from_bytes(data: &[u8]) -> PyResult<Self> {
466        let inner = UniversalString::from_ucs4_be(data)
467            .map_err(|e| PyValueError::new_err(format!("Invalid UniversalString: {:?}", e)))?;
468        Ok(Self { inner })
469    }
470
471    fn as_str(&self) -> &str {
472        self.inner.as_str()
473    }
474
475    fn __str__(&self) -> &str {
476        self.inner.as_str()
477    }
478
479    fn __repr__(&self) -> String {
480        format!("UniversalString('{}')", self.inner.as_str())
481    }
482
483    fn __eq__(&self, other: &Self) -> bool {
484        self.inner == other.inner
485    }
486
487    fn __len__(&self) -> usize {
488        self.inner.as_str().chars().count()
489    }
490
491    /// Return the DER encoding of this ``UniversalString``.
492    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
493        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
494    }
495
496    /// Parse a DER-encoded ``UniversalString``.
497    ///
498    /// :raises ValueError: if the bytes cannot be decoded.
499    #[staticmethod]
500    fn from_der(data: &[u8]) -> PyResult<Self> {
501        let inner = UniversalString::from_der(data).map_err(SyntaErr)?;
502        Ok(Self { inner })
503    }
504}
505
506/// Python wrapper for ASN.1 BMPString (tag 30) — UCS-2 big-endian (BMP only)
507#[pyclass(name = "BmpString")]
508#[derive(Debug, Clone)]
509pub struct PyBmpString {
510    pub(crate) inner: BmpString,
511}
512
513#[pymethods]
514impl PyBmpString {
515    /// Create from a Python str (must be BMP-only, i.e. no code points > U+FFFF).
516    #[new]
517    fn new(value: &str) -> PyResult<Self> {
518        let inner = BmpString::new(value.to_string())
519            .map_err(|e| PyValueError::new_err(format!("Invalid BMPString: {:?}", e)))?;
520        Ok(Self { inner })
521    }
522
523    /// Create from raw UCS-2 big-endian bytes.
524    #[staticmethod]
525    fn from_bytes(data: &[u8]) -> PyResult<Self> {
526        let inner = BmpString::from_ucs2_be(data)
527            .map_err(|e| PyValueError::new_err(format!("Invalid BMPString: {:?}", e)))?;
528        Ok(Self { inner })
529    }
530
531    fn as_str(&self) -> &str {
532        self.inner.as_str()
533    }
534
535    fn __str__(&self) -> &str {
536        self.inner.as_str()
537    }
538
539    fn __repr__(&self) -> String {
540        format!("BmpString('{}')", self.inner.as_str())
541    }
542
543    fn __eq__(&self, other: &Self) -> bool {
544        self.inner == other.inner
545    }
546
547    fn __len__(&self) -> usize {
548        self.inner.as_str().chars().count()
549    }
550
551    /// Return the DER encoding of this ``BmpString``.
552    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
553        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
554    }
555
556    /// Parse a DER-encoded ``BmpString``.
557    ///
558    /// :raises ValueError: if the bytes cannot be decoded.
559    #[staticmethod]
560    fn from_der(data: &[u8]) -> PyResult<Self> {
561        let inner = BmpString::from_der(data).map_err(SyntaErr)?;
562        Ok(Self { inner })
563    }
564}