winbrew_app\operations\install/
types.rs1use anyhow::Error;
9use std::io;
10use thiserror::Error;
11
12use super::state::InstallStateError;
13use crate::catalog::{self, InstallerSelectionError, SelectionContext};
14use crate::core::cancel::CancellationError;
15use crate::core::hash::HashError;
16use crate::models::catalog::CatalogInstaller;
17use crate::models::domains::install::InstallFailureClass;
18use crate::models::domains::shared::HashAlgorithm;
19use crate::windows::host::HostProfile;
20
21pub(crate) fn select_installer(
27 installers: &[CatalogInstaller],
28 selection_context: SelectionContext,
29) -> std::result::Result<CatalogInstaller, InstallerSelectionError> {
30 catalog::select_installer(installers, selection_context)
31}
32
33#[derive(Debug, Error)]
40pub enum InstallError {
41 #[error("package '{name}' is already installed")]
42 AlreadyInstalled { name: String },
43
44 #[error("package '{name}' is already being installed")]
45 AlreadyInstalling { name: String },
46
47 #[error("package '{name}' is currently updating")]
48 CurrentlyUpdating { name: String },
49
50 #[error("installer checksum mismatch: expected {expected}, got {actual}")]
51 ChecksumMismatch { expected: String, actual: String },
52
53 #[error("{algorithm} checksums are disabled by default for security")]
54 LegacyChecksumAlgorithm { algorithm: HashAlgorithm },
55
56 #[error("catalog package has no installers")]
57 NoInstallers,
58
59 #[error("no installer matches this host ({host})")]
60 NoCompatibleInstaller { host: HostProfile },
61
62 #[error("no installer matches this host's install scope ({host})")]
63 NoScopeCompatibleInstaller { host: HostProfile },
64
65 #[error("command '{command}' is already exposed by package '{package}'")]
66 CommandAlreadyExposed { command: String, package: String },
67
68 #[error(
69 "command '{command}' was claimed by another install while this install was in progress"
70 )]
71 CommandClaimedWhileInProgress { command: String },
72
73 #[error("runtime bootstrap for {runtime} was declined")]
74 RuntimeBootstrapDeclined { runtime: String },
75
76 #[error("cancelled")]
77 Cancelled,
78
79 #[error(transparent)]
80 Unexpected(Error),
81}
82
83pub type Result<T> = std::result::Result<T, InstallError>;
85
86impl InstallError {
87 pub fn failure_class(&self) -> InstallFailureClass {
89 match self {
90 Self::AlreadyInstalled { .. }
91 | Self::AlreadyInstalling { .. }
92 | Self::CurrentlyUpdating { .. } => InstallFailureClass::Preflight,
93 Self::ChecksumMismatch { .. } | Self::LegacyChecksumAlgorithm { .. } => {
94 InstallFailureClass::Verification
95 }
96 Self::NoInstallers
97 | Self::NoCompatibleInstaller { .. }
98 | Self::NoScopeCompatibleInstaller { .. }
99 | Self::CommandAlreadyExposed { .. }
100 | Self::RuntimeBootstrapDeclined { .. } => InstallFailureClass::Preflight,
101 Self::CommandClaimedWhileInProgress { .. } => InstallFailureClass::StateTransition,
102 Self::Cancelled => InstallFailureClass::Cancelled,
103 Self::Unexpected(_) => InstallFailureClass::Runtime,
104 }
105 }
106}
107
108impl From<InstallStateError> for InstallError {
109 fn from(value: InstallStateError) -> Self {
110 match value {
111 InstallStateError::AlreadyInstalled { name } => Self::AlreadyInstalled { name },
112 InstallStateError::AlreadyInstalling { name } => Self::AlreadyInstalling { name },
113 InstallStateError::CurrentlyUpdating { name } => Self::CurrentlyUpdating { name },
114 InstallStateError::CommandAlreadyExposed { command, package } => {
115 Self::CommandAlreadyExposed { command, package }
116 }
117 other => Self::Unexpected(Error::new(other)),
118 }
119 }
120}
121
122impl From<HashError> for InstallError {
123 fn from(value: HashError) -> Self {
124 match value {
125 HashError::ChecksumMismatch { expected, actual } => {
126 Self::ChecksumMismatch { expected, actual }
127 }
128 HashError::LegacyChecksumAlgorithm { algorithm } => {
129 Self::LegacyChecksumAlgorithm { algorithm }
130 }
131 }
132 }
133}
134
135impl From<CancellationError> for InstallError {
136 fn from(_: CancellationError) -> Self {
137 Self::Cancelled
138 }
139}
140
141impl From<InstallerSelectionError> for InstallError {
142 fn from(value: InstallerSelectionError) -> Self {
143 match value {
144 InstallerSelectionError::NoInstallers => Self::NoInstallers,
145 InstallerSelectionError::PlatformMismatch { host } => {
146 Self::NoCompatibleInstaller { host }
147 }
148 InstallerSelectionError::ScopeMismatch { host } => {
149 Self::NoScopeCompatibleInstaller { host }
150 }
151 }
152 }
153}
154
155impl From<io::Error> for InstallError {
156 fn from(value: io::Error) -> Self {
157 Self::Unexpected(Error::new(value))
158 }
159}
160
161impl From<Error> for InstallError {
162 fn from(value: Error) -> Self {
163 if let Some(hash_error) = value.downcast_ref::<HashError>() {
164 return Self::from(hash_error.clone());
165 }
166
167 if let Some(selection_error) = value.downcast_ref::<InstallerSelectionError>() {
168 return Self::from(*selection_error);
169 }
170
171 if value.downcast_ref::<CancellationError>().is_some() {
172 return Self::Cancelled;
173 }
174
175 Self::Unexpected(value)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::{InstallError, InstallStateError};
182 use crate::catalog::InstallerSelectionError;
183 use crate::core::cancel::CancellationError;
184 use crate::core::hash::HashError;
185 use crate::models::domains::install::Architecture;
186 use crate::models::domains::install::InstallFailureClass;
187 use crate::models::domains::shared::HashAlgorithm;
188 use crate::windows::host::HostProfile;
189
190 #[test]
191 fn maps_state_conflicts_to_user_facing_errors() {
192 let err = InstallError::from(InstallStateError::AlreadyInstalled {
193 name: "Contoso.App".to_string(),
194 });
195
196 assert!(matches!(err, InstallError::AlreadyInstalled { .. }));
197 }
198
199 #[test]
200 fn maps_hash_errors_to_user_facing_errors() {
201 let err = InstallError::from(HashError::LegacyChecksumAlgorithm {
202 algorithm: HashAlgorithm::Sha1,
203 });
204
205 assert!(matches!(err, InstallError::LegacyChecksumAlgorithm { .. }));
206 }
207
208 #[test]
209 fn maps_cancellation_to_cancelled() {
210 let err = InstallError::from(CancellationError);
211
212 assert!(matches!(err, InstallError::Cancelled));
213 }
214
215 #[test]
216 fn maps_selection_failures_to_user_facing_errors() {
217 let err = InstallError::from(InstallerSelectionError::NoInstallers);
218 assert!(matches!(err, InstallError::NoInstallers));
219
220 let err = InstallError::from(InstallerSelectionError::PlatformMismatch {
221 host: HostProfile {
222 is_server: true,
223 architecture: Architecture::Arm64,
224 },
225 });
226 assert!(matches!(err, InstallError::NoCompatibleInstaller { .. }));
227
228 let err = InstallError::from(InstallerSelectionError::ScopeMismatch {
229 host: HostProfile {
230 is_server: false,
231 architecture: Architecture::X64,
232 },
233 });
234 assert!(matches!(
235 err,
236 InstallError::NoScopeCompatibleInstaller { .. }
237 ));
238 }
239
240 #[test]
241 fn maps_command_conflicts_to_user_facing_errors() {
242 assert_eq!(
243 InstallError::CommandAlreadyExposed {
244 command: "grep".to_string(),
245 package: "Contoso.Grep".to_string(),
246 }
247 .failure_class(),
248 InstallFailureClass::Preflight
249 );
250
251 assert_eq!(
252 InstallError::CommandClaimedWhileInProgress {
253 command: "grep".to_string(),
254 }
255 .failure_class(),
256 InstallFailureClass::StateTransition
257 );
258 }
259
260 #[test]
261 fn failure_class_groups_expected_variants() {
262 assert_eq!(
263 InstallError::from(InstallStateError::AlreadyInstalling {
264 name: "Contoso.App".to_string(),
265 })
266 .failure_class(),
267 InstallFailureClass::Preflight
268 );
269 assert_eq!(
270 InstallError::from(HashError::ChecksumMismatch {
271 expected: "a".to_string(),
272 actual: "b".to_string(),
273 })
274 .failure_class(),
275 InstallFailureClass::Verification
276 );
277 assert_eq!(
278 InstallError::Cancelled.failure_class(),
279 InstallFailureClass::Cancelled
280 );
281 assert_eq!(
282 InstallError::RuntimeBootstrapDeclined {
283 runtime: "7-Zip runtime".to_string(),
284 }
285 .failure_class(),
286 InstallFailureClass::Preflight
287 );
288 }
289}