winget_types/shared/
package_version.rs

1use core::{fmt, str::FromStr};
2
3use thiserror::Error;
4
5use super::{DISALLOWED_CHARACTERS, version::Version};
6
7#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "serde", serde(try_from = "Version"))]
10#[repr(transparent)]
11pub struct PackageVersion(Version);
12
13#[derive(Error, Debug, Eq, PartialEq)]
14pub enum PackageVersionError {
15    #[error("Package version contains invalid character {_0:?}")]
16    InvalidCharacter(char),
17    #[error("Package version cannot be empty")]
18    Empty,
19    #[error(
20        "Package version cannot be more than {} characters long",
21        PackageVersion::MAX_CHAR_LENGTH
22    )]
23    TooLong,
24}
25
26impl PackageVersion {
27    const MAX_CHAR_LENGTH: usize = 128;
28
29    /// Creates a new `PackageVersion` from any type that implements `AsRef<str>`.
30    ///
31    /// # Errors
32    ///
33    /// Returns an `Err` if the `PackageVersion` is empty, more than 128 characters long, or
34    /// contains a disallowed character (control or one of [`DISALLOWED_CHARACTERS`]).
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use winget_types::PackageVersion;
40    /// # use winget_types::PackageVersionError;
41    ///
42    /// # fn main() -> Result<(), PackageVersionError>  {
43    /// let version = PackageVersion::new("1.2.3")?;
44    /// let other_version = PackageVersion::new("1.2.4.0")?;
45    ///
46    /// assert!(version < other_version);
47    /// # Ok(())
48    /// # }
49    /// ```
50    pub fn new<T: AsRef<str>>(version: T) -> Result<Self, PackageVersionError> {
51        let version = version.as_ref();
52
53        if version.is_empty() {
54            return Err(PackageVersionError::Empty);
55        }
56
57        let char_count = version.chars().try_fold(0, |char_count, char| {
58            if DISALLOWED_CHARACTERS.contains(&char) || char.is_control() {
59                return Err(PackageVersionError::InvalidCharacter(char));
60            }
61
62            Ok(char_count + 1)
63        })?;
64
65        if char_count > Self::MAX_CHAR_LENGTH {
66            return Err(PackageVersionError::TooLong);
67        }
68
69        Ok(Self(Version::new(version)))
70    }
71
72    /// Creates a new `PackageVersion` from any type that implements `AsRef<str>`, without checking
73    /// its validity.
74    ///
75    /// # Safety
76    ///
77    /// The package version must not be more than 128 characters long, or contain a disallowed
78    /// character (control, or one of [`DISALLOWED_CHARACTERS`]).
79    #[inline]
80    pub unsafe fn new_unchecked<T: AsRef<str>>(version: T) -> Self {
81        Self(Version::new(version))
82    }
83
84    /// Returns true if the version matches `latest` (case-insensitive).
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use winget_types::PackageVersion;
90    /// # use winget_types::PackageVersionError;
91    ///
92    /// # fn main() -> Result<(), PackageVersionError> {
93    /// assert!(PackageVersion::new("latest")?.is_latest());
94    /// assert!(PackageVersion::new("LATEST")?.is_latest());
95    /// assert!(!PackageVersion::new("1.2.3")?.is_latest());
96    /// # Ok(())
97    /// # }
98    /// ```
99    #[must_use]
100    #[inline]
101    pub fn is_latest(&self) -> bool {
102        self.0.is_latest()
103    }
104
105    /// Extracts a string slice containing the entire `PackageVersion`.
106    #[must_use]
107    #[inline]
108    pub fn as_str(&self) -> &str {
109        self.0.as_str()
110    }
111
112    /// Extracts the inner `Version`.
113    #[must_use]
114    #[inline]
115    pub const fn inner(&self) -> &Version {
116        &self.0
117    }
118
119    /// Finds the closest version to this version from a given list of package versions.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use winget_types::PackageVersion;
125    /// # use winget_types::PackageVersionError;
126    ///
127    /// # fn main() -> Result<(), PackageVersionError> {
128    /// let versions = [PackageVersion::new("1.2.5")?, PackageVersion::new("1.2.0")?];
129    ///
130    /// let version = PackageVersion::new("1.2.3")?;
131    ///
132    /// assert_eq!(version.closest(&versions).map(PackageVersion::as_str), Some("1.2.5"));
133    /// # Ok(())
134    /// # }
135    /// ```
136    #[inline]
137    pub fn closest<'iter, I>(&self, versions: I) -> Option<&'iter Self>
138    where
139        I: IntoIterator<Item = &'iter Self>,
140    {
141        self.0.closest(versions)
142    }
143}
144
145impl fmt::Display for PackageVersion {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        self.0.fmt(f)
148    }
149}
150
151impl FromStr for PackageVersion {
152    type Err = PackageVersionError;
153
154    fn from_str(s: &str) -> Result<Self, Self::Err> {
155        Self::new(s)
156    }
157}
158
159impl<'ver> From<&'ver PackageVersion> for &'ver Version {
160    #[inline]
161    fn from(value: &'ver PackageVersion) -> Self {
162        &value.0
163    }
164}
165
166impl TryFrom<Version> for PackageVersion {
167    type Error = PackageVersionError;
168
169    #[inline]
170    fn try_from(value: Version) -> Result<Self, Self::Error> {
171        Self::new(value)
172    }
173}
174
175impl PartialEq<Version> for PackageVersion {
176    fn eq(&self, other: &Version) -> bool {
177        self.0.eq(other)
178    }
179}
180
181impl PartialEq<PackageVersion> for Version {
182    fn eq(&self, other: &PackageVersion) -> bool {
183        self.eq(&other.0)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use alloc::format;
190
191    use super::{DISALLOWED_CHARACTERS, PackageVersion, PackageVersionError};
192
193    #[test]
194    fn empty_package_version() {
195        assert_eq!(PackageVersion::new(""), Err(PackageVersionError::Empty));
196    }
197
198    #[test]
199    fn disallowed_characters_in_package_version() {
200        for char in DISALLOWED_CHARACTERS {
201            assert_eq!(
202                format!("1.2{char}3").parse::<PackageVersion>(),
203                Err(PackageVersionError::InvalidCharacter(char))
204            )
205        }
206    }
207
208    #[test]
209    fn control_characters_in_package_version() {
210        assert_eq!(
211            "1.2\03".parse::<PackageVersion>(),
212            Err(PackageVersionError::InvalidCharacter('\0'))
213        );
214    }
215
216    #[test]
217    fn unicode_package_version_max_length() {
218        let version = "🦀".repeat(PackageVersion::MAX_CHAR_LENGTH);
219
220        // Ensure that it's character length that's being checked and not byte or UTF-16 length
221        assert!(version.len() > PackageVersion::MAX_CHAR_LENGTH);
222        assert!(version.encode_utf16().count() > PackageVersion::MAX_CHAR_LENGTH);
223        assert_eq!(version.chars().count(), PackageVersion::MAX_CHAR_LENGTH);
224        assert!(PackageVersion::new(version).is_ok());
225    }
226
227    #[test]
228    fn package_version_too_long() {
229        let version = "🦀".repeat(PackageVersion::MAX_CHAR_LENGTH + 1);
230
231        assert_eq!(
232            version.parse::<PackageVersion>(),
233            Err(PackageVersionError::TooLong)
234        );
235    }
236}