winget_types/shared/
package_identifier.rs

1use std::str::FromStr;
2
3use derive_more::{Deref, Display};
4use serde::Serialize;
5use serde_with::DeserializeFromStr;
6use thiserror::Error;
7
8use crate::shared::DISALLOWED_CHARACTERS;
9
10#[derive(
11    Clone,
12    Debug,
13    Default,
14    Deref,
15    Display,
16    Eq,
17    PartialEq,
18    Ord,
19    PartialOrd,
20    Hash,
21    Serialize,
22    DeserializeFromStr,
23)]
24pub struct PackageIdentifier(String);
25
26#[derive(Error, Debug, PartialEq, Eq)]
27pub enum PackageIdentifierError {
28    #[error("Package identifier cannot be empty")]
29    Empty,
30    #[error(
31        "Package identifier cannot be more than {} characters long",
32        PackageIdentifier::MAX_LENGTH
33    )]
34    TooLong,
35    #[error("Package identifier may not contain whitespace")]
36    ContainsWhitespace,
37    #[error("Package identifier may not contain any control characters")]
38    ContainsControlChars,
39    #[error(
40        "Package identifier may not contain any of the following characters: {:?}",
41        DISALLOWED_CHARACTERS
42    )]
43    DisallowedCharacters,
44    #[error(
45        "The length of a part in a package identifier cannot be more than {} characters long",
46        PackageIdentifier::MAX_PART_LENGTH
47    )]
48    PartTooLong,
49    #[error(
50        "The number of parts in the package identifier must be between {} and {}",
51        PackageIdentifier::MIN_PARTS,
52        PackageIdentifier::MAX_PARTS
53    )]
54    InvalidPartCount,
55}
56
57/// A Package Identifier parser and validator modelled off the regex pattern:
58/// ^[^.\s\\/:*?"<>|\x01-\x1f]{1,32}(\.[^.\s\\/:*?"<>|\x01-\x1f]{1,32}){1,7}$
59impl PackageIdentifier {
60    const MAX_LENGTH: usize = 128;
61    const MIN_PARTS: usize = 2;
62    const MAX_PARTS: usize = 8;
63    const MAX_PART_LENGTH: usize = 32;
64
65    pub fn new(input: &str) -> Result<Self, PackageIdentifierError> {
66        if input.is_empty() {
67            return Err(PackageIdentifierError::Empty);
68        } else if input.chars().count() > Self::MAX_LENGTH {
69            return Err(PackageIdentifierError::TooLong);
70        } else if input.contains(DISALLOWED_CHARACTERS) {
71            return Err(PackageIdentifierError::DisallowedCharacters);
72        } else if input.contains(char::is_whitespace) {
73            return Err(PackageIdentifierError::ContainsWhitespace);
74        } else if input.contains(char::is_control) {
75            return Err(PackageIdentifierError::ContainsControlChars);
76        }
77
78        let mut parts_count = 0;
79        for part in input.split('.') {
80            parts_count += 1;
81            if part.contains(DISALLOWED_CHARACTERS) {
82                return Err(PackageIdentifierError::DisallowedCharacters);
83            }
84
85            if part.chars().count() > Self::MAX_PART_LENGTH {
86                return Err(PackageIdentifierError::PartTooLong);
87            }
88        }
89
90        if !(Self::MIN_PARTS..=Self::MAX_PARTS).contains(&parts_count) {
91            return Err(PackageIdentifierError::InvalidPartCount);
92        }
93
94        Ok(Self(input.to_string()))
95    }
96}
97
98impl FromStr for PackageIdentifier {
99    type Err = PackageIdentifierError;
100
101    fn from_str(s: &str) -> Result<Self, PackageIdentifierError> {
102        Self::new(s)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use std::{iter::repeat_n, str::FromStr};
109
110    use crate::shared::{
111        DISALLOWED_CHARACTERS,
112        package_identifier::{PackageIdentifier, PackageIdentifierError},
113    };
114
115    #[test]
116    fn valid_package_identifier() {
117        assert!(PackageIdentifier::new("Package.Identifier").is_ok());
118    }
119
120    #[test]
121    fn too_long_package_identifier() {
122        let num_delimiters = PackageIdentifier::MAX_PARTS - 1;
123        let part_length =
124            (PackageIdentifier::MAX_LENGTH - num_delimiters).div_ceil(PackageIdentifier::MAX_PARTS);
125
126        let part = "a".repeat(part_length);
127
128        let identifier =
129            itertools::intersperse(repeat_n(&*part, PackageIdentifier::MAX_PARTS), ".")
130                .collect::<String>();
131
132        assert_eq!(
133            PackageIdentifier::from_str(&identifier).err(),
134            Some(PackageIdentifierError::TooLong)
135        );
136    }
137
138    #[test]
139    fn too_many_parts_package_identifier() {
140        let identifier =
141            itertools::intersperse(repeat_n('a', PackageIdentifier::MAX_PARTS + 1), '.')
142                .collect::<String>();
143
144        assert_eq!(
145            PackageIdentifier::from_str(&identifier).err(),
146            Some(PackageIdentifierError::InvalidPartCount)
147        );
148    }
149
150    #[test]
151    fn package_identifier_parts_too_long() {
152        let part = "a".repeat(PackageIdentifier::MAX_PART_LENGTH as usize + 1);
153
154        let identifier =
155            itertools::intersperse(repeat_n(&*part, PackageIdentifier::MIN_PARTS), ".")
156                .collect::<String>();
157
158        assert_eq!(
159            PackageIdentifier::new(&identifier).err().unwrap(),
160            PackageIdentifierError::PartTooLong
161        )
162    }
163
164    #[test]
165    fn too_few_parts_package_identifier() {
166        let identifier = "a".repeat(PackageIdentifier::MIN_PARTS - 1);
167
168        assert_eq!(
169            PackageIdentifier::from_str(&identifier).err().unwrap(),
170            PackageIdentifierError::InvalidPartCount
171        )
172    }
173
174    #[test]
175    fn package_identifier_contains_whitespace() {
176        assert_eq!(
177            PackageIdentifier::from_str("Publisher.Pack age")
178                .err()
179                .unwrap(),
180            PackageIdentifierError::ContainsWhitespace
181        );
182    }
183
184    #[test]
185    fn package_identifier_contains_control_chars() {
186        assert_eq!(
187            PackageIdentifier::from_str("Publisher.Pack\0age")
188                .err()
189                .unwrap(),
190            PackageIdentifierError::ContainsControlChars
191        );
192    }
193
194    #[test]
195    fn package_identifier_disallowed_characters() {
196        for char in DISALLOWED_CHARACTERS {
197            let identifier = format!("Publisher.Pack{char}age");
198
199            assert_eq!(
200                PackageIdentifier::from_str(&identifier).err().unwrap(),
201                PackageIdentifierError::DisallowedCharacters
202            )
203        }
204    }
205}