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    #[new]
191    fn new(value: &str) -> PyResult<Self> {
192        let inner = NumericString::new(value.to_string())
193            .map_err(|e| PyValueError::new_err(format!("Invalid NumericString: {:?}", e)))?;
194        Ok(Self { inner })
195    }
196
197    fn as_str(&self) -> &str {
198        self.inner.as_str()
199    }
200
201    fn __str__(&self) -> &str {
202        self.inner.as_str()
203    }
204
205    fn __repr__(&self) -> String {
206        format!("NumericString('{}')", self.inner.as_str())
207    }
208
209    fn __eq__(&self, other: &Self) -> bool {
210        self.inner == other.inner
211    }
212
213    fn __len__(&self) -> usize {
214        self.inner.as_str().chars().count()
215    }
216
217    /// Return the DER encoding of this ``NumericString``.
218    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
219        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
220    }
221
222    /// Parse a DER-encoded ``NumericString``.
223    ///
224    /// :raises ValueError: if the bytes cannot be decoded.
225    #[staticmethod]
226    fn from_der(data: &[u8]) -> PyResult<Self> {
227        let inner = NumericString::from_der(data).map_err(SyntaErr)?;
228        Ok(Self { inner })
229    }
230}
231
232/// Python wrapper for ASN.1 TeletexString / T61String (tag 20) — arbitrary 8-bit bytes
233#[pyclass(name = "TeletexString")]
234#[derive(Debug, Clone)]
235pub struct PyTeletexString {
236    pub(crate) inner: TeletexString,
237}
238
239#[pymethods]
240impl PyTeletexString {
241    /// Create from raw bytes.
242    #[new]
243    fn new(data: Vec<u8>) -> Self {
244        Self {
245            inner: TeletexString::new(data),
246        }
247    }
248
249    /// Create from a Python str, encoding as Latin-1.
250    ///
251    /// Raises ValueError if any character is outside the Latin-1 range (U+0000..=U+00FF).
252    #[staticmethod]
253    fn from_latin1(value: &str) -> PyResult<Self> {
254        TeletexString::from_latin1(value)
255            .map(|inner| Self { inner })
256            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
257    }
258
259    /// Create from a Python str, encoding as Latin-1.
260    ///
261    /// Raises ValueError if any character is outside the Latin-1 range (U+0000..=U+00FF).
262    #[staticmethod]
263    fn from_str(value: &str) -> PyResult<Self> {
264        TeletexString::from_latin1(value)
265            .map(|inner| Self { inner })
266            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
267    }
268
269    /// Return the raw bytes.
270    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
271        PyBytes::new(py, self.inner.as_bytes())
272    }
273
274    /// Decode as a Latin-1 string.
275    fn as_str(&self) -> String {
276        self.inner.as_latin1_string()
277    }
278
279    fn __str__(&self) -> String {
280        self.inner.as_latin1_string()
281    }
282
283    fn __repr__(&self) -> String {
284        format!("TeletexString(<{} bytes>)", self.inner.as_bytes().len())
285    }
286
287    fn __len__(&self) -> usize {
288        self.inner.as_bytes().len()
289    }
290
291    fn __eq__(&self, other: &Self) -> bool {
292        self.inner == other.inner
293    }
294
295    /// Return the DER encoding of this ``TeletexString``.
296    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
297        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
298    }
299
300    /// Parse a DER-encoded ``TeletexString``.
301    ///
302    /// :raises ValueError: if the bytes cannot be decoded.
303    #[staticmethod]
304    fn from_der(data: &[u8]) -> PyResult<Self> {
305        let inner = TeletexString::from_der(data).map_err(SyntaErr)?;
306        Ok(Self { inner })
307    }
308}
309
310/// Python wrapper for ASN.1 VisibleString (tag 26) — printable ASCII
311#[pyclass(name = "VisibleString")]
312#[derive(Debug, Clone)]
313pub struct PyVisibleString {
314    pub(crate) inner: VisibleString,
315}
316
317#[pymethods]
318impl PyVisibleString {
319    #[new]
320    fn new(value: &str) -> PyResult<Self> {
321        let inner = VisibleString::new(value.to_string())
322            .map_err(|e| PyValueError::new_err(format!("Invalid VisibleString: {:?}", e)))?;
323        Ok(Self { inner })
324    }
325
326    fn as_str(&self) -> &str {
327        self.inner.as_str()
328    }
329
330    fn __str__(&self) -> &str {
331        self.inner.as_str()
332    }
333
334    fn __repr__(&self) -> String {
335        format!("VisibleString('{}')", self.inner.as_str())
336    }
337
338    fn __eq__(&self, other: &Self) -> bool {
339        self.inner == other.inner
340    }
341
342    fn __len__(&self) -> usize {
343        self.inner.as_str().len()
344    }
345
346    /// Return the DER encoding of this ``VisibleString``.
347    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
348        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
349    }
350
351    /// Parse a DER-encoded ``VisibleString``.
352    ///
353    /// :raises ValueError: if the bytes cannot be decoded.
354    #[staticmethod]
355    fn from_der(data: &[u8]) -> PyResult<Self> {
356        let inner = VisibleString::from_der(data).map_err(SyntaErr)?;
357        Ok(Self { inner })
358    }
359}
360
361/// Python wrapper for ASN.1 GeneralString (tag 27) — 8-bit bytes, treated as Latin-1
362#[pyclass(name = "GeneralString")]
363#[derive(Debug, Clone)]
364pub struct PyGeneralString {
365    pub(crate) inner: GeneralString,
366}
367
368#[pymethods]
369impl PyGeneralString {
370    /// Create from raw bytes.
371    #[new]
372    fn new(data: Vec<u8>) -> Self {
373        Self {
374            inner: GeneralString::new(data),
375        }
376    }
377
378    /// Create from a Python str, encoding as ASCII.
379    ///
380    /// Raises ValueError if any character is outside the ASCII range (U+0000..=U+007F).
381    #[staticmethod]
382    fn from_ascii(value: &str) -> PyResult<Self> {
383        GeneralString::from_ascii(value)
384            .map(|inner| Self { inner })
385            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
386    }
387
388    /// Create from a Python str, encoding as ASCII.
389    ///
390    /// Raises ValueError if any character is outside the ASCII range (U+0000..=U+007F).
391    #[staticmethod]
392    fn from_str(value: &str) -> PyResult<Self> {
393        GeneralString::from_ascii(value)
394            .map(|inner| Self { inner })
395            .map_err(|e| PyValueError::new_err(format!("{:?}", e)))
396    }
397
398    /// Return the raw bytes.
399    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
400        PyBytes::new(py, self.inner.as_bytes())
401    }
402
403    /// Decode as a Latin-1 string.
404    fn as_str(&self) -> String {
405        self.inner.as_latin1_string()
406    }
407
408    fn __str__(&self) -> String {
409        self.inner.as_latin1_string()
410    }
411
412    fn __repr__(&self) -> String {
413        format!("GeneralString(<{} bytes>)", self.inner.as_bytes().len())
414    }
415
416    fn __len__(&self) -> usize {
417        self.inner.as_bytes().len()
418    }
419
420    fn __eq__(&self, other: &Self) -> bool {
421        self.inner == other.inner
422    }
423
424    /// Return the DER encoding of this ``GeneralString``.
425    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
426        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
427    }
428
429    /// Parse a DER-encoded ``GeneralString``.
430    ///
431    /// :raises ValueError: if the bytes cannot be decoded.
432    #[staticmethod]
433    fn from_der(data: &[u8]) -> PyResult<Self> {
434        let inner = GeneralString::from_der(data).map_err(SyntaErr)?;
435        Ok(Self { inner })
436    }
437}
438
439/// Python wrapper for ASN.1 UniversalString (tag 28) — UCS-4 big-endian
440#[pyclass(name = "UniversalString")]
441#[derive(Debug, Clone)]
442pub struct PyUniversalString {
443    pub(crate) inner: UniversalString,
444}
445
446#[pymethods]
447impl PyUniversalString {
448    /// Create from a Python str.
449    #[new]
450    fn new(value: &str) -> Self {
451        Self {
452            inner: UniversalString::new(value.to_string()),
453        }
454    }
455
456    /// Create from raw UCS-4 big-endian bytes.
457    #[staticmethod]
458    fn from_bytes(data: &[u8]) -> PyResult<Self> {
459        let inner = UniversalString::from_ucs4_be(data)
460            .map_err(|e| PyValueError::new_err(format!("Invalid UniversalString: {:?}", e)))?;
461        Ok(Self { inner })
462    }
463
464    fn as_str(&self) -> &str {
465        self.inner.as_str()
466    }
467
468    fn __str__(&self) -> &str {
469        self.inner.as_str()
470    }
471
472    fn __repr__(&self) -> String {
473        format!("UniversalString('{}')", self.inner.as_str())
474    }
475
476    fn __eq__(&self, other: &Self) -> bool {
477        self.inner == other.inner
478    }
479
480    fn __len__(&self) -> usize {
481        self.inner.as_str().chars().count()
482    }
483
484    /// Return the DER encoding of this ``UniversalString``.
485    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
486        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
487    }
488
489    /// Parse a DER-encoded ``UniversalString``.
490    ///
491    /// :raises ValueError: if the bytes cannot be decoded.
492    #[staticmethod]
493    fn from_der(data: &[u8]) -> PyResult<Self> {
494        let inner = UniversalString::from_der(data).map_err(SyntaErr)?;
495        Ok(Self { inner })
496    }
497}
498
499/// Python wrapper for ASN.1 BMPString (tag 30) — UCS-2 big-endian (BMP only)
500#[pyclass(name = "BmpString")]
501#[derive(Debug, Clone)]
502pub struct PyBmpString {
503    pub(crate) inner: BmpString,
504}
505
506#[pymethods]
507impl PyBmpString {
508    /// Create from a Python str (must be BMP-only, i.e. no code points > U+FFFF).
509    #[new]
510    fn new(value: &str) -> PyResult<Self> {
511        let inner = BmpString::new(value.to_string())
512            .map_err(|e| PyValueError::new_err(format!("Invalid BMPString: {:?}", e)))?;
513        Ok(Self { inner })
514    }
515
516    /// Create from raw UCS-2 big-endian bytes.
517    #[staticmethod]
518    fn from_bytes(data: &[u8]) -> PyResult<Self> {
519        let inner = BmpString::from_ucs2_be(data)
520            .map_err(|e| PyValueError::new_err(format!("Invalid BMPString: {:?}", e)))?;
521        Ok(Self { inner })
522    }
523
524    fn as_str(&self) -> &str {
525        self.inner.as_str()
526    }
527
528    fn __str__(&self) -> &str {
529        self.inner.as_str()
530    }
531
532    fn __repr__(&self) -> String {
533        format!("BmpString('{}')", self.inner.as_str())
534    }
535
536    fn __eq__(&self, other: &Self) -> bool {
537        self.inner == other.inner
538    }
539
540    fn __len__(&self) -> usize {
541        self.inner.as_str().chars().count()
542    }
543
544    /// Return the DER encoding of this ``BmpString``.
545    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
546        Ok(PyBytes::new(py, &self.inner.to_der().map_err(SyntaErr)?))
547    }
548
549    /// Parse a DER-encoded ``BmpString``.
550    ///
551    /// :raises ValueError: if the bytes cannot be decoded.
552    #[staticmethod]
553    fn from_der(data: &[u8]) -> PyResult<Self> {
554        let inner = BmpString::from_der(data).map_err(SyntaErr)?;
555        Ok(Self { inner })
556    }
557}