Skip to main content

happy_cracking/crypto/
base62.rs

1use anyhow::{Context, Result};
2use clap::Subcommand;
3use num_bigint::BigUint;
4use num_traits::Zero;
5use std::sync::LazyLock;
6
7#[derive(Subcommand)]
8pub enum Base62Action {
9    #[command(about = "Encode to Base62")]
10    Encode {
11        #[arg(help = "Input text")]
12        input: String,
13    },
14    #[command(about = "Decode from Base62")]
15    Decode {
16        #[arg(help = "Base62 encoded string")]
17        input: String,
18    },
19}
20
21pub fn run(action: Base62Action) -> Result<()> {
22    match action {
23        Base62Action::Encode { input } => {
24            println!("{}", encode(&input));
25        }
26        Base62Action::Decode { input } => {
27            println!("{}", decode(&input)?);
28        }
29    }
30    Ok(())
31}
32
33const ALPHABET: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
34const BASE: u32 = 62;
35
36// Reverse lookup table: maps byte value -> alphabet index (0..61), or 0xFF for invalid.
37// This replaces O(62) linear scan per character with O(1) array indexing.
38static DECODE_TABLE: LazyLock<[u8; 256]> = LazyLock::new(|| {
39    let mut table = [0xFFu8; 256];
40    for (i, &c) in ALPHABET.iter().enumerate() {
41        table[c as usize] = i as u8;
42    }
43    table
44});
45
46pub fn encode(input: &str) -> String {
47    let bytes = input.as_bytes();
48    if bytes.is_empty() {
49        return String::new();
50    }
51
52    let leading_zeros = bytes.iter().take_while(|&&b| b == 0).count();
53
54    let mut num = BigUint::from_bytes_be(bytes);
55    let base = BigUint::from(BASE);
56    let mut encoded = Vec::new();
57
58    while !num.is_zero() {
59        let remainder = &num % &base;
60        let digit = remainder.to_u32_digits().first().copied().unwrap_or(0) as usize;
61        encoded.push(ALPHABET[digit] as char);
62        num /= &base;
63    }
64
65    for _ in 0..leading_zeros {
66        encoded.push(ALPHABET[0] as char);
67    }
68
69    encoded.iter().rev().collect()
70}
71
72pub fn decode(input: &str) -> Result<String> {
73    let input = input.trim();
74    if input.is_empty() {
75        return Ok(String::new());
76    }
77
78    let leading_zeros = input.bytes().take_while(|&b| b == ALPHABET[0]).count();
79
80    let base = BigUint::from(BASE);
81    let mut num = BigUint::zero();
82
83    for c in input.bytes() {
84        let digit = DECODE_TABLE[c as usize];
85        if digit == 0xFF {
86            anyhow::bail!("Invalid Base62 character: {}", c as char);
87        }
88        num = num * &base + BigUint::from(digit as u32);
89    }
90
91    let mut bytes = num.to_bytes_be();
92
93    let mut result = vec![0u8; leading_zeros];
94    result.append(&mut bytes);
95
96    if result.is_empty() {
97        return Ok(String::new());
98    }
99
100    String::from_utf8(result).context("Decoded data is not valid UTF-8")
101}