Skip to main content

happy_cracking/crypto/
base45.rs

1use 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}