use std::io;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("http error: {0}")]
Http(String),
#[error("refusing to fetch {0} without HTTPS (plain HTTP is allowed for loopback hosts only)")]
InsecureUrl(String),
#[error("failed to parse release metadata: {0}")]
Parse(String),
#[error("no release asset matched the current platform ({target_os}/{target_arch})")]
NoMatchingAsset {
target_os: &'static str,
target_arch: &'static str,
},
#[error("checksum mismatch: expected {expected}, got {actual}")]
ChecksumMismatch {
expected: String,
actual: String,
},
#[error("signature verification failed: {0}")]
Signature(String),
#[error("verification policy not satisfied: {0}")]
VerificationRequired(String),
#[error("update payload rejected: {0}")]
Payload(String),
#[error("release manifest expired at {expires}")]
ManifestExpired {
expires: String,
},
#[error("install failed: {0}")]
Install(String),
#[error("cannot update the app where it is: {0}")]
Location(String),
#[error("permission denied: {0}")]
Permission(String),
#[error("cancelled")]
Cancelled,
#[error("delta update failed: {0}")]
Delta(String),
#[error("update install is not supported on this platform ({0})")]
UnsupportedPlatform(&'static str),
#[error(transparent)]
Io(#[from] io::Error),
}
impl Error {
pub fn kind(&self) -> ErrorKind {
match self {
Self::Http(_) => ErrorKind::Network,
Self::Parse(_) | Self::NoMatchingAsset { .. } => ErrorKind::Metadata,
Self::InsecureUrl(_)
| Self::ChecksumMismatch { .. }
| Self::Signature(_)
| Self::VerificationRequired(_)
| Self::Payload(_)
| Self::ManifestExpired { .. } => ErrorKind::Verification,
Self::Install(_) => ErrorKind::Install,
Self::Location(_) => ErrorKind::Location,
Self::Permission(_) => ErrorKind::Permission,
Self::Cancelled => ErrorKind::Cancelled,
Self::UnsupportedPlatform(_) => ErrorKind::Unsupported,
Self::Delta(_) => ErrorKind::Other,
Self::Io(e) => match e.kind() {
io::ErrorKind::PermissionDenied => ErrorKind::Permission,
io::ErrorKind::ReadOnlyFilesystem => ErrorKind::Location,
_ => ErrorKind::Other,
},
}
}
#[cfg_attr(
all(
not(test),
not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
),
expect(dead_code, reason = "only platforms with an install strategy call it")
)]
pub(crate) fn install_io(context: &str, e: &io::Error) -> Self {
let message = format!("{context}: {e}");
match e.kind() {
io::ErrorKind::PermissionDenied => Self::Permission(message),
io::ErrorKind::ReadOnlyFilesystem => Self::Location(message),
_ => Self::Install(message),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
Network,
Metadata,
Verification,
Location,
Permission,
Install,
Unsupported,
Cancelled,
Other,
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn io_errors_are_classified_by_cause() {
let denied = io::Error::from(io::ErrorKind::PermissionDenied);
let read_only = io::Error::from(io::ErrorKind::ReadOnlyFilesystem);
let other = io::Error::from(io::ErrorKind::NotFound);
assert_eq!(Error::Io(denied).kind(), ErrorKind::Permission);
assert_eq!(Error::Io(read_only).kind(), ErrorKind::Location);
assert_eq!(Error::Io(other).kind(), ErrorKind::Other);
}
#[test]
fn install_io_keeps_the_cause() {
let denied = io::Error::from(io::ErrorKind::PermissionDenied);
let error = Error::install_io("could not replace binary", &denied);
assert!(matches!(error, Error::Permission(_)), "{error:?}");
assert!(error.to_string().contains("could not replace binary"));
let busy = io::Error::other("busy");
assert_eq!(Error::install_io("x", &busy).kind(), ErrorKind::Install);
}
#[test]
fn verification_failures_share_a_kind() {
for error in [
Error::InsecureUrl("http://x".into()),
Error::Signature("bad".into()),
Error::VerificationRequired("none".into()),
Error::Payload("team".into()),
Error::ManifestExpired {
expires: "2020-01-01T00:00:00Z".into(),
},
] {
assert_eq!(error.kind(), ErrorKind::Verification, "{error:?}");
}
assert_eq!(Error::Http("503".into()).kind(), ErrorKind::Network);
assert_eq!(Error::Cancelled.kind(), ErrorKind::Cancelled);
}
}