1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#[deny(missing_docs)]

/// Encodes bytes into a uuencoded string.
pub fn uuencode(filename: &str, input: &[u8]) -> String {
    let mut output : Vec<u8> = Vec::new();
    // in rust, char != u8, so we need to prefix with a b
    output.extend(b"begin 644 ");
    output.extend(filename.as_bytes());
    output.extend(b"\n");
    for line in input.chunks(45) {
        let line_length = line.len() as u8 + 32;
        output.push(line_length);
        for c in line.chunks(3) {
            output.extend(uuencode_chuck(c).into_iter());
        }
        output.push(b'\n');
    }
    output.extend(b"`\nend");

    String::from_utf8(output).unwrap()
}

/// Decodes a uuencoded string into bytes, and returns those bytes and the file's name
pub fn uudecode(encoded: &str) -> Option<(Vec<u8>, String)> {
    let mut lines = encoded.lines();

    let name = lines.next().expect("No next lines!").split(" ").collect::<Vec<_>>()[2].to_string(); //eugh

    let mut output: Vec<u8> = Vec::new();
    for line in lines {
        let padded_line = maybe_pad_line(line);
        if let Some(chr) = padded_line.chars().nth(0) {
            match chr {
                '`' => break,
                ' '..='_' => {
                    for dc in padded_line[1..].as_bytes().chunks(4) {
                        output.extend( uudecode_chunk(dc) );
                    }
                },
                _ => break
            }
        }
    }
    Some((output, name))
}

/// Encodes a few bytes
fn uuencode_chuck(input: &[u8]) -> [u8;4] {
    // padding is hard
    let i = [ input[0],
        *input.get(1).unwrap_or(&0),
        *input.get(2).unwrap_or(&0) ];

    [ 32 + (i[0]>>2),
        32 + ((i[0]<<6 | i[1]>>2) >> 2),
        32 + ((i[1]<<4 | i[2]>>4) >> 2),
        32 + ((i[2]<<2) >> 2) ]
}

/// Decodes a few bytes
fn uudecode_chunk(input: &[u8]) -> impl Iterator<Item=u8> {
    let combined: u32 = input.iter().enumerate()
        .fold(0, | acc, (index, &val) | {
            acc + (((val as u32) - 32) << 6 * (3 - index))
        });

    (0..3).rev().map(move |val| {
        let val = (combined >> (8 * val)) & 255;
        val as u8
    })
}

/// Ensures that a line has sufficient padding
fn maybe_pad_line(line: &str) -> String {
    const REQUIRED_LENGTH: usize = 61;
    let actual_length = line.len();
    let diff = REQUIRED_LENGTH - actual_length;
    match diff {
        d if d <= 0 => String::from(line),
        _ => {
            let mut padded = String::from(line);
            for _i in 1..=diff {
                padded.push(' ');
            }
            return padded;
        },
    }
}


mod test {
    use crate::*;

    #[test]
    fn test_cat() {
        let filename = "wow.jpg";
        let original_encoded = "begin 644 wow.jpg\nM0V%T                                                        \n`\nend";
        let decoded = uudecode(original_encoded).unwrap();
        let encoded = uuencode(filename, decoded.0.as_slice());
        assert_eq!(original_encoded, encoded);
    }

    #[test]
    fn test_logo() {
        let filename = "amglogoa09.jpg";
        let original_encoded = include_str!("../images/logo_encoded_padded").trim();
        let decoded = uudecode(original_encoded).unwrap();
        let encoded = uuencode(filename, decoded.0.as_slice());
        assert_eq!(original_encoded, encoded);
    }

    #[test]
    fn test_piechart() {
        let filename = "aumpiechartscombinded5217v4.jpg";
        let original_encoded = include_str!("../images/piechart_encoded_padded").trim();
        let decoded = uudecode(original_encoded).unwrap();
        let encoded = uuencode(filename, decoded.0.as_slice());
        assert_eq!(original_encoded, encoded);
    }
    
    #[test]
    fn test_pad_line() {
        let unpadded = r#"=HHH **** "BBB@ HHHH **** "BBB@ HHHH _]D!"#;
        let padded = r#"=HHH **** "BBB@ HHHH **** "BBB@ HHHH _]D!                    "#;
        let r = maybe_pad_line(unpadded);
        assert_eq!(padded, r);
    }
}