use std::fmt;
use std::path::PathBuf;
#[derive(Debug)]
#[non_exhaustive]
pub enum BcfError {
Io {
path: Option<PathBuf>,
source: std::io::Error,
},
NotAnArchive {
detail: String,
},
NoTopics,
LimitExceeded {
limit: &'static str,
allowed: u64,
requested: u64,
},
UnsafeEntry {
name: String,
},
}
impl fmt::Display for BcfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BcfError::Io { path: Some(p), .. } => {
write!(f, "cannot read BCF archive {}", p.display())
}
BcfError::Io { path: None, .. } => f.write_str("cannot read BCF archive"),
BcfError::NotAnArchive { detail } => {
write!(f, "not a readable BCF (ZIP) archive: {detail}")
}
BcfError::NoTopics => f.write_str("archive contains no BCF markup document"),
BcfError::LimitExceeded {
limit,
allowed,
requested,
} => write!(
f,
"BCF archive exceeds the {limit} limit ({requested} > {allowed})"
),
BcfError::UnsafeEntry { name } => {
write!(f, "BCF archive entry escapes the archive root: {name:?}")
}
}
}
}
impl std::error::Error for BcfError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
BcfError::Io { source, .. } => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for BcfError {
fn from(source: std::io::Error) -> Self {
BcfError::Io { path: None, source }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
#[test]
fn io_errors_keep_their_source_and_path() {
let err = BcfError::Io {
path: Some(PathBuf::from("/tmp/x.bcfzip")),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
};
assert!(err.to_string().contains("/tmp/x.bcfzip"));
assert!(err.source().is_some());
}
#[test]
fn limit_messages_name_the_limit_and_both_numbers() {
let msg = BcfError::LimitExceeded {
limit: "uncompressed size",
allowed: 10,
requested: 99,
}
.to_string();
assert!(msg.contains("uncompressed size"), "{msg}");
assert!(msg.contains("99") && msg.contains("10"), "{msg}");
}
#[test]
fn unsafe_entries_are_reported_verbatim() {
let msg = BcfError::UnsafeEntry {
name: "../../etc/passwd".into(),
}
.to_string();
assert!(msg.contains("../../etc/passwd"), "{msg}");
}
#[test]
fn no_topics_is_distinct_from_not_an_archive() {
assert_ne!(
BcfError::NoTopics.to_string(),
BcfError::NotAnArchive { detail: "x".into() }.to_string()
);
}
}