// percent_codec.mbt — RFC 3986 percent-encoding codec for RFC 8187 values.
//
// RFC 8187 extended values percent-encode every value byte that is not an
// `attr-char`. This module provides the two primitive operations used by
// the RFC 8187 parser and serializer: strict percent-decoding of a byte
// range, and deterministic percent-encoding of a UTF-8 string with
// uppercase hex digits. The decoding is deliberately strict: `%` must be
// followed by two hex digits, and the decoded bytes must be valid UTF-8
// (checked by the caller with the appropriate charset).
///|
/// Decodes the percent-encoded octets in `bytes[start, end)` into a raw
/// byte array. Every `%` must be followed by exactly two hex digits.
///
/// Errors: `PercentEncoding::InvalidPercentEncoding` (`%` not followed by
/// two hex digits, or a truncated escape at the end of the range).
pub fn percent_decode_bytes(
bytes : Bytes,
start : Int,
end : Int
) -> Result[Array[Byte], DispositionError] {
let out : Array[Byte] = []
let mut i = start
while i < end {
let b = bytes[i]
if b == 37 {
// '%' followed by two hex digits, with explicit bounds checks so a
// truncated escape at the end of the range is reported, not read
// out of bounds.
let h1 = if i + 1 < end { bytes[i + 1] } else { b'\x00' }
let h2 = if i + 2 < end { bytes[i + 2] } else { b'\x00' }
let v1 = hex_value(h1)
let v2 = hex_value(h2)
if i + 2 < end && v1 >= 0 && v2 >= 0 {
out.push((v1 * 16 + v2).to_byte())
i = i + 3
} else {
return Err(
disposition_error_at(
PercentEncoding,
InvalidPercentEncoding,
i,
"expected '%' followed by two hex digits",
),
)
}
} else {
out.push(b)
i = i + 1
}
}
Ok(out)
}
///|
/// Percent-encodes a string for use as an RFC 8187 value. The string is
/// UTF-8 encoded; every byte that is not an `attr-char` is percent-encoded
/// with uppercase hex digits. The result is deterministic.
pub fn percent_encode_attr_value(value : String) -> String {
let bytes = @utf8.encode(value)
let sb = StringBuilder()
for i = 0; i < bytes.length(); i = i + 1 {
let b = bytes[i]
if attr_char(b) {
sb.write_char(b.to_char())
} else {
sb.write_char('%')
sb.write_char(hex_char(b.to_int() >> 4))
sb.write_char(hex_char(b.to_int() & 0xF))
}
}
sb.to_string()
}
///|
/// Percent-decodes a whole string and validates the decoded bytes as UTF-8.
///
/// Errors: `PercentEncoding::InvalidPercentEncoding` and
/// `PercentEncoding::InvalidUtf8`.
pub fn percent_decode_string(input : String) -> Result[String, DispositionError] {
let bytes = @utf8.encode(input)
let raw = match percent_decode_bytes(bytes, 0, bytes.length()) {
Ok(r) => r
Err(e) => return Err(e)
}
let encoded = Bytes::from_array(raw)
let value = try {
@utf8.decode(encoded.view(start=0, end=encoded.length()))
} catch {
_ =>
return Err(
disposition_error(PercentEncoding, InvalidUtf8, "decoded bytes are not valid UTF-8"),
)
}
Ok(value)
}
///|
/// Whether a string contains any percent-encoded octets (used by tests).
pub fn has_percent_encoding(value : String) -> Bool {
value.contains("%")
}