base_d/encoders/algorithms/
errors.rs1use std::fmt;
2
3#[derive(Debug, PartialEq, Eq)]
5pub enum DecodeError {
6 InvalidCharacter {
8 char: char,
9 position: usize,
10 input: String,
11 valid_chars: String,
12 },
13 EmptyInput,
15 InvalidPadding,
17 InvalidLength {
19 actual: usize,
20 expected: String,
21 hint: String,
22 },
23}
24
25impl DecodeError {
26 pub fn invalid_character(c: char, position: usize, input: &str, valid_chars: &str) -> Self {
28 let display_input = if input.len() > 60 {
30 format!("{}...", &input[..60])
31 } else {
32 input.to_string()
33 };
34
35 DecodeError::InvalidCharacter {
36 char: c,
37 position,
38 input: display_input,
39 valid_chars: valid_chars.to_string(),
40 }
41 }
42
43 pub fn invalid_length(
45 actual: usize,
46 expected: impl Into<String>,
47 hint: impl Into<String>,
48 ) -> Self {
49 DecodeError::InvalidLength {
50 actual,
51 expected: expected.into(),
52 hint: hint.into(),
53 }
54 }
55}
56
57impl fmt::Display for DecodeError {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 let use_color = should_use_color();
60
61 match self {
62 DecodeError::InvalidCharacter {
63 char: c,
64 position,
65 input,
66 valid_chars,
67 } => {
68 if use_color {
70 writeln!(
71 f,
72 "\x1b[1;31merror:\x1b[0m invalid character '{}' at position {}",
73 c, position
74 )?;
75 } else {
76 writeln!(
77 f,
78 "error: invalid character '{}' at position {}",
79 c, position
80 )?;
81 }
82 writeln!(f)?;
83
84 let char_position = input.chars().take(*position).count();
87 writeln!(f, " {}", input)?;
88 write!(f, " {}", " ".repeat(char_position))?;
89 if use_color {
90 writeln!(f, "\x1b[1;31m^\x1b[0m")?;
91 } else {
92 writeln!(f, "^")?;
93 }
94 writeln!(f)?;
95
96 let hint_chars = if valid_chars.len() > 80 {
98 format!("{}...", &valid_chars[..80])
99 } else {
100 valid_chars.clone()
101 };
102
103 if use_color {
104 write!(f, "\x1b[1;36mhint:\x1b[0m valid characters: {}", hint_chars)?;
105 } else {
106 write!(f, "hint: valid characters: {}", hint_chars)?;
107 }
108 Ok(())
109 }
110 DecodeError::EmptyInput => {
111 if use_color {
112 write!(f, "\x1b[1;31merror:\x1b[0m cannot decode empty input")?;
113 } else {
114 write!(f, "error: cannot decode empty input")?;
115 }
116 Ok(())
117 }
118 DecodeError::InvalidPadding => {
119 if use_color {
120 writeln!(f, "\x1b[1;31merror:\x1b[0m invalid padding")?;
121 write!(
122 f,
123 "\n\x1b[1;36mhint:\x1b[0m check for missing or incorrect '=' characters at end of input"
124 )?;
125 } else {
126 writeln!(f, "error: invalid padding")?;
127 write!(
128 f,
129 "\nhint: check for missing or incorrect '=' characters at end of input"
130 )?;
131 }
132 Ok(())
133 }
134 DecodeError::InvalidLength {
135 actual,
136 expected,
137 hint,
138 } => {
139 if use_color {
140 writeln!(f, "\x1b[1;31merror:\x1b[0m invalid length for decode",)?;
141 } else {
142 writeln!(f, "error: invalid length for decode")?;
143 }
144 writeln!(f)?;
145 writeln!(f, " input is {} characters, expected {}", actual, expected)?;
146 writeln!(f)?;
147 if use_color {
148 write!(f, "\x1b[1;36mhint:\x1b[0m {}", hint)?;
149 } else {
150 write!(f, "hint: {}", hint)?;
151 }
152 Ok(())
153 }
154 }
155 }
156}
157
158impl std::error::Error for DecodeError {}
159
160fn should_use_color() -> bool {
162 if std::env::var("NO_COLOR").is_ok() {
164 return false;
165 }
166
167 use std::io::IsTerminal;
169 std::io::stderr().is_terminal()
170}
171
172#[derive(Debug)]
174pub struct DictionaryNotFoundError {
175 pub name: String,
176 pub suggestion: Option<String>,
177}
178
179impl DictionaryNotFoundError {
180 pub fn new(name: impl Into<String>) -> Self {
181 Self {
182 name: name.into(),
183 suggestion: None,
184 }
185 }
186
187 pub fn with_suggestion(name: impl Into<String>, suggestion: Option<String>) -> Self {
188 Self {
189 name: name.into(),
190 suggestion,
191 }
192 }
193
194 pub fn with_cause(name: impl Into<String>, cause: impl std::fmt::Display) -> Self {
195 Self {
196 name: name.into(),
197 suggestion: Some(format!("build failed: {}", cause)),
198 }
199 }
200}
201
202impl fmt::Display for DictionaryNotFoundError {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 let use_color = should_use_color();
205
206 if use_color {
207 writeln!(
208 f,
209 "\x1b[1;31merror:\x1b[0m dictionary '{}' not found",
210 self.name
211 )?;
212 } else {
213 writeln!(f, "error: dictionary '{}' not found", self.name)?;
214 }
215
216 writeln!(f)?;
217
218 if let Some(suggestion) = &self.suggestion {
219 if use_color {
220 writeln!(f, "\x1b[1;36mhint:\x1b[0m did you mean '{}'?", suggestion)?;
221 } else {
222 writeln!(f, "hint: did you mean '{}'?", suggestion)?;
223 }
224 }
225
226 if use_color {
227 write!(
228 f,
229 " run \x1b[1m`base-d config --dictionaries`\x1b[0m to see all dictionaries"
230 )?;
231 } else {
232 write!(
233 f,
234 " run `base-d config --dictionaries` to see all dictionaries"
235 )?;
236 }
237
238 Ok(())
239 }
240}
241
242impl std::error::Error for DictionaryNotFoundError {}
243
244fn levenshtein_distance(s1: &str, s2: &str) -> usize {
246 let len1 = s1.chars().count();
247 let len2 = s2.chars().count();
248
249 if len1 == 0 {
250 return len2;
251 }
252 if len2 == 0 {
253 return len1;
254 }
255
256 let mut prev_row: Vec<usize> = (0..=len2).collect();
257 let mut curr_row = vec![0; len2 + 1];
258
259 for (i, c1) in s1.chars().enumerate() {
260 curr_row[0] = i + 1;
261
262 for (j, c2) in s2.chars().enumerate() {
263 let cost = if c1 == c2 { 0 } else { 1 };
264 curr_row[j + 1] = (curr_row[j] + 1)
265 .min(prev_row[j + 1] + 1)
266 .min(prev_row[j] + cost);
267 }
268
269 std::mem::swap(&mut prev_row, &mut curr_row);
270 }
271
272 prev_row[len2]
273}
274
275pub fn find_closest_dictionary(name: &str, available: &[String]) -> Option<String> {
277 if available.is_empty() {
278 return None;
279 }
280
281 let mut best_match = None;
282 let mut best_distance = usize::MAX;
283
284 for dict_name in available {
285 let distance = levenshtein_distance(name, dict_name);
286
287 let threshold = if name.len() < 5 { 2 } else { 3 };
290
291 if distance < best_distance && distance <= threshold {
292 best_distance = distance;
293 best_match = Some(dict_name.clone());
294 }
295 }
296
297 best_match
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_levenshtein_distance() {
306 assert_eq!(levenshtein_distance("base64", "base64"), 0);
307 assert_eq!(levenshtein_distance("base64", "base32"), 2);
308 assert_eq!(levenshtein_distance("bas64", "base64"), 1);
309 assert_eq!(levenshtein_distance("", "base64"), 6);
310 }
311
312 #[test]
313 fn test_find_closest_dictionary() {
314 let dicts = vec![
315 "base64".to_string(),
316 "base32".to_string(),
317 "base16".to_string(),
318 "hex".to_string(),
319 ];
320
321 assert_eq!(
322 find_closest_dictionary("bas64", &dicts),
323 Some("base64".to_string())
324 );
325 assert_eq!(
326 find_closest_dictionary("base63", &dicts),
327 Some("base64".to_string())
328 );
329 assert_eq!(
330 find_closest_dictionary("hex_radix", &dicts),
331 None );
333 }
334
335 #[test]
336 fn test_error_display_no_color() {
337 unsafe {
340 std::env::set_var("NO_COLOR", "1");
341 }
342
343 let err = DecodeError::invalid_character('_', 12, "SGVsbG9faW52YWxpZA==", "A-Za-z0-9+/=");
344 let display = format!("{}", err);
345
346 assert!(display.contains("invalid character '_' at position 12"));
347 assert!(display.contains("SGVsbG9faW52YWxpZA=="));
348 assert!(display.contains("^"));
349 assert!(display.contains("hint:"));
350
351 unsafe {
354 std::env::remove_var("NO_COLOR");
355 }
356 }
357
358 #[test]
359 fn test_invalid_length_error() {
360 unsafe {
363 std::env::set_var("NO_COLOR", "1");
364 }
365
366 let err = DecodeError::invalid_length(
367 13,
368 "multiple of 4",
369 "add padding (=) or check for missing characters",
370 );
371 let display = format!("{}", err);
372
373 assert!(display.contains("invalid length"));
374 assert!(display.contains("13 characters"));
375 assert!(display.contains("multiple of 4"));
376 assert!(display.contains("add padding"));
377
378 unsafe {
381 std::env::remove_var("NO_COLOR");
382 }
383 }
384
385 #[test]
386 fn test_dictionary_not_found_error() {
387 unsafe {
390 std::env::set_var("NO_COLOR", "1");
391 }
392
393 let err = DictionaryNotFoundError::with_suggestion("bas64", Some("base64".to_string()));
394 let display = format!("{}", err);
395
396 assert!(display.contains("dictionary 'bas64' not found"));
397 assert!(display.contains("did you mean 'base64'?"));
398 assert!(display.contains("base-d config --dictionaries"));
399
400 unsafe {
403 std::env::remove_var("NO_COLOR");
404 }
405 }
406}