// charset.mbt — RFC 8187 charset policy.
//
// RFC 8187 Section 2 requires producers to use the UTF-8 charset for
// extended parameter values. Recipients may encounter other charsets.
// Rather than guessing, this library supports exactly two charsets, both
// implemented correctly and tested explicitly:
//
// - `UTF-8` — the required producer charset; decoded strictly.
// - `ISO-8859-1` — the safest legacy charset to support; each byte maps
// to the same Unicode code point (U+0080-U+00FF for the high half).
//
// Any other charset is rejected with
// `DispositionErrorKind::UnsupportedCharset`. There is deliberately no
// encoding sniffing and no "try UTF-8, then guess GBK" behaviour: an
// unsupported charset is reported, never guessed.
///|
/// Whether a charset name is supported (case-insensitive `UTF-8` or
/// `ISO-8859-1`).
pub fn is_supported_charset(charset : String) -> Bool {
charset.equal_ignore_ascii_case("UTF-8") || charset.equal_ignore_ascii_case("ISO-8859-1")
}
///|
/// The canonical form of a supported charset name, or `None` for an
/// unsupported charset. The canonical form is what the model stores and
/// what the serializer emits.
pub fn canonical_charset(charset : String) -> String? {
if charset.equal_ignore_ascii_case("UTF-8") {
Some("UTF-8")
} else if charset.equal_ignore_ascii_case("ISO-8859-1") {
Some("ISO-8859-1")
} else {
None
}
}
///|
/// Decodes a raw byte array using the given charset name. For `UTF-8` the
/// bytes must form valid UTF-8; for `ISO-8859-1` every byte maps to the
/// code point with the same value.
///
/// Errors: `Charset::UnsupportedCharset` (unknown charset) and
/// `Charset::InvalidUtf8` (bytes are not valid UTF-8).
pub fn decode_bytes(
charset : String,
bytes : Bytes
) -> Result[String, DispositionError] {
if charset.equal_ignore_ascii_case("UTF-8") {
let value = try {
@utf8.decode(bytes.view(start=0, end=bytes.length()))
} catch {
_ =>
return Err(
disposition_error(Charset, InvalidUtf8, "percent-decoded bytes are not valid UTF-8"),
)
}
Ok(value)
} else if charset.equal_ignore_ascii_case("ISO-8859-1") {
Ok(decode_iso8859_1(bytes))
} else {
Err(
disposition_error(
Charset,
UnsupportedCharset,
"unsupported charset: \{charset} (supported: UTF-8, ISO-8859-1)",
),
)
}
}
///|
/// Encodes a value string to raw bytes using the given charset name, for
/// serialisation. `UTF-8` always succeeds; `ISO-8859-1` succeeds only when
/// every code point in the value fits in Latin-1 (U+0000-U+00FF), otherwise
/// the encoder falls back to UTF-8 (a documented, deterministic
/// normalisation).
///
/// Errors: `Charset::UnsupportedCharset` (unknown charset).
pub fn encode_to_bytes(charset : String, value : String) -> Result[Bytes, DispositionError] {
if charset.equal_ignore_ascii_case("UTF-8") {
Ok(@utf8.encode(value))
} else if charset.equal_ignore_ascii_case("ISO-8859-1") {
match encode_iso8859_1(value) {
Some(bytes) => Ok(bytes)
None => Ok(@utf8.encode(value))
}
} else {
Err(
disposition_error(
Charset,
UnsupportedCharset,
"unsupported charset: \{charset} (supported: UTF-8, ISO-8859-1)",
),
)
}
}
///|
/// Whether every code point of `value` can be represented in ISO-8859-1.
pub fn fits_iso8859_1(value : String) -> Bool {
for ch in value {
if ch.to_int() > 0xFF {
return false
}
}
true
}
// Decodes ISO-8859-1 bytes to a UTF-8 encoded MoonBit String.
fn decode_iso8859_1(bytes : Bytes) -> String {
let out : Array[Byte] = []
for i = 0; i < bytes.length(); i = i + 1 {
let v = bytes[i].to_int()
if v < 0x80 {
out.push(bytes[i])
} else {
// Two-byte UTF-8 for U+0080-U+00FF. For 0x80-0xFF the lead byte is
// always 0xC2 or 0xC3, never an overlong form.
out.push((0xC0 | (v >> 6)).to_byte())
out.push((0x80 | (v & 0x3F)).to_byte())
}
}
let encoded = Bytes::from_array(out)
// The constructed byte array is always valid UTF-8 (every 0x80-0xFF byte
// maps to the two-byte sequence C2/C3 80-BF, never an overlong form), so
// the raise is purely a type-correctness measure and cannot fire.
let decoded = try {
@utf8.decode(encoded.view(start=0, end=encoded.length()))
} catch {
_ => abort("internal error: ISO-8859-1 decoding produced invalid UTF-8")
}
decoded
}
// Encodes a value string to ISO-8859-1 bytes, or `None` when a code point
// is outside Latin-1.
fn encode_iso8859_1(value : String) -> Bytes? {
let out : Array[Byte] = []
for ch in value {
let v = ch.to_int()
if v > 0xFF {
return None
}
out.push(v.to_byte())
}
Some(Bytes::from_array(out))
}