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.

What is NOT lenient is an unrecognised character, or a length that cannot have come from an encoder. Both are Err.

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.