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