1use std::fmt;
28
29use zeroize::Zeroizing;
30
31const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
33
34pub const ROW_DATA: usize = 28;
36
37pub const ROW_PARITY: usize = 4;
39
40pub const ROW_LEN: usize = ROW_DATA + ROW_PARITY;
42
43const MAX_ERASURES: usize = 2;
46
47const POLY: u8 = 0b10_0101;
51
52const fn gf_mul(a: u8, b: u8) -> u8 {
54 let mut a = a & 31;
55 let mut b = b & 31;
56 let mut product = 0u8;
57 while b != 0 {
58 if b & 1 == 1 {
59 product ^= a;
60 }
61 b >>= 1;
62 a <<= 1;
63 if a & 0b10_0000 != 0 {
64 a ^= POLY;
65 }
66 }
67 product & 31
68}
69
70#[allow(clippy::indexing_slicing)] const INVERSES: [u8; 32] = {
73 let mut table = [0u8; 32];
74 let mut a = 1u8;
75 while a < 32 {
76 let mut x = 1u8;
77 while x < 32 {
78 if gf_mul(a, x) == 1 {
79 table[a as usize] = x;
80 }
81 x += 1;
82 }
83 a += 1;
84 }
85 table
86};
87
88fn gf_inv(a: u8) -> u8 {
90 INVERSES.get(usize::from(a & 31)).copied().unwrap_or(0)
91}
92
93fn interpolate(points: &[(u8, u8)], x: u8) -> u8 {
95 let mut result = 0u8;
96 for (i, &(xi, yi)) in points.iter().enumerate() {
97 let mut basis = 1u8;
98 for (j, &(xj, _)) in points.iter().enumerate() {
99 if i != j {
100 basis = gf_mul(basis, gf_mul(x ^ xj, gf_inv(xi ^ xj)));
101 }
102 }
103 result ^= gf_mul(yi, basis);
104 }
105 result
106}
107
108fn point(position: usize) -> u8 {
113 u8::try_from(position & 31).unwrap_or(0)
114}
115
116fn parity(data: &[u8]) -> [u8; ROW_PARITY] {
118 let points: Vec<(u8, u8)> = data
119 .iter()
120 .enumerate()
121 .map(|(i, &d)| (point(i), d))
122 .collect();
123 let mut out = [0u8; ROW_PARITY];
124 for (k, slot) in out.iter_mut().enumerate() {
125 *slot = interpolate(&points, point(ROW_DATA.saturating_add(k)));
126 }
127 out
128}
129
130fn to_symbols(bytes: &[u8]) -> Vec<u8> {
132 let mut out = Vec::with_capacity(bytes.len().saturating_mul(8).div_ceil(5));
133 let mut acc: u32 = 0;
134 let mut bits: u32 = 0;
135 for &b in bytes {
136 acc = (acc << 8) | u32::from(b);
137 bits = bits.saturating_add(8);
138 while bits >= 5 {
139 bits = bits.saturating_sub(5);
140 out.push(u8::try_from((acc >> bits) & 31).unwrap_or(0));
141 }
142 }
143 if bits > 0 {
144 out.push(u8::try_from((acc << (5u32.saturating_sub(bits))) & 31).unwrap_or(0));
145 }
146 out
147}
148
149fn from_symbols(symbols: &[u8]) -> Option<Vec<u8>> {
152 let mut out = Vec::with_capacity(symbols.len().saturating_mul(5) / 8);
153 let mut acc: u32 = 0;
154 let mut bits: u32 = 0;
155 for &s in symbols {
156 acc = (acc << 5) | u32::from(s & 31);
157 bits = bits.saturating_add(5);
158 while bits >= 8 {
159 bits = bits.saturating_sub(8);
160 out.push(u8::try_from((acc >> bits) & 0xff).unwrap_or(0));
161 }
162 }
163 if bits >= 5 || (acc << (8u32.saturating_sub(bits))) & 0xff != 0 {
164 return None;
165 }
166 Some(out)
167}
168
169pub fn encode(bytes: &[u8]) -> String {
181 let symbols = Zeroizing::new(to_symbols(bytes));
182 let mut out = String::with_capacity(symbols.len().saturating_add(ROW_LEN).saturating_mul(2));
183 for data in symbols.chunks(ROW_DATA) {
184 for &s in data {
185 out.push(letter(s));
186 }
187 for p in parity(data) {
188 out.push(letter(p));
189 }
190 }
191 out
192}
193
194fn letter(symbol: u8) -> char {
195 ALPHABET
196 .get(usize::from(symbol & 31))
197 .map_or('0', |&b| char::from(b))
198}
199
200enum Read {
202 Separator,
204 Erasure,
206 Value(u8),
208 Unknown,
210}
211
212fn read(c: char) -> Read {
213 let c = c.to_ascii_uppercase();
214 match c {
215 ' ' | '-' | '|' | '\t' | '\n' | '\r' | '.' => Read::Separator,
216 '?' | 'U' => Read::Erasure,
219 'O' => Read::Value(0),
220 'I' | 'L' => Read::Value(1),
221 _ => ALPHABET
222 .iter()
223 .position(|&x| char::from(x) == c)
224 .and_then(|p| u8::try_from(p).ok())
225 .map_or(Read::Unknown, Read::Value),
226 }
227}
228
229pub fn decode(text: &str) -> Result<Decoded, RowError> {
254 let mut symbols: Vec<Option<u8>> = Vec::with_capacity(text.len());
255 for (position, c) in text.chars().enumerate() {
256 match read(c) {
257 Read::Separator => {}
258 Read::Erasure => symbols.push(None),
259 Read::Value(v) => symbols.push(Some(v)),
260 Read::Unknown => return Err(RowError::Character { position }),
261 }
262 }
263 if symbols.is_empty() {
264 return Err(RowError::Empty);
265 }
266 let tail = symbols.len() % ROW_LEN;
267 if tail != 0 && tail <= ROW_PARITY {
268 return Err(RowError::Shape {
269 characters: symbols.len(),
270 });
271 }
272
273 let mut data = Zeroizing::new(Vec::with_capacity(symbols.len()));
274 let mut repaired = Vec::new();
275 for (index, row) in symbols.chunks(ROW_LEN).enumerate() {
276 let row_number = index.saturating_add(1);
277 let (fixed, repair) = repair_row(row).ok_or(RowError::Row {
278 row: row_number,
279 unreadable: row.iter().filter(|s| s.is_none()).count(),
280 })?;
281 if let Some(repair) = repair {
282 repaired.push(Repair {
283 row: row_number,
284 kind: repair,
285 });
286 }
287 data.extend_from_slice(
288 fixed
289 .get(..row.len().saturating_sub(ROW_PARITY))
290 .unwrap_or(&[]),
291 );
292 }
293 let bytes = from_symbols(&data).ok_or(RowError::Padding)?;
294 Ok(Decoded {
295 bytes: Zeroizing::new(bytes),
296 repaired,
297 })
298}
299
300pub fn read_row(text: &str, row: usize) -> Result<RowRead, RowError> {
321 let mut symbols: Vec<Option<u8>> = Vec::with_capacity(ROW_LEN);
322 for (position, c) in text.chars().enumerate() {
323 match read(c) {
324 Read::Separator => {}
325 Read::Erasure => symbols.push(None),
326 Read::Value(v) => symbols.push(Some(v)),
327 Read::Unknown => return Err(RowError::Character { position }),
328 }
329 }
330 if symbols.is_empty() {
331 return Err(RowError::Empty);
332 }
333 if symbols.len() <= ROW_PARITY || symbols.len() > ROW_LEN {
334 return Err(RowError::Shape {
335 characters: symbols.len(),
336 });
337 }
338 let (fixed, repair) = repair_row(&symbols).ok_or(RowError::Row {
339 row,
340 unreadable: symbols.iter().filter(|s| s.is_none()).count(),
341 })?;
342 Ok(RowRead {
343 text: Zeroizing::new(fixed.iter().map(|&s| letter(s)).collect()),
344 repair: repair.map(|kind| Repair { row, kind }),
345 })
346}
347
348#[derive(Debug)]
350pub struct RowRead {
351 pub text: Zeroizing<String>,
353 pub repair: Option<Repair>,
355}
356
357fn repair_row(row: &[Option<u8>]) -> Option<(Vec<u8>, Option<RepairKind>)> {
360 let erased: Vec<usize> = row
361 .iter()
362 .enumerate()
363 .filter_map(|(i, s)| s.is_none().then_some(i))
364 .collect();
365 if erased.len() > MAX_ERASURES {
366 return None;
367 }
368 let data_len = row.len().saturating_sub(ROW_PARITY);
369
370 let filled = fill(row, &erased, data_len)?;
372 if checks(&filled, data_len) {
373 let repair = (!erased.is_empty()).then_some(RepairKind::Unreadable {
374 count: erased.len(),
375 });
376 return Some((filled, repair));
377 }
378 if !erased.is_empty() {
379 return None;
382 }
383
384 let mut found: Option<(usize, Vec<u8>)> = None;
388 for position in 0..row.len() {
389 let candidate = fill(row, &[position], data_len)?;
390 if checks(&candidate, data_len) && candidate.get(position) != row.get(position)?.as_ref() {
391 if found.is_some() {
392 return None;
393 }
394 found = Some((position, candidate));
395 }
396 }
397 let (position, fixed) = found?;
398 Some((fixed, Some(RepairKind::Wrong { position })))
399}
400
401fn fill(row: &[Option<u8>], holes: &[usize], data_len: usize) -> Option<Vec<u8>> {
405 let known: Vec<(u8, u8)> = row
406 .iter()
407 .enumerate()
408 .filter(|(i, s)| s.is_some() && !holes.contains(i))
409 .filter_map(|(i, s)| s.map(|v| (row_point(i, row.len()), v)))
410 .take(data_len)
411 .collect();
412 if known.len() < data_len {
413 return None;
414 }
415 let mut out = Vec::with_capacity(row.len());
416 for (i, s) in row.iter().enumerate() {
417 match s {
418 Some(v) if !holes.contains(&i) => out.push(*v),
419 _ => out.push(interpolate(&known, row_point(i, row.len()))),
420 }
421 }
422 Some(out)
423}
424
425fn row_point(i: usize, len: usize) -> u8 {
429 let data_len = len.saturating_sub(ROW_PARITY);
430 if i < data_len {
431 point(i)
432 } else {
433 point(ROW_DATA.saturating_add(i.saturating_sub(data_len)))
434 }
435}
436
437fn checks(row: &[u8], data_len: usize) -> bool {
439 let (data, given) = row.split_at(data_len.min(row.len()));
440 parity(data) == given
441}
442
443#[derive(Debug)]
445pub struct Decoded {
446 pub bytes: Zeroizing<Vec<u8>>,
448 pub repaired: Vec<Repair>,
450}
451
452#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct Repair {
455 pub row: usize,
457 pub kind: RepairKind,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
463#[non_exhaustive]
464pub enum RepairKind {
465 Wrong {
467 position: usize,
469 },
470 Unreadable {
472 count: usize,
474 },
475}
476
477impl fmt::Display for Repair {
478 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479 match self.kind {
480 RepairKind::Wrong { position } => write!(
481 f,
482 "row {}: character {} was wrong and has been corrected; check it against the sheet",
483 self.row,
484 position.saturating_add(1)
485 ),
486 RepairKind::Unreadable { count } => write!(
487 f,
488 "row {}: {count} unreadable character{} recovered",
489 self.row,
490 if count == 1 { "" } else { "s" }
491 ),
492 }
493 }
494}
495
496#[derive(Debug, Clone, PartialEq, Eq)]
498#[non_exhaustive]
499pub enum RowError {
500 Empty,
502 Character {
505 position: usize,
507 },
508 Shape {
511 characters: usize,
513 },
514 Row {
516 row: usize,
518 unreadable: usize,
520 },
521 Padding,
523}
524
525impl fmt::Display for RowError {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 match self {
528 Self::Empty => write!(f, "nothing to read"),
529 Self::Character { position } => write!(
530 f,
531 "character {} is not a digit or a letter; write ? for one that cannot be read",
532 position.saturating_add(1)
533 ),
534 Self::Shape { characters } => write!(
535 f,
536 "{characters} characters do not make rows of 32; a last row has at least 5"
537 ),
538 Self::Row { row, unreadable } => match unreadable {
539 0 => write!(
540 f,
541 "row {row} does not check and has more than one wrong character; read it \
542 again from the sheet"
543 ),
544 1 | 2 => write!(
545 f,
546 "row {row} has {unreadable} unreadable and still does not check, so another \
547 character is wrong; a row recovers two unreadable or one wrong, not both; \
548 read it again from the sheet"
549 ),
550 _ => write!(
551 f,
552 "row {row} has {unreadable} unreadable characters and recovers at most two; \
553 read it again from the sheet"
554 ),
555 },
556 Self::Padding => write!(f, "the characters do not end on a whole byte"),
557 }
558 }
559}
560
561impl std::error::Error for RowError {}
562
563#[cfg(test)]
564#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn the_field_has_inverses_and_the_polynomial_is_primitive() {
570 for a in 1..32u8 {
571 assert_eq!(gf_mul(a, gf_inv(a)), 1, "{a}");
572 }
573 let mut seen = std::collections::BTreeSet::new();
575 let mut x = 1u8;
576 for _ in 0..31 {
577 seen.insert(x);
578 x = gf_mul(x, 2);
579 }
580 assert_eq!(seen.len(), 31);
581 assert_eq!(x, 1);
582 }
583
584 #[test]
585 fn a_row_is_its_data_and_four_parity_characters() {
586 let text = encode(&[0u8; 35]);
587 assert_eq!(text.len(), 64);
588 assert_eq!(&text[..28], "0".repeat(28));
589 assert_eq!(&text[28..32], "0000");
590 let text = encode(&[0xFF; 35]);
591 assert_eq!(text.len(), 64);
592 assert!(text.chars().all(|c| ALPHABET.contains(&(c as u8))));
593 }
594
595 #[test]
596 fn bytes_round_trip_at_every_length() {
597 for len in 0..=100usize {
598 let bytes: Vec<u8> = (0..len)
599 .map(|i| u8::try_from(i.wrapping_mul(53).wrapping_add(7) & 0xff).unwrap())
600 .collect();
601 let text = encode(&bytes);
602 let rows = len.saturating_mul(8).div_ceil(5).div_ceil(ROW_DATA);
603 assert_eq!(
604 text.len(),
605 len.saturating_mul(8).div_ceil(5) + rows * ROW_PARITY,
606 "{len}"
607 );
608 if len == 0 {
609 assert_eq!(decode(&text).unwrap_err(), RowError::Empty);
610 continue;
611 }
612 let decoded = decode(&text).unwrap_or_else(|e| panic!("{len}: {e}"));
613 assert_eq!(*decoded.bytes, bytes, "{len}");
614 assert!(decoded.repaired.is_empty());
615 }
616 }
617
618 #[test]
619 fn case_confusions_and_separators_are_read_through() {
620 let bytes = b"a value to write";
621 let text = encode(bytes);
622 let grouped: String = text
623 .to_ascii_lowercase()
624 .as_bytes()
625 .chunks(4)
626 .map(|g| std::str::from_utf8(g).unwrap())
627 .collect::<Vec<_>>()
628 .join(" ");
629 assert_eq!(*decode(&grouped).unwrap().bytes, bytes);
630 let confused = text.replace('0', "O").replace('1', "l");
632 assert_eq!(*decode(&confused).unwrap().bytes, bytes);
633 let mut chars: Vec<char> = text.chars().collect();
635 chars[0] = 'u';
636 let with_u: String = chars.iter().collect();
637 let decoded = decode(&with_u).unwrap();
638 assert_eq!(*decoded.bytes, bytes);
639 assert_eq!(
640 decoded.repaired,
641 vec![Repair {
642 row: 1,
643 kind: RepairKind::Unreadable { count: 1 }
644 }]
645 );
646 chars[0] = '#';
648 let outside: String = chars.iter().collect();
649 assert_eq!(
650 decode(&outside).unwrap_err(),
651 RowError::Character { position: 0 }
652 );
653 }
654
655 #[test]
656 fn one_wrong_character_anywhere_in_a_row_is_corrected_and_named() {
657 let bytes = [0x5A; 35];
658 let text = encode(&bytes);
659 let chars: Vec<char> = text.chars().collect();
660 for position in 0..chars.len() {
661 let mut wrong = chars.clone();
662 wrong[position] = if wrong[position] == 'Q' { 'R' } else { 'Q' };
663 let wrong: String = wrong.into_iter().collect();
664 let decoded = decode(&wrong).unwrap_or_else(|e| panic!("position {position}: {e}"));
665 assert_eq!(*decoded.bytes, bytes, "position {position}");
666 assert_eq!(
667 decoded.repaired,
668 [Repair {
669 row: position / ROW_LEN + 1,
670 kind: RepairKind::Wrong {
671 position: position % ROW_LEN
672 }
673 }],
674 "position {position}"
675 );
676 }
677 }
678
679 #[test]
680 fn two_unreadable_characters_in_a_row_are_recovered() {
681 let bytes = b"thirty-two bytes of some secret!";
682 let text = encode(bytes);
683 let mut chars: Vec<char> = text.chars().collect();
684 chars[3] = '?';
685 chars[30] = '?';
686 chars[40] = '?';
687 let smudged: String = chars.iter().collect();
688 let decoded = decode(&smudged).unwrap();
689 assert_eq!(*decoded.bytes, bytes);
690 assert_eq!(decoded.repaired.len(), 2);
691 assert_eq!(
692 decoded.repaired[0].to_string(),
693 "row 1: 2 unreadable characters recovered"
694 );
695 assert_eq!(
696 decoded.repaired[1],
697 Repair {
698 row: 2,
699 kind: RepairKind::Unreadable { count: 1 }
700 }
701 );
702 }
703
704 #[test]
705 fn a_row_beyond_repair_is_refused_by_name() {
706 let text = encode(&[0x33; 35]);
707 let mut chars: Vec<char> = text.chars().collect();
708 chars[35] = if chars[35] == 'A' { 'B' } else { 'A' };
710 chars[50] = if chars[50] == 'A' { 'B' } else { 'A' };
711 let wrong: String = chars.iter().collect();
712 assert_eq!(
713 decode(&wrong).unwrap_err(),
714 RowError::Row {
715 row: 2,
716 unreadable: 0
717 }
718 );
719 let mut chars: Vec<char> = text.chars().collect();
721 chars[0] = '?';
722 chars[1] = '?';
723 chars[2] = '?';
724 let smudged: String = chars.iter().collect();
725 assert_eq!(
726 decode(&smudged).unwrap_err(),
727 RowError::Row {
728 row: 1,
729 unreadable: 3
730 }
731 );
732 let mut chars: Vec<char> = text.chars().collect();
734 chars[5] = '?';
735 chars[9] = if chars[9] == 'A' { 'B' } else { 'A' };
736 let mixed: String = chars.iter().collect();
737 let err = decode(&mixed).unwrap_err();
738 assert_eq!(
739 err,
740 RowError::Row {
741 row: 1,
742 unreadable: 1
743 }
744 );
745 assert!(err.to_string().contains("not both"));
746 }
747
748 #[test]
749 fn a_shape_that_is_not_rows_is_refused() {
750 assert_eq!(
751 decode("ABCD").unwrap_err(),
752 RowError::Shape { characters: 4 }
753 );
754 assert_eq!(
755 decode(&"A".repeat(34)).unwrap_err(),
756 RowError::Shape { characters: 34 }
757 );
758 assert_eq!(decode(" - ").unwrap_err(), RowError::Empty);
759 }
760}