Skip to main content

happy_cracking/crypto/
uuencode.rs

1use anyhow::{Context, Result};
2use clap::Subcommand;
3
4#[derive(Subcommand)]
5pub enum UuencodeAction {
6    #[command(about = "Encode to uuencode format")]
7    Encode {
8        #[arg(help = "Input text")]
9        input: String,
10        #[arg(long, default_value = "data", help = "Filename for the begin header")]
11        filename: String,
12    },
13    #[command(about = "Decode from uuencode format")]
14    Decode {
15        #[arg(help = "uuencoded text")]
16        input: String,
17    },
18}
19
20pub fn run(action: UuencodeAction) -> Result<()> {
21    match action {
22        UuencodeAction::Encode { input, filename } => {
23            println!("{}", encode(input.as_bytes(), &filename));
24        }
25        UuencodeAction::Decode { input } => {
26            let bytes = decode(&input)?;
27            match String::from_utf8(bytes.clone()) {
28                Ok(s) => println!("{s}"),
29                Err(_) => println!("{}", String::from_utf8_lossy(&bytes)),
30            }
31        }
32    }
33    Ok(())
34}
35
36fn uu_char(value: u8) -> u8 {
37    (value & 0x3F) + 0x20
38}
39
40pub fn encode(data: &[u8], filename: &str) -> String {
41    let mut out = String::new();
42    out.push_str(&format!("begin 644 {filename}\n"));
43
44    for chunk in data.chunks(45) {
45        out.push(uu_char(chunk.len() as u8) as char);
46
47        for triple in chunk.chunks(3) {
48            let b0 = triple[0];
49            let b1 = *triple.get(1).unwrap_or(&0);
50            let b2 = *triple.get(2).unwrap_or(&0);
51
52            out.push(uu_char(b0 >> 2) as char);
53            out.push(uu_char(((b0 << 4) | (b1 >> 4)) & 0x3F) as char);
54            out.push(uu_char(((b1 << 2) | (b2 >> 6)) & 0x3F) as char);
55            out.push(uu_char(b2 & 0x3F) as char);
56        }
57        out.push('\n');
58    }
59
60    out.push('`');
61    out.push('\n');
62    out.push_str("end\n");
63    out
64}
65
66fn uu_value(c: u8) -> Result<u8> {
67    match c {
68        b'`' | b' ' => Ok(0),
69        0x21..=0x60 => Ok(c - 0x20),
70        _ => anyhow::bail!("Invalid uuencode character: {:?}", c as char),
71    }
72}
73
74pub fn decode(s: &str) -> Result<Vec<u8>> {
75    let mut lines = s.lines();
76
77    let mut started = false;
78    for line in lines.by_ref() {
79        if line.starts_with("begin ") {
80            started = true;
81            break;
82        }
83    }
84    if !started {
85        anyhow::bail!("Missing uuencode 'begin' header");
86    }
87
88    let mut result = Vec::new();
89    for line in lines {
90        if line.starts_with("end") {
91            return Ok(result);
92        }
93        let bytes = line.as_bytes();
94        if bytes.is_empty() {
95            continue;
96        }
97
98        let count = uu_value(bytes[0]).context("Failed to read uuencode line length")? as usize;
99        if count == 0 {
100            continue;
101        }
102
103        let mut decoded = Vec::new();
104        let body = &bytes[1..];
105        for group in body.chunks(4) {
106            let v0 = uu_value(group[0]).context("Failed to decode uuencode data")?;
107            let v1 = uu_value(*group.get(1).unwrap_or(&b'`'))
108                .context("Failed to decode uuencode data")?;
109            let v2 = uu_value(*group.get(2).unwrap_or(&b'`'))
110                .context("Failed to decode uuencode data")?;
111            let v3 = uu_value(*group.get(3).unwrap_or(&b'`'))
112                .context("Failed to decode uuencode data")?;
113
114            decoded.push((v0 << 2) | (v1 >> 4));
115            decoded.push((v1 << 4) | (v2 >> 2));
116            decoded.push((v2 << 6) | v3);
117        }
118
119        if count > decoded.len() {
120            anyhow::bail!("uuencode line declares more bytes than it contains");
121        }
122        decoded.truncate(count);
123        result.extend_from_slice(&decoded);
124    }
125
126    anyhow::bail!("Missing uuencode 'end' footer");
127}