Skip to content

Commit

Permalink
der: add BmpString (#1164)
Browse files Browse the repository at this point in the history
Adds initial support for a Basic Multilingual Plane (a.k.a. UCS-2)
string type.
  • Loading branch information
tarcieri authored Jul 24, 2023
1 parent 47a73cc commit f8f2cbd
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 0 deletions.
3 changes: 3 additions & 0 deletions der/src/asn1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ mod internal_macros;

mod any;
mod bit_string;
#[cfg(feature = "alloc")]
mod bmp_string;
mod boolean;
mod choice;
mod context_specific;
Expand Down Expand Up @@ -52,6 +54,7 @@ pub use self::{
pub use self::{
any::Any,
bit_string::BitString,
bmp_string::BmpString,
ia5_string::Ia5String,
integer::{int::Int, uint::Uint},
octet_string::OctetString,
Expand Down
164 changes: 164 additions & 0 deletions der/src/asn1/bmp_string.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//! ASN.1 `BMPString` support.

use crate::{
BytesOwned, DecodeValue, EncodeValue, Error, FixedTag, Header, Length, Reader, Result, Tag,
Writer,
};
use alloc::{boxed::Box, vec::Vec};
use core::{fmt, str::FromStr};

/// ASN.1 `BMPString` type.
///
/// Encodes Basic Multilingual Plane (BMP) subset of Unicode (ISO 10646),
/// a.k.a. UCS-2.
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord)]
pub struct BmpString {
bytes: BytesOwned,
}

impl BmpString {
/// Create a new [`BmpString`] from its UCS-2 encoding.
pub fn from_ucs2(bytes: impl Into<Box<[u8]>>) -> Result<Self> {
let bytes = bytes.into();

if bytes.len() % 2 != 0 {
return Err(Tag::BmpString.length_error());
}

let ret = Self {
bytes: bytes.try_into()?,
};

for maybe_char in char::decode_utf16(ret.codepoints()) {
match maybe_char {
// All surrogates paired and character is in the Basic Multilingual Plane
Ok(c) if (c as u64) < u64::from(u16::MAX) => (),
// Unpaired surrogates or characters outside Basic Multilingual Plane
_ => return Err(Tag::BmpString.value_error()),
}
}

Ok(ret)
}

/// Create a new [`BmpString`] from a UTF-8 string.
pub fn from_utf8(utf8: &str) -> Result<Self> {
let capacity = utf8
.len()
.checked_mul(2)
.ok_or_else(|| Tag::BmpString.length_error())?;

let mut bytes = Vec::with_capacity(capacity);

for code_point in utf8.encode_utf16() {
bytes.extend(code_point.to_be_bytes());
}

Self::from_ucs2(bytes)
}

/// Borrow the encoded UCS-2 as bytes.
pub fn as_bytes(&self) -> &[u8] {
self.bytes.as_ref()
}

/// Obtain the inner bytes.
#[inline]
pub fn into_bytes(self) -> Box<[u8]> {
self.bytes.into()
}

/// Get an iterator over characters in the string.
pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
char::decode_utf16(self.codepoints())
.map(|maybe_char| maybe_char.expect("unpaired surrogates checked in constructor"))
}

/// Get an iterator over the `u16` codepoints.
pub fn codepoints(&self) -> impl Iterator<Item = u16> + '_ {
// TODO(tarcieri): use `array_chunks`
self.as_bytes()
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes(chunk.try_into().expect("two bytes")))
}
}

impl AsRef<[u8]> for BmpString {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}

impl<'a> DecodeValue<'a> for BmpString {
fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> Result<Self> {
Self::from_ucs2(reader.read_vec(header.length)?)
}
}

impl EncodeValue for BmpString {
fn value_len(&self) -> Result<Length> {
Ok(self.bytes.len())
}

fn encode_value(&self, writer: &mut impl Writer) -> Result<()> {
writer.write(self.as_bytes())
}
}

impl FixedTag for BmpString {
const TAG: Tag = Tag::BmpString;
}

impl FromStr for BmpString {
type Err = Error;

fn from_str(s: &str) -> Result<Self> {
Self::from_utf8(s)
}
}

impl fmt::Debug for BmpString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "BmpString(\"{}\")", self)
}
}

impl fmt::Display for BmpString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for c in self.chars() {
write!(f, "{}", c)?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::BmpString;
use crate::{Decode, Encode};
use alloc::string::ToString;
use hex_literal::hex;

const EXAMPLE_BYTES: &[u8] = &hex!(
"1e 26 00 43 00 65 00 72 00 74"
" 00 69 00 66 00 69 00 63"
" 00 61 00 74 00 65 00 54"
" 00 65 00 6d 00 70 00 6c"
" 00 61 00 74 00 65"
);

const EXAMPLE_UTF8: &str = "CertificateTemplate";

#[test]
fn decode() {
let bmp_string = BmpString::from_der(EXAMPLE_BYTES).unwrap();
assert_eq!(bmp_string.to_string(), EXAMPLE_UTF8);
}

#[test]
fn encode() {
let bmp_string = BmpString::from_utf8(EXAMPLE_UTF8).unwrap();
let encoded = bmp_string.to_der().unwrap();
assert_eq!(encoded, EXAMPLE_BYTES);
}
}
6 changes: 6 additions & 0 deletions der/src/bytes_owned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ impl DerOrd for BytesOwned {
}
}

impl From<BytesOwned> for Box<[u8]> {
fn from(bytes: BytesOwned) -> Box<[u8]> {
bytes.inner
}
}

impl From<StrRef<'_>> for BytesOwned {
fn from(s: StrRef<'_>) -> BytesOwned {
let bytes = s.as_bytes();
Expand Down

0 comments on commit f8f2cbd

Please sign in to comment.