#![deny(missing_docs)]
use std::borrow::Cow;
use std::convert::{From, TryFrom};
use std::marker::PhantomData;
use std::str::{self, FromStr};
use derive_more::Display;
use hex::FromHexError;
pub type Error = FromHexError;
pub type UpperHexString = HexString<Uppercase>;
pub type LowerHexString = HexString<Lowercase>;
mod private {
pub trait Sealed {}
}
pub trait Case: private::Sealed {
fn encode(bytes: &[u8]) -> String;
fn is_valid(c: char) -> bool;
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Lowercase;
impl Case for Lowercase {
fn encode(bytes: &[u8]) -> String {
hex::encode(bytes)
}
fn is_valid(c: char) -> bool {
matches!(c, '0'..='9' | 'a'..='f')
}
}
impl private::Sealed for Lowercase {}
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Uppercase;
impl Case for Uppercase {
fn encode(bytes: &[u8]) -> String {
hex::encode_upper(bytes)
}
fn is_valid(c: char) -> bool {
matches!(c, '0'..='9' | 'A'..='F')
}
}
impl private::Sealed for Uppercase {}
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(try_from = "String")
)]
#[derive(Clone, Debug, Default, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[display("{}", _0)]
#[repr(transparent)]
pub struct HexString<C: Case>(Cow<'static, str>, PhantomData<C>);
impl<C: Case> HexString<C> {
pub fn new(s: impl Into<Cow<'static, str>>) -> Result<Self, Error> {
let s = s.into();
if s.len() & 1 != 0 {
return Err(Error::OddLength);
}
if let Some((index, c)) = s.chars().enumerate().find(|(_, c)| !C::is_valid(*c)) {
return Err(Error::InvalidHexCharacter { c, index });
}
Ok(Self(s, PhantomData))
}
pub unsafe fn new_unchecked(s: impl Into<Cow<'static, str>>) -> Self {
Self(s.into(), PhantomData)
}
}
impl HexString<Lowercase> {
pub fn to_uppercase(self) -> HexString<Uppercase> {
let mut s = self.0.into_owned();
s.make_ascii_uppercase();
unsafe { HexString::new_unchecked(s) }
}
}
impl HexString<Uppercase> {
pub fn to_lowercase(self) -> HexString<Lowercase> {
let mut s = self.0.into_owned();
s.make_ascii_lowercase();
unsafe { HexString::new_unchecked(s) }
}
}
impl<C: Case> From<&[u8]> for HexString<C> {
fn from(bytes: &[u8]) -> Self {
let s = C::encode(bytes);
unsafe { Self::new_unchecked(s) }
}
}
impl<C: Case> From<Vec<u8>> for HexString<C> {
fn from(bytes: Vec<u8>) -> Self {
Self::from(&bytes[..])
}
}
impl<C: Case, const N: usize> From<[u8; N]> for HexString<C> {
fn from(bytes: [u8; N]) -> Self {
Self::from(&bytes[..])
}
}
impl<C: Case> From<HexString<C>> for Vec<u8> {
fn from(s: HexString<C>) -> Self {
hex::decode(s.0.as_ref()).unwrap()
}
}
impl<C: Case> FromStr for HexString<C> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_owned())
}
}
impl<C: Case, const N: usize> TryFrom<HexString<C>> for [u8; N] {
type Error = Error;
fn try_from(s: HexString<C>) -> Result<Self, Self::Error> {
let mut bytes = [0u8; N];
hex::decode_to_slice(s.0.as_ref(), &mut bytes).map(|_| bytes)
}
}
#[cfg(feature = "serde")]
mod seal {
use super::*;
use std::convert::TryFrom;
#[doc(hidden)]
impl<C: Case> TryFrom<String> for HexString<C> {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_constructs_from_owned_str() {
assert_eq!(
LowerHexString::new("ab04ff".to_string()),
Ok(HexString(Cow::Owned("ab04ff".to_string()), PhantomData))
);
assert_eq!(
UpperHexString::new("AB04FF".to_string()),
Ok(HexString(Cow::Owned("AB04FF".to_string()), PhantomData))
);
}
#[test]
fn it_constructs_from_borrowed_str() {
assert_eq!(
LowerHexString::new("ab04ff"),
Ok(HexString(Cow::Borrowed("ab04ff"), PhantomData))
);
assert_eq!(
UpperHexString::new("AB04FF"),
Ok(HexString(Cow::Borrowed("AB04FF"), PhantomData))
);
}
#[test]
fn it_constructs_from_empty_str() {
assert!(LowerHexString::new("").is_ok());
assert!(UpperHexString::new("").is_ok());
}
#[test]
fn it_constructs_from_bytes() {
assert_eq!(
LowerHexString::from([42, 15, 5]),
HexString::<Lowercase>(Cow::Borrowed("2a0f05"), PhantomData)
);
assert_eq!(
UpperHexString::from([42, 15, 5]),
HexString::<Uppercase>(Cow::Borrowed("2A0F05"), PhantomData)
);
assert_eq!(
LowerHexString::from(vec![1, 2, 3, 4, 5]),
HexString::<Lowercase>(Cow::Borrowed("0102030405"), PhantomData)
);
assert_eq!(
UpperHexString::from(vec![1, 2, 3, 4, 5]),
HexString::<Uppercase>(Cow::Borrowed("0102030405"), PhantomData)
);
}
#[test]
fn it_rejects_str_with_odd_length() {
assert_eq!(LowerHexString::new("abc"), Err(Error::OddLength));
assert_eq!(UpperHexString::new("abcde"), Err(Error::OddLength));
}
#[test]
fn it_rejects_str_with_invalid_chars() {
assert_eq!(
LowerHexString::new("abcdZ109"),
Err(Error::InvalidHexCharacter { c: 'Z', index: 4 })
);
assert_eq!(
UpperHexString::new("ABVCD109"),
Err(Error::InvalidHexCharacter { c: 'V', index: 2 })
);
}
#[test]
fn it_constructs_from_unchecked_str() {
let hex = unsafe { LowerHexString::new_unchecked("0a0b0c0d0e") };
let bytes = Vec::from(hex);
assert_eq!(&bytes[..], [10, 11, 12, 13, 14]);
}
#[test]
#[should_panic]
fn it_fails_to_convert_into_bytes_from_invalid_unchecked_str() {
let hex = unsafe { LowerHexString::new_unchecked("thisisnotvalid") };
let _ = Vec::from(hex);
}
#[test]
fn it_converts_into_bytes() {
let hex = LowerHexString::new("2a1a02").unwrap();
let bytes = Vec::from(hex);
assert_eq!(&bytes[..], [42, 26, 2]);
let hex = UpperHexString::new("2A1A02").unwrap();
let bytes = Vec::from(hex);
assert_eq!(&bytes[..], [42, 26, 2]);
}
#[test]
fn it_converts_into_fixed_array_of_bytes() {
use std::convert::TryInto;
let bytes: [u8; 4] = LowerHexString::new("142a020a").unwrap().try_into().unwrap();
assert_eq!(bytes, [20, 42, 2, 10]);
let bytes: [u8; 5] = UpperHexString::new("142A020A0F")
.unwrap()
.try_into()
.unwrap();
assert_eq!(bytes, [20, 42, 2, 10, 15]);
}
#[test]
fn it_converts_from_str() {
let hex = "aabbccddee".parse::<LowerHexString>().unwrap();
let expected_hex = HexString::<Lowercase>(Cow::Owned("aabbccddee".to_string()), PhantomData);
assert_eq!(hex, expected_hex);
}
#[test]
fn it_creates_upper_hex_str_from_lower_hex_str() {
let s = "aabbccddee";
let hex = LowerHexString::new(s).unwrap().to_uppercase();
let expected_hex = HexString::<Uppercase>(Cow::Owned("AABBCCDDEE".to_string()), PhantomData);
assert_ne!(s, hex.0.as_ref());
assert_eq!(hex, expected_hex);
let hex = LowerHexString::new(s.to_string()).unwrap().to_uppercase();
assert_eq!(hex, expected_hex);
}
#[test]
fn it_creates_lower_hex_str_from_upper_str() {
let s = "AABBCCDDEE";
let hex = UpperHexString::new(s).unwrap().to_lowercase();
let expected_hex = HexString::<Lowercase>(Cow::Owned("aabbccddee".to_string()), PhantomData);
assert_ne!(s, hex.0.as_ref());
assert_eq!(hex, expected_hex);
let hex = UpperHexString::new(s.to_string()).unwrap().to_lowercase();
assert_eq!(hex, expected_hex);
}
#[cfg(feature = "serde")]
mod serde {
use super::*;
use serde_json::error::Category;
#[test]
fn it_deser_hex_str() {
let result: Result<LowerHexString, _> = serde_json::from_str("\"abcd09\"");
assert!(result.is_ok());
let result: Result<UpperHexString, _> = serde_json::from_str("\"ABCD09\"");
assert!(result.is_ok());
}
#[test]
fn it_fails_to_deser_invalid_hex_str() {
let result: Result<LowerHexString, serde_json::Error> =
serde_json::from_str("\"invalid hex str\"");
assert_eq!(result.unwrap_err().classify(), Category::Data);
let result: Result<UpperHexString, serde_json::Error> =
serde_json::from_str("\"INVALID HEX STR\"");
assert_eq!(result.unwrap_err().classify(), Category::Data);
}
}
}