1use crate::errors::{PackagePathSegmentError, ResourcePathError};
6use serde::{Deserialize, Serialize};
7use std::convert::TryInto as _;
8
9pub const MAX_PACKAGE_PATH_SEGMENT_BYTES: usize = 255;
10pub const MAX_RESOURCE_PATH_SEGMENT_BYTES: usize = 255;
11
12pub fn validate_package_path_segment(string: &str) -> Result<(), PackagePathSegmentError> {
14 if string.is_empty() {
15 return Err(PackagePathSegmentError::Empty);
16 }
17 if string.len() > MAX_PACKAGE_PATH_SEGMENT_BYTES {
18 return Err(PackagePathSegmentError::TooLong(string.len()));
19 }
20 if let Some(invalid_byte) = string.bytes().find(|&b| {
21 !(b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'.' || b == b'_')
22 }) {
23 return Err(PackagePathSegmentError::InvalidCharacter { character: invalid_byte.into() });
24 }
25 if string == "." {
26 return Err(PackagePathSegmentError::DotSegment);
27 }
28 if string == ".." {
29 return Err(PackagePathSegmentError::DotDotSegment);
30 }
31
32 Ok(())
33}
34
35#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash, Serialize)]
38pub struct PackageName(String);
39
40impl std::str::FromStr for PackageName {
41 type Err = PackagePathSegmentError;
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 let () = validate_package_path_segment(s)?;
44 Ok(Self(s.into()))
45 }
46}
47
48impl TryFrom<String> for PackageName {
49 type Error = PackagePathSegmentError;
50 fn try_from(value: String) -> Result<Self, Self::Error> {
51 let () = validate_package_path_segment(&value)?;
52 Ok(Self(value))
53 }
54}
55
56impl From<PackageName> for String {
57 fn from(name: PackageName) -> Self {
58 name.0
59 }
60}
61
62impl From<&PackageName> for String {
63 fn from(name: &PackageName) -> Self {
64 name.0.clone()
65 }
66}
67
68impl std::fmt::Display for PackageName {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(f, "{}", self.0)
71 }
72}
73
74impl AsRef<str> for PackageName {
75 fn as_ref(&self) -> &str {
76 &self.0
77 }
78}
79
80impl<'de> Deserialize<'de> for PackageName {
81 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82 where
83 D: serde::Deserializer<'de>,
84 {
85 let value = String::deserialize(deserializer)?;
86 value
87 .try_into()
88 .map_err(|e| serde::de::Error::custom(format!("invalid package name: {}", e)))
89 }
90}
91
92#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash, Serialize)]
94pub struct PackageVariant(String);
95
96impl PackageVariant {
97 pub const ZERO_STR: &str = "0";
99
100 pub fn zero() -> Self {
102 Self::ZERO_STR.parse().expect("\"0\" is a valid variant")
103 }
104
105 pub fn is_zero(&self) -> bool {
107 self.0 == Self::ZERO_STR
108 }
109}
110
111impl std::str::FromStr for PackageVariant {
112 type Err = PackagePathSegmentError;
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
114 let () = validate_package_path_segment(s)?;
115 Ok(Self(s.into()))
116 }
117}
118
119impl TryFrom<String> for PackageVariant {
120 type Error = PackagePathSegmentError;
121 fn try_from(value: String) -> Result<Self, Self::Error> {
122 let () = validate_package_path_segment(&value)?;
123 Ok(Self(value))
124 }
125}
126
127impl std::fmt::Display for PackageVariant {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write!(f, "{}", self.0)
130 }
131}
132
133impl AsRef<str> for PackageVariant {
134 fn as_ref(&self) -> &str {
135 &self.0
136 }
137}
138
139impl<'de> Deserialize<'de> for PackageVariant {
140 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141 where
142 D: serde::Deserializer<'de>,
143 {
144 let value = String::deserialize(deserializer)?;
145 value
146 .try_into()
147 .map_err(|e| serde::de::Error::custom(format!("invalid package variant {}", e)))
148 }
149}
150
151pub fn validate_resource_path(input: &str) -> Result<(), ResourcePathError> {
156 if input.is_empty() {
157 return Err(ResourcePathError::PathIsEmpty);
158 }
159 if input.starts_with('/') {
160 return Err(ResourcePathError::PathStartsWithSlash);
161 }
162 if input.ends_with('/') {
163 return Err(ResourcePathError::PathEndsWithSlash);
164 }
165 for segment in input.split('/') {
166 if segment.contains('\0') {
167 return Err(ResourcePathError::NameContainsNull);
168 }
169 if segment == "." {
170 return Err(ResourcePathError::NameIsDot);
171 }
172 if segment == ".." {
173 return Err(ResourcePathError::NameIsDotDot);
174 }
175 if segment.is_empty() {
176 return Err(ResourcePathError::NameEmpty);
177 }
178 if segment.len() > MAX_RESOURCE_PATH_SEGMENT_BYTES {
179 return Err(ResourcePathError::NameTooLong);
180 }
181 if segment.contains('\n') {
183 return Err(ResourcePathError::NameContainsNewline);
184 }
185 }
186 Ok(())
187}
188
189#[cfg(test)]
190mod test_validate_package_path_segment {
191 use super::*;
192 use crate::test::random_package_segment;
193 use proptest::prelude::*;
194
195 #[test]
196 fn reject_empty_segment() {
197 assert_eq!(validate_package_path_segment(""), Err(PackagePathSegmentError::Empty));
198 }
199
200 #[test]
201 fn reject_dot_segment() {
202 assert_eq!(validate_package_path_segment("."), Err(PackagePathSegmentError::DotSegment));
203 }
204
205 #[test]
206 fn reject_dot_dot_segment() {
207 assert_eq!(
208 validate_package_path_segment(".."),
209 Err(PackagePathSegmentError::DotDotSegment)
210 );
211 }
212
213 proptest! {
214 #![proptest_config(ProptestConfig{
215 failure_persistence: None,
216 ..Default::default()
217 })]
218
219 #[test]
220 fn reject_segment_too_long(ref s in r"[-_0-9a-z\.]{256, 300}")
221 {
222 prop_assert_eq!(
223 validate_package_path_segment(s),
224 Err(PackagePathSegmentError::TooLong(s.len()))
225 );
226 }
227
228 #[test]
229 fn reject_invalid_character(ref s in r"[-_0-9a-z\.]{0, 48}[^-_0-9a-z\.][-_0-9a-z\.]{0, 48}")
230 {
231 let pass = matches!(
232 validate_package_path_segment(s),
233 Err(PackagePathSegmentError::InvalidCharacter{..})
234 );
235 prop_assert!(pass);
236 }
237
238 #[test]
239 fn valid_segment(ref s in random_package_segment())
240 {
241 prop_assert_eq!(
242 validate_package_path_segment(s),
243 Ok(())
244 );
245 }
246 }
247}
248
249#[cfg(test)]
250mod test_package_name {
251 use super::*;
252
253 #[test]
254 fn from_str_rejects_invalid() {
255 assert_eq!(
256 "?".parse::<PackageName>(),
257 Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
258 );
259 }
260
261 #[test]
262 fn from_str_succeeds() {
263 "package-name".parse::<PackageName>().unwrap();
264 }
265
266 #[test]
267 fn try_from_rejects_invalid() {
268 assert_eq!(
269 PackageName::try_from("?".to_string()),
270 Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
271 );
272 }
273
274 #[test]
275 fn try_from_succeeds() {
276 PackageName::try_from("valid-name".to_string()).unwrap();
277 }
278
279 #[test]
280 fn from_succeeds() {
281 assert_eq!(
282 String::from("package-name".parse::<PackageName>().unwrap()),
283 "package-name".to_string()
284 );
285 }
286
287 #[test]
288 fn display() {
289 let path: PackageName = "package-name".parse().unwrap();
290 assert_eq!(format!("{}", path), "package-name");
291 }
292
293 #[test]
294 fn as_ref() {
295 let path: PackageName = "package-name".parse().unwrap();
296 assert_eq!(path.as_ref(), "package-name");
297 }
298
299 #[test]
300 fn deserialize_success() {
301 let actual_value =
302 serde_json::from_str::<PackageName>("\"package-name\"").expect("json to deserialize");
303 assert_eq!(actual_value, "package-name".parse::<PackageName>().unwrap());
304 }
305
306 #[test]
307 fn deserialize_rejects_invalid() {
308 let msg = serde_json::from_str::<PackageName>("\"pack!age-name\"").unwrap_err().to_string();
309 assert!(msg.contains("invalid package name"), r#"Bad error message: "{}""#, msg);
310 }
311}
312
313#[cfg(test)]
314mod test_package_variant {
315 use super::*;
316
317 #[test]
318 fn zero() {
319 assert_eq!(PackageVariant::zero().as_ref(), "0");
320 assert!(PackageVariant::zero().is_zero());
321 assert_eq!("1".parse::<PackageVariant>().unwrap().is_zero(), false);
322 }
323
324 #[test]
325 fn from_str_rejects_invalid() {
326 assert_eq!(
327 "?".parse::<PackageVariant>(),
328 Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
329 );
330 }
331
332 #[test]
333 fn from_str_succeeds() {
334 "package-variant".parse::<PackageVariant>().unwrap();
335 }
336
337 #[test]
338 fn try_from_rejects_invalid() {
339 assert_eq!(
340 PackageVariant::try_from("?".to_string()),
341 Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
342 );
343 }
344
345 #[test]
346 fn try_from_succeeds() {
347 PackageVariant::try_from("valid-variant".to_string()).unwrap();
348 }
349
350 #[test]
351 fn display() {
352 let path: PackageVariant = "package-variant".parse().unwrap();
353 assert_eq!(format!("{}", path), "package-variant");
354 }
355
356 #[test]
357 fn as_ref() {
358 let path: PackageVariant = "package-variant".parse().unwrap();
359 assert_eq!(path.as_ref(), "package-variant");
360 }
361
362 #[test]
363 fn deserialize_success() {
364 let actual_value = serde_json::from_str::<PackageVariant>("\"package-variant\"")
365 .expect("json to deserialize");
366 assert_eq!(actual_value, "package-variant".parse::<PackageVariant>().unwrap());
367 }
368
369 #[test]
370 fn deserialize_rejects_invalid() {
371 let msg =
372 serde_json::from_str::<PackageVariant>("\"pack!age-variant\"").unwrap_err().to_string();
373 assert!(msg.contains("invalid package variant"), r#"Bad error message: "{}""#, msg);
374 }
375}
376
377#[cfg(test)]
378mod test_validate_resource_path {
379 use super::*;
380 use crate::test::*;
381 use proptest::prelude::*;
382
383 #[test]
385 fn test_empty_string() {
386 assert_eq!(validate_resource_path(""), Err(ResourcePathError::PathIsEmpty));
387 }
388
389 proptest! {
390 #![proptest_config(ProptestConfig{
391 failure_persistence: None,
392 ..Default::default()
393 })]
394
395 #[test]
396 fn test_reject_empty_object_name(
397 ref s in random_resource_path_with_regex_segment_str(5, "")) {
398 prop_assume!(!s.starts_with('/') && !s.ends_with('/'));
399 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::NameEmpty));
400 }
401
402 #[test]
403 fn test_reject_long_object_name(
404 ref s in random_resource_path_with_regex_segment_str(5, r"[[[:ascii:]]--\.--/--\x00]{256}")) {
405 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::NameTooLong));
406 }
407
408 #[test]
409 fn test_reject_contains_null(
410 ref s in random_resource_path_with_regex_segment_string(
411 5, format!(r"{}{{0,3}}\x00{}{{0,3}}",
412 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE,
413 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE))) {
414 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::NameContainsNull));
415 }
416
417 #[test]
418 fn test_reject_name_is_dot(
419 ref s in random_resource_path_with_regex_segment_str(5, r"\.")) {
420 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::NameIsDot));
421 }
422
423 #[test]
424 fn test_reject_name_is_dot_dot(
425 ref s in random_resource_path_with_regex_segment_str(5, r"\.\.")) {
426 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::NameIsDotDot));
427 }
428
429 #[test]
430 fn test_reject_starts_with_slash(
431 ref s in format!(
432 "/{}{{1,5}}",
433 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE).as_str()) {
434 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::PathStartsWithSlash));
435 }
436
437 #[test]
438 fn test_reject_ends_with_slash(
439 ref s in format!(
440 "{}{{1,5}}/",
441 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE).as_str()) {
442 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::PathEndsWithSlash));
443 }
444
445 #[test]
446 fn test_reject_contains_newline(
447 ref s in random_resource_path_with_regex_segment_string(
448 5, format!(r"{}{{0,3}}\x0a{}{{0,3}}",
449 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE,
450 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE))) {
451 prop_assert_eq!(validate_resource_path(s), Err(ResourcePathError::NameContainsNewline));
452 }
453 }
454
455 proptest! {
457 #![proptest_config(ProptestConfig{
458 failure_persistence: None,
459 ..Default::default()
460 })]
461
462 #[test]
463 fn test_name_contains_dot(
464 ref s in random_resource_path_with_regex_segment_string(
465 5, format!(r"{}{{1,4}}\.{}{{1,4}}",
466 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE,
467 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE)))
468 {
469 prop_assert_eq!(validate_resource_path(s), Ok(()));
470 }
471
472 #[test]
473 fn test_name_contains_dot_dot(
474 ref s in random_resource_path_with_regex_segment_string(
475 5, format!(r"{}{{1,4}}\.\.{}{{1,4}}",
476 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE,
477 ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE)))
478 {
479 prop_assert_eq!(validate_resource_path(s), Ok(()));
480 }
481
482 #[test]
483 fn test_single_segment(ref s in always_valid_resource_path_chars(1, 4)) {
484 prop_assert_eq!(validate_resource_path(s), Ok(()));
485 }
486
487 #[test]
488 fn test_multi_segment(
489 ref s in prop::collection::vec(always_valid_resource_path_chars(1, 4), 1..5))
490 {
491 let path = s.join("/");
492 prop_assert_eq!(validate_resource_path(&path), Ok(()));
493 }
494
495 #[test]
496 fn test_long_name(
497 ref s in random_resource_path_with_regex_segment_str(
498 5, "[[[:ascii:]]--\0--/--\n]{255}")) {
500 prop_assert_eq!(validate_resource_path(s), Ok(()));
501 }
502 }
503}