Skip to main content

fuchsia_url/
parse.rs

1// Copyright 2018 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::errors::PackagePathSegmentError;
6use serde::{Deserialize, Serialize};
7use std::convert::TryInto as _;
8
9pub const MAX_PACKAGE_PATH_SEGMENT_BYTES: usize = 255;
10
11/// Check if a string conforms to r"^[0-9a-z\-\._]{1,255}$" and is neither "." nor ".."
12pub fn validate_package_path_segment(string: &str) -> Result<(), PackagePathSegmentError> {
13    if string.is_empty() {
14        return Err(PackagePathSegmentError::Empty);
15    }
16    if string.len() > MAX_PACKAGE_PATH_SEGMENT_BYTES {
17        return Err(PackagePathSegmentError::TooLong(string.len()));
18    }
19    if let Some(invalid_byte) = string.bytes().find(|&b| {
20        !(b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'.' || b == b'_')
21    }) {
22        return Err(PackagePathSegmentError::InvalidCharacter { character: invalid_byte.into() });
23    }
24    if string == "." {
25        return Err(PackagePathSegmentError::DotSegment);
26    }
27    if string == ".." {
28        return Err(PackagePathSegmentError::DotDotSegment);
29    }
30
31    Ok(())
32}
33
34/// A Fuchsia Package Name. Package names are the first segment of the path.
35/// https://fuchsia.dev/fuchsia-src/concepts/packages/package_url#package-name
36#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash, Serialize)]
37pub struct PackageName(String);
38
39impl std::str::FromStr for PackageName {
40    type Err = PackagePathSegmentError;
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        let () = validate_package_path_segment(s)?;
43        Ok(Self(s.into()))
44    }
45}
46
47impl TryFrom<String> for PackageName {
48    type Error = PackagePathSegmentError;
49    fn try_from(value: String) -> Result<Self, Self::Error> {
50        let () = validate_package_path_segment(&value)?;
51        Ok(Self(value))
52    }
53}
54
55impl TryFrom<&crate::Path> for PackageName {
56    type Error = crate::ParseError;
57    fn try_from(path: &crate::Path) -> Result<Self, Self::Error> {
58        // A PackageName is a Path with a single segment.
59        path.parse().map_err(Self::Error::InvalidName)
60    }
61}
62
63impl From<PackageName> for String {
64    fn from(name: PackageName) -> Self {
65        name.0
66    }
67}
68
69impl From<&PackageName> for String {
70    fn from(name: &PackageName) -> Self {
71        name.0.clone()
72    }
73}
74
75impl std::fmt::Display for PackageName {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "{}", self.0)
78    }
79}
80
81impl AsRef<str> for PackageName {
82    fn as_ref(&self) -> &str {
83        &self.0
84    }
85}
86
87impl<'de> Deserialize<'de> for PackageName {
88    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89    where
90        D: serde::Deserializer<'de>,
91    {
92        let value = String::deserialize(deserializer)?;
93        value
94            .try_into()
95            .map_err(|e| serde::de::Error::custom(format!("invalid package name: {}", e)))
96    }
97}
98
99/// A Fuchsia Package Variant. Package variants are the optional second segment of the path.
100#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash, Serialize)]
101pub struct PackageVariant(String);
102
103impl PackageVariant {
104    /// The string representation, "0", of the zero package variant.
105    pub const ZERO_STR: &str = "0";
106
107    /// Create a `PackageVariant` of "0".
108    pub fn zero() -> Self {
109        Self::ZERO_STR.parse().expect("\"0\" is a valid variant")
110    }
111
112    /// Returns true iff the variant is "0".
113    pub fn is_zero(&self) -> bool {
114        self.0 == Self::ZERO_STR
115    }
116}
117
118impl std::str::FromStr for PackageVariant {
119    type Err = PackagePathSegmentError;
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        let () = validate_package_path_segment(s)?;
122        Ok(Self(s.into()))
123    }
124}
125
126impl TryFrom<String> for PackageVariant {
127    type Error = PackagePathSegmentError;
128    fn try_from(value: String) -> Result<Self, Self::Error> {
129        let () = validate_package_path_segment(&value)?;
130        Ok(Self(value))
131    }
132}
133
134impl std::fmt::Display for PackageVariant {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        write!(f, "{}", self.0)
137    }
138}
139
140impl AsRef<str> for PackageVariant {
141    fn as_ref(&self) -> &str {
142        &self.0
143    }
144}
145
146impl<'de> Deserialize<'de> for PackageVariant {
147    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
148    where
149        D: serde::Deserializer<'de>,
150    {
151        let value = String::deserialize(deserializer)?;
152        value
153            .try_into()
154            .map_err(|e| serde::de::Error::custom(format!("invalid package variant {}", e)))
155    }
156}
157
158#[cfg(test)]
159mod test_validate_package_path_segment {
160    use super::*;
161    use crate::test::random_package_segment;
162    use proptest::prelude::*;
163
164    #[test]
165    fn reject_empty_segment() {
166        assert_eq!(validate_package_path_segment(""), Err(PackagePathSegmentError::Empty));
167    }
168
169    #[test]
170    fn reject_dot_segment() {
171        assert_eq!(validate_package_path_segment("."), Err(PackagePathSegmentError::DotSegment));
172    }
173
174    #[test]
175    fn reject_dot_dot_segment() {
176        assert_eq!(
177            validate_package_path_segment(".."),
178            Err(PackagePathSegmentError::DotDotSegment)
179        );
180    }
181
182    proptest! {
183        #![proptest_config(ProptestConfig{
184            failure_persistence: None,
185            ..Default::default()
186        })]
187
188        #[test]
189        fn reject_segment_too_long(ref s in r"[-_0-9a-z\.]{256, 300}")
190        {
191            prop_assert_eq!(
192                validate_package_path_segment(s),
193                Err(PackagePathSegmentError::TooLong(s.len()))
194            );
195        }
196
197        #[test]
198        fn reject_invalid_character(ref s in r"[-_0-9a-z\.]{0, 48}[^-_0-9a-z\.][-_0-9a-z\.]{0, 48}")
199        {
200            let pass = matches!(
201                validate_package_path_segment(s),
202                Err(PackagePathSegmentError::InvalidCharacter{..})
203            );
204            prop_assert!(pass);
205        }
206
207        #[test]
208        fn valid_segment(ref s in random_package_segment())
209        {
210            prop_assert_eq!(
211                validate_package_path_segment(s),
212                Ok(())
213            );
214        }
215    }
216}
217
218#[cfg(test)]
219mod test_package_name {
220    use super::*;
221    use assert_matches::assert_matches;
222
223    #[test]
224    fn from_str_rejects_invalid() {
225        assert_eq!(
226            "?".parse::<PackageName>(),
227            Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
228        );
229    }
230
231    #[test]
232    fn from_str_succeeds() {
233        "package-name".parse::<PackageName>().unwrap();
234    }
235
236    #[test]
237    fn try_from_rejects_invalid() {
238        assert_eq!(
239            PackageName::try_from("?".to_string()),
240            Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
241        );
242    }
243
244    #[test]
245    fn try_from_succeeds() {
246        PackageName::try_from("valid-name".to_string()).unwrap();
247    }
248
249    #[test]
250    fn from_succeeds() {
251        assert_eq!(
252            String::from("package-name".parse::<PackageName>().unwrap()),
253            "package-name".to_string()
254        );
255    }
256
257    #[test]
258    fn display() {
259        let path: PackageName = "package-name".parse().unwrap();
260        assert_eq!(format!("{}", path), "package-name");
261    }
262
263    #[test]
264    fn as_ref() {
265        let path: PackageName = "package-name".parse().unwrap();
266        assert_eq!(path.as_ref(), "package-name");
267    }
268
269    #[test]
270    fn deserialize_success() {
271        let actual_value =
272            serde_json::from_str::<PackageName>("\"package-name\"").expect("json to deserialize");
273        assert_eq!(actual_value, "package-name".parse::<PackageName>().unwrap());
274    }
275
276    #[test]
277    fn deserialize_rejects_invalid() {
278        let msg = serde_json::from_str::<PackageName>("\"pack!age-name\"").unwrap_err().to_string();
279        assert!(msg.contains("invalid package name"), r#"Bad error message: "{}""#, msg);
280    }
281
282    #[test]
283    fn try_from_path_ref_success() {
284        let path: crate::Path = "valid-name".parse().unwrap();
285        assert_eq!(PackageName::try_from(&path).unwrap().as_ref(), "valid-name");
286    }
287
288    #[test]
289    fn try_from_path_ref_error() {
290        let path: crate::Path = "in/valid/name".parse().unwrap();
291        assert_matches!(
292            PackageName::try_from(&path),
293            Err(crate::ParseError::InvalidName(PackagePathSegmentError::InvalidCharacter {
294                character: '/'
295            }))
296        );
297    }
298}
299
300#[cfg(test)]
301mod test_package_variant {
302    use super::*;
303
304    #[test]
305    fn zero() {
306        assert_eq!(PackageVariant::zero().as_ref(), "0");
307        assert!(PackageVariant::zero().is_zero());
308        assert_eq!("1".parse::<PackageVariant>().unwrap().is_zero(), false);
309    }
310
311    #[test]
312    fn from_str_rejects_invalid() {
313        assert_eq!(
314            "?".parse::<PackageVariant>(),
315            Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
316        );
317    }
318
319    #[test]
320    fn from_str_succeeds() {
321        "package-variant".parse::<PackageVariant>().unwrap();
322    }
323
324    #[test]
325    fn try_from_rejects_invalid() {
326        assert_eq!(
327            PackageVariant::try_from("?".to_string()),
328            Err(PackagePathSegmentError::InvalidCharacter { character: '?'.into() })
329        );
330    }
331
332    #[test]
333    fn try_from_succeeds() {
334        PackageVariant::try_from("valid-variant".to_string()).unwrap();
335    }
336
337    #[test]
338    fn display() {
339        let path: PackageVariant = "package-variant".parse().unwrap();
340        assert_eq!(format!("{}", path), "package-variant");
341    }
342
343    #[test]
344    fn as_ref() {
345        let path: PackageVariant = "package-variant".parse().unwrap();
346        assert_eq!(path.as_ref(), "package-variant");
347    }
348
349    #[test]
350    fn deserialize_success() {
351        let actual_value = serde_json::from_str::<PackageVariant>("\"package-variant\"")
352            .expect("json to deserialize");
353        assert_eq!(actual_value, "package-variant".parse::<PackageVariant>().unwrap());
354    }
355
356    #[test]
357    fn deserialize_rejects_invalid() {
358        let msg =
359            serde_json::from_str::<PackageVariant>("\"pack!age-variant\"").unwrap_err().to_string();
360        assert!(msg.contains("invalid package variant"), r#"Bad error message: "{}""#, msg);
361    }
362}