use crate::{Error, Result};
use core::{convert::TryFrom, fmt, ops::Deref, str};
#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Ident<'a>(&'a str);
impl<'a> Ident<'a> {
const MAX_LENGTH: usize = 32;
pub const fn new(s: &'a str) -> Self {
let input = s.as_bytes();
macro_rules! const_assert {
($bool:expr, $msg:expr) => {
[$msg][!$bool as usize]
};
}
const_assert!(!input.is_empty(), "PHC ident string can't be empty");
const_assert!(input.len() <= Self::MAX_LENGTH, "PHC ident string too long");
macro_rules! validate_chars {
($($pos:expr),+) => {
$(
if $pos < input.len() {
const_assert!(
is_char_valid(input[$pos]),
"invalid character in PHC string ident"
);
}
)+
};
}
validate_chars!(
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31
);
Self(s)
}
pub fn as_str(&self) -> &'a str {
self.0
}
}
impl<'a> AsRef<str> for Ident<'a> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<'a> Deref for Ident<'a> {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl<'a> TryFrom<&'a str> for Ident<'a> {
type Error = Error;
fn try_from(s: &'a str) -> Result<Self> {
if s.is_empty() {
return Err(Error::ParamNameInvalid);
}
let bytes = s.as_bytes();
let too_long = bytes.len() > Self::MAX_LENGTH;
for &c in bytes {
if !is_char_valid(c) {
return Err(Error::ParamNameInvalid);
}
}
if too_long {
return Err(Error::ParamNameInvalid);
}
Ok(Self::new(s))
}
}
impl<'a> fmt::Display for Ident<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&*self)
}
}
impl<'a> fmt::Debug for Ident<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Ident").field(&self.as_ref()).finish()
}
}
const fn is_char_valid(c: u8) -> bool {
matches!(c, b'a'..=b'z' | b'0'..=b'9' | b'-')
}
#[cfg(test)]
mod tests {
use super::{Error, Ident};
use core::convert::TryFrom;
const INVALID_EMPTY: &str = "";
const INVALID_CHAR: &str = "argon2;d";
const INVALID_TOO_LONG: &str = "012345678911234567892123456789312";
const INVALID_CHAR_AND_TOO_LONG: &str = "0!2345678911234567892123456789312";
#[test]
fn parse_valid() {
let valid_examples = ["6", "x", "argon2d", "01234567891123456789212345678931"];
for &example in &valid_examples {
let const_val = Ident::new(example);
let try_from_val = Ident::try_from(example).unwrap();
assert_eq!(example, &*const_val);
assert_eq!(example, &*try_from_val);
}
}
#[test]
#[should_panic]
fn reject_empty_const() {
Ident::new(INVALID_EMPTY);
}
#[test]
fn reject_empty_fallible() {
let err = Ident::try_from(INVALID_EMPTY).err().unwrap();
assert_eq!(err, Error::ParamNameInvalid);
}
#[test]
#[should_panic]
fn reject_invalid_char_const() {
Ident::new(INVALID_CHAR);
}
#[test]
fn reject_invalid_char_fallible() {
let err = Ident::try_from(INVALID_CHAR).err().unwrap();
assert_eq!(err, Error::ParamNameInvalid);
}
#[test]
#[should_panic]
fn reject_too_long_const() {
Ident::new(INVALID_TOO_LONG);
}
#[test]
fn reject_too_long_fallible() {
let err = Ident::try_from(INVALID_TOO_LONG).err().unwrap();
assert_eq!(err, Error::ParamNameInvalid);
}
#[test]
#[should_panic]
fn reject_invalid_char_and_too_long_const() {
Ident::new(INVALID_CHAR_AND_TOO_LONG);
}
#[test]
fn reject_invalid_char_and_too_long_fallible() {
let err = Ident::try_from(INVALID_CHAR_AND_TOO_LONG).err().unwrap();
assert_eq!(err, Error::ParamNameInvalid);
}
}