winget_types/shared/
package_version.rs

1use std::str::FromStr;
2
3use derive_more::{Deref, Display, Into};
4use serde_with::{DeserializeFromStr, SerializeDisplay};
5use thiserror::Error;
6
7use crate::shared::{DISALLOWED_CHARACTERS, version::Version};
8
9#[derive(
10    Clone,
11    Debug,
12    Default,
13    Deref,
14    Display,
15    Eq,
16    PartialEq,
17    Ord,
18    PartialOrd,
19    Hash,
20    Into,
21    SerializeDisplay,
22    DeserializeFromStr,
23)]
24#[into(ref)]
25pub struct PackageVersion(Version);
26
27#[derive(Error, Debug, Eq, PartialEq)]
28pub enum PackageVersionError {
29    #[error("Package version may not contain any control characters")]
30    ContainsControlChars,
31    #[error(
32        "Package version may not contain any of the following characters: {:?}",
33        DISALLOWED_CHARACTERS
34    )]
35    DisallowedCharacters,
36    #[error(
37        "Package version cannot be more than {} characters long",
38        PackageVersion::MAX_LENGTH
39    )]
40    TooLong,
41}
42
43impl PackageVersion {
44    const MAX_LENGTH: usize = 1 << 7;
45
46    pub fn new<S: AsRef<str>>(input: S) -> Result<Self, PackageVersionError> {
47        let input = input.as_ref();
48
49        if input.contains(DISALLOWED_CHARACTERS) {
50            Err(PackageVersionError::DisallowedCharacters)
51        } else if input.contains(char::is_control) {
52            Err(PackageVersionError::ContainsControlChars)
53        } else if input.chars().count() > Self::MAX_LENGTH {
54            Err(PackageVersionError::TooLong)
55        } else {
56            Ok(Self(Version::new(input)))
57        }
58    }
59}
60
61impl FromStr for PackageVersion {
62    type Err = PackageVersionError;
63
64    fn from_str(s: &str) -> Result<Self, Self::Err> {
65        Self::new(s)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use crate::shared::{
72        DISALLOWED_CHARACTERS,
73        package_version::{PackageVersion, PackageVersionError},
74    };
75
76    #[test]
77    fn package_version_disallowed_characters() {
78        for char in DISALLOWED_CHARACTERS {
79            assert_eq!(
80                PackageVersion::new(format!("1.2{char}3")).err().unwrap(),
81                PackageVersionError::DisallowedCharacters
82            )
83        }
84    }
85
86    #[test]
87    fn package_version_contains_control_chars() {
88        assert_eq!(
89            PackageVersion::new("1.2\03").err().unwrap(),
90            PackageVersionError::ContainsControlChars
91        );
92    }
93
94    #[test]
95    fn unicode_package_version_max_length() {
96        let version = "🦀".repeat(PackageVersion::MAX_LENGTH);
97
98        // Ensure that it's character length that's being checked and not byte or UTF-16 length
99        assert!(version.len() > PackageVersion::MAX_LENGTH);
100        assert!(version.encode_utf16().count() > PackageVersion::MAX_LENGTH);
101        assert_eq!(version.chars().count(), PackageVersion::MAX_LENGTH);
102        assert!(PackageVersion::new(version).is_ok());
103    }
104
105    #[test]
106    fn package_version_too_long() {
107        let version = "🦀".repeat(PackageVersion::MAX_LENGTH + 1);
108
109        assert_eq!(
110            PackageVersion::new(version).err().unwrap(),
111            PackageVersionError::TooLong
112        );
113    }
114}