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