1use std::fmt;
10use std::path::PathBuf;
11
12#[derive(Debug)]
14#[non_exhaustive]
15pub enum BcfError {
16 Io {
18 path: Option<PathBuf>,
20 source: std::io::Error,
22 },
23 NotAnArchive {
25 detail: String,
27 },
28 NoTopics,
34 LimitExceeded {
39 limit: &'static str,
41 allowed: u64,
43 requested: u64,
45 },
46 UnsafeEntry {
51 name: String,
53 },
54}
55
56impl fmt::Display for BcfError {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 BcfError::Io { path: Some(p), .. } => {
60 write!(f, "cannot read BCF archive {}", p.display())
61 }
62 BcfError::Io { path: None, .. } => f.write_str("cannot read BCF archive"),
63 BcfError::NotAnArchive { detail } => {
64 write!(f, "not a readable BCF (ZIP) archive: {detail}")
65 }
66 BcfError::NoTopics => f.write_str("archive contains no BCF markup document"),
67 BcfError::LimitExceeded {
68 limit,
69 allowed,
70 requested,
71 } => write!(
72 f,
73 "BCF archive exceeds the {limit} limit ({requested} > {allowed})"
74 ),
75 BcfError::UnsafeEntry { name } => {
76 write!(f, "BCF archive entry escapes the archive root: {name:?}")
77 }
78 }
79 }
80}
81
82impl std::error::Error for BcfError {
83 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
84 match self {
85 BcfError::Io { source, .. } => Some(source),
86 _ => None,
87 }
88 }
89}
90
91impl From<std::io::Error> for BcfError {
92 fn from(source: std::io::Error) -> Self {
93 BcfError::Io { path: None, source }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::error::Error as _;
101
102 #[test]
103 fn io_errors_keep_their_source_and_path() {
104 let err = BcfError::Io {
105 path: Some(PathBuf::from("/tmp/x.bcfzip")),
106 source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
107 };
108 assert!(err.to_string().contains("/tmp/x.bcfzip"));
109 assert!(err.source().is_some());
110 }
111
112 #[test]
113 fn limit_messages_name_the_limit_and_both_numbers() {
114 let msg = BcfError::LimitExceeded {
115 limit: "uncompressed size",
116 allowed: 10,
117 requested: 99,
118 }
119 .to_string();
120 assert!(msg.contains("uncompressed size"), "{msg}");
121 assert!(msg.contains("99") && msg.contains("10"), "{msg}");
122 }
123
124 #[test]
125 fn unsafe_entries_are_reported_verbatim() {
126 let msg = BcfError::UnsafeEntry {
127 name: "../../etc/passwd".into(),
128 }
129 .to_string();
130 assert!(msg.contains("../../etc/passwd"), "{msg}");
131 }
132
133 #[test]
134 fn no_topics_is_distinct_from_not_an_archive() {
135 assert_ne!(
136 BcfError::NoTopics.to_string(),
137 BcfError::NotAnArchive { detail: "x".into() }.to_string()
138 );
139 }
140}