1use core::str::FromStr;
9use serde::{Deserialize, Serialize};
10
11use super::installer::InstallerType;
12use crate::msi_inventory::MsiInventorySnapshot;
13use crate::shared::ModelError;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum EngineKind {
19 Msix,
21 Zip,
23 Portable,
25 Msi,
27 NativeExe,
29 Font,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum InstallScope {
37 Installed,
39 Provisioned,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(tag = "kind", rename_all = "lowercase")]
46pub enum EngineMetadata {
47 Msix {
49 package_full_name: String,
51 scope: InstallScope,
53 },
54 Msi {
56 product_code: String,
58 upgrade_code: Option<String>,
60 scope: InstallScope,
62 registry_keys: Vec<String>,
64 shortcuts: Vec<String>,
66 },
67 NativeExe {
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 quiet_uninstall_command: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 uninstall_command: Option<String>,
75 },
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct EngineInstallReceipt {
86 pub engine_kind: EngineKind,
88 pub install_dir: String,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub msi_inventory_snapshot: Option<MsiInventorySnapshot>,
93 pub engine_metadata: Option<EngineMetadata>,
95}
96
97impl EngineInstallReceipt {
98 pub fn new(
100 engine_kind: EngineKind,
101 install_dir: impl Into<String>,
102 engine_metadata: Option<EngineMetadata>,
103 ) -> Self {
104 Self {
105 engine_kind,
106 install_dir: install_dir.into(),
107 msi_inventory_snapshot: None,
108 engine_metadata,
109 }
110 }
111
112 pub fn install_dir(&self) -> &str {
114 &self.install_dir
115 }
116}
117
118impl EngineKind {
119 pub fn as_str(self) -> &'static str {
120 match self {
121 Self::Msix => "msix",
122 Self::Zip => "zip",
123 Self::Portable => "portable",
124 Self::Msi => "msi",
125 Self::NativeExe => "nativeexe",
126 Self::Font => "font",
127 }
128 }
129
130 pub fn from_installer_type(kind: InstallerType) -> Self {
131 match kind {
132 InstallerType::Msix | InstallerType::Appx => Self::Msix,
133 InstallerType::Msi | InstallerType::Wix => Self::Msi,
134 InstallerType::Zip => Self::Zip,
135 InstallerType::Portable => Self::Portable,
136 InstallerType::Exe
137 | InstallerType::Inno
138 | InstallerType::Nullsoft
139 | InstallerType::Burn
140 | InstallerType::Pwa => Self::NativeExe,
141 InstallerType::Font => Self::Font,
142 }
143 }
144}
145
146impl InstallScope {
147 pub fn as_str(self) -> &'static str {
148 match self {
149 Self::Installed => "installed",
150 Self::Provisioned => "provisioned",
151 }
152 }
153}
154
155impl EngineMetadata {
156 pub fn msix(package_full_name: impl Into<String>, scope: InstallScope) -> Self {
157 Self::Msix {
158 package_full_name: package_full_name.into(),
159 scope,
160 }
161 }
162
163 pub fn msix_package_full_name(&self) -> Option<&str> {
164 match self {
165 Self::Msix {
166 package_full_name, ..
167 } => Some(package_full_name.as_str()),
168 Self::Msi { .. } | Self::NativeExe { .. } => None,
169 }
170 }
171
172 pub fn native_exe(
174 quiet_uninstall_command: Option<String>,
175 uninstall_command: Option<String>,
176 ) -> Self {
177 Self::NativeExe {
178 quiet_uninstall_command,
179 uninstall_command,
180 }
181 }
182
183 pub fn native_exe_quiet_uninstall_command(&self) -> Option<&str> {
185 match self {
186 Self::NativeExe {
187 quiet_uninstall_command: Some(command),
188 ..
189 } => Some(command.as_str()),
190 _ => None,
191 }
192 }
193
194 pub fn native_exe_uninstall_command(&self) -> Option<&str> {
196 match self {
197 Self::NativeExe {
198 quiet_uninstall_command: Some(command),
199 ..
200 } => Some(command.as_str()),
201 Self::NativeExe {
202 uninstall_command: Some(command),
203 ..
204 } => Some(command.as_str()),
205 _ => None,
206 }
207 }
208}
209
210impl From<InstallerType> for EngineKind {
211 fn from(value: InstallerType) -> Self {
212 Self::from_installer_type(value)
213 }
214}
215
216impl FromStr for EngineKind {
217 type Err = ModelError;
218
219 fn from_str(s: &str) -> Result<Self, Self::Err> {
220 match s.trim().to_ascii_lowercase().as_str() {
221 "msix" => Ok(Self::Msix),
222 "zip" => Ok(Self::Zip),
223 "portable" => Ok(Self::Portable),
224 "msi" => Ok(Self::Msi),
225 "nativeexe" => Ok(Self::NativeExe),
226 "font" => Ok(Self::Font),
227 other => Err(ModelError::invalid_enum_value("engine.kind", other)),
228 }
229 }
230}
231
232impl FromStr for InstallScope {
233 type Err = ModelError;
234
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 match s.trim().to_ascii_lowercase().as_str() {
237 "installed" => Ok(Self::Installed),
238 "provisioned" => Ok(Self::Provisioned),
239 other => Err(ModelError::invalid_enum_value("engine.scope", other)),
240 }
241 }
242}
243
244impl core::fmt::Display for EngineKind {
245 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
246 f.write_str(self.as_str())
247 }
248}
249
250impl From<EngineKind> for String {
251 fn from(value: EngineKind) -> Self {
252 value.to_string()
253 }
254}
255
256impl core::fmt::Display for InstallScope {
257 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
258 f.write_str(self.as_str())
259 }
260}
261
262impl From<InstallScope> for String {
263 fn from(value: InstallScope) -> Self {
264 value.to_string()
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::{EngineKind, EngineMetadata};
271 use crate::install::installer::InstallerType;
272 use core::str::FromStr;
273
274 #[test]
275 fn engine_kind_rejects_exe_alias() {
276 let err = EngineKind::from_str("exe").expect_err("exe should not parse as an engine kind");
277
278 assert!(err.to_string().contains("invalid engine.kind: exe"));
279 }
280
281 #[test]
282 fn engine_kind_parses_font() {
283 assert_eq!(
284 EngineKind::from_str("font").expect("font"),
285 EngineKind::Font
286 );
287 assert_eq!(EngineKind::Font.to_string(), "font");
288 }
289
290 #[test]
291 fn engine_kind_round_trips_font_to_installer_type() {
292 assert_eq!(InstallerType::from(EngineKind::Font), InstallerType::Font);
293 }
294
295 #[test]
296 fn native_exe_metadata_prefers_quiet_uninstall_command() {
297 let metadata = EngineMetadata::native_exe(
298 Some("C:\\Apps\\Demo\\uninstall.exe /S".to_string()),
299 Some("C:\\Apps\\Demo\\uninstall.exe".to_string()),
300 );
301
302 assert_eq!(
303 metadata.native_exe_quiet_uninstall_command(),
304 Some("C:\\Apps\\Demo\\uninstall.exe /S")
305 );
306 assert_eq!(
307 metadata.native_exe_uninstall_command(),
308 Some("C:\\Apps\\Demo\\uninstall.exe /S")
309 );
310 }
311
312 #[test]
313 fn native_exe_metadata_falls_back_to_uninstall_command() {
314 let metadata =
315 EngineMetadata::native_exe(None, Some("C:\\Apps\\Demo\\uninstall.exe".to_string()));
316
317 assert_eq!(metadata.native_exe_quiet_uninstall_command(), None);
318 assert_eq!(
319 metadata.native_exe_uninstall_command(),
320 Some("C:\\Apps\\Demo\\uninstall.exe")
321 );
322 assert_eq!(metadata.msix_package_full_name(), None);
323 }
324}