winbrew_models\catalog/
installer_type.rs

1use core::str::FromStr;
2
3use serde::{Deserialize, Serialize};
4
5use crate::install::installer::InstallerType;
6use crate::package::PackageSource;
7use crate::shared::ModelError;
8
9/// Normalized installer family stored on catalog installer rows.
10///
11/// This is intentionally broader than the source-facing `InstallerType` enum.
12/// It captures direct installer families as well as package-manager families
13/// such as Scoop and Chocolatey.
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum CatalogInstallerType {
17    Msi,
18    Msix,
19    Appx,
20    Msstore,
21    Exe,
22    Inno,
23    Nullsoft,
24    Wix,
25    Burn,
26    Pwa,
27    Font,
28    Portable,
29    Zip,
30    Nuget,
31    Scoop,
32    #[default]
33    Unknown,
34}
35
36impl CatalogInstallerType {
37    /// Return the canonical storage string for this installer family.
38    pub fn as_str(self) -> &'static str {
39        match self {
40            Self::Msi => "msi",
41            Self::Msix => "msix",
42            Self::Appx => "appx",
43            Self::Msstore => "msstore",
44            Self::Exe => "exe",
45            Self::Inno => "inno",
46            Self::Nullsoft => "nullsoft",
47            Self::Wix => "wix",
48            Self::Burn => "burn",
49            Self::Pwa => "pwa",
50            Self::Font => "font",
51            Self::Portable => "portable",
52            Self::Zip => "zip",
53            Self::Nuget => "nuget",
54            Self::Scoop => "scoop",
55            Self::Unknown => "unknown",
56        }
57    }
58
59    /// Return `true` when the value is the fallback bucket.
60    pub fn is_unknown(value: &Self) -> bool {
61        matches!(value, Self::Unknown)
62    }
63
64    /// Normalize the installer family from source metadata and raw installer type.
65    pub fn normalize(source: PackageSource, kind: InstallerType, url: &str) -> Self {
66        match source {
67            PackageSource::Chocolatey => Self::Nuget,
68            PackageSource::Scoop => match kind {
69                InstallerType::Portable if is_archive_url(url) => Self::Zip,
70                InstallerType::Portable => Self::Scoop,
71                InstallerType::Zip => Self::Zip,
72                InstallerType::Msix => Self::Msix,
73                InstallerType::Appx => Self::Appx,
74                InstallerType::Msi => Self::Msi,
75                InstallerType::Exe => Self::Exe,
76                InstallerType::Inno => Self::Inno,
77                InstallerType::Nullsoft => Self::Nullsoft,
78                InstallerType::Wix => Self::Wix,
79                InstallerType::Burn => Self::Burn,
80                InstallerType::Pwa => Self::Pwa,
81                InstallerType::Font => Self::Font,
82            },
83            PackageSource::Winget | PackageSource::Winbrew => match kind {
84                InstallerType::Portable if is_archive_url(url) => Self::Zip,
85                InstallerType::Portable => Self::Portable,
86                InstallerType::Zip => Self::Zip,
87                InstallerType::Msix => Self::Msix,
88                InstallerType::Appx => Self::Appx,
89                InstallerType::Msi => Self::Msi,
90                InstallerType::Exe => Self::Exe,
91                InstallerType::Inno => Self::Inno,
92                InstallerType::Nullsoft => Self::Nullsoft,
93                InstallerType::Wix => Self::Wix,
94                InstallerType::Burn => Self::Burn,
95                InstallerType::Pwa => Self::Pwa,
96                InstallerType::Font => Self::Font,
97            },
98        }
99    }
100}
101
102impl FromStr for CatalogInstallerType {
103    type Err = ModelError;
104
105    fn from_str(value: &str) -> Result<Self, Self::Err> {
106        match value.trim().to_ascii_lowercase().as_str() {
107            "msi" => Ok(Self::Msi),
108            "msix" => Ok(Self::Msix),
109            "appx" => Ok(Self::Appx),
110            "msstore" => Ok(Self::Msstore),
111            "exe" => Ok(Self::Exe),
112            "inno" => Ok(Self::Inno),
113            "nsis" | "nullsoft" => Ok(Self::Nullsoft),
114            "wix" => Ok(Self::Wix),
115            "burn" => Ok(Self::Burn),
116            "pwa" => Ok(Self::Pwa),
117            "font" => Ok(Self::Font),
118            "portable" => Ok(Self::Portable),
119            "zip" => Ok(Self::Zip),
120            "nuget" => Ok(Self::Nuget),
121            "scoop" => Ok(Self::Scoop),
122            "unknown" => Ok(Self::Unknown),
123            other => Err(ModelError::invalid_enum_value(
124                "catalog_installer.installer_type",
125                other,
126            )),
127        }
128    }
129}
130
131impl core::fmt::Display for CatalogInstallerType {
132    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
133        f.write_str(self.as_str())
134    }
135}
136
137impl From<CatalogInstallerType> for String {
138    fn from(value: CatalogInstallerType) -> Self {
139        value.to_string()
140    }
141}
142
143impl AsRef<str> for CatalogInstallerType {
144    fn as_ref(&self) -> &str {
145        self.as_str()
146    }
147}
148
149fn is_archive_url(url: &str) -> bool {
150    let normalized = url
151        .split(['?', '#'])
152        .next()
153        .unwrap_or(url)
154        .trim()
155        .to_ascii_lowercase();
156
157    normalized.ends_with(".zip")
158        || normalized.ends_with(".tar")
159        || normalized.ends_with(".tar.gz")
160        || normalized.ends_with(".tgz")
161        || normalized.ends_with(".tbz2")
162        || normalized.ends_with(".tar.bz2")
163        || normalized.ends_with(".gz")
164        || normalized.ends_with(".7z")
165        || normalized.ends_with(".rar")
166}
167
168#[cfg(test)]
169mod tests {
170    use super::CatalogInstallerType;
171    use crate::install::InstallerType;
172    use crate::package::PackageSource;
173
174    #[test]
175    fn parses_nullsoft_alias() {
176        assert_eq!(
177            "nsis".parse::<CatalogInstallerType>().expect("nsis"),
178            CatalogInstallerType::Nullsoft
179        );
180        assert_eq!(
181            "nullsoft"
182                .parse::<CatalogInstallerType>()
183                .expect("nullsoft"),
184            CatalogInstallerType::Nullsoft
185        );
186        assert_eq!(
187            "msstore".parse::<CatalogInstallerType>().expect("msstore"),
188            CatalogInstallerType::Msstore
189        );
190    }
191
192    #[test]
193    fn normalizes_raw_installer_families() {
194        assert_eq!(
195            CatalogInstallerType::normalize(
196                PackageSource::Winget,
197                InstallerType::Appx,
198                "https://example.test/app.appx"
199            ),
200            CatalogInstallerType::Appx
201        );
202        assert_eq!(
203            CatalogInstallerType::normalize(
204                PackageSource::Winget,
205                InstallerType::Portable,
206                "https://example.test/app.exe"
207            ),
208            CatalogInstallerType::Portable
209        );
210        assert_eq!(
211            CatalogInstallerType::normalize(
212                PackageSource::Winget,
213                InstallerType::Portable,
214                "https://example.test/app.zip"
215            ),
216            CatalogInstallerType::Zip
217        );
218        assert_eq!(
219            CatalogInstallerType::normalize(
220                PackageSource::Winget,
221                InstallerType::Portable,
222                "https://example.test/app.tar.bz2"
223            ),
224            CatalogInstallerType::Zip
225        );
226        assert_eq!(
227            CatalogInstallerType::normalize(
228                PackageSource::Winget,
229                InstallerType::Portable,
230                "https://example.test/app.gz"
231            ),
232            CatalogInstallerType::Zip
233        );
234        assert_eq!(
235            CatalogInstallerType::normalize(
236                PackageSource::Winget,
237                InstallerType::Nullsoft,
238                "https://example.test/app.exe"
239            ),
240            CatalogInstallerType::Nullsoft
241        );
242    }
243}