Encoding

let hex_encode (b: Bytes): String

Encodes bytes as lowercase hex, two characters per byte.

The same function as Bytes.to_hex, which is in the prelude and needs no use. Offered here so that a program already reaching for Encoding finds the whole family in one place.

let hex_decode (s: String): Result[Bytes, String]

Decodes a hex string to bytes, accepting upper and lower case.

Requires an even number of digits. An odd-length string has no reading, and guessing one (a leading zero? a trailing one?) would silently produce different bytes than whatever wrote it intended.

Err on any character that is not a hex digit, and on an odd length. Hex usually arrives from somewhere else, so both are ordinary outcomes rather than programmer error.

let base64_encode (b: Bytes): String

Encodes bytes as base64, RFC 4648, using the standard alphabet (A-Z a-z 0-9 + /) with = padding.

Padding makes the output a multiple of four, so a decoder can frame it without being told the length.

let base64_url_encode (b: Bytes): String

The URL-safe alphabet (- and _ for + and /), with padding OMITTED, which is what JWTs and data: URLs expect, since = needs escaping in a query string.

let base64_decode (s: String): Result[Bytes, String]

Decodes base64 to bytes, accepting EITHER alphabet and treating padding as optional.

Deliberately lenient, and worth knowing rather than discovering: a strict pair would reject a URL-safe token handed to the standard decoder, which is the most common way this call is written wrong, and rejecting it buys nothing; the two alphabets do not overlap, so no input has two readings.

Returns Err for unrecognised characters, impossible lengths, malformed padding, or nonzero unused tail bits. If present, padding must be a suffix of one or two = characters completing a group of four. Unused tail bits must be zero, with or without padding.

let base64_url_decode (s: String): Result[Bytes, String]

The same decoder as base64_decode, which already accepts the URL-safe alphabet. Offered as a name so a caller reading a JWT can say what they mean.

let percent_encode (b: Bytes): String

Percent-encodes bytes for use in a URL, RFC 3986.

Unreserved is A-Z a-z 0-9 - . _ ~; every other byte becomes %XX with UPPERCASE hex, which the RFC prefers and which makes encoded output comparable byte for byte.

A space becomes %20, not +. The + convention belongs to HTML form bodies (application/x-www-form-urlencoded), not to URLs, and a decoder that accepted both would turn a literal + in a query value into a space.

let percent_decode (s: String): Result[Bytes, String]

Decodes percent-escapes back to bytes.

Err on a % that is not followed by two hex digits. + decodes as a literal plus sign, matching percent_encode.

Returns Bytes rather than String because an escape can encode any byte, including sequences that are not valid UTF-8. Call Bytes.as_string when text is what was expected.