rcgen/
string_types.rs

1use std::{fmt, str::FromStr};
2
3use crate::{Error, InvalidAsn1String};
4
5/// ASN.1 `PrintableString` type.
6///
7/// Supports a subset of the ASCII printable characters (described below).
8///
9/// For the full ASCII character set, use
10/// [`Ia5String`][`crate::Ia5String`].
11///
12/// # Examples
13///
14/// You can create a `PrintableString` from [a literal string][`&str`] with [`PrintableString::try_from`]:
15///
16/// ```
17/// use rcgen::PrintableString;
18/// let hello = PrintableString::try_from("hello").unwrap();
19/// ```
20///
21/// # Supported characters
22///
23/// PrintableString is a subset of the [ASCII printable characters].
24/// For instance, `'@'` is a printable character as per ASCII but can't be part of [ASN.1's `PrintableString`].
25///
26/// The following ASCII characters/ranges are supported:
27///
28/// - `A..Z`
29/// - `a..z`
30/// - `0..9`
31/// - "` `" (i.e. space)
32/// - `\`
33/// - `(`
34/// - `)`
35/// - `+`
36/// - `,`
37/// - `-`
38/// - `.`
39/// - `/`
40/// - `:`
41/// - `=`
42/// - `?`
43///
44/// [ASCII printable characters]: https://en.wikipedia.org/wiki/ASCII#Printable_characters
45/// [ASN.1's `PrintableString`]: https://en.wikipedia.org/wiki/PrintableString
46#[derive(Debug, PartialEq, Eq, Hash, Clone)]
47pub struct PrintableString(String);
48
49impl PrintableString {
50	/// Extracts a string slice containing the entire `PrintableString`.
51	pub fn as_str(&self) -> &str {
52		&self.0
53	}
54}
55
56impl TryFrom<&str> for PrintableString {
57	type Error = Error;
58
59	/// Converts a `&str` to a [`PrintableString`].
60	///
61	/// Any character not in the [`PrintableString`] charset will be rejected.
62	/// See [`PrintableString`] documentation for more information.
63	///
64	/// The result is allocated on the heap.
65	fn try_from(input: &str) -> Result<Self, Error> {
66		input.to_string().try_into()
67	}
68}
69
70impl TryFrom<String> for PrintableString {
71	type Error = Error;
72
73	/// Converts a [`String`][`std::string::String`] into a [`PrintableString`]
74	///
75	/// Any character not in the [`PrintableString`] charset will be rejected.
76	/// See [`PrintableString`] documentation for more information.
77	///
78	/// This conversion does not allocate or copy memory.
79	fn try_from(value: String) -> Result<Self, Self::Error> {
80		for &c in value.as_bytes() {
81			match c {
82				b'A'..=b'Z'
83				| b'a'..=b'z'
84				| b'0'..=b'9'
85				| b' '
86				| b'\''
87				| b'('
88				| b')'
89				| b'+'
90				| b','
91				| b'-'
92				| b'.'
93				| b'/'
94				| b':'
95				| b'='
96				| b'?' => (),
97				_ => {
98					return Err(Error::InvalidAsn1String(
99						InvalidAsn1String::PrintableString(value),
100					))
101				},
102			}
103		}
104		Ok(Self(value))
105	}
106}
107
108impl FromStr for PrintableString {
109	type Err = Error;
110
111	fn from_str(s: &str) -> Result<Self, Self::Err> {
112		s.try_into()
113	}
114}
115
116impl AsRef<str> for PrintableString {
117	fn as_ref(&self) -> &str {
118		&self.0
119	}
120}
121
122impl fmt::Display for PrintableString {
123	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124		fmt::Display::fmt(self.as_str(), f)
125	}
126}
127
128impl PartialEq<str> for PrintableString {
129	fn eq(&self, other: &str) -> bool {
130		self.as_str() == other
131	}
132}
133
134impl PartialEq<String> for PrintableString {
135	fn eq(&self, other: &String) -> bool {
136		self.as_str() == other.as_str()
137	}
138}
139
140impl PartialEq<&str> for PrintableString {
141	fn eq(&self, other: &&str) -> bool {
142		self.as_str() == *other
143	}
144}
145
146impl PartialEq<&String> for PrintableString {
147	fn eq(&self, other: &&String) -> bool {
148		self.as_str() == other.as_str()
149	}
150}
151
152/// ASN.1 `IA5String` type.
153///
154/// # Examples
155///
156/// You can create a `Ia5String` from [a literal string][`&str`] with [`Ia5String::try_from`]:
157///
158/// ```
159/// use rcgen::Ia5String;
160/// let hello = Ia5String::try_from("hello").unwrap();
161/// ```
162///
163/// # Supported characters
164///
165/// Supports the [International Alphabet No. 5 (IA5)] character encoding, i.e.
166/// the 128 characters of the ASCII alphabet. (Note: IA5 is now
167/// technically known as the International Reference Alphabet or IRA as
168/// specified in the ITU-T's T.50 recommendation).
169///
170/// For UTF-8, use [`String`][`std::string::String`].
171///
172/// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_(standard)
173#[derive(Debug, PartialEq, Eq, Hash, Clone)]
174pub struct Ia5String(String);
175
176impl Ia5String {
177	/// Extracts a string slice containing the entire `Ia5String`.
178	pub fn as_str(&self) -> &str {
179		&self.0
180	}
181}
182
183impl TryFrom<&str> for Ia5String {
184	type Error = Error;
185
186	/// Converts a `&str` to a [`Ia5String`].
187	///
188	/// Any character not in the [`Ia5String`] charset will be rejected.
189	/// See [`Ia5String`] documentation for more information.
190	///
191	/// The result is allocated on the heap.
192	fn try_from(input: &str) -> Result<Self, Error> {
193		input.to_string().try_into()
194	}
195}
196
197impl TryFrom<String> for Ia5String {
198	type Error = Error;
199
200	/// Converts a [`String`][`std::string::String`] into a [`Ia5String`]
201	///
202	/// Any character not in the [`Ia5String`] charset will be rejected.
203	/// See [`Ia5String`] documentation for more information.
204	fn try_from(input: String) -> Result<Self, Error> {
205		if !input.is_ascii() {
206			return Err(Error::InvalidAsn1String(InvalidAsn1String::Ia5String(
207				input,
208			)));
209		}
210		Ok(Self(input))
211	}
212}
213
214impl FromStr for Ia5String {
215	type Err = Error;
216
217	fn from_str(s: &str) -> Result<Self, Self::Err> {
218		s.try_into()
219	}
220}
221
222impl AsRef<str> for Ia5String {
223	fn as_ref(&self) -> &str {
224		&self.0
225	}
226}
227
228impl fmt::Display for Ia5String {
229	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230		fmt::Display::fmt(self.as_str(), f)
231	}
232}
233
234impl PartialEq<str> for Ia5String {
235	fn eq(&self, other: &str) -> bool {
236		self.as_str() == other
237	}
238}
239
240impl PartialEq<String> for Ia5String {
241	fn eq(&self, other: &String) -> bool {
242		self.as_str() == other.as_str()
243	}
244}
245
246impl PartialEq<&str> for Ia5String {
247	fn eq(&self, other: &&str) -> bool {
248		self.as_str() == *other
249	}
250}
251
252impl PartialEq<&String> for Ia5String {
253	fn eq(&self, other: &&String) -> bool {
254		self.as_str() == other.as_str()
255	}
256}
257
258/// ASN.1 `TeletexString` type.
259///
260/// # Examples
261///
262/// You can create a `TeletexString` from [a literal string][`&str`] with [`TeletexString::try_from`]:
263///
264/// ```
265/// use rcgen::TeletexString;
266/// let hello = TeletexString::try_from("hello").unwrap();
267/// ```
268///
269/// # Supported characters
270///
271/// The standard defines a complex character set allowed in this type. However, quoting the ASN.1
272/// [mailing list], "a sizable volume of software in the world treats TeletexString (T61String) as a
273/// simple 8-bit string with mostly Windows Latin 1 (superset of iso-8859-1) encoding".
274///
275/// `TeletexString` is included for backward compatibility, [RFC 5280] say it
276/// SHOULD NOT be used for certificates for new subjects.
277///
278/// [mailing list]: https://www.mail-archive.com/asn1@asn1.org/msg00460.html
279/// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25
280#[derive(Debug, PartialEq, Eq, Hash, Clone)]
281pub struct TeletexString(String);
282
283impl TeletexString {
284	/// Extracts a string slice containing the entire `TeletexString`.
285	pub fn as_str(&self) -> &str {
286		&self.0
287	}
288
289	/// Returns a byte slice of this `TeletexString`’s contents.
290	pub fn as_bytes(&self) -> &[u8] {
291		self.0.as_bytes()
292	}
293}
294
295impl TryFrom<&str> for TeletexString {
296	type Error = Error;
297
298	/// Converts a `&str` to a [`TeletexString`].
299	///
300	/// Any character not in the [`TeletexString`] charset will be rejected.
301	/// See [`TeletexString`] documentation for more information.
302	///
303	/// The result is allocated on the heap.
304	fn try_from(input: &str) -> Result<Self, Error> {
305		input.to_string().try_into()
306	}
307}
308
309impl TryFrom<String> for TeletexString {
310	type Error = Error;
311
312	/// Converts a [`String`][`std::string::String`] into a [`TeletexString`]
313	///
314	/// Any character not in the [`TeletexString`] charset will be rejected.
315	/// See [`TeletexString`] documentation for more information.
316	///
317	/// This conversion does not allocate or copy memory.
318	fn try_from(input: String) -> Result<Self, Error> {
319		// Check all bytes are visible
320		if !input.as_bytes().iter().all(|b| (0x20..=0x7f).contains(b)) {
321			return Err(Error::InvalidAsn1String(InvalidAsn1String::TeletexString(
322				input,
323			)));
324		}
325		Ok(Self(input))
326	}
327}
328
329impl FromStr for TeletexString {
330	type Err = Error;
331
332	fn from_str(s: &str) -> Result<Self, Self::Err> {
333		s.try_into()
334	}
335}
336
337impl AsRef<str> for TeletexString {
338	fn as_ref(&self) -> &str {
339		&self.0
340	}
341}
342
343impl fmt::Display for TeletexString {
344	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345		fmt::Display::fmt(self.as_str(), f)
346	}
347}
348
349impl PartialEq<str> for TeletexString {
350	fn eq(&self, other: &str) -> bool {
351		self.as_str() == other
352	}
353}
354
355impl PartialEq<String> for TeletexString {
356	fn eq(&self, other: &String) -> bool {
357		self.as_str() == other.as_str()
358	}
359}
360
361impl PartialEq<&str> for TeletexString {
362	fn eq(&self, other: &&str) -> bool {
363		self.as_str() == *other
364	}
365}
366
367impl PartialEq<&String> for TeletexString {
368	fn eq(&self, other: &&String) -> bool {
369		self.as_str() == other.as_str()
370	}
371}
372
373/// ASN.1 `BMPString` type.
374///
375/// # Examples
376///
377/// You can create a `BmpString` from [a literal string][`&str`] with [`BmpString::try_from`]:
378///
379/// ```
380/// use rcgen::BmpString;
381/// let hello = BmpString::try_from("hello").unwrap();
382/// ```
383///
384/// # Supported characters
385///
386/// Encodes Basic Multilingual Plane (BMP) subset of Unicode (ISO 10646),
387/// a.k.a. UCS-2.
388///
389/// Bytes are encoded as UTF-16 big-endian.
390///
391/// `BMPString` is included for backward compatibility, [RFC 5280] say it
392/// SHOULD NOT be used for certificates for new subjects.
393///
394/// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25
395#[derive(Debug, PartialEq, Eq, Hash, Clone)]
396pub struct BmpString(Vec<u8>);
397
398impl BmpString {
399	/// Returns a byte slice of this `BmpString`'s contents.
400	///
401	/// The inverse of this method is [`from_utf16be`].
402	///
403	/// [`from_utf16be`]: BmpString::from_utf16be
404	///
405	/// # Examples
406	///
407	/// ```
408	/// use rcgen::BmpString;
409	/// let s = BmpString::try_from("hello").unwrap();
410	///
411	/// assert_eq!(&[0, 104, 0, 101, 0, 108, 0, 108, 0, 111], s.as_bytes());
412	/// ```
413	pub fn as_bytes(&self) -> &[u8] {
414		&self.0
415	}
416
417	/// Decode a UTF-16BE–encoded vector `vec` into a `BmpString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data.
418	pub fn from_utf16be(vec: Vec<u8>) -> Result<Self, Error> {
419		if vec.len() % 2 != 0 {
420			return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString(
421				"Invalid UTF-16 encoding".to_string(),
422			)));
423		}
424
425		// FIXME: Update this when `array_chunks` is stabilized.
426		for maybe_char in char::decode_utf16(
427			vec.chunks_exact(2)
428				.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])),
429		) {
430			// We check we only use the BMP subset of Unicode (the first 65 536 code points)
431			match maybe_char {
432				// Character is in the Basic Multilingual Plane
433				Ok(c) if (c as u64) < u64::from(u16::MAX) => (),
434				// Characters outside Basic Multilingual Plane or unpaired surrogates
435				_ => {
436					return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString(
437						"Invalid UTF-16 encoding".to_string(),
438					)));
439				},
440			}
441		}
442		Ok(Self(vec.to_vec()))
443	}
444}
445
446impl TryFrom<&str> for BmpString {
447	type Error = Error;
448
449	/// Converts a `&str` to a [`BmpString`].
450	///
451	/// Any character not in the [`BmpString`] charset will be rejected.
452	/// See [`BmpString`] documentation for more information.
453	///
454	/// The result is allocated on the heap.
455	fn try_from(value: &str) -> Result<Self, Self::Error> {
456		let capacity = value.len().checked_mul(2).ok_or_else(|| {
457			Error::InvalidAsn1String(InvalidAsn1String::BmpString(value.to_string()))
458		})?;
459
460		let mut bytes = Vec::with_capacity(capacity);
461
462		for code_point in value.encode_utf16() {
463			bytes.extend(code_point.to_be_bytes());
464		}
465
466		BmpString::from_utf16be(bytes)
467	}
468}
469
470impl TryFrom<String> for BmpString {
471	type Error = Error;
472
473	/// Converts a [`String`][`std::string::String`] into a [`BmpString`]
474	///
475	/// Any character not in the [`BmpString`] charset will be rejected.
476	/// See [`BmpString`] documentation for more information.
477	///
478	/// Parsing a `BmpString` allocates memory since the UTF-8 to UTF-16 conversion requires a memory allocation.
479	fn try_from(value: String) -> Result<Self, Self::Error> {
480		value.as_str().try_into()
481	}
482}
483
484impl FromStr for BmpString {
485	type Err = Error;
486
487	fn from_str(s: &str) -> Result<Self, Self::Err> {
488		s.try_into()
489	}
490}
491
492/// ASN.1 `UniversalString` type.
493///
494/// # Examples
495///
496/// You can create a `UniversalString` from [a literal string][`&str`] with [`UniversalString::try_from`]:
497///
498/// ```
499/// use rcgen::UniversalString;
500/// let hello = UniversalString::try_from("hello").unwrap();
501/// ```
502///
503/// # Supported characters
504///
505/// The characters which can appear in the `UniversalString` type are any of the characters allowed by
506/// ISO/IEC 10646 (Unicode).
507///
508/// Bytes are encoded like UTF-32 big-endian.
509///
510/// `UniversalString` is included for backward compatibility, [RFC 5280] say it
511/// SHOULD NOT be used for certificates for new subjects.
512///
513/// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25
514#[derive(Debug, PartialEq, Eq, Hash, Clone)]
515pub struct UniversalString(Vec<u8>);
516
517impl UniversalString {
518	/// Returns a byte slice of this `UniversalString`'s contents.
519	///
520	/// The inverse of this method is [`from_utf32be`].
521	///
522	/// [`from_utf32be`]: UniversalString::from_utf32be
523	///
524	/// # Examples
525	///
526	/// ```
527	/// use rcgen::UniversalString;
528	/// let s = UniversalString::try_from("hello").unwrap();
529	///
530	/// assert_eq!(&[0, 0, 0, 104, 0, 0, 0, 101, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 111], s.as_bytes());
531	/// ```
532	pub fn as_bytes(&self) -> &[u8] {
533		&self.0
534	}
535
536	/// Decode a UTF-32BE–encoded vector `vec` into a `UniversalString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data.
537	pub fn from_utf32be(vec: Vec<u8>) -> Result<UniversalString, Error> {
538		if vec.len() % 4 != 0 {
539			return Err(Error::InvalidAsn1String(
540				InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()),
541			));
542		}
543
544		// FIXME: Update this when `array_chunks` is stabilized.
545		for maybe_char in vec
546			.chunks_exact(4)
547			.map(|chunk| u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
548		{
549			if core::char::from_u32(maybe_char).is_none() {
550				return Err(Error::InvalidAsn1String(
551					InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()),
552				));
553			}
554		}
555
556		Ok(Self(vec))
557	}
558}
559
560impl TryFrom<&str> for UniversalString {
561	type Error = Error;
562
563	/// Converts a `&str` to a [`UniversalString`].
564	///
565	/// Any character not in the [`UniversalString`] charset will be rejected.
566	/// See [`UniversalString`] documentation for more information.
567	///
568	/// The result is allocated on the heap.
569	fn try_from(value: &str) -> Result<Self, Self::Error> {
570		let capacity = value.len().checked_mul(4).ok_or_else(|| {
571			Error::InvalidAsn1String(InvalidAsn1String::UniversalString(value.to_string()))
572		})?;
573
574		let mut bytes = Vec::with_capacity(capacity);
575
576		// A `char` is any ‘Unicode code point’ other than a surrogate code point.
577		// The code units for UTF-32 correspond exactly to Unicode code points.
578		// (https://www.unicode.org/reports/tr19/tr19-9.html#Introduction)
579		// So any `char` is a valid UTF-32, we just cast it to perform the convertion.
580		for char in value.chars().map(|char| char as u32) {
581			bytes.extend(char.to_be_bytes())
582		}
583
584		UniversalString::from_utf32be(bytes)
585	}
586}
587
588impl TryFrom<String> for UniversalString {
589	type Error = Error;
590
591	/// Converts a [`String`][`std::string::String`] into a [`UniversalString`]
592	///
593	/// Any character not in the [`UniversalString`] charset will be rejected.
594	/// See [`UniversalString`] documentation for more information.
595	///
596	/// Parsing a `UniversalString` allocates memory since the UTF-8 to UTF-32 conversion requires a memory allocation.
597	fn try_from(value: String) -> Result<Self, Self::Error> {
598		value.as_str().try_into()
599	}
600}
601
602#[cfg(test)]
603#[allow(clippy::unwrap_used)]
604mod tests {
605
606	use crate::{BmpString, Ia5String, PrintableString, TeletexString, UniversalString};
607
608	#[test]
609	fn printable_string() {
610		const EXAMPLE_UTF8: &str = "CertificateTemplate";
611		let printable_string = PrintableString::try_from(EXAMPLE_UTF8).unwrap();
612		assert_eq!(printable_string, EXAMPLE_UTF8);
613		assert!(PrintableString::try_from("@").is_err());
614		assert!(PrintableString::try_from("*").is_err());
615	}
616
617	#[test]
618	fn ia5_string() {
619		const EXAMPLE_UTF8: &str = "CertificateTemplate";
620		let ia5_string = Ia5String::try_from(EXAMPLE_UTF8).unwrap();
621		assert_eq!(ia5_string, EXAMPLE_UTF8);
622		assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok());
623		assert!(Ia5String::try_from(String::from('\u{8F}')).is_err());
624	}
625
626	#[test]
627	fn teletext_string() {
628		const EXAMPLE_UTF8: &str = "CertificateTemplate";
629		let teletext_string = TeletexString::try_from(EXAMPLE_UTF8).unwrap();
630		assert_eq!(teletext_string, EXAMPLE_UTF8);
631		assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok());
632		assert!(Ia5String::try_from(String::from('\u{8F}')).is_err());
633	}
634
635	#[test]
636	fn bmp_string() {
637		const EXPECTED_BYTES: &[u8] = &[
638			0x00, 0x43, 0x00, 0x65, 0x00, 0x72, 0x00, 0x74, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69,
639			0x00, 0x63, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x54, 0x00, 0x65, 0x00, 0x6d,
640			0x00, 0x70, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65,
641		];
642		const EXAMPLE_UTF8: &str = "CertificateTemplate";
643		let bmp_string = BmpString::try_from(EXAMPLE_UTF8).unwrap();
644		assert_eq!(bmp_string.as_bytes(), EXPECTED_BYTES);
645		assert!(BmpString::try_from(String::from('\u{FFFE}')).is_ok());
646		assert!(BmpString::try_from(String::from('\u{FFFF}')).is_err());
647	}
648
649	#[test]
650	fn universal_string() {
651		const EXPECTED_BYTES: &[u8] = &[
652			0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00,
653			0x00, 0x74, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x69,
654			0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00,
655			0x00, 0x65, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6d,
656			0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00,
657			0x00, 0x74, 0x00, 0x00, 0x00, 0x65,
658		];
659		const EXAMPLE_UTF8: &str = "CertificateTemplate";
660		let universal_string = UniversalString::try_from(EXAMPLE_UTF8).unwrap();
661		assert_eq!(universal_string.as_bytes(), EXPECTED_BYTES);
662	}
663}