use crate::{B64, Error, Result};
use base64ct::Encoding;
use core::{fmt, str};
pub type Decimal = u32;
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Value<'a>(&'a str);
impl<'a> Value<'a> {
pub const MAX_LENGTH: usize = 64;
pub fn new(input: &'a str) -> Result<Self> {
if input.len() > Self::MAX_LENGTH {
return Err(Error::ParamValueTooLong);
}
assert_valid_value(input)?;
Ok(Self(input))
}
pub fn b64_decode<'b>(&self, buf: &'b mut [u8]) -> Result<&'b [u8]> {
Ok(B64::decode(self.as_str(), buf)?)
}
pub fn as_str(&self) -> &'a str {
self.0
}
pub fn as_bytes(&self) -> &'a [u8] {
self.as_str().as_bytes()
}
pub fn len(&self) -> usize {
self.as_str().len()
}
pub fn is_empty(&self) -> bool {
self.as_str().is_empty()
}
pub fn decimal(&self) -> Result<Decimal> {
let value = self.as_str();
if value.is_empty() {
return Err(Error::ParamValueInvalid);
}
for c in value.chars() {
if !c.is_ascii_digit() {
return Err(Error::ParamValueInvalid);
}
}
if value.starts_with('0') && value.len() > 1 {
return Err(Error::ParamValueInvalid);
}
value.parse().map_err(|_| Error::ParamValueInvalid)
}
pub fn is_decimal(&self) -> bool {
self.decimal().is_ok()
}
}
impl AsRef<str> for Value<'_> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<'a> TryFrom<&'a str> for Value<'a> {
type Error = Error;
fn try_from(input: &'a str) -> Result<Self> {
Self::new(input)
}
}
impl<'a> TryFrom<Value<'a>> for Decimal {
type Error = Error;
fn try_from(value: Value<'a>) -> Result<Decimal> {
Decimal::try_from(&value)
}
}
impl<'a> TryFrom<&Value<'a>> for Decimal {
type Error = Error;
fn try_from(value: &Value<'a>) -> Result<Decimal> {
value.decimal()
}
}
impl fmt::Display for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
fn assert_valid_value(input: &str) -> Result<()> {
for c in input.chars() {
if !is_char_valid(c) {
return Err(Error::ParamValueInvalid);
}
}
Ok(())
}
fn is_char_valid(c: char) -> bool {
matches!(c, 'A' ..= 'Z' | 'a'..='z' | '0'..='9' | '/' | '+' | '.' | '-')
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::{Error, Value};
const INVALID_CHAR: &str = "x;y";
const INVALID_TOO_LONG: &str =
"01234567891123456789212345678931234567894123456785234567896234567";
const INVALID_CHAR_AND_TOO_LONG: &str =
"0!234567891123456789212345678931234567894123456785234567896234567";
#[test]
fn decimal_value() {
let valid_decimals = &[("0", 0u32), ("1", 1u32), ("4294967295", u32::MAX)];
for &(s, i) in valid_decimals {
let value = Value::new(s).unwrap();
assert!(value.is_decimal());
assert_eq!(value.decimal().unwrap(), i)
}
}
#[test]
fn reject_decimal_with_leading_zero() {
let value = Value::new("01").unwrap();
let err = u32::try_from(value).err().unwrap();
assert_eq!(err, Error::ParamValueInvalid);
}
#[test]
fn reject_overlong_decimal() {
let value = Value::new("4294967296").unwrap();
let err = u32::try_from(value).err().unwrap();
assert_eq!(err, Error::ParamValueInvalid);
}
#[test]
fn reject_negative() {
let value = Value::new("-1").unwrap();
let err = u32::try_from(value).err().unwrap();
assert_eq!(err, Error::ParamValueInvalid);
}
#[test]
fn string_value() {
let valid_examples = [
"",
"X",
"x",
"xXx",
"a+b.c-d",
"1/2",
"01234567891123456789212345678931",
];
for &example in &valid_examples {
let value = Value::new(example).unwrap();
assert_eq!(value.as_str(), example);
}
}
#[test]
fn reject_invalid_char() {
let err = Value::new(INVALID_CHAR).err().unwrap();
assert_eq!(err, Error::ParamValueInvalid);
}
#[test]
fn reject_too_long() {
let err = Value::new(INVALID_TOO_LONG).err().unwrap();
assert_eq!(err, Error::ParamValueTooLong);
}
#[test]
fn reject_invalid_char_and_too_long() {
let err = Value::new(INVALID_CHAR_AND_TOO_LONG).err().unwrap();
assert_eq!(err, Error::ParamValueTooLong);
}
}