winget_types/installer/
file_extension.rs

1use core::{fmt, str::FromStr};
2
3use compact_str::CompactString;
4use thiserror::Error;
5
6use crate::DISALLOWED_CHARACTERS;
7
8#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[cfg_attr(feature = "serde", serde(try_from = "CompactString"))]
11#[repr(transparent)]
12pub struct FileExtension(CompactString);
13
14#[derive(Debug, Error, Eq, PartialEq)]
15pub enum FileExtensionError {
16    #[error("File extension contains invalid character {_0:?}")]
17    InvalidCharacter(char),
18    #[error("File extension must not be empty")]
19    Empty,
20    #[error(
21        "File extension must not have more than {} characters but has {_0}",
22        FileExtension::MAX_CHAR_LENGTH
23    )]
24    TooLong(usize),
25}
26
27impl FileExtension {
28    pub const MAX_CHAR_LENGTH: usize = 64;
29
30    /// Creates a new `FileExtension` from any type that implements `AsRef<str>`.
31    ///
32    /// Leading dots (`.`) are trimmed.
33    ///
34    /// # Errors
35    ///
36    /// Returns an `Err` if the file extension is empty, more than 64 characters long, or contains a
37    /// disallowed character (control or one of [`DISALLOWED_CHARACTERS`]).
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use winget_types::installer::FileExtension;
43    /// # use winget_types::installer::FileExtensionError;
44    ///
45    /// # fn main() -> Result<(), FileExtensionError>  {
46    /// let extension = FileExtension::new("xml")?;
47    ///
48    /// assert_eq!(extension.as_str(), "xml");
49    /// # Ok(())
50    /// # }
51    /// ```
52    pub fn new<T: AsRef<str>>(file_extension: T) -> Result<Self, FileExtensionError> {
53        let extension = file_extension.as_ref().trim_start_matches('.');
54
55        if extension.is_empty() {
56            return Err(FileExtensionError::Empty);
57        }
58
59        let char_count = extension.chars().try_fold(0, |char_count, char| {
60            if DISALLOWED_CHARACTERS.contains(&char) || char.is_control() {
61                return Err(FileExtensionError::InvalidCharacter(char));
62            }
63
64            Ok(char_count + 1)
65        })?;
66
67        if char_count > Self::MAX_CHAR_LENGTH {
68            return Err(FileExtensionError::TooLong(char_count));
69        }
70
71        Ok(Self(extension.into()))
72    }
73
74    /// Creates a new `FileExtension` from any type that implements `Into<CompactString>` without
75    /// checking its validity.
76    ///
77    /// # Safety
78    ///
79    /// The file extension must not be empty, be more than 64 characters long, or contain a
80    /// disallowed character (control or one of [`DISALLOWED_CHARACTERS`]).
81    #[must_use]
82    #[inline]
83    pub unsafe fn new_unchecked<T: Into<CompactString>>(file_extension: T) -> Self {
84        Self(file_extension.into())
85    }
86
87    /// Extracts a string slice containing the entire `FileExtension`.
88    #[must_use]
89    #[inline]
90    pub fn as_str(&self) -> &str {
91        self.0.as_str()
92    }
93}
94
95impl AsRef<str> for FileExtension {
96    #[inline]
97    fn as_ref(&self) -> &str {
98        self.as_str()
99    }
100}
101
102impl fmt::Display for FileExtension {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        self.0.fmt(f)
105    }
106}
107
108impl FromStr for FileExtension {
109    type Err = FileExtensionError;
110
111    #[inline]
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        Self::new(s)
114    }
115}
116
117impl TryFrom<CompactString> for FileExtension {
118    type Error = FileExtensionError;
119
120    #[inline]
121    fn try_from(value: CompactString) -> Result<Self, Self::Error> {
122        Self::new(value)
123    }
124}