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 mut chunks = data.chunks_exact(2);
50    for chunk in chunks.by_ref() {
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    let rest = chunks.remainder();
61    if let [b] = rest {
62        let n = *b as u16;
63        let c = (n % 45) as usize;
64        let d = ((n / 45) % 45) as usize;
65        result.push(ALPHABET[c] as char);
66        result.push(ALPHABET[d] as char);
67    }
68
69    result
70}
71
72pub fn decode(s: &str) -> Result<Vec<u8>> {
73    let s = s.trim();
74    if s.is_empty() {
75        return Ok(Vec::new());
76    }
77
78    let mut values = Vec::with_capacity(s.len());
79    for ch in s.chars() {
80        if !ch.is_ascii() {
81            anyhow::bail!("Invalid Base45 character: {ch}");
82        }
83        let v = DECODE_TABLE[ch as usize];
84        if v == 255 {
85            anyhow::bail!("Invalid Base45 character: {ch}");
86        }
87        values.push(v as u32);
88    }
89
90    let mut result = Vec::with_capacity(values.len() / 3 * 2);
91    let mut chunks = values.chunks_exact(3);
92    for chunk in chunks.by_ref() {
93        let n = chunk[0] + chunk[1] * 45 + chunk[2] * 45 * 45;
94        if n > 0xFFFF {
95            anyhow::bail!("Invalid Base45 group: value {n} exceeds 0xFFFF");
96        }
97        result.push((n / 256) as u8);
98        result.push((n % 256) as u8);
99    }
100
101    let rest = chunks.remainder();
102    match rest.len() {
103        0 => {}
104        2 => {
105            let n = rest[0] + rest[1] * 45;
106            if n > 0xFF {
107                anyhow::bail!("Invalid Base45 group: value {n} exceeds 0xFF");
108            }
109            result.push(n as u8);
110        }
111        _ => {
112            anyhow::bail!("Invalid Base45 length: trailing group must be 2 characters");
113        }
114    }
115
116    Ok(result)
117}