northstar_runtime/common/
name.rs1use 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
11const MAX_LENGTH: usize = 1024;
13
14#[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#[derive(Error, Debug)]
41#[error(transparent)]
42pub struct InvalidNameError(#[from] anyhow::Error);
43
44fn 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}