Skip to main content

happy_cracking/crypto/
base85.rs

1use anyhow::{Context, Result};
2use clap::Subcommand;
3
4#[derive(Subcommand)]
5pub enum Base85Action {
6    #[command(about = "Encode to Base85 (Ascii85)")]
7    Encode {
8        #[arg(help = "Input text")]
9        input: String,
10    },
11    #[command(about = "Decode from Base85 (Ascii85)")]
12    Decode {
13        #[arg(help = "Base85 encoded string")]
14        input: String,
15    },
16}
17
18pub fn run(action: Base85Action) -> Result<()> {
19    match action {
20        Base85Action::Encode { input } => {
21            println!("{}", encode(&input));
22        }
23        Base85Action::Decode { input } => {
24            println!("{}", decode(&input)?);
25        }
26    }
27    Ok(())
28}
29
30pub fn encode(input: &str) -> String {
31    let bytes = input.as_bytes();
32    if bytes.is_empty() {
33        return String::new();
34    }
35
36    let mut result = String::new();
37    let chunks = bytes.chunks(4);
38
39    for chunk in chunks {
40        if chunk.len() == 4 {
41            let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
42            if value == 0 {
43                result.push('z');
44            } else {
45                let mut chars = [0u8; 5];
46                let mut v = value;
47                for i in (0..5).rev() {
48                    chars[i] = (v % 85) as u8 + b'!';
49                    v /= 85;
50                }
51                for c in &chars {
52                    result.push(*c as char);
53                }
54            }
55        } else {
56            // Pad with zeros to 4 bytes
57            let mut padded = [0u8; 4];
58            padded[..chunk.len()].copy_from_slice(chunk);
59            let value = u32::from_be_bytes(padded);
60            let mut chars = [0u8; 5];
61            let mut v = value;
62            for i in (0..5).rev() {
63                chars[i] = (v % 85) as u8 + b'!';
64                v /= 85;
65            }
66            // Output ceil(chunk.len() + 1) characters for partial block
67            let output_len = chunk.len() + 1;
68            for c in &chars[..output_len] {
69                result.push(*c as char);
70            }
71        }
72    }
73
74    result
75}
76
77pub fn decode(input: &str) -> Result<String> {
78    let input = input.trim();
79    if input.is_empty() {
80        return Ok(String::new());
81    }
82
83    let mut bytes = Vec::new();
84    let chars: Vec<u8> = input.bytes().collect();
85    let mut i = 0;
86
87    while i < chars.len() {
88        if chars[i] == b'z' {
89            bytes.extend_from_slice(&[0, 0, 0, 0]);
90            i += 1;
91        } else {
92            let remaining = chars.len() - i;
93            let block_len = remaining.min(5);
94
95            if block_len < 2 {
96                anyhow::bail!("Invalid Base85 input: incomplete block");
97            }
98
99            // Pad short blocks with 'u' (ASCII 117, the highest value char)
100            let mut block = [b'u'; 5];
101            for j in 0..block_len {
102                let c = chars[i + j];
103                if !(b'!'..=b'u').contains(&c) {
104                    anyhow::bail!("Invalid Base85 character: {}", c as char);
105                }
106                block[j] = c;
107            }
108
109            let mut value: u64 = 0;
110            for &b in &block {
111                value = value * 85 + (b - b'!') as u64;
112            }
113
114            if value > u32::MAX as u64 {
115                anyhow::bail!("Invalid Base85 input: block value overflow");
116            }
117
118            let decoded = (value as u32).to_be_bytes();
119
120            if block_len == 5 {
121                bytes.extend_from_slice(&decoded);
122            } else {
123                // Partial block: output (block_len - 1) bytes
124                let output_len = block_len - 1;
125                bytes.extend_from_slice(&decoded[..output_len]);
126            }
127
128            i += block_len;
129        }
130    }
131
132    String::from_utf8(bytes).context("Decoded data is not valid UTF-8")
133}