gpui_updater_core/error.rs
1//! Error types for the updater.
2
3use std::io;
4
5/// Errors produced while checking for, downloading, verifying, or installing an
6/// update.
7///
8/// Use [`Error::kind`] to decide what to tell the user; the variants carry the
9/// details.
10#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum Error {
13 /// A network request failed or returned a non-success status.
14 #[error("http error: {0}")]
15 Http(String),
16
17 /// A URL would be fetched without TLS: it, or a redirect it led to, is not
18 /// `https://`. Plain HTTP is accepted only for loopback hosts
19 /// (`localhost`, `127.0.0.0/8`, `::1`).
20 #[error("refusing to fetch {0} without HTTPS (plain HTTP is allowed for loopback hosts only)")]
21 InsecureUrl(String),
22
23 /// Release metadata could not be parsed (bad JSON, missing fields, or an
24 /// unparseable version tag).
25 #[error("failed to parse release metadata: {0}")]
26 Parse(String),
27
28 /// No release asset matched the running platform.
29 #[error("no release asset matched the current platform ({target_os}/{target_arch})")]
30 NoMatchingAsset {
31 /// `std::env::consts::OS` of the running build.
32 target_os: &'static str,
33 /// `std::env::consts::ARCH` of the running build.
34 target_arch: &'static str,
35 },
36
37 /// The downloaded file's SHA-256 did not match the expected checksum.
38 #[error("checksum mismatch: expected {expected}, got {actual}")]
39 ChecksumMismatch {
40 /// The checksum published alongside the release.
41 expected: String,
42 /// The checksum computed from the downloaded bytes.
43 actual: String,
44 },
45
46 /// Minisign signature verification failed.
47 #[error("signature verification failed: {0}")]
48 Signature(String),
49
50 /// The configured [`Verification`](crate::Verification) policy required a
51 /// check that could not run because its input was missing — no public key,
52 /// no published signature, or no published checksum.
53 #[error("verification policy not satisfied: {0}")]
54 VerificationRequired(String),
55
56 /// The downloaded app is not one the policy accepts: its bundle version
57 /// differs from the release version, or it is not code-signed by the
58 /// expected team.
59 #[error("update payload rejected: {0}")]
60 Payload(String),
61 /// The signed release manifest is past its `expires` time, so it may be
62 /// a stale copy replayed to hide newer releases.
63 #[error("release manifest expired at {expires}")]
64 ManifestExpired {
65 /// The manifest's `expires` value.
66 expires: String,
67 },
68 /// Installing the downloaded update failed.
69 #[error("install failed: {0}")]
70 Install(String),
71
72 /// The app runs from a location it cannot update in place: a read-only
73 /// volume such as a mounted disk image, or the randomized read-only path
74 /// macOS App Translocation chose. Moving the app (for example into
75 /// `/Applications`) fixes it.
76 #[error("cannot update the app where it is: {0}")]
77 Location(String),
78
79 /// The current user may not write where the update must go, such as an
80 /// install directory owned by another user or by the system.
81 #[error("permission denied: {0}")]
82 Permission(String),
83
84 /// The operation was cancelled through a
85 /// [`CancelToken`](crate::CancelToken).
86 #[error("cancelled")]
87 Cancelled,
88
89 /// A binary delta could not be created or applied: it is malformed, or the
90 /// installed files are not the version it expects. The engine treats this
91 /// as recoverable and falls back to the full download.
92 #[error("delta update failed: {0}")]
93 Delta(String),
94
95 /// The current platform has no install strategy yet.
96 #[error("update install is not supported on this platform ({0})")]
97 UnsupportedPlatform(&'static str),
98
99 /// An underlying I/O error.
100 #[error(transparent)]
101 Io(#[from] io::Error),
102}
103
104impl Error {
105 /// What kind of failure this is, for choosing what to tell the user.
106 pub fn kind(&self) -> ErrorKind {
107 match self {
108 Self::Http(_) => ErrorKind::Network,
109 Self::Parse(_) | Self::NoMatchingAsset { .. } => ErrorKind::Metadata,
110 Self::InsecureUrl(_)
111 | Self::ChecksumMismatch { .. }
112 | Self::Signature(_)
113 | Self::VerificationRequired(_)
114 | Self::Payload(_)
115 | Self::ManifestExpired { .. } => ErrorKind::Verification,
116 Self::Install(_) => ErrorKind::Install,
117 Self::Location(_) => ErrorKind::Location,
118 Self::Permission(_) => ErrorKind::Permission,
119 Self::Cancelled => ErrorKind::Cancelled,
120 Self::UnsupportedPlatform(_) => ErrorKind::Unsupported,
121 Self::Delta(_) => ErrorKind::Other,
122 Self::Io(e) => match e.kind() {
123 io::ErrorKind::PermissionDenied => ErrorKind::Permission,
124 io::ErrorKind::ReadOnlyFilesystem => ErrorKind::Location,
125 _ => ErrorKind::Other,
126 },
127 }
128 }
129
130 /// An install-step I/O failure, classified by its cause: no permission
131 /// becomes [`Error::Permission`], a read-only file system
132 /// [`Error::Location`], and anything else [`Error::Install`].
133 #[cfg_attr(
134 all(
135 not(test),
136 not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
137 ),
138 expect(dead_code, reason = "only platforms with an install strategy call it")
139 )]
140 pub(crate) fn install_io(context: &str, e: &io::Error) -> Self {
141 let message = format!("{context}: {e}");
142 match e.kind() {
143 io::ErrorKind::PermissionDenied => Self::Permission(message),
144 io::ErrorKind::ReadOnlyFilesystem => Self::Location(message),
145 _ => Self::Install(message),
146 }
147 }
148}
149
150/// The broad class of an [`Error`], stable enough to drive user interface
151/// decisions: which message to show, and whether retrying can help.
152///
153/// [`Error::kind`] and [`UpdateError::kind`](crate::UpdateError::kind)
154/// return the same classification. A typical user interface reacts like
155/// this:
156///
157/// | Kind | Typical user interface |
158/// | --- | --- |
159/// | [`Network`](Self::Network) | "Try again later" |
160/// | [`Metadata`](Self::Metadata) | "Contact the developer" |
161/// | [`Verification`](Self::Verification) | A security warning; do not offer a retry |
162/// | [`Location`](Self::Location) | "Move the app to Applications" |
163/// | [`Permission`](Self::Permission) | "Ask an administrator", or a manual download |
164/// | [`Install`](Self::Install) | A manual download link |
165/// | [`Unsupported`](Self::Unsupported) | A manual download link |
166/// | [`Cancelled`](Self::Cancelled) | Nothing |
167/// | [`Other`](Self::Other) | Show the message |
168///
169/// `Location` and `Permission` come before anything is downloaded; see
170/// [`check_location`](crate::install::check_location).
171///
172/// Marked `#[non_exhaustive]`: match it with a wildcard arm.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174#[non_exhaustive]
175pub enum ErrorKind {
176 /// The network failed: DNS, connect, timeout, or a non-success HTTP
177 /// status. Retrying later may help.
178 Network,
179 /// The release metadata is malformed or lists no artifact for this
180 /// platform. The publisher has to fix it.
181 Metadata,
182 /// The update failed an integrity, authenticity, or transport-security
183 /// check, or the policy requires a check the release cannot satisfy. Do
184 /// not install it.
185 Verification,
186 /// The app runs from a location it cannot update in place, such as a disk
187 /// image or an App Translocation path. Moving the app fixes it.
188 Location,
189 /// The current user may not write where the update must go.
190 Permission,
191 /// Installing failed for another reason.
192 Install,
193 /// This platform or artifact type has no install strategy.
194 Unsupported,
195 /// The operation was cancelled on request.
196 Cancelled,
197 /// Anything else, such as a local I/O error.
198 Other,
199}
200
201/// Convenience alias for results returned by this crate.
202pub type Result<T> = std::result::Result<T, Error>;
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn io_errors_are_classified_by_cause() {
210 let denied = io::Error::from(io::ErrorKind::PermissionDenied);
211 let read_only = io::Error::from(io::ErrorKind::ReadOnlyFilesystem);
212 let other = io::Error::from(io::ErrorKind::NotFound);
213 assert_eq!(Error::Io(denied).kind(), ErrorKind::Permission);
214 assert_eq!(Error::Io(read_only).kind(), ErrorKind::Location);
215 assert_eq!(Error::Io(other).kind(), ErrorKind::Other);
216 }
217
218 #[test]
219 fn install_io_keeps_the_cause() {
220 let denied = io::Error::from(io::ErrorKind::PermissionDenied);
221 let error = Error::install_io("could not replace binary", &denied);
222 assert!(matches!(error, Error::Permission(_)), "{error:?}");
223 assert!(error.to_string().contains("could not replace binary"));
224 let busy = io::Error::other("busy");
225 assert_eq!(Error::install_io("x", &busy).kind(), ErrorKind::Install);
226 }
227
228 #[test]
229 fn verification_failures_share_a_kind() {
230 for error in [
231 Error::InsecureUrl("http://x".into()),
232 Error::Signature("bad".into()),
233 Error::VerificationRequired("none".into()),
234 Error::Payload("team".into()),
235 Error::ManifestExpired {
236 expires: "2020-01-01T00:00:00Z".into(),
237 },
238 ] {
239 assert_eq!(error.kind(), ErrorKind::Verification, "{error:?}");
240 }
241 assert_eq!(Error::Http("503".into()).kind(), ErrorKind::Network);
242 assert_eq!(Error::Cancelled.kind(), ErrorKind::Cancelled);
243 }
244}