#[derive(Debug)]
pub struct UUEncodeError {
line: usize,
character: usize,
msg: String,
}
impl std::error::Error for UUEncodeError {}
impl std::fmt::Display for UUEncodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} at line {} character {}", self.msg, self.line, self.character)
}
}
macro_rules! ok_or_decode_error {
($f:ident, $input:expr, $cur_line:expr, $cur_char:expr) => {
match $f($input) {
Some(value) => value,
None => {
return Err(UUEncodeError {
line: $cur_line,
character: $cur_char,
msg: format!("Invalid character in input: {}", $input as char),
});
}
}
}
}
pub fn uuencode(data: &[u8]) -> Result<String, UUEncodeError> {
let mut encoded = String::new();
let mut buffer = [0u8; 3];
let mut cur_line = 0;
let mut line_chunks = data.chunks(45).into_iter().peekable();
while let Some(line_chunk) = line_chunks.next() {
let mut cur_char = 0;
encoded.push(ok_or_decode_error!(encode_char, line_chunk.len() as u8, cur_line, cur_char).into());
for chunk in line_chunk.chunks(3) {
let len = chunk.len();
buffer.fill(0u8);
buffer[..len].copy_from_slice(chunk);
encoded.push(ok_or_decode_error!(encode_char, (buffer[0] >> 2) & 0x3F, cur_line, cur_char).into());
encoded.push(ok_or_decode_error!(encode_char, ((buffer[0] << 4) | (buffer[1] >> 4)) & 0x3F, cur_line, cur_char+1).into());
encoded.push(ok_or_decode_error!(encode_char, ((buffer[1] << 2) | (buffer[2] >> 6)) & 0x3F, cur_line, cur_char+1).into());
encoded.push(ok_or_decode_error!(encode_char, buffer[2] & 0x3F, cur_line, cur_char+2).into());
cur_char += len;
}
if line_chunks.peek().is_some() {
cur_line += 1;
encoded.push('\n');
}
}
Ok(encoded)
}
#[inline]
fn encoded_to_raw_len(encoded_len: usize) -> usize {
((encoded_len + 3) / 4) * 3
}
pub fn uudecode(data: &[u8]) -> Result<Vec<u8>, UUEncodeError> {
let mut decoded = Vec::with_capacity(encoded_to_raw_len(data.len()));
let mut buffer = [0u8; 4];
let mut cur_line = 0;
let mut input_iter = data.into_iter();
loop {
let mut cur_input_char = 0;
let mut cur_output_char = 0;
let next_token = input_iter.next();
let output_char_count: usize = match next_token {
None => return Ok(decoded),
Some(ch) => {
ok_or_decode_error!(decode_char, *ch, cur_line, cur_input_char) as usize
},
};
loop {
let mut chunk = [0u8;4];
let input_chunk = input_iter.by_ref().take(4).copied().collect::<Vec<_>>();
chunk[..].copy_from_slice(&input_chunk);
buffer[0] = ok_or_decode_error!(decode_char, chunk[0], cur_line, cur_input_char);
buffer[1] = ok_or_decode_error!(decode_char, chunk[1], cur_line, cur_input_char+1);
buffer[2] = ok_or_decode_error!(decode_char, chunk[2], cur_line, cur_input_char+2);
buffer[3] = ok_or_decode_error!(decode_char, chunk[3], cur_line, cur_input_char+3);
decoded.push(((buffer[0] << 2) | (buffer[1] >> 4)).into());
let byte2 = (buffer[1] << 4) | (buffer[2] >> 2);
if cur_output_char+1 < output_char_count {
decoded.push(byte2.into());
}
let byte3 = (buffer[2] << 6) | buffer[3];
if cur_output_char+2 < output_char_count {
decoded.push(byte3.into());
}
cur_output_char += 3;
cur_input_char += 4;
if cur_output_char >= output_char_count {
break;
}
}
input_iter.next(); cur_line += 1;
}
}
#[inline]
pub fn encode_char(value: u8) -> Option<u8> {
if value == 0 {
Some(b'`')
} else {
value.checked_add(32)
}
}
#[inline]
pub fn decode_char(value: u8) -> Option<u8> {
if value == b'`' || value == 0 {
Some(0)
} else {
value.checked_sub(32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cat() {
let data = b"cat";
let encoded = uuencode(data).unwrap();
assert_eq!(encoded, "#8V%T", "can uuencode a small text");
}
#[test]
fn test_the_machine_stops() {
let source_data = std::fs::read("test_data/the_machine_stops.txt").expect("Can open test data");
let expected_data = std::fs::read_to_string(&"test_data/the_machine_stops.txt.uu").expect("Can open test data").trim_end().to_string();
let actual = uuencode(&source_data).unwrap();
assert_eq!(actual, expected_data, "can uuencode a large text");
}
#[test]
fn test_random_data() {
let source_data = std::fs::read("test_data/random_data.bin").expect("Can open test data");
let expected_data = std::fs::read_to_string(&"test_data/random_data.bin.uu").expect("Can open test data").trim_end().to_string();
let actual = uuencode(&source_data).unwrap();
assert_eq!(actual, expected_data, "can uuencode random data");
}
#[test]
fn test_decode_cat() {
let data = b"#8V%T";
let encoded = uudecode(data).unwrap();
assert_eq!(String::from_utf8_lossy(&encoded), "cat", "can uuencode a small text");
}
#[test]
fn test_decode_the_machine_stops() {
let source_data = std::fs::read("test_data/the_machine_stops.txt.uu").expect("Can open test data");
let original_data = std::fs::read("test_data/the_machine_stops.txt").expect("Can open test data");
let orig_as_str = String::from_utf8_lossy(&original_data);
let decoded = uudecode(&source_data).unwrap();
assert_eq!(String::from_utf8_lossy(&decoded), orig_as_str, "can uudecode a large text");
}
#[test]
fn test_can_decode_last_line() {
let data = b".;W)K+`HQ.38X*2X*,C4`";
let encoded = uudecode(data).unwrap();
assert_eq!(String::from_utf8_lossy(&encoded), "ork,\n1968).\n25", "can uudecode a small text");
}
#[test]
fn test_rt() {
let source_data = std::fs::read("test_data/the_machine_stops.txt").expect("Can open test data");
let source_as_string = String::from_utf8_lossy(&source_data).trim_end().to_string();
let encoded = uuencode(&source_data).unwrap();
let decoded = uudecode(encoded.as_bytes()).unwrap();
assert_eq!(String::from_utf8_lossy(&decoded), source_as_string, "can uuencode and uudecode");
}
}