// base64_value.mbt — Base64 and hex helpers.
//
// RFC 9651 `sf-bytes` (used by the Signature field and by Content-Digest)
// serializes a byte sequence with standard, padded base64 (RFC 4648 §4).
// The core library exposes `encode`/`decode`, but `decode` raises on
// malformed input. These wrappers present a `Result`-based API that maps
// malformed input onto `HsError` so callers never see a panic.
///|
/// Encodes bytes as standard padded base64 (the `sf-bytes` serialization).
pub fn base64_encode_bytes(bytes : Bytes) -> String {
@b64.encode(bytes)
}
///|
/// Decodes a standard padded base64 string into bytes.
///
/// Returns `InvalidBase64` with a byte offset on any malformed input,
/// including bad characters, bad padding, and stray whitespace.
pub fn base64_decode_bytes(s : String) -> Result[Bytes, HsError] {
Ok(@b64.decode(s)) catch {
_ =>
Err(
hs_error(
StructuredFieldParsing,
InvalidBase64,
"invalid base64 byte sequence",
),
)
}
}
///|
/// Encodes bytes as lowercase hex (used for SHA-256 digests in diagnostics
/// and for the fixture integrity checksum).
pub fn hex_encode_bytes(bytes : Bytes) -> String {
let b = @buf.Buffer::Buffer(size_hint=bytes.length() * 2)
for byte in bytes {
b.write_byte(hex_nibble((byte.to_int() >> 4) & 0x0F))
b.write_byte(hex_nibble(byte.to_int() & 0x0F))
}
// Buffer holds ASCII bytes; decode as UTF-8 (lossless for hex).
@utf8.decode(b.to_bytes()) catch {
_ => abort("hex buffer decode failed")
}
}
///|
/// Returns the lowercase hex nibble for a value in 0..15.
fn hex_nibble(v : Int) -> Byte {
if v < 10 {
(b'0'.to_int() + v).to_byte()
} else {
(b'a'.to_int() + v - 10).to_byte()
}
}
///|
/// Converts a UTF-8 byte buffer to a `String`.
///
/// Core's `Buffer::to_string()` interprets the buffer as UTF-16, which is
/// wrong for content written with `write_string_utf8`/`write_char_utf8`.
/// All string-building in this library uses UTF-8, so every conversion must
/// go through `@utf8.decode`. The content we build is always valid UTF-8, so
/// the raise can never fire in practice.
pub fn buffer_to_string(buf : @buf.Buffer) -> String raise HsError {
@utf8.decode(buf.to_bytes()) catch {
_ =>
raise hs_error(
StructuredFieldParsing,
SerializationFailure,
"internal UTF-8 decode failed",
)
}
}
///|
/// Decodes a lowercase or uppercase hex string into bytes.
///
/// Returns `InvalidBase64` (reusing the generic base64 kind for byte-decoding
/// failures) when the input length is odd or a character is not hex.
pub fn hex_decode_bytes(s : String) -> Result[Bytes, HsError] {
if s.length() % 2 != 0 {
return Err(
hs_error(StructuredFieldParsing, InvalidBase64, "odd-length hex input"),
)
}
let raw = FixedArray::make(s.length() / 2, b'\x00')
for i = 0; i < s.length(); i = i + 2 {
let hi = hex_digit(s.get_char(i))
let lo = hex_digit(s.get_char(i + 1))
if hi < 0 || lo < 0 {
return Err(
hs_error(StructuredFieldParsing, InvalidBase64, "invalid hex digit"),
)
}
raw[i / 2] = ((hi << 4) | lo).to_byte()
}
Ok(Bytes::from_array(raw))
}
///|
/// Returns the numeric value of a hex digit character, or `-1` if absent or
/// not a hex digit.
fn hex_digit(c : Char?) -> Int {
match c {
Some(ch) => {
let i = ch.to_int()
if i >= '0'.to_int() && i <= '9'.to_int() {
i - '0'.to_int()
} else if i >= 'a'.to_int() && i <= 'f'.to_int() {
i - 'a'.to_int() + 10
} else if i >= 'A'.to_int() && i <= 'F'.to_int() {
i - 'A'.to_int() + 10
} else {
-1
}
}
None => -1
}
}