winbrew_models\install/
installer.rs

1use core::str::FromStr;
2use serde::{Deserialize, Serialize};
3
4use super::engine::EngineKind;
5use crate::shared::DeploymentKind;
6use crate::shared::ModelError;
7use crate::shared::validation::{Validate, ensure_hash, ensure_http_url};
8
9/// The target architecture of an installer payload.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum Architecture {
13    /// x64 / amd64 payload.
14    X64,
15    /// x86 payload.
16    X86,
17    /// ARM64 payload.
18    Arm64,
19    /// Architecture-neutral payload.
20    Any,
21}
22
23/// The installer format represented by a catalog record.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum InstallerType {
27    /// Windows Installer package.
28    Msi,
29    /// Windows App Installer / MSIX package.
30    Msix,
31    /// Windows AppX package.
32    Appx,
33    /// Native executable installer.
34    Exe,
35    /// Inno Setup installer.
36    Inno,
37    /// Nullsoft installer.
38    Nullsoft,
39    /// WiX installer.
40    Wix,
41    /// Burn bootstrapper.
42    Burn,
43    /// Progressive Web App installer.
44    Pwa,
45    /// Font installer.
46    Font,
47    /// Portable archive or copy-based package.
48    Portable,
49    /// Zip archive installer.
50    Zip,
51}
52
53/// A resolved installer candidate for a package.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Installer {
56    /// Download URL for the installer.
57    pub url: String,
58    /// Checksum used for verification.
59    pub hash: String,
60    /// Target architecture.
61    pub architecture: Architecture,
62    /// Installer format.
63    pub kind: InstallerType,
64}
65
66impl Installer {
67    /// Validate the URL and checksum contract for the installer.
68    pub fn validate(&self) -> Result<(), ModelError> {
69        ensure_http_url("installer.url", &self.url)?;
70        ensure_hash("installer.hash", &self.hash)
71    }
72}
73
74impl Validate for Installer {
75    fn validate(&self) -> Result<(), ModelError> {
76        Installer::validate(self)
77    }
78}
79
80impl Architecture {
81    /// Return the canonical display string for the architecture.
82    pub fn as_str(self) -> &'static str {
83        match self {
84            Self::X64 => "x64",
85            Self::X86 => "x86",
86            Self::Arm64 => "arm64",
87            Self::Any => "",
88        }
89    }
90
91    /// Return the current host architecture when it can be classified.
92    pub fn current() -> Self {
93        match std::env::consts::ARCH {
94            "x86_64" => Self::X64,
95            "x86" => Self::X86,
96            "aarch64" => Self::Arm64,
97            _ => Self::Any,
98        }
99    }
100}
101
102impl FromStr for Architecture {
103    type Err = ModelError;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        match s.trim().to_ascii_lowercase().as_str() {
107            "x64" => Ok(Self::X64),
108            "x86" => Ok(Self::X86),
109            "arm64" => Ok(Self::Arm64),
110            "" => Ok(Self::Any),
111            other => Err(ModelError::invalid_enum_value("installer.arch", other)),
112        }
113    }
114}
115
116impl InstallerType {
117    /// Return the canonical display string for the installer format.
118    pub fn as_str(self) -> &'static str {
119        match self {
120            Self::Msi => "msi",
121            Self::Msix => "msix",
122            Self::Appx => "appx",
123            Self::Exe => "exe",
124            Self::Inno => "inno",
125            Self::Nullsoft => "nullsoft",
126            Self::Wix => "wix",
127            Self::Burn => "burn",
128            Self::Pwa => "pwa",
129            Self::Font => "font",
130            Self::Portable => "portable",
131            Self::Zip => "zip",
132        }
133    }
134
135    /// Return the semantic deployment outcome associated with this installer type.
136    pub fn deployment_kind(self) -> DeploymentKind {
137        self.into()
138    }
139
140    /// Return `true` when this installer comes from a Windows package family.
141    pub fn is_windows_package(self) -> bool {
142        matches!(self, Self::Msix | Self::Appx)
143    }
144
145    /// Return `true` when this installer belongs to an MSI-based family.
146    pub fn is_msi_family(self) -> bool {
147        matches!(self, Self::Msi | Self::Wix)
148    }
149
150    /// Return `true` when this installer belongs to a native executable family.
151    pub fn is_native_exe_family(self) -> bool {
152        matches!(self, Self::Exe | Self::Inno | Self::Nullsoft | Self::Burn)
153    }
154
155    /// Return `true` when this installer belongs to the Windows font family.
156    pub fn is_font_family(self) -> bool {
157        matches!(self, Self::Font)
158    }
159
160    /// Return `true` when this installer needs a dedicated special-case adapter.
161    pub fn is_special_case(self) -> bool {
162        matches!(self, Self::Pwa)
163    }
164
165    /// Return `true` when the payload is archive-shaped and should be unpacked.
166    pub fn is_archive(self) -> bool {
167        matches!(self, Self::Zip)
168    }
169}
170
171impl FromStr for InstallerType {
172    type Err = ModelError;
173
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        match s.trim().to_ascii_lowercase().as_str() {
176            "msi" => Ok(Self::Msi),
177            "msix" => Ok(Self::Msix),
178            "appx" => Ok(Self::Appx),
179            "exe" => Ok(Self::Exe),
180            "inno" => Ok(Self::Inno),
181            "nullsoft" => Ok(Self::Nullsoft),
182            "wix" => Ok(Self::Wix),
183            "burn" => Ok(Self::Burn),
184            "pwa" => Ok(Self::Pwa),
185            "font" => Ok(Self::Font),
186            "portable" => Ok(Self::Portable),
187            "zip" => Ok(Self::Zip),
188            other => Err(ModelError::invalid_enum_value("installer.kind", other)),
189        }
190    }
191}
192
193impl core::fmt::Display for Architecture {
194    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
195        f.write_str(self.as_str())
196    }
197}
198
199impl From<Architecture> for String {
200    fn from(value: Architecture) -> Self {
201        value.to_string()
202    }
203}
204
205impl core::fmt::Display for InstallerType {
206    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
207        f.write_str(self.as_str())
208    }
209}
210
211impl From<InstallerType> for String {
212    fn from(value: InstallerType) -> Self {
213        value.to_string()
214    }
215}
216
217impl From<EngineKind> for InstallerType {
218    fn from(value: EngineKind) -> Self {
219        match value {
220            EngineKind::Msix => Self::Msix,
221            EngineKind::Zip => Self::Zip,
222            EngineKind::Portable => Self::Portable,
223            EngineKind::Msi => Self::Msi,
224            EngineKind::NativeExe => Self::Exe,
225            EngineKind::Font => Self::Font,
226        }
227    }
228}
229
230impl From<InstallerType> for DeploymentKind {
231    fn from(value: InstallerType) -> Self {
232        match value {
233            InstallerType::Portable | InstallerType::Zip => Self::Portable,
234            _ => Self::Installed,
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use crate::shared::DeploymentKind;
242
243    use super::InstallerType;
244    use core::str::FromStr;
245
246    #[test]
247    fn installer_type_parses_official_winget_values() {
248        assert_eq!(
249            InstallerType::from_str("appx").expect("appx"),
250            InstallerType::Appx
251        );
252        assert_eq!(
253            InstallerType::from_str("inno").expect("inno"),
254            InstallerType::Inno
255        );
256        assert_eq!(
257            InstallerType::from_str("nullsoft").expect("nullsoft"),
258            InstallerType::Nullsoft
259        );
260        assert_eq!(
261            InstallerType::from_str("wix").expect("wix"),
262            InstallerType::Wix
263        );
264        assert_eq!(
265            InstallerType::from_str("burn").expect("burn"),
266            InstallerType::Burn
267        );
268        assert_eq!(
269            InstallerType::from_str("pwa").expect("pwa"),
270            InstallerType::Pwa
271        );
272        assert_eq!(
273            InstallerType::from_str("font").expect("font"),
274            InstallerType::Font
275        );
276    }
277
278    #[test]
279    fn installer_type_classifies_deployment_kind() {
280        assert_eq!(
281            InstallerType::Portable.deployment_kind(),
282            DeploymentKind::Portable
283        );
284        assert_eq!(
285            InstallerType::Zip.deployment_kind(),
286            DeploymentKind::Portable
287        );
288        assert_eq!(
289            InstallerType::Msi.deployment_kind(),
290            DeploymentKind::Installed
291        );
292        assert_eq!(
293            InstallerType::Exe.deployment_kind(),
294            DeploymentKind::Installed
295        );
296        assert_eq!(
297            InstallerType::Inno.deployment_kind(),
298            DeploymentKind::Installed
299        );
300        assert_eq!(
301            InstallerType::Nullsoft.deployment_kind(),
302            DeploymentKind::Installed
303        );
304        assert_eq!(
305            InstallerType::Burn.deployment_kind(),
306            DeploymentKind::Installed
307        );
308        assert!(InstallerType::Msix.is_windows_package());
309        assert!(InstallerType::Wix.is_msi_family());
310        assert!(InstallerType::Exe.is_native_exe_family());
311        assert!(InstallerType::Inno.is_native_exe_family());
312        assert!(InstallerType::Nullsoft.is_native_exe_family());
313        assert!(InstallerType::Burn.is_native_exe_family());
314        assert!(InstallerType::Font.is_font_family());
315        assert!(!InstallerType::Font.is_native_exe_family());
316        assert!(!InstallerType::Font.is_special_case());
317        assert!(InstallerType::Pwa.is_special_case());
318    }
319}