1use avila_error::{Error, ErrorKind, Result};
4
5const BASE64_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7
8pub 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
41pub 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
81pub 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 } 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 if line_len >= 73 {
103 output.push_str("=\r\n");
104 line_len = 0;
105 }
106 }
107
108 output
109}
110
111pub 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
129pub 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}