use thiserror::Error;
#[derive(Debug, Error)]
pub enum DownloadError {
#[error("transport error from provider {provider}: {reason}")]
Transport {
provider: String,
reason: String,
},
#[error("range fetch from provider {provider} timed out")]
Timeout {
provider: String,
},
#[error("integrity failure: {0}")]
Verify(#[from] VerifyError),
#[error("no providers left holding the content (needed {needed} more range(s))")]
NoProviders {
needed: usize,
},
#[error("content not found: {content}")]
NotFound {
content: String,
},
#[error("download cancelled")]
Cancelled,
#[error("state store error: {0}")]
State(String),
#[error("sink write error: {0}")]
Sink(String),
#[error("content id is not directly downloadable (needs a root/capsule or resource, got a bare store id)")]
NotDownloadable,
#[error("download task ended without a result")]
TaskEnded,
}
impl DownloadError {
pub fn transport(provider: impl Into<String>, reason: impl std::fmt::Display) -> Self {
DownloadError::Transport {
provider: provider.into(),
reason: reason.to_string(),
}
}
pub fn sink(reason: impl std::fmt::Display) -> Self {
DownloadError::Sink(reason.to_string())
}
pub fn state(reason: impl std::fmt::Display) -> Self {
DownloadError::State(reason.to_string())
}
pub fn is_recoverable(&self) -> bool {
matches!(
self,
DownloadError::Transport { .. }
| DownloadError::Verify(_)
| DownloadError::Timeout { .. }
)
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum VerifyError {
#[error("range length mismatch: expected {expected} bytes for chunks, got {actual}")]
Length {
expected: u64,
actual: u64,
},
#[error("range metadata mismatch with the resource commitment: {0}")]
Metadata(String),
#[error("range is not chunk-aligned: {0}")]
Alignment(String),
#[error("resource does not verify against the chain-anchored root")]
Root,
#[error("first frame is missing verification metadata ({0})")]
MissingMetadata(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transport_helper_formats_with_provider() {
let e = DownloadError::transport("abcd", "connection refused");
assert!(e.to_string().contains("abcd"));
assert!(e.to_string().contains("connection refused"));
assert!(e.is_recoverable());
}
#[test]
fn verify_errors_are_recoverable() {
let e: DownloadError = VerifyError::Length {
expected: 10,
actual: 9,
}
.into();
assert!(e.is_recoverable());
}
#[test]
fn timeout_is_recoverable() {
let e = DownloadError::Timeout {
provider: "abcd".into(),
};
assert!(e.is_recoverable());
assert!(e.to_string().contains("abcd"));
assert!(e.to_string().contains("timed out"));
}
#[test]
fn terminal_errors_are_not_recoverable() {
assert!(!DownloadError::NoProviders { needed: 1 }.is_recoverable());
assert!(!DownloadError::Cancelled.is_recoverable());
assert!(!DownloadError::NotDownloadable.is_recoverable());
}
#[test]
fn sink_and_state_helpers_format() {
assert!(DownloadError::sink("disk full")
.to_string()
.contains("disk full"));
assert!(DownloadError::state("corrupt")
.to_string()
.contains("corrupt"));
}
#[test]
fn verify_error_display_is_descriptive() {
assert!(VerifyError::Root
.to_string()
.contains("chain-anchored root"));
assert!(VerifyError::Metadata("x".into())
.to_string()
.contains("commitment"));
assert!(VerifyError::Alignment("y".into())
.to_string()
.contains("chunk-aligned"));
assert!(VerifyError::MissingMetadata("z".into())
.to_string()
.contains("missing verification metadata"));
}
}