firefly_meta/
validators.rs1use core::fmt::Display;
2
3pub enum ValidationError {
4 TrailingMinus,
5 DoubleMinus,
6 Empty,
7 InvalidChar(u8),
8 InvalidFirstChar(u8),
9 TooLong,
10 TrailingSpace,
11 TrailingDot,
12}
13
14impl ValidationError {
15 #[must_use]
16 pub const fn as_str(&self) -> &str {
17 match self {
18 Self::TrailingMinus => "must not start or end with minus",
19 Self::DoubleMinus => "must not contain '--'",
20 Self::Empty => "must not be empty",
21 Self::InvalidChar(c) => match char::from_u32(*c as u32) {
22 Some(_) => "contains invalid character",
23 None => "must contain only valid ASCII characters",
24 },
25 Self::InvalidFirstChar(c) => match char::from_u32(*c as u32) {
26 Some(_) => "starts with invalid character",
27 None => "must start with an ASCII character",
28 },
29 Self::TooLong => "too long",
30 Self::TrailingSpace => "must not start or end with space",
31 Self::TrailingDot => "must not start or end with dot",
32 }
33 }
34}
35
36impl Display for ValidationError {
37 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38 match self {
39 Self::TrailingMinus => write!(f, "must not start or end with minus"),
40 Self::DoubleMinus => write!(f, "must not contain '--'"),
41 Self::Empty => write!(f, "must not be empty"),
42 Self::InvalidChar(c) => match char::from_u32(u32::from(*c)) {
43 Some(c) => write!(f, "must not contain {c}"),
44 None => write!(f, "must contain only valid ASCII characters"),
45 },
46 Self::InvalidFirstChar(c) => match char::from_u32(u32::from(*c)) {
47 Some(c) => write!(f, "must not start with {c}"),
48 None => write!(f, "must start with an ASCII character"),
49 },
50 Self::TooLong => write!(f, "too long"),
51 Self::TrailingSpace => write!(f, "must not start or end with space"),
52 Self::TrailingDot => write!(f, "must not start or end with dot"),
53 }
54 }
55}
56
57pub fn validate_id(s: &str) -> Result<(), ValidationError> {
69 if s.len() > 16 {
70 return Err(ValidationError::TooLong);
71 }
72 if s.starts_with('-') || s.ends_with('-') {
73 return Err(ValidationError::TrailingMinus);
74 }
75 if s.contains("--") {
76 return Err(ValidationError::DoubleMinus);
77 }
78 if s.is_empty() {
79 return Err(ValidationError::Empty);
80 }
81 for c in s.bytes() {
82 if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != b'-' {
83 return Err(ValidationError::InvalidChar(c));
84 }
85 }
86 Ok(())
87}
88
89pub fn validate_name(s: &str) -> Result<(), ValidationError> {
100 if s.len() > 40 {
101 return Err(ValidationError::TooLong);
102 }
103 if s.ends_with(' ') {
104 return Err(ValidationError::TrailingSpace);
105 }
106 let mut b = s.bytes();
107 match b.next() {
108 Some(c) => {
110 if !c.is_ascii_alphabetic() {
111 return Err(ValidationError::InvalidFirstChar(c));
112 }
113 }
114 None => return Err(ValidationError::Empty),
116 }
117 for c in b {
118 if c.is_ascii_alphanumeric() {
119 continue;
120 }
121 if !c.is_ascii_punctuation() && c != b' ' {
122 return Err(ValidationError::InvalidChar(c));
123 }
124 }
125 Ok(())
126}
127
128pub fn validate_path_part(s: &str) -> Result<(), ValidationError> {
134 if s.starts_with('.') {
135 return Err(ValidationError::TrailingDot);
136 }
137 if s.is_empty() {
138 return Err(ValidationError::Empty);
139 }
140 for c in s.bytes() {
141 if c.is_ascii_alphanumeric() {
142 continue;
143 }
144 if c != b'.' && c != b'_' && c != b'-' {
145 return Err(ValidationError::InvalidChar(c));
146 }
147 }
148 Ok(())
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_valid_id() {
157 assert!(validate_id("app").is_ok());
158 assert!(validate_id("some-app").is_ok());
159 assert!(validate_id("some-app-13").is_ok());
160 assert!(validate_id("13app").is_ok());
161 assert!(validate_id("a").is_ok());
162 assert!(validate_id("a-bit-long-name").is_ok());
163 }
164
165 #[test]
166 fn test_invalid_id() {
167 assert!(validate_id("app.name").is_err());
168 assert!(validate_id("app--name").is_err());
169 assert!(validate_id("-appname").is_err());
170 assert!(validate_id("-app-name").is_err());
171 assert!(validate_id("-appname").is_err());
172 assert!(validate_id("app-name-").is_err());
173 assert!(validate_id("appname-").is_err());
174 assert!(validate_id("-appname-").is_err());
175 assert!(validate_id("app name").is_err());
176 assert!(validate_id("appname ").is_err());
177 assert!(validate_id(" appname").is_err());
178 assert!(validate_id("App").is_err());
179 assert!(validate_id("AppName").is_err());
180 assert!(validate_id("APPNAME").is_err());
181 assert!(validate_id("").is_err());
182 assert!(validate_id(" ").is_err());
183 assert!(validate_id("-").is_err());
184 assert!(validate_id("--").is_err());
185 assert!(validate_id("?hello").is_err());
186 assert!(validate_id("a-very-long-app-name").is_err());
187 }
188
189 #[test]
190 fn test_valid_name() {
191 assert!(validate_name("app").is_ok());
192 assert!(validate_name("a").is_ok());
193 assert!(validate_name("some-app").is_ok());
194 assert!(validate_name("App").is_ok());
195 assert!(validate_name("Some app").is_ok());
196 assert!(validate_name("Some App").is_ok());
197 assert!(validate_name("SOME APP").is_ok());
198 assert!(validate_name("Hello").is_ok());
199 assert!(validate_name("Hello?").is_ok());
200 assert!(validate_name("Yes? Yes!").is_ok());
201 }
202
203 #[test]
204 fn test_invalid_name() {
205 assert!(validate_name(" ").is_err());
206 assert!(validate_name(" ").is_err());
207 assert!(validate_name("").is_err());
208 assert!(validate_name(" abc").is_err());
209 assert!(validate_name("abc ").is_err());
210 assert!(validate_name("ab\tcd").is_err());
211 assert!(validate_name("ัะตัั").is_err());
212 assert!(validate_name("?hello").is_err());
213 }
214
215 #[test]
216 fn test_valid_path_part() {
217 assert!(validate_path_part("app").is_ok());
218 assert!(validate_path_part("a").is_ok());
219 assert!(validate_path_part("some-app").is_ok());
220 assert!(validate_path_part("App").is_ok());
221 assert!(validate_path_part("file.wasm").is_ok());
222 assert!(validate_path_part("file_name.wasm").is_ok());
223 assert!(validate_path_part("FileName.wasm").is_ok());
224 }
225
226 #[test]
227 fn test_invalid_path_part() {
228 assert!(validate_path_part(".gitignore").is_err());
229 assert!(validate_path_part("..").is_err());
230 assert!(validate_path_part("/").is_err());
231 assert!(validate_path_part("./").is_err());
232 assert!(validate_path_part("???").is_err());
233 assert!(validate_path_part("file/../root").is_err());
234 assert!(validate_path_part("file name").is_err());
235 assert!(validate_path_part(" file").is_err());
236 assert!(validate_path_part("file ").is_err());
237 assert!(validate_path_part("").is_err());
238 assert!(validate_path_part(" ").is_err());
239 }
240}