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#[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 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 pub fn is_unknown(value: &Self) -> bool {
61 matches!(value, Self::Unknown)
62 }
63
64 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}