From 9a4176b99709f429c28b6ba6e482d749b0ff70b9 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 22 Mar 2020 12:05:34 -0500 Subject: [PATCH 1/5] Remove `DnsName`. --- src/name.rs | 52 --------------------------------------------------- src/webpki.rs | 3 --- 2 files changed, 55 deletions(-) diff --git a/src/name.rs b/src/name.rs index 60703978..ee020ccd 100644 --- a/src/name.rs +++ b/src/name.rs @@ -17,49 +17,6 @@ use crate::{ der, Error, }; -/// A DNS Name suitable for use in the TLS Server Name Indication (SNI) -/// extension and/or for use as the reference hostname for which to verify a -/// certificate. -/// -/// A `DnsName` is guaranteed to be syntactically valid. The validity rules are -/// specified in [RFC 5280 Section 7.2], except that underscores are also -/// allowed. -/// -/// `DnsName` stores a copy of the input it was constructed from in a `String` -/// and so it is only available when the `std` default feature is enabled. -/// -/// `Eq`, `PartialEq`, etc. are not implemented because name comparison -/// frequently should be done case-insensitively and/or with other caveats that -/// depend on the specific circumstances in which the comparison is done. -/// -/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 -#[cfg(feature = "std")] -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct DnsName(String); - -#[cfg(feature = "std")] -impl DnsName { - /// Returns a `DnsNameRef` that refers to this `DnsName`. - pub fn as_ref(&self) -> DnsNameRef { - DnsNameRef(self.0.as_bytes()) - } -} - -#[cfg(feature = "std")] -impl AsRef for DnsName { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -// Deprecated -#[cfg(feature = "std")] -impl From> for DnsName { - fn from(dns_name: DnsNameRef) -> Self { - dns_name.to_owned() - } -} - /// A reference to a DNS Name suitable for use in the TLS Server Name Indication /// (SNI) extension and/or for use as the reference hostname for which to verify /// a certificate. @@ -106,15 +63,6 @@ impl<'a> DnsNameRef<'a> { pub fn try_from_ascii_str(dns_name: &'a str) -> Result { Self::try_from_ascii(dns_name.as_bytes()) } - - /// Constructs a `DnsName` from this `DnsNameRef` - #[cfg(feature = "std")] - pub fn to_owned(&self) -> DnsName { - // DnsNameRef is already guaranteed to be valid ASCII, which is a - // subset of UTF-8. - let s: &str = self.clone().into(); - DnsName(s.to_ascii_lowercase()) - } } #[cfg(feature = "std")] diff --git a/src/webpki.rs b/src/webpki.rs index 7076de8b..6fdcd066 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -43,9 +43,6 @@ mod verify_cert; pub use error::Error; pub use name::{DnsNameRef, InvalidDnsNameError}; -#[cfg(feature = "std")] -pub use name::DnsName; - pub use signed_data::{ SignatureAlgorithm, ECDSA_P256_SHA256, ECDSA_P256_SHA384, ECDSA_P384_SHA256, ECDSA_P384_SHA384, ED25519, RSA_PKCS1_2048_8192_SHA256, RSA_PKCS1_2048_8192_SHA384, RSA_PKCS1_2048_8192_SHA512, From c59080f5e852f9d69d5a83a4003637044bbf9802 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 22 Mar 2020 12:07:04 -0500 Subject: [PATCH 2/5] Rename `DnsNameRef` to `DnsName`. --- src/name.rs | 24 ++++++++++++------------ src/webpki.rs | 10 +++++----- tests/dns_name_tests.rs | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/name.rs b/src/name.rs index ee020ccd..5f58f6e7 100644 --- a/src/name.rs +++ b/src/name.rs @@ -21,7 +21,7 @@ use crate::{ /// (SNI) extension and/or for use as the reference hostname for which to verify /// a certificate. /// -/// A `DnsNameRef` is guaranteed to be syntactically valid. The validity rules +/// A `DnsName` is guaranteed to be syntactically valid. The validity rules /// are specified in [RFC 5280 Section 7.2], except that underscores are also /// allowed. /// @@ -31,9 +31,9 @@ use crate::{ /// /// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 #[derive(Clone, Copy)] -pub struct DnsNameRef<'a>(&'a [u8]); +pub struct DnsName<'a>(&'a [u8]); -/// An error indicating that a `DnsNameRef` could not built because the input +/// An error indicating that a `DnsName` could not built because the input /// is not a syntactically-valid DNS Name. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct InvalidDnsNameError; @@ -47,8 +47,8 @@ impl core::fmt::Display for InvalidDnsNameError { #[cfg(feature = "std")] impl ::std::error::Error for InvalidDnsNameError {} -impl<'a> DnsNameRef<'a> { - /// Constructs a `DnsNameRef` from the given input if the input is a +impl<'a> DnsName<'a> { + /// Constructs a `DnsName` from the given input if the input is a /// syntactically-valid DNS name. pub fn try_from_ascii(dns_name: &'a [u8]) -> Result { if !is_valid_reference_dns_id(untrusted::Input::from(dns_name)) { @@ -58,7 +58,7 @@ impl<'a> DnsNameRef<'a> { Ok(Self(dns_name)) } - /// Constructs a `DnsNameRef` from the given input if the input is a + /// Constructs a `DnsName` from the given input if the input is a /// syntactically-valid DNS name. pub fn try_from_ascii_str(dns_name: &'a str) -> Result { Self::try_from_ascii(dns_name.as_bytes()) @@ -66,16 +66,16 @@ impl<'a> DnsNameRef<'a> { } #[cfg(feature = "std")] -impl core::fmt::Debug for DnsNameRef<'_> { +impl core::fmt::Debug for DnsName<'_> { fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { let lowercase = self.clone().to_owned(); - f.debug_tuple("DnsNameRef").field(&lowercase.0).finish() + f.debug_tuple("DnsName").field(&lowercase.0).finish() } } -impl<'a> From> for &'a str { - fn from(DnsNameRef(d): DnsNameRef<'a>) -> Self { - // The unwrap won't fail because DnsNameRefs are guaranteed to be ASCII +impl<'a> From> for &'a str { + fn from(DnsName(d): DnsName<'a>) -> Self { + // The unwrap won't fail because DnsNames are guaranteed to be ASCII // and ASCII is a subset of UTF-8. core::str::from_utf8(d).unwrap() } @@ -83,7 +83,7 @@ impl<'a> From> for &'a str { pub fn verify_cert_dns_name( cert: &super::EndEntityCert, - DnsNameRef(dns_name): DnsNameRef, + DnsName(dns_name): DnsName, ) -> Result<(), Error> { let cert = &cert.inner; let dns_name = untrusted::Input::from(dns_name); diff --git a/src/webpki.rs b/src/webpki.rs index 6fdcd066..e1aeab13 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -41,7 +41,7 @@ pub mod trust_anchor_util; mod verify_cert; pub use error::Error; -pub use name::{DnsNameRef, InvalidDnsNameError}; +pub use name::{DnsName, InvalidDnsNameError}; pub use signed_data::{ SignatureAlgorithm, ECDSA_P256_SHA256, ECDSA_P256_SHA384, ECDSA_P384_SHA256, ECDSA_P384_SHA384, @@ -164,7 +164,7 @@ impl<'a> EndEntityCert<'a> { } /// Verifies that the certificate is valid for the given DNS host name. - pub fn verify_is_valid_for_dns_name(&self, dns_name: DnsNameRef) -> Result<(), Error> { + pub fn verify_is_valid_for_dns_name(&self, dns_name: DnsName) -> Result<(), Error> { name::verify_cert_dns_name(&self, dns_name) } @@ -181,11 +181,11 @@ impl<'a> EndEntityCert<'a> { pub fn verify_is_valid_for_at_least_one_dns_name<'names, Names>( &self, dns_names: Names, - ) -> Result>, Error> + ) -> Result>, Error> where - Names: Iterator>, + Names: Iterator>, { - let result: Vec> = dns_names + let result: Vec> = dns_names .filter(|n| self.verify_is_valid_for_dns_name(*n).is_ok()) .collect(); if result.is_empty() { diff --git a/tests/dns_name_tests.rs b/tests/dns_name_tests.rs index b3a3adc4..bb4a0ccf 100644 --- a/tests/dns_name_tests.rs +++ b/tests/dns_name_tests.rs @@ -399,7 +399,7 @@ fn dns_name_ref_try_from_ascii_test() { .chain(IP_ADDRESS_DNS_VALIDITY.iter()) { assert_eq!( - webpki::DnsNameRef::try_from_ascii(s).is_ok(), + webpki::DnsName::try_from_ascii(s).is_ok(), is_valid, "DnsNameRef::try_from_ascii_str failed for \"{:?}\"", s From 5e1223b6818e53e6d3c3703430d71991b42f83d0 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 22 Mar 2020 18:01:33 -0500 Subject: [PATCH 3/5] Merge old `DnsName` functionality into new `DnsName`. Allow `DnsName` to store an owned value instead of just a referenced value. --- src/name.rs | 129 ++++++++++++++++++++++++++++++++-------- src/webpki.rs | 13 ++-- tests/dns_name_tests.rs | 101 ++++++++++++++++++++++++++++++- 3 files changed, 209 insertions(+), 34 deletions(-) diff --git a/src/name.rs b/src/name.rs index 5f58f6e7..4c7dc360 100644 --- a/src/name.rs +++ b/src/name.rs @@ -16,6 +16,7 @@ use crate::{ cert::{Cert, EndEntityOrCA}, der, Error, }; +use core::fmt::Write as _; /// A reference to a DNS Name suitable for use in the TLS Server Name Indication /// (SNI) extension and/or for use as the reference hostname for which to verify @@ -25,13 +26,10 @@ use crate::{ /// are specified in [RFC 5280 Section 7.2], except that underscores are also /// allowed. /// -/// `Eq`, `PartialEq`, etc. are not implemented because name comparison -/// frequently should be done case-insensitively and/or with other caveats that -/// depend on the specific circumstances in which the comparison is done. -/// /// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 -#[derive(Clone, Copy)] -pub struct DnsName<'a>(&'a [u8]); +pub struct DnsName(B) +where + B: AsRef<[u8]>; /// An error indicating that a `DnsName` could not built because the input /// is not a syntactically-valid DNS Name. @@ -47,43 +45,122 @@ impl core::fmt::Display for InvalidDnsNameError { #[cfg(feature = "std")] impl ::std::error::Error for InvalidDnsNameError {} -impl<'a> DnsName<'a> { +impl DnsName +where + B: AsRef<[u8]>, +{ /// Constructs a `DnsName` from the given input if the input is a /// syntactically-valid DNS name. - pub fn try_from_ascii(dns_name: &'a [u8]) -> Result { - if !is_valid_reference_dns_id(untrusted::Input::from(dns_name)) { + pub fn try_from_punycode(dns_name: A) -> Result + where + A: AsRef<[u8]>, + A: Into, + { + if !is_valid_reference_dns_id(untrusted::Input::from(dns_name.as_ref())) { return Err(InvalidDnsNameError); } - Ok(Self(dns_name)) + Ok(Self(dns_name.into())) } - /// Constructs a `DnsName` from the given input if the input is a - /// syntactically-valid DNS name. - pub fn try_from_ascii_str(dns_name: &'a str) -> Result { - Self::try_from_ascii(dns_name.as_bytes()) + /// Borrows any `DnsName` as a `DnsName<&[u8]>`. + /// + /// Use `DnsName<&[u8]>` when you don't *need* to be generic over the + /// underlying representation to reduce monomorphization overhead. + #[inline] + pub fn borrow(&self) -> DnsName<&[u8]> { + DnsName(self.0.as_ref()) + } + + /// Returns an iterator of the punycode characters of the DNS name, as + /// bytes. + /// + /// The iterator implements many specialized iterator traits, such as + /// `ExactSizeIterator` and `DoubleEndedIterator`. + /// + /// ``` + /// # #[cfg(feature = "std")] + /// use std::{iter::FromIterator, string::String}; + /// + /// # #[cfg(feature = "std")] + /// fn string_from_dns_name(dns_name: webpki::DnsName) -> String where B: AsRef<[u8]> { + /// String::from_iter(dns_name.punycode_lowercase_bytes().map(char::from)) + /// } + /// ``` + #[inline] + pub fn punycode_lowercase_bytes<'b>( + &self, + ) -> core::iter::Map, fn(&u8) -> u8> + where + B: 'b, + { + // The unwrap won't fail because DnsNames are guaranteed to be ASCII + // and ASCII is a subset of UTF-8. + self.0.as_ref().iter().map(u8::to_ascii_lowercase) } } -#[cfg(feature = "std")] -impl core::fmt::Debug for DnsName<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { - let lowercase = self.clone().to_owned(); - f.debug_tuple("DnsName").field(&lowercase.0).finish() +impl Clone for DnsName +where + B: AsRef<[u8]>, + B: Clone, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) } } -impl<'a> From> for &'a str { - fn from(DnsName(d): DnsName<'a>) -> Self { - // The unwrap won't fail because DnsNames are guaranteed to be ASCII - // and ASCII is a subset of UTF-8. - core::str::from_utf8(d).unwrap() +impl core::fmt::Debug for DnsName +where + B: AsRef<[u8]>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.write_str("DnsName(\"")?; + // None of the characters in a `DnsName` need to be escaped. + self.punycode_lowercase_bytes() + .try_for_each(|b| f.write_char(b.into()))?; + f.write_str("\")") + } +} + +impl core::fmt::Display for DnsName +where + B: AsRef<[u8]>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.punycode_lowercase_bytes() + .try_for_each(|b| f.write_char(b.into())) } } -pub fn verify_cert_dns_name( +impl core::hash::Hash for DnsName +where + B: AsRef<[u8]>, +{ + fn hash(&self, state: &mut H) { + // This is modeled after the implementation of `Hash` for `[T]`. + let lowercase = self.punycode_lowercase_bytes(); + state.write_usize(lowercase.len()); + lowercase.for_each(|b| state.write_u8(b)); + } +} + +impl PartialEq> for DnsName +where + B1: AsRef<[u8]>, + B2: AsRef<[u8]>, +{ + #[inline] + fn eq(&self, other: &DnsName) -> bool { + self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref()) + } +} + +impl Eq for DnsName where B: AsRef<[u8]> {} + +pub(crate) fn verify_cert_dns_name( cert: &super::EndEntityCert, - DnsName(dns_name): DnsName, + DnsName(dns_name): DnsName<&[u8]>, ) -> Result<(), Error> { let cert = &cert.inner; let dns_name = untrusted::Input::from(dns_name); diff --git a/src/webpki.rs b/src/webpki.rs index e1aeab13..ce2cef77 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -164,7 +164,7 @@ impl<'a> EndEntityCert<'a> { } /// Verifies that the certificate is valid for the given DNS host name. - pub fn verify_is_valid_for_dns_name(&self, dns_name: DnsName) -> Result<(), Error> { + pub fn verify_is_valid_for_dns_name(&self, dns_name: DnsName<&[u8]>) -> Result<(), Error> { name::verify_cert_dns_name(&self, dns_name) } @@ -178,15 +178,16 @@ impl<'a> EndEntityCert<'a> { /// Requires the `std` default feature; i.e. this isn't available in /// `#![no_std]` configurations. #[cfg(feature = "std")] - pub fn verify_is_valid_for_at_least_one_dns_name<'names, Names>( + pub fn verify_is_valid_for_at_least_one_dns_name<'names, B, Names>( &self, dns_names: Names, - ) -> Result>, Error> + ) -> Result>, Error> where - Names: Iterator>, + B: AsRef<[u8]>, + Names: Iterator>, { - let result: Vec> = dns_names - .filter(|n| self.verify_is_valid_for_dns_name(*n).is_ok()) + let result: Vec<_> = dns_names + .filter(|n| self.verify_is_valid_for_dns_name(n.borrow()).is_ok()) .collect(); if result.is_empty() { return Err(Error::CertNotValidForName); diff --git a/tests/dns_name_tests.rs b/tests/dns_name_tests.rs index bb4a0ccf..45a5ebf0 100644 --- a/tests/dns_name_tests.rs +++ b/tests/dns_name_tests.rs @@ -1,5 +1,7 @@ // Copyright 2014-2017 Brian Smith. +use webpki::DnsName; + // (name, is_valid) static DNS_NAME_VALIDITY: &[(&[u8], bool)] = &[ (b"a", true), @@ -399,10 +401,105 @@ fn dns_name_ref_try_from_ascii_test() { .chain(IP_ADDRESS_DNS_VALIDITY.iter()) { assert_eq!( - webpki::DnsName::try_from_ascii(s).is_ok(), + DnsName::<&[u8]>::try_from_punycode(s).is_ok(), is_valid, - "DnsNameRef::try_from_ascii_str failed for \"{:?}\"", + "DnsName::try_from_punycode failed for \"{:?}\"", s ); } } + +const DNS_NAME_LOWERCASE_TEST_CASES: &[(&str, &str)] = &[ + // (expected_lowercase, input) + ("abc", "abc"), + ("abc", "Abc"), + ("abc", "aBc"), + ("abc", "abC"), + ("abc1", "abC1"), + ("abc.def", "abC.Def"), +]; + +#[test] +fn test_dns_name_ascii_lowercase_chars() { + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let dns_name: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + let actual_lowercase = dns_name.punycode_lowercase_bytes(); + + assert_eq!(expected_lowercase.len(), actual_lowercase.len()); + assert!(expected_lowercase + .chars() + .zip(actual_lowercase) + .all(|(a, b)| a == b.into())); + } +} + +// XXX: This shouldn't require the "std" feature. +#[cfg(feature = "std")] +#[test] +fn test_dns_name_fmt() { + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let dns_name: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + + // Test `Display` implementation. + assert_eq!(*expected_lowercase, format!("{}", dns_name)); + + // Test `Debug` implementation. + let debug_formatted = format!("{:?}", &dns_name); + assert_eq!( + String::from("DnsName(\"") + *expected_lowercase + "\")", + debug_formatted + ); + } +} + +// XXX: We need more test cases. +#[test] +fn test_dns_name_eq_different_len() { + #[rustfmt::skip] + const NOT_EQUAL: &[(&str, &str)] = &[ + ("a", "aa"), + ("aa", "a"), + ("aaa", "a"), + ("a", "aaa") + ]; + + for (a, b) in NOT_EQUAL { + let a: DnsName<&str> = DnsName::try_from_punycode(*a).unwrap(); + let b: DnsName<&str> = DnsName::try_from_punycode(*b).unwrap(); + assert_ne!(a, b) + } +} + +/// XXX: We need more test cases. +#[test] +fn test_dns_name_eq_case() { + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let a: DnsName<&str> = DnsName::try_from_punycode(*expected_lowercase).unwrap(); + let b: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + assert_eq!(a, b); + } +} + +/// XXX: We need more test cases. +#[cfg(feature = "std")] +#[test] +fn test_dns_name_eq_various_types() { + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let a: DnsName<&str> = DnsName::try_from_punycode(*expected_lowercase).unwrap(); + let b: DnsName = DnsName::try_from_punycode(*input).unwrap(); + assert_eq!(a, b); + } + + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let a: DnsName = DnsName::try_from_punycode(*expected_lowercase).unwrap(); + let b: DnsName<&[u8]> = DnsName::try_from_punycode(input.as_ref()).unwrap(); + assert_eq!(a, b); + } + + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let a: DnsName> = + DnsName::try_from_punycode(expected_lowercase.as_ref()).unwrap(); + let b: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + assert_eq!(a, b); + } +} From 417afdbbce1aff8cdf31708166e82aca1d90c5f9 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 22 Mar 2020 21:02:22 -0500 Subject: [PATCH 4/5] extended API --- src/name.rs | 27 +++++++++++++++++++++++++++ src/webpki.rs | 5 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/name.rs b/src/name.rs index 4c7dc360..e9d69d50 100644 --- a/src/name.rs +++ b/src/name.rs @@ -31,6 +31,17 @@ pub struct DnsName(B) where B: AsRef<[u8]>; +/// A borrowed `DnsName`. +/// +/// This is an alias for `DnsName<&'a [u8]>`. +pub type DnsNameRef<'a> = DnsName<&'a [u8]>; + +/// An owned `DnsName` +/// +/// This is an alias for `DnsName>`. +#[cfg(feature = "std")] +pub type DnsNameBox = DnsName>; + /// An error indicating that a `DnsName` could not built because the input /// is not a syntactically-valid DNS Name. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -49,6 +60,14 @@ impl DnsName where B: AsRef<[u8]>, { + /// TODO: docs + pub fn try_from_punycode_str<'a>(dns_name: &'a str) -> Result + where + B: From<&'a [u8]>, + { + Self::try_from_punycode(dns_name.as_ref()) + } + /// Constructs a `DnsName` from the given input if the input is a /// syntactically-valid DNS name. pub fn try_from_punycode(dns_name: A) -> Result @@ -72,6 +91,12 @@ where DnsName(self.0.as_ref()) } + /// TODO: + #[cfg(feature = "std")] + pub fn to_owned(&self) -> DnsName> { + DnsName(Box::from(self.0.as_ref())) + } + /// Returns an iterator of the punycode characters of the DNS name, as /// bytes. /// @@ -110,6 +135,8 @@ where } } +impl Copy for DnsName where B: AsRef<[u8]> + Copy {} + impl core::fmt::Debug for DnsName where B: AsRef<[u8]>, diff --git a/src/webpki.rs b/src/webpki.rs index ce2cef77..2675058d 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -41,7 +41,10 @@ pub mod trust_anchor_util; mod verify_cert; pub use error::Error; -pub use name::{DnsName, InvalidDnsNameError}; +pub use name::{DnsName, DnsNameRef, InvalidDnsNameError}; + +#[cfg(feature = "std")] +pub use name::DnsNameBox; pub use signed_data::{ SignatureAlgorithm, ECDSA_P256_SHA256, ECDSA_P256_SHA384, ECDSA_P384_SHA256, ECDSA_P384_SHA384, From 34b10322e27913441d73aacc08eaf856ce88d896 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 23 Mar 2020 00:00:59 -0500 Subject: [PATCH 5/5] Add `DnsNameInput` to make constructing `DnsName` more convenient. --- src/name.rs | 70 +++++++++++++++++++++++++++++++---------- tests/dns_name_tests.rs | 31 +++++++++--------- 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/src/name.rs b/src/name.rs index e9d69d50..d88926d4 100644 --- a/src/name.rs +++ b/src/name.rs @@ -56,32 +56,70 @@ impl core::fmt::Display for InvalidDnsNameError { #[cfg(feature = "std")] impl ::std::error::Error for InvalidDnsNameError {} +pub trait DnsNameInput: AsRef<[u8]> + Sized { + type Storage: AsRef<[u8]>; + fn into_storage(self) -> Self::Storage; +} + +#[cfg(feature = "std")] +impl DnsNameInput for String { + type Storage = Box<[u8]>; + fn into_storage(self) -> Self::Storage { + self.into_boxed_str().into() + } +} + +#[cfg(feature = "std")] +impl DnsNameInput for Box<[u8]> { + type Storage = Box<[u8]>; + fn into_storage(self) -> Self::Storage { + self + } +} + +#[cfg(feature = "std")] +impl DnsNameInput for Vec { + type Storage = Box<[u8]>; + fn into_storage(self) -> Self::Storage { + self.into() + } +} + +impl<'a> DnsNameInput for &'a str { + type Storage = &'a [u8]; + fn into_storage(self) -> Self::Storage { + self.as_ref() + } +} + +impl<'a> DnsNameInput for &'a [u8] { + type Storage = &'a [u8]; + fn into_storage(self) -> Self::Storage { + self.as_ref() + } +} + impl DnsName where B: AsRef<[u8]>, { - /// TODO: docs - pub fn try_from_punycode_str<'a>(dns_name: &'a str) -> Result - where - B: From<&'a [u8]>, - { - Self::try_from_punycode(dns_name.as_ref()) - } - /// Constructs a `DnsName` from the given input if the input is a /// syntactically-valid DNS name. - pub fn try_from_punycode(dns_name: A) -> Result - where - A: AsRef<[u8]>, - A: Into, - { - if !is_valid_reference_dns_id(untrusted::Input::from(dns_name.as_ref())) { + pub fn try_from_punycode( + input: impl DnsNameInput, + ) -> Result { + if !is_valid_reference_dns_id(untrusted::Input::from(input.as_ref())) { return Err(InvalidDnsNameError); } - Ok(Self(dns_name.into())) + Ok(Self(input.into_storage())) } +} +impl DnsName +where + B: AsRef<[u8]>, +{ /// Borrows any `DnsName` as a `DnsName<&[u8]>`. /// /// Use `DnsName<&[u8]>` when you don't *need* to be generic over the @@ -93,7 +131,7 @@ where /// TODO: #[cfg(feature = "std")] - pub fn to_owned(&self) -> DnsName> { + pub fn into_owned(self) -> DnsName> { DnsName(Box::from(self.0.as_ref())) } diff --git a/tests/dns_name_tests.rs b/tests/dns_name_tests.rs index 45a5ebf0..636bfb00 100644 --- a/tests/dns_name_tests.rs +++ b/tests/dns_name_tests.rs @@ -1,6 +1,6 @@ // Copyright 2014-2017 Brian Smith. -use webpki::DnsName; +use webpki::{DnsName, DnsNameRef}; // (name, is_valid) static DNS_NAME_VALIDITY: &[(&[u8], bool)] = &[ @@ -422,7 +422,7 @@ const DNS_NAME_LOWERCASE_TEST_CASES: &[(&str, &str)] = &[ #[test] fn test_dns_name_ascii_lowercase_chars() { for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { - let dns_name: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + let dns_name: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); let actual_lowercase = dns_name.punycode_lowercase_bytes(); assert_eq!(expected_lowercase.len(), actual_lowercase.len()); @@ -438,7 +438,7 @@ fn test_dns_name_ascii_lowercase_chars() { #[test] fn test_dns_name_fmt() { for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { - let dns_name: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + let dns_name: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); // Test `Display` implementation. assert_eq!(*expected_lowercase, format!("{}", dns_name)); @@ -464,8 +464,8 @@ fn test_dns_name_eq_different_len() { ]; for (a, b) in NOT_EQUAL { - let a: DnsName<&str> = DnsName::try_from_punycode(*a).unwrap(); - let b: DnsName<&str> = DnsName::try_from_punycode(*b).unwrap(); + let a: DnsNameRef = DnsName::try_from_punycode(*a).unwrap(); + let b: DnsNameRef = DnsName::try_from_punycode(*b).unwrap(); assert_ne!(a, b) } } @@ -474,8 +474,8 @@ fn test_dns_name_eq_different_len() { #[test] fn test_dns_name_eq_case() { for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { - let a: DnsName<&str> = DnsName::try_from_punycode(*expected_lowercase).unwrap(); - let b: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + let a: DnsNameRef = DnsName::try_from_punycode(*expected_lowercase).unwrap(); + let b: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); assert_eq!(a, b); } } @@ -484,22 +484,19 @@ fn test_dns_name_eq_case() { #[cfg(feature = "std")] #[test] fn test_dns_name_eq_various_types() { - for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { - let a: DnsName<&str> = DnsName::try_from_punycode(*expected_lowercase).unwrap(); - let b: DnsName = DnsName::try_from_punycode(*input).unwrap(); - assert_eq!(a, b); - } + use webpki::DnsNameBox; for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { - let a: DnsName = DnsName::try_from_punycode(*expected_lowercase).unwrap(); - let b: DnsName<&[u8]> = DnsName::try_from_punycode(input.as_ref()).unwrap(); + let a: DnsNameRef = DnsName::try_from_punycode(*expected_lowercase).unwrap(); + let b: DnsNameBox = DnsName::try_from_punycode(*input).unwrap().into_owned(); assert_eq!(a, b); } for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { - let a: DnsName> = - DnsName::try_from_punycode(expected_lowercase.as_ref()).unwrap(); - let b: DnsName<&str> = DnsName::try_from_punycode(*input).unwrap(); + let a: DnsNameBox = DnsName::try_from_punycode(*expected_lowercase) + .unwrap() + .into_owned(); + let b: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); assert_eq!(a, b); } }