happy_cracking/crypto/
base45.rs1use anyhow::{Context, Result};
2use clap::Subcommand;
3use std::sync::LazyLock;
4
5const ALPHABET: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
6
7static DECODE_TABLE: LazyLock<[u8; 256]> = LazyLock::new(|| {
8 let mut table = [255u8; 256];
9 for (i, &c) in ALPHABET.iter().enumerate() {
10 table[c as usize] = i as u8;
11 }
12 table
13});
14
15#[derive(Subcommand)]
16pub enum Base45Action {
17 #[command(about = "Encode to Base45 (RFC 9285)")]
18 Encode {
19 #[arg(help = "Input text")]
20 input: String,
21 },
22 #[command(about = "Decode from Base45 (RFC 9285)")]
23 Decode {
24 #[arg(help = "Base45 encoded string")]
25 input: String,
26 },
27}
28
29pub fn run(action: Base45Action) -> Result<()> {
30 match action {
31 Base45Action::Encode { input } => {
32 println!("{}", encode(input.as_bytes()));
33 }
34 Base45Action::Decode { input } => {
35 let bytes = decode(&input)?;
36
37 match String::from_utf8(bytes.clone()).context("Decoded data is not valid UTF-8") {
38 Ok(s) => println!("{s}"),
39 Err(_) => println!("(lossy) {}", String::from_utf8_lossy(&bytes)),
40 }
41 }
42 }
43 Ok(())
44}
45
46pub fn encode(data: &[u8]) -> String {
47 let mut result = String::with_capacity(data.len().div_ceil(2) * 3);
48
49 let (chunks, rest) = data.as_chunks::<2>();
50 for chunk in chunks {
51 let n = (chunk[0] as u16) * 256 + (chunk[1] as u16);
52 let c = (n % 45) as usize;
53 let d = ((n / 45) % 45) as usize;
54 let e = ((n / 45 / 45) % 45) as usize;
55 result.push(ALPHABET[c] as char);
56 result.push(ALPHABET[d] as char);
57 result.push(ALPHABET[e] as char);
58 }
59
60 if let [b] = rest {
61 let n = *b as u16;
62 let c = (n % 45) as usize;
63 let d = ((n / 45) % 45) as usize;
64 result.push(ALPHABET[c] as char);
65 result.push(ALPHABET[d] as char);
66 }
67
68 result
69}
70
71pub fn decode(s: &str) -> Result<Vec<u8>> {
72 let s = s.trim();
73 if s.is_empty() {
74 return Ok(Vec::new());
75 }
76
77 let mut values = Vec::with_capacity(s.len());
78 for ch in s.chars() {
79 if !ch.is_ascii() {
80 anyhow::bail!("Invalid Base45 character: {ch}");
81 }
82 let v = DECODE_TABLE[ch as usize];
83 if v == 255 {
84 anyhow::bail!("Invalid Base45 character: {ch}");
85 }
86 values.push(v as u32);
87 }
88
89 let mut result = Vec::with_capacity(values.len() / 3 * 2);
90 let (chunks, rest) = values.as_chunks::<3>();
91 for chunk in chunks {
92 let n = chunk[0] + chunk[1] * 45 + chunk[2] * 45 * 45;
93 if n > 0xFFFF {
94 anyhow::bail!("Invalid Base45 group: value {n} exceeds 0xFFFF");
95 }
96 result.push((n / 256) as u8);
97 result.push((n % 256) as u8);
98 }
99
100 match rest.len() {
101 0 => {}
102 2 => {
103 let n = rest[0] + rest[1] * 45;
104 if n > 0xFF {
105 anyhow::bail!("Invalid Base45 group: value {n} exceeds 0xFF");
106 }
107 result.push(n as u8);
108 }
109 _ => {
110 anyhow::bail!("Invalid Base45 length: trailing group must be 2 characters");
111 }
112 }
113
114 Ok(result)
115}