Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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