happy_cracking/crypto/
uuencode.rs1use 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}