Skip to main content

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}