std.encoding
import std.encoding provides hex, base32, base64, PEM and custom codecs and
fixed-width integer and IEEE 754 float packing over std.bytes
buffers, with one error type, EncodingError.
Byte order, range checks, growth and byte manipulation are checked Plenty code. Four std-only compiler intrinsics reinterpret same-width integer/float bits with a native bitcast; they add no runtime library or dependency. Bytes are accessed through validated buffer operations. Availability matches integer packing on the supported targets; the float tests are qualified on x86-64 Linux GNU.
These are binary encodings. They do not normalize, transcode, or validate text;
turning decoded bytes into a str is the separate, strict
Bytes.to_text conversion.
import std.bytes
import std.encoding
from std.encoding import EncodingError, Endian
def main() -> Result[(), AllocError | IoError | std.bytes.BytesError | EncodingError]:
data = std.bytes.from_text("hi!")?
print(std.encoding.hex_text(&data)?)?
print(std.encoding.base64_text(&data)?)?
print(std.encoding.base64_from_text("aGkh")?.to_text()?)?
mut header = std.bytes.with_capacity(4)?
std.encoding.append_u32(&mut header, 258u32, Endian.Big)?
print(std.encoding.hex_text(&header)?)?
print(std.encoding.unpack_u32(&header, 0, Endian.Big))?
Ok(())
686921
aGkh
hi!
00000102
258
Codecs
source is a byte view &[u8]: pass &buffer for a whole Bytes,
&buffer[start:stop] for part of it, or a b"..." literal.
| Function | Result | Contract |
|---|---|---|
hex_encode(source) | Result[Bytes, EncodingError] | Two lowercase digits per byte |
hex_decode(source) | Result[Bytes, EncodingError] | Digits of either case; even length |
base64_encode(source) | Result[Bytes, EncodingError] | RFC 4648 section 4, padded |
base64_decode(source) | Result[Bytes, EncodingError] | The padded form only |
base64url_encode(source) | Result[Bytes, EncodingError] | RFC 4648 section 5, unpadded |
base64url_decode(source) | Result[Bytes, EncodingError] | The unpadded form only |
hex_text, base64_text, base64url_text (source) | Result[str, EncodingError] | The same encoding as a str |
hex_from_text, base64_from_text, base64url_from_text (text: &str) | Result[Bytes, EncodingError] | Decode the whole text |
Each result is a new buffer whose capacity equals its length. Encoders produce ASCII only.
For base32/base32hex, MIME line wrapping, PEM, and reusable custom alphabets, see additional codecs. They use the same views, result types, and byte indexes as the functions here.
Accepted forms
The codecs in this table accept one form. They never skip, trim, or repair input: whitespace, line breaks, and any other byte outside the alphabet are errors.
| Codec | Alphabet | Length | Padding |
|---|---|---|---|
| hex | 0-9, a-f, A-F; encoder writes lowercase | Even | None |
| base64 | A-Z, a-z, 0-9, +, / | Multiple of 4 | = required to complete the last group |
| base64url | A-Z, a-z, 0-9, -, _ | Any except 1 more than a multiple of 4 | = rejected |
Base64 text must be canonical: bits of the last symbol that encode no byte must be zero, so each byte sequence has exactly one accepted text per codec. The empty range encodes and decodes to an empty buffer.
Decoder checks, in order:
- The range, then the length.
- For base64, up to two trailing
=are set aside as padding. - Bytes are examined in order. The first one outside the alphabet is
InvalidPaddingif it is=andInvalidByteotherwise. - The trailing bits of a final partial group.
Integer packing
value has the type in the function name; offset is i64; order is
Endian.Little or Endian.Big. There is no default or native order.
| Function | Result | Contract |
|---|---|---|
pack_u16, pack_u32, pack_u64, pack_i16, pack_i32, pack_i64 (target: &mut Bytes, offset, value, order) | () | Overwrite the bytes at offset; never grows |
append_u16, append_u32, append_u64, append_i16, append_i32, append_i64 (target: &mut Bytes, value, order) | Result[(), EncodingError] | Grow by exactly the width |
unpack_u16, unpack_u32, unpack_u64, unpack_i16, unpack_i32, unpack_i64 (source: &[u8], offset, order) | u16, and so on | Read the bytes at offset |
Endian.Big puts the most significant byte at the lowest offset. Signed values
use two’s complement. pack_ and unpack_ require
0 <= offset <= len - width; a short buffer or invalid offset traps before anything is
read or written. Bytes are accessed one at a time, so no offset has an
alignment requirement. A single byte needs no order: use Bytes.get, set,
and push.
Floating-point packing
| Function | Result | Contract |
|---|---|---|
pack_f32, pack_f64 (target: &mut Bytes, offset: i64, value, order: Endian) | () | Overwrite exactly four/eight existing bytes; never grows |
append_f32, append_f64 (target: &mut Bytes, value, order: Endian) | Result[(), EncodingError] | Append exactly four/eight bytes |
unpack_f32, unpack_f64 (source: &[u8], offset: i64, order: Endian) | f32, f64 | Read exactly four/eight bytes |
The value type is the width in the name: f32 is IEEE 754 binary32 and f64
is binary64. Endianness is explicit, with no native-order default, alignment,
padding, or object-layout serialization. Sources use the same byte views as
integer unpacking: a whole buffer, a borrowed range, or a byte literal.
These operations reinterpret bits without numeric conversion, promotion, arithmetic, or text formatting. They preserve every representation, including positive and negative zero, infinities, subnormals, and quiet/signaling NaNs with their signs and payloads. Every input bit pattern is accepted. Subsequent arithmetic on a decoded float can change a NaN representation and lies outside this codec’s guarantee. Compare re-encoded bytes when testing representations; float equality cannot distinguish signed zeros or verify NaNs.
The integer bounds and failure guarantees apply unchanged. pack_ and
unpack_ require 0 <= offset <= len - width, including at unaligned offsets
inside a larger record. Negative, excessively large, and truncated ranges
trap before reading or modifying bytes. The messages are pack: offset outside target buffer
and unpack: offset outside source buffer. Packing leaves length,
capacity, and surrounding bytes unchanged. Appending keeps the existing prefix;
when growth fails, contents and length remain unchanged and the error is
Allocation(cause). Range checks avoid overflowing offset arithmetic.
Errors
pub enum EncodingError:
InvalidLength
InvalidByte(i64)
InvalidPadding(i64)
NonCanonical(i64)
InvalidAlphabet
InvalidPem(i64)
PemLabel
Allocation(AllocError)
| Variant | Meaning |
|---|---|
InvalidLength | The encoded length is impossible for the codec |
InvalidByte(index) | The byte at index is outside the alphabet |
InvalidPadding(index) | A configured padding byte at index where the codec does not accept it |
NonCanonical(index) | The symbol at index has nonzero bits that encode no byte |
InvalidAlphabet | A custom alphabet or padding symbol violates its construction rules |
InvalidPem(index) | The PEM framing is malformed at index |
PemLabel | The PEM label is not the expected one, or the expected label is invalid |
Allocation(cause) | Storage for the result could not be allocated, including an unrepresentable expanded size (CapacityOverflow) |
An index is a position in the source view; for the
_from_text forms it is a UTF-8 byte offset into the text. Every fallible
operation returns Result[T, EncodingError]. A failed operation leaves its
target unchanged and releases anything it allocated. Wrap a result with
std.error.context to name the operation.
Hex, base64, MIME and PEM expansion sizes are checked before allocating output.
They return Allocation(CapacityOverflow) rather than trapping on integer
overflow. Bounded decoder cursors, fixed-width packing and alphabet arithmetic
use explicit unchecked: regions whose source comments state their bounds.
PEM decoding records the validated body endpoint instead of reconstructing it
with a potentially overflowing size formula. Invalid input keeps its existing
codec error and source offset; callers’ arithmetic modes do not change these
contracts.
Ownership and threads
Sources are shared loans and targets exclusive loans, so the view rules apply: a buffer cannot be packed into while a view of it is live, and cannot be encoded into itself. The module holds no global state. Its functions pass the ordinary worker-effect checks and may run in any thread.
Costs
This table covers the original hex/base64 codecs and integer/float packing. See additional codec costs for custom table construction and MIME’s temporary buffers.
| Operation | Allocates | Time |
|---|---|---|
_encode, _decode | One buffer of exactly the output size, after the range and length checks | Linear in the range |
_text | The encoded buffer, then a str copy of it | Linear |
_from_text | A Bytes copy of the text, then the decoded buffer | Linear |
append_ | Only when capacity is exceeded, as Bytes.resize | Constant, amortized |
pack_, unpack_ | Nothing | Constant |
Output sizes for n input bytes: hex 2n; base64 4 * ceil(n / 3);
base64url ceil(4n / 3). A decoder allocates its output before it examines
the alphabet, so rejected input pays for that buffer, which is released before
returning.
Every byte goes through a checked single-element Bytes call. Measured with a
release compiler on x86-64 Linux, pinned to one core, on an 8,388,608-byte
input:
| Operation | Wall time |
|---|---|
hex_encode | 1.9 s |
hex_decode of the 16,777,216 digits | 2.5 s |
base64_encode | 1.3 s |
base64_decode of the 11,184,812 symbols | 1.3 s |
1,048,576 pack_u64 and 1,048,576 unpack_u64 | 2.9 s |
That is a few megabytes per second. Bulk native primitives would be faster and are not implemented.
Limits
- No streaming or incremental codecs: input and output are whole buffers.
- No arbitrary line widths, automatic case folding, or permissive recovery from damaged encodings.
- No variable-length integers, 128-bit integers, or additional float formats.
- No record-layout description: pack fields one call at a time.
Acknowledgements
- RFC 4648 specifies the base16, base32 and base64 formats implemented by the codecs.
- RFC 7468 specifies the textual encapsulation and label syntax used by PEM encoding.
- IEEE 754 defines the binary32 and binary64 representations used by float packing; no standard text or external implementation is shipped.