happy-cracking 0.5.0

A fast, comprehensive CTF toolkit for cryptographic encoding/decoding, classic ciphers, hash operations, and analysis tools
Documentation
use anyhow::{Context, Result};
use clap::Subcommand;
use std::sync::LazyLock;

const ALPHABET: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";

static DECODE_TABLE: LazyLock<[u8; 256]> = LazyLock::new(|| {
    let mut table = [255u8; 256];
    for (i, &c) in ALPHABET.iter().enumerate() {
        table[c as usize] = i as u8;
    }
    table
});

#[derive(Subcommand)]
pub enum Base45Action {
    #[command(about = "Encode to Base45 (RFC 9285)")]
    Encode {
        #[arg(help = "Input text")]
        input: String,
    },
    #[command(about = "Decode from Base45 (RFC 9285)")]
    Decode {
        #[arg(help = "Base45 encoded string")]
        input: String,
    },
}

pub fn run(action: Base45Action) -> Result<()> {
    match action {
        Base45Action::Encode { input } => {
            println!("{}", encode(input.as_bytes()));
        }
        Base45Action::Decode { input } => {
            let bytes = decode(&input)?;

            match String::from_utf8(bytes.clone()).context("Decoded data is not valid UTF-8") {
                Ok(s) => println!("{s}"),
                Err(_) => println!("(lossy) {}", String::from_utf8_lossy(&bytes)),
            }
        }
    }
    Ok(())
}

pub fn encode(data: &[u8]) -> String {
    let mut result = String::with_capacity(data.len().div_ceil(2) * 3);

    let (chunks, rest) = data.as_chunks::<2>();
    for chunk in chunks {
        let n = (chunk[0] as u16) * 256 + (chunk[1] as u16);
        let c = (n % 45) as usize;
        let d = ((n / 45) % 45) as usize;
        let e = ((n / 45 / 45) % 45) as usize;
        result.push(ALPHABET[c] as char);
        result.push(ALPHABET[d] as char);
        result.push(ALPHABET[e] as char);
    }

    if let [b] = rest {
        let n = *b as u16;
        let c = (n % 45) as usize;
        let d = ((n / 45) % 45) as usize;
        result.push(ALPHABET[c] as char);
        result.push(ALPHABET[d] as char);
    }

    result
}

pub fn decode(s: &str) -> Result<Vec<u8>> {
    let s = s.trim();
    if s.is_empty() {
        return Ok(Vec::new());
    }

    let mut values = Vec::with_capacity(s.len());
    for ch in s.chars() {
        if !ch.is_ascii() {
            anyhow::bail!("Invalid Base45 character: {ch}");
        }
        let v = DECODE_TABLE[ch as usize];
        if v == 255 {
            anyhow::bail!("Invalid Base45 character: {ch}");
        }
        values.push(v as u32);
    }

    let mut result = Vec::with_capacity(values.len() / 3 * 2);
    let (chunks, rest) = values.as_chunks::<3>();
    for chunk in chunks {
        let n = chunk[0] + chunk[1] * 45 + chunk[2] * 45 * 45;
        if n > 0xFFFF {
            anyhow::bail!("Invalid Base45 group: value {n} exceeds 0xFFFF");
        }
        result.push((n / 256) as u8);
        result.push((n % 256) as u8);
    }

    match rest.len() {
        0 => {}
        2 => {
            let n = rest[0] + rest[1] * 45;
            if n > 0xFF {
                anyhow::bail!("Invalid Base45 group: value {n} exceeds 0xFF");
            }
            result.push(n as u8);
        }
        _ => {
            anyhow::bail!("Invalid Base45 length: trailing group must be 2 characters");
        }
    }

    Ok(result)
}