northstar_runtime/common/
name.rs

1use anyhow::{bail, Result};
2use serde::{de::Visitor, Deserialize, Serialize, Serializer};
3use std::{
4    convert::{TryFrom, TryInto},
5    fmt::{self, Formatter},
6};
7use thiserror::Error;
8
9use super::non_nul_string::NonNulString;
10
11/// Maximum length allowed for a container name
12const MAX_LENGTH: usize = 1024;
13
14/// Name of a container. A Container name cannot be empty and cannot contain a null bytes
15/// because it is used to generated file names etc.. There's a set of valid characters
16/// allowed in container names: '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-'.
17/// The maximum length allowed for a container name is 1024 characters.
18#[derive(Clone, Eq, PartialOrd, Ord, PartialEq, Hash)]
19pub struct Name(NonNulString);
20
21impl fmt::Debug for Name {
22    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
23        write!(f, "\"{}\"", self.0)
24    }
25}
26
27impl fmt::Display for Name {
28    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
29        write!(f, "{}", self.0)
30    }
31}
32
33impl AsRef<str> for Name {
34    fn as_ref(&self) -> &str {
35        self.0.as_ref()
36    }
37}
38
39/// Name parse error
40#[derive(Error, Debug)]
41#[error(transparent)]
42pub struct InvalidNameError(#[from] anyhow::Error);
43
44/// Validate the input string as a container name
45fn validate(name: String) -> Result<Name> {
46    let name = NonNulString::try_from(name)?;
47
48    if name.len() == 0 {
49        bail!("container name is empty");
50    } else if name.len() > MAX_LENGTH {
51        bail!("container name is longer than 1024 characters");
52    }
53
54    if let Some(c) = name
55        .chars()
56        .find(|c| !matches!(c, '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-'))
57    {
58        bail!("invalid character: {}", c);
59    }
60
61    Ok(Name(name))
62}
63
64impl TryFrom<String> for Name {
65    type Error = InvalidNameError;
66
67    fn try_from(value: String) -> Result<Self, Self::Error> {
68        Ok(validate(value)?)
69    }
70}
71
72impl TryFrom<&str> for Name {
73    type Error = InvalidNameError;
74
75    fn try_from(value: &str) -> Result<Self, Self::Error> {
76        value.to_string().try_into()
77    }
78}
79
80impl From<Name> for NonNulString {
81    fn from(value: Name) -> Self {
82        value.0
83    }
84}
85
86impl Serialize for Name {
87    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88    where
89        S: Serializer,
90    {
91        serializer.serialize_str(self.as_ref())
92    }
93}
94
95impl<'de> Deserialize<'de> for Name {
96    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97    where
98        D: serde::Deserializer<'de>,
99    {
100        struct NameVisitor;
101
102        impl Visitor<'_> for NameVisitor {
103            type Value = Name;
104
105            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
106                formatter.write_str(
107                    "valid non empty string without nul bytes: ('0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '_' | '-')+",
108                )
109            }
110
111            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
112                v.try_into().map_err(|_| E::custom("invalid name"))
113            }
114        }
115
116        deserializer.deserialize_str(NameVisitor)
117    }
118}
119
120#[test]
121fn try_empty() {
122    assert!(Name::try_from("").is_err());
123}
124
125#[test]
126fn try_too_long() {
127    assert!(Name::try_from("a".repeat(1024)).is_ok());
128    assert!(Name::try_from("a".repeat(2000)).is_err());
129}
130
131#[test]
132fn try_invalid_char() {
133    assert!(Name::try_from("a%").is_err());
134    assert!(Name::try_from("^foo").is_err());
135    assert!(Name::try_from("test*").is_err());
136}
137
138#[test]
139fn try_from_nul_bytes() {
140    assert!(Name::try_from("\0").is_err());
141    assert!(Name::try_from("a\0b").is_err());
142}
143
144#[test]
145#[allow(clippy::unwrap_used)]
146fn serialize() {
147    assert!(matches!(
148        serde_json::to_string(&Name::try_from("a").unwrap()),
149        Ok(s) if s == "\"a\""
150    ));
151}
152
153#[test]
154#[allow(clippy::unwrap_used)]
155fn deserialize() {
156    assert!(matches!(
157        serde_json::from_str::<Name>("\"a\""),
158        Ok(n) if n == Name::try_from("a").unwrap()
159    ));
160    assert!(serde_json::from_str::<Name>("\"a\0\"").is_err());
161}
162
163#[test]
164#[should_panic]
165#[allow(clippy::unwrap_used)]
166fn deserialize_name_contains_slash() {
167    serde_json::from_str::<Name>("test/../test").unwrap();
168}