winget_types/shared/
package_identifier.rs1use 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
57impl 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}