/// Content-Transfer-Encoding codecs for MIME bodies.
///
/// Implements the two standard MIME content transfer encodings
/// defined in RFC 2045 section 6:
/// - Base64 (section 6.8)
/// - Quoted-Printable (section 6.7)
///
/// All public functions use the b64_ / qp_ prefixes to avoid name
/// collisions with internal helpers in other modules.
///|
/// Encode a byte sequence as Base64, with 76-character line wrapping.
pub fn b64_encode_bytes(data : Array[Int]) -> String {
let table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
let sb = StringBuilder::new()
let len = data.length()
let mut col = 0
let mut i = 0
while i < len {
let b1 = data[i] & 0xFF
let b2 = if i + 1 < len { data[i + 1] & 0xFF } else { 0 }
let b3 = if i + 2 < len { data[i + 2] & 0xFF } else { 0 }
let c1 = (b1 >> 2) & 0x3F
let c2 = ((b1 << 4) | (b2 >> 4)) & 0x3F
let c3 = if i + 1 < len { ((b2 << 2) | (b3 >> 6)) & 0x3F } else { 64 }
let c4 = if i + 2 < len { b3 & 0x3F } else { 64 }
sb.write_char(table.get_char(c1).unwrap())
sb.write_char(table.get_char(c2).unwrap())
sb.write_char(if i + 1 < len { table.get_char(c3).unwrap() } else { '=' })
sb.write_char(if i + 2 < len { table.get_char(c4).unwrap() } else { '=' })
col = col + 4
if col >= 76 && i + 3 < len {
sb.write_string("\r\n")
col = 0
}
i = i + 3
}
sb.to_string()
}
///|
/// Decode a Base64 string into a byte sequence.
/// Returns None if the input is not valid Base64.
pub fn b64_decode_bytes(input : String) -> Array[Int]? {
let table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
let compact = strip_b64_ws(input)
let len = compact.length()
if len % 4 != 0 {
return None
}
let result : Array[Int] = []
let mut i = 0
while i < len {
let c1 = compact.get_char(i).unwrap()
let c2 = compact.get_char(i + 1).unwrap()
let c3 = compact.get_char(i + 2).unwrap()
let c4 = compact.get_char(i + 3).unwrap()
let v1 = b64_index_of(table, c1)
let v2 = b64_index_of(table, c2)
let v3 = b64_index_of(table, c3)
let v4 = b64_index_of(table, c4)
match (v1, v2, v3, v4) {
(Some(b1), Some(b2), Some(b3), Some(b4)) => {
let byte1 = (b1 << 2) | (b2 >> 4)
result.push(byte1)
if c3 != '=' {
let byte2 = ((b2 & 0xF) << 4) | (b3 >> 2)
result.push(byte2)
}
if c4 != '=' {
let byte3 = ((b3 & 0x3) << 6) | b4
result.push(byte3)
}
}
_ => return None
}
i = i + 4
}
Some(result)
}
///|
/// Decode a Base64 string into a text string (Latin-1 mapping).
pub fn b64_decode_text(input : String) -> String? {
let bytes = b64_decode_bytes(input)
match bytes {
None => None
Some(bs) => {
let sb = StringBuilder::new()
for b in bs {
sb.write_char(Int::unsafe_to_char(b))
}
Some(sb.to_string())
}
}
}
///|
/// Encode a text string as Base64 using Latin-1 mapping.
pub fn b64_encode_text(s : String) -> String {
let bytes : Array[Int] = []
for c in s {
bytes.push(c.to_int())
}
b64_encode_bytes(bytes)
}
///|
/// Encode a byte sequence as Quoted-Printable (RFC 2045 6.7).
/// Non-printable bytes and '=' are escaped as =XX. Soft line breaks
/// are inserted at 75 columns.
pub fn qp_encode_bytes(data : Array[Int]) -> String {
let sb = StringBuilder::new()
let len = data.length()
let mut col = 0
let mut i = 0
while i < len {
let b = data[i] & 0xFF
// Check if this byte needs escaping
let printable = (b >= 33 && b <= 60) || (b >= 62 && b <= 126)
if printable {
// Need to check for line length soft break
if col >= 75 {
sb.write_string("=\r\n")
col = 0
}
sb.write_char(Int::unsafe_to_char(b))
col = col + 1
} else if b == 9 || b == 32 {
// Tab and space: only escape if at end of line (conservative)
if col >= 75 {
sb.write_string("=\r\n")
col = 0
}
sb.write_char(Int::unsafe_to_char(b))
col = col + 1
} else {
if col >= 74 {
sb.write_string("=\r\n")
col = 0
}
sb.write_string("=")
sb.write_string(hex_byte_upper(b))
col = col + 3
}
i = i + 1
}
sb.to_string()
}
///|
/// Encode a text string as Quoted-Printable.
pub fn qp_encode_text(s : String) -> String {
let bytes : Array[Int] = []
for c in s {
bytes.push(c.to_int())
}
qp_encode_bytes(bytes)
}
///|
/// Decode a Quoted-Printable encoded string (RFC 2045 6.7).
/// Handles soft line breaks (=CRLF, =LF) and hex escapes (=XX).
pub fn qp_decode(input : String) -> String? {
let sb = StringBuilder::new()
let len = input.length()
let mut i = 0
while i < len {
let c = input.get_char(i).unwrap()
if c == '=' {
// Soft line break
if i + 1 < len && input.get_char(i + 1).unwrap() == '\n' {
i = i + 2
continue
} else if i + 2 < len &&
input.get_char(i + 1).unwrap() == '\r' &&
input.get_char(i + 2).unwrap() == '\n' {
i = i + 3
continue
} else if i + 2 < len {
let byte = hex_char_to_int(input.get_char(i + 1).unwrap())
match byte {
Some(b1) => {
let b2 = hex_char_to_int(input.get_char(i + 2).unwrap())
match b2 {
Some(l) => {
sb.write_char(Int::unsafe_to_char(b1 * 16 + l))
i = i + 3
continue
}
None => ()
}
}
None => ()
}
// Invalid escape: pass '=' through
sb.write_char('=')
i = i + 1
continue
} else {
sb.write_char(c)
i = i + 1
continue
}
}
sb.write_char(c)
i = i + 1
}
Some(sb.to_string())
}
///|
/// Convert a hex character to its integer value (0-15).
pub fn hex_char_to_int(c : Char) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'A' && c <= 'F' {
Some(c.to_int() - 'A'.to_int() + 10)
} else if c >= 'a' && c <= 'f' {
Some(c.to_int() - 'a'.to_int() + 10)
} else {
None
}
}
///|
/// Format a byte as two uppercase hex digits.
fn hex_byte_upper(b : Int) -> String {
let digits = "0123456789ABCDEF"
let hi = (b >> 4) & 0xF
let lo = b & 0xF
let sb = StringBuilder::new()
sb.write_char(digits.get_char(hi).unwrap())
sb.write_char(digits.get_char(lo).unwrap())
sb.to_string()
}
///|
/// Strip whitespace and '=' padding artifacts from a Base64 string.
fn strip_b64_ws(input : String) -> String {
let sb = StringBuilder::new()
for c in input {
if c != ' ' && c != '\t' && c != '\r' && c != '\n' {
sb.write_char(c)
}
}
sb.to_string()
}
///|
/// Find the index of a Base64 character in the standard alphabet.
fn b64_index_of(table : String, c : Char) -> Int? {
let mut i = 0
while i < table.length() {
if table.get_char(i).unwrap() == c {
return Some(i)
}
i = i + 1
}
if c == '=' {
return Some(0)
}
None
}
///|
/// Check whether a string contains only ASCII characters (0-127).
pub fn is_ascii_only(s : String) -> Bool {
for c in s {
if c.to_int() > 127 {
return false
}
}
true
}
///|
/// Check whether a string is valid printable ASCII text.
pub fn is_printable_ascii(s : String) -> Bool {
for c in s {
let v = c.to_int()
if v < 32 && v != 9 && v != 10 && v != 13 {
return false
}
if v > 126 {
return false
}
}
true
}
///|
/// Encode a non-ASCII string using RFC 2047 encoded-word Base64 form.
/// Only encodes if the input contains non-ASCII characters.
pub fn encode_encoded_word_b(s : String) -> String {
if is_ascii_only(s) {
return s
}
let encoded = b64_encode_text(s)
"=?UTF-8?B?" + encoded + "?="
}
///|
/// Encode a non-ASCII string using RFC 2047 encoded-word Q form.
/// Spaces become '_' in the encoded form.
pub fn encode_encoded_word_q(s : String) -> String {
if is_ascii_only(s) {
return s
}
let sb = StringBuilder::new()
for c in s {
let v = c.to_int()
if v > 127 || c == '=' || c == '?' || c == '_' {
sb.write_string("=")
sb.write_string(hex_byte_upper(v))
} else if c == ' ' {
sb.write_char('_')
} else {
sb.write_char(c)
}
}
"=?UTF-8?Q?" + sb.to_string() + "?="
}