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!(f, "\n\x1b[1;36mhint:\x1b[0m check for missing or incorrect '=' characters at end of input")?;
122 } else {
123 writeln!(f, "error: invalid padding")?;
124 write!(
125 f,
126 "\nhint: check for missing or incorrect '=' characters at end of input"
127 )?;
128 }
129 Ok(())
130 }
131 DecodeError::InvalidLength {
132 actual,
133 expected,
134 hint,
135 } => {
136 if use_color {
137 writeln!(f, "\x1b[1;31merror:\x1b[0m invalid length for decode",)?;
138 } else {
139 writeln!(f, "error: invalid length for decode")?;
140 }
141 writeln!(f)?;
142 writeln!(f, " input is {} characters, expected {}", actual, expected)?;
143 writeln!(f)?;
144 if use_color {
145 write!(f, "\x1b[1;36mhint:\x1b[0m {}", hint)?;
146 } else {
147 write!(f, "hint: {}", hint)?;
148 }
149 Ok(())
150 }
151 }
152 }
153}
154
155impl std::error::Error for DecodeError {}
156
157fn should_use_color() -> bool {
159 if std::env::var("NO_COLOR").is_ok() {
161 return false;
162 }
163
164 use std::io::IsTerminal;
166 std::io::stderr().is_terminal()
167}
168
169#[derive(Debug)]
171pub struct DictionaryNotFoundError {
172 pub name: String,
173 pub suggestion: Option<String>,
174}
175
176impl DictionaryNotFoundError {
177 pub fn new(name: impl Into<String>, suggestion: Option<String>) -> Self {
178 Self {
179 name: name.into(),
180 suggestion,
181 }
182 }
183}
184
185impl fmt::Display for DictionaryNotFoundError {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 let use_color = should_use_color();
188
189 if use_color {
190 writeln!(
191 f,
192 "\x1b[1;31merror:\x1b[0m dictionary '{}' not found",
193 self.name
194 )?;
195 } else {
196 writeln!(f, "error: dictionary '{}' not found", self.name)?;
197 }
198
199 writeln!(f)?;
200
201 if let Some(suggestion) = &self.suggestion {
202 if use_color {
203 writeln!(f, "\x1b[1;36mhint:\x1b[0m did you mean '{}'?", suggestion)?;
204 } else {
205 writeln!(f, "hint: did you mean '{}'?", suggestion)?;
206 }
207 }
208
209 if use_color {
210 write!(
211 f,
212 " run \x1b[1m`base-d config --dictionaries`\x1b[0m to see all dictionaries"
213 )?;
214 } else {
215 write!(
216 f,
217 " run `base-d config --dictionaries` to see all dictionaries"
218 )?;
219 }
220
221 Ok(())
222 }
223}
224
225impl std::error::Error for DictionaryNotFoundError {}
226
227fn levenshtein_distance(s1: &str, s2: &str) -> usize {
229 let len1 = s1.chars().count();
230 let len2 = s2.chars().count();
231
232 if len1 == 0 {
233 return len2;
234 }
235 if len2 == 0 {
236 return len1;
237 }
238
239 let mut prev_row: Vec<usize> = (0..=len2).collect();
240 let mut curr_row = vec![0; len2 + 1];
241
242 for (i, c1) in s1.chars().enumerate() {
243 curr_row[0] = i + 1;
244
245 for (j, c2) in s2.chars().enumerate() {
246 let cost = if c1 == c2 { 0 } else { 1 };
247 curr_row[j + 1] = (curr_row[j] + 1)
248 .min(prev_row[j + 1] + 1)
249 .min(prev_row[j] + cost);
250 }
251
252 std::mem::swap(&mut prev_row, &mut curr_row);
253 }
254
255 prev_row[len2]
256}
257
258pub fn find_closest_dictionary(name: &str, available: &[String]) -> Option<String> {
260 if available.is_empty() {
261 return None;
262 }
263
264 let mut best_match = None;
265 let mut best_distance = usize::MAX;
266
267 for dict_name in available {
268 let distance = levenshtein_distance(name, dict_name);
269
270 let threshold = if name.len() < 5 { 2 } else { 3 };
273
274 if distance < best_distance && distance <= threshold {
275 best_distance = distance;
276 best_match = Some(dict_name.clone());
277 }
278 }
279
280 best_match
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_levenshtein_distance() {
289 assert_eq!(levenshtein_distance("base64", "base64"), 0);
290 assert_eq!(levenshtein_distance("base64", "base32"), 2);
291 assert_eq!(levenshtein_distance("bas64", "base64"), 1);
292 assert_eq!(levenshtein_distance("", "base64"), 6);
293 }
294
295 #[test]
296 fn test_find_closest_dictionary() {
297 let dicts = vec![
298 "base64".to_string(),
299 "base32".to_string(),
300 "base16".to_string(),
301 "hex".to_string(),
302 ];
303
304 assert_eq!(
305 find_closest_dictionary("bas64", &dicts),
306 Some("base64".to_string())
307 );
308 assert_eq!(
309 find_closest_dictionary("base63", &dicts),
310 Some("base64".to_string())
311 );
312 assert_eq!(
313 find_closest_dictionary("hex_math", &dicts),
314 None );
316 }
317
318 #[test]
319 fn test_error_display_no_color() {
320 std::env::set_var("NO_COLOR", "1");
322
323 let err = DecodeError::invalid_character('_', 12, "SGVsbG9faW52YWxpZA==", "A-Za-z0-9+/=");
324 let display = format!("{}", err);
325
326 assert!(display.contains("invalid character '_' at position 12"));
327 assert!(display.contains("SGVsbG9faW52YWxpZA=="));
328 assert!(display.contains("^"));
329 assert!(display.contains("hint:"));
330
331 std::env::remove_var("NO_COLOR");
332 }
333
334 #[test]
335 fn test_invalid_length_error() {
336 std::env::set_var("NO_COLOR", "1");
337
338 let err = DecodeError::invalid_length(
339 13,
340 "multiple of 4",
341 "add padding (=) or check for missing characters",
342 );
343 let display = format!("{}", err);
344
345 assert!(display.contains("invalid length"));
346 assert!(display.contains("13 characters"));
347 assert!(display.contains("multiple of 4"));
348 assert!(display.contains("add padding"));
349
350 std::env::remove_var("NO_COLOR");
351 }
352
353 #[test]
354 fn test_dictionary_not_found_error() {
355 std::env::set_var("NO_COLOR", "1");
356
357 let err = DictionaryNotFoundError::new("bas64", Some("base64".to_string()));
358 let display = format!("{}", err);
359
360 assert!(display.contains("dictionary 'bas64' not found"));
361 assert!(display.contains("did you mean 'base64'?"));
362 assert!(display.contains("base-d config --dictionaries"));
363
364 std::env::remove_var("NO_COLOR");
365 }
366}