1use std::fmt::Write as _;
21use std::ops::Range;
22
23pub type Result<T> = std::result::Result<T, Error>;
24
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27 #[error("error reading {path}: {source}")]
30 Io {
31 path: String,
32 #[source]
33 source: std::io::Error,
34 },
35
36 #[error("failed to write {path} ({what})")]
41 Write { path: String, what: String },
42
43 #[error("error fetching {url}{}: {message}", .status.map(|s| format!(" (HTTP {s})")).unwrap_or_default())]
45 Http {
46 url: String,
47 status: Option<u16>,
48 message: String,
49 },
50
51 #[error("{path} is closed")]
54 Closed { path: String },
55
56 #[error("{}", unknown_chr_message(.id, *.key_size, .truncated_match, .available))]
65 UnknownChromosome {
66 id: String,
67 key_size: Option<usize>,
68 truncated_match: Option<String>,
69 available: Vec<String>,
70 },
71
72 #[error("{0}")]
76 InvalidArgument(String),
77
78 #[error("{path}: {what}")]
81 Format { path: String, what: String },
82
83 #[error("{path}: {what} (at offset {offset})")]
87 Corrupt {
88 path: String,
89 offset: u64,
90 what: String,
91 },
92
93 #[error("{0}")]
95 Unsupported(String),
96}
97
98fn unknown_chr_message(
105 id: &str,
106 key_size: Option<usize>,
107 truncated_match: &Option<String>,
108 available: &[String],
109) -> String {
110 let mut message = format!("Chromosome {id} not found");
111 if let Some(key_size) = key_size {
112 let _ = write!(
113 message,
114 ", and cannot be: it is {} characters long and this file stores \
115 chromosome names in a {key_size}-character field",
116 id.chars().count()
117 );
118 if let Some(name) = truncated_match {
119 let _ = write!(
120 message,
121 ". The file does hold {name}, which is what the first {key_size} \
122 characters spell: if it was written from names too long for its \
123 field, that is this chromosome stored truncated. Ids are matched \
124 whole, so ask for it by the name the file carries"
125 );
126 }
127 }
128 let _ = write!(message, " (available: {})", available.join(", "));
129 message
130}
131
132impl Error {
133 pub fn invalid(msg: impl Into<String>) -> Self {
134 Error::InvalidArgument(msg.into())
135 }
136
137 pub fn format(path: impl Into<String>, what: impl Into<String>) -> Self {
138 Error::Format {
139 path: path.into(),
140 what: what.into(),
141 }
142 }
143
144 pub fn corrupt(path: impl Into<String>, offset: u64, what: impl Into<String>) -> Self {
145 Error::Corrupt {
146 path: path.into(),
147 offset,
148 what: what.into(),
149 }
150 }
151
152 pub fn write(path: impl Into<String>, what: impl Into<String>) -> Self {
153 Error::Write {
154 path: path.into(),
155 what: what.into(),
156 }
157 }
158
159 pub fn io(path: impl Into<String>, source: std::io::Error) -> Self {
160 Error::Io {
161 path: path.into(),
162 source,
163 }
164 }
165}
166
167pub fn check_range(path: &str, buf_len: usize, range: Range<usize>, what: &str) -> Result<()> {
171 if range.end > buf_len || range.start > range.end {
172 return Err(Error::corrupt(
173 path,
174 range.start as u64,
175 format!(
176 "{what}: bytes {}..{} of a {buf_len}-byte buffer",
177 range.start, range.end
178 ),
179 ));
180 }
181 Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn names(ids: &[&str]) -> Vec<String> {
189 ids.iter().map(|s| s.to_string()).collect()
190 }
191
192 #[test]
193 fn an_unknown_chromosome_names_itself_and_what_was_available() {
194 let err = Error::UnknownChromosome {
195 id: "chrZ".into(),
196 key_size: None,
197 truncated_match: None,
198 available: names(&["chr1", "chr2"]),
199 };
200 assert_eq!(
201 err.to_string(),
202 "Chromosome chrZ not found (available: chr1, chr2)"
203 );
204 }
205
206 #[test]
207 fn an_over_long_id_says_the_field_is_too_narrow() {
208 let err = Error::UnknownChromosome {
209 id: "chrXVII".into(),
210 key_size: Some(4),
211 truncated_match: None,
212 available: names(&["chrI"]),
213 };
214 let message = err.to_string();
215 assert!(message.contains("7 characters long"), "{message}");
216 assert!(message.contains("4-character field"), "{message}");
217 }
218
219 #[test]
220 fn a_prefix_that_spells_a_real_chromosome_is_named() {
221 let err = Error::UnknownChromosome {
222 id: "chrXVII".into(),
223 key_size: Some(6),
224 truncated_match: Some("chrXVI".into()),
225 available: names(&["chrXVI"]),
226 };
227 let message = err.to_string();
228 assert!(message.contains("The file does hold chrXVI"), "{message}");
229 }
230
231 #[test]
234 fn the_parts_are_reachable_without_parsing_the_message() {
235 let err = Error::UnknownChromosome {
236 id: "12".into(),
237 key_size: Some(1),
238 truncated_match: Some("chr1".into()),
239 available: names(&["chr1"]),
240 };
241 let Error::UnknownChromosome {
242 id,
243 key_size,
244 truncated_match,
245 available,
246 } = &err
247 else {
248 panic!("wrong variant");
249 };
250 assert_eq!(id, "12");
251 assert_eq!(*key_size, Some(1));
252 assert_eq!(truncated_match.as_deref(), Some("chr1"));
253 assert_eq!(available.len(), 1);
254 }
255}