use anyhow::{bail, Result};
use serde::{de::Visitor, Deserialize, Serialize, Serializer};
use std::{
convert::{TryFrom, TryInto},
fmt::{self, Formatter},
};
use thiserror::Error;
use super::non_nul_string::NonNulString;
const MAX_LENGTH: usize = 1024;
#[derive(Clone, Eq, PartialOrd, Ord, PartialEq, Hash)]
pub struct Name(NonNulString);
impl fmt::Debug for Name {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "\"{}\"", self.0)
}
}
impl fmt::Display for Name {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
#[derive(Error, Debug)]
#[error(transparent)]
pub struct InvalidNameError(#[from] anyhow::Error);
fn validate(name: String) -> Result<Name> {
let name = NonNulString::try_from(name)?;
if name.len() == 0 {
bail!("container name is empty");
} else if name.len() > MAX_LENGTH {
bail!("container name is longer than 1024 characters");
}
if let Some(c) = name
.chars()
.find(|c| !matches!(c, '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-'))
{
bail!("invalid character: {}", c);
}
Ok(Name(name))
}
impl TryFrom<String> for Name {
type Error = InvalidNameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(validate(value)?)
}
}
impl TryFrom<&str> for Name {
type Error = InvalidNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.to_string().try_into()
}
}
impl From<Name> for NonNulString {
fn from(value: Name) -> Self {
value.0
}
}
impl Serialize for Name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> Deserialize<'de> for Name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct NameVisitor;
impl<'de> Visitor<'de> for NameVisitor {
type Value = Name;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"valid non empty string without nul bytes: ('0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-')+",
)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.try_into().map_err(|_| E::custom("invalid name"))
}
}
deserializer.deserialize_str(NameVisitor)
}
}
#[test]
fn try_empty() {
assert!(Name::try_from("").is_err());
}
#[test]
fn try_too_long() {
assert!(Name::try_from("a".repeat(1024)).is_ok());
assert!(Name::try_from("a".repeat(2000)).is_err());
}
#[test]
fn try_invalid_char() {
assert!(Name::try_from("a%").is_err());
assert!(Name::try_from("^foo").is_err());
assert!(Name::try_from("test*").is_err());
}
#[test]
fn try_from_nul_bytes() {
assert!(Name::try_from("\0").is_err());
assert!(Name::try_from("a\0b").is_err());
}
#[test]
#[allow(clippy::unwrap_used)]
fn serialize() {
assert!(matches!(
serde_json::to_string(&Name::try_from("a").unwrap()),
Ok(s) if s == "\"a\""
));
}
#[test]
#[allow(clippy::unwrap_used)]
fn deserialize() {
assert!(matches!(
serde_json::from_str::<Name>("\"a\""),
Ok(n) if n == Name::try_from("a").unwrap()
));
assert!(matches!(serde_json::from_str::<Name>("\"a\0\""), Err(_)));
}
#[test]
#[should_panic]
#[allow(clippy::unwrap_used)]
fn deserialize_name_contains_slash() {
serde_json::from_str::<Name>("test/../test").unwrap();
}