avila_cell/
encoding.rs

1//! Encoding utilities - Base64, Quoted-Printable, URL encoding
2
3use avila_error::{Error, ErrorKind, Result};
4
5/// Base64 alphabet
6const BASE64_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7
8/// Encodes bytes to Base64
9pub fn base64_encode(input: &[u8]) -> String {
10    let mut output = String::new();
11    let mut i = 0;
12
13    while i < input.len() {
14        let b1 = input[i];
15        let b2 = input.get(i + 1).copied().unwrap_or(0);
16        let b3 = input.get(i + 2).copied().unwrap_or(0);
17
18        let n = ((b1 as u32) << 16) | ((b2 as u32) << 8) | (b3 as u32);
19
20        output.push(BASE64_ALPHABET[((n >> 18) & 0x3F) as usize] as char);
21        output.push(BASE64_ALPHABET[((n >> 12) & 0x3F) as usize] as char);
22
23        if i + 1 < input.len() {
24            output.push(BASE64_ALPHABET[((n >> 6) & 0x3F) as usize] as char);
25        } else {
26            output.push('=');
27        }
28
29        if i + 2 < input.len() {
30            output.push(BASE64_ALPHABET[(n & 0x3F) as usize] as char);
31        } else {
32            output.push('=');
33        }
34
35        i += 3;
36    }
37
38    output
39}
40
41/// Decodes Base64 to bytes
42pub fn base64_decode(input: &str) -> Result<Vec<u8>> {
43    let input = input.trim_end_matches('=');
44    let mut output = Vec::new();
45    let bytes = input.as_bytes();
46
47    let mut i = 0;
48    while i < bytes.len() {
49        let c1 = decode_base64_char(bytes[i])?;
50        let c2 = bytes.get(i + 1).map(|&b| decode_base64_char(b)).transpose()?.unwrap_or(0);
51        let c3 = bytes.get(i + 2).map(|&b| decode_base64_char(b)).transpose()?.unwrap_or(0);
52        let c4 = bytes.get(i + 3).map(|&b| decode_base64_char(b)).transpose()?.unwrap_or(0);
53
54        let n = ((c1 as u32) << 18) | ((c2 as u32) << 12) | ((c3 as u32) << 6) | (c4 as u32);
55
56        output.push((n >> 16) as u8);
57        if i + 2 < input.len() {
58            output.push((n >> 8) as u8);
59        }
60        if i + 3 < input.len() {
61            output.push(n as u8);
62        }
63
64        i += 4;
65    }
66
67    Ok(output)
68}
69
70fn decode_base64_char(c: u8) -> Result<u8> {
71    match c {
72        b'A'..=b'Z' => Ok(c - b'A'),
73        b'a'..=b'z' => Ok(c - b'a' + 26),
74        b'0'..=b'9' => Ok(c - b'0' + 52),
75        b'+' => Ok(62),
76        b'/' => Ok(63),
77        _ => Err(Error::new(ErrorKind::InvalidInput, "Invalid base64 character")),
78    }
79}
80
81/// Encodes text to Quoted-Printable
82pub fn quoted_printable_encode(input: &str) -> String {
83    let mut output = String::new();
84    let mut line_len = 0;
85
86    for byte in input.as_bytes() {
87        if *byte == b'\n' {
88            output.push_str("\r\n");
89            line_len = 0;
90        } else if *byte == b'\r' {
91            // Skip, serĂ¡ adicionado com \n
92        } else if (32..=126).contains(byte) && *byte != b'=' {
93            output.push(*byte as char);
94            line_len += 1;
95        } else {
96            let encoded = format!("={:02X}", byte);
97            output.push_str(&encoded);
98            line_len += 3;
99        }
100
101        // Soft line break at 76 characters
102        if line_len >= 73 {
103            output.push_str("=\r\n");
104            line_len = 0;
105        }
106    }
107
108    output
109}
110
111/// URL encodes a string
112pub fn url_encode(input: &str) -> String {
113    let mut output = String::new();
114
115    for byte in input.as_bytes() {
116        match byte {
117            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
118                output.push(*byte as char);
119            }
120            _ => {
121                output.push_str(&format!("%{:02X}", byte));
122            }
123        }
124    }
125
126    output
127}
128
129/// Generates boundary for multipart messages
130pub fn generate_boundary() -> String {
131    use avila_time::DateTime;
132    let timestamp = DateTime::now().timestamp();
133    format!("----=_Part_{}_{}@avila.inc", timestamp, timestamp % 100000)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_base64_encode() {
142        assert_eq!(base64_encode(b"Hello"), "SGVsbG8=");
143        assert_eq!(base64_encode(b"Hello World!"), "SGVsbG8gV29ybGQh");
144    }
145
146    #[test]
147    fn test_base64_decode() {
148        assert_eq!(base64_decode("SGVsbG8=").unwrap(), b"Hello");
149        assert_eq!(base64_decode("SGVsbG8gV29ybGQh").unwrap(), b"Hello World!");
150    }
151
152    #[test]
153    fn test_quoted_printable() {
154        let encoded = quoted_printable_encode("Hello\nWorld!");
155        assert!(encoded.contains("Hello"));
156        assert!(encoded.contains("World"));
157    }
158
159    #[test]
160    fn test_url_encode() {
161        assert_eq!(url_encode("hello world"), "hello%20world");
162        assert_eq!(url_encode("test@example.com"), "test%40example.com");
163    }
164}