Pure-Elixir encoder / decoder for the XDR (External Data Representation) standard — RFC 4506.
XDR is the wire format for ONC RPC and every NFSv3 data structure, so everything in the downstream NFS stack depends on this module.
Design
Big-endian. XDR is always big-endian on the wire, regardless of host endianness. All integer and float encodings use
big-signed-integer/big-unsigned-integer/big-floatbit specifiers.4-byte alignment. Length-prefixed and variable-length encodings (opaque, string, variable arrays) are padded with zero bytes so the encoded length is a multiple of 4. Decoders skip the matching padding.
Pair-of-functions per type. Primitives expose
encode_<type>/1anddecode_<type>/1as sibling public functions. Decoders consume a prefix of the binary and return{:ok, value, rest} | {:error, reason}, in the usual Elixir binary-pattern-matching style. This keeps composition trivial:with {:ok, major, rest} <- XDR.decode_uint(binary), {:ok, minor, rest} <- XDR.decode_uint(rest), {:ok, name, rest} <- XDR.decode_string(rest) do {:ok, %MyRecord{major: major, minor: minor, name: name}, rest} endComposite helpers.
encode_array/3,decode_var_array/2,encode_optional/2,decode_union/3etc. let you assemble composite types from primitives without a full macro DSL. The DSL can be added later if the NFSv3 type hand-rolling becomes repetitive.
Errors
All decoders return {:error, reason} on a bad input. Reasons are:
:short_binary— fewer bytes than the wire format requires.{:bad_bool, n}— a bool-encoded integer was neither 0 nor 1.{:bad_pad, bytes}— alignment padding bytes were non-zero.{:bad_discriminant, n}— a discriminated-union discriminant wasn't recognised by the caller's arm map.
Summary
Types
A decoder function — takes a binary, returns the parsed value and the remaining bytes (or an error tuple).
An encoder function — takes a value, returns its wire bytes.
Functions
Decode a boolean.
Decode an IEEE 754 double-precision float.
Decode an enum (same wire format as int).
Decode a fixed-length array (§4.12) of length elements using
decoder.
Decode a fixed-length opaque byte array (§4.9).
Decode an IEEE 754 single-precision float.
Decode a 64-bit signed integer (hyper).
Decode a 32-bit signed integer.
Decode an optional value (§4.19).
Decode a string (same wire format as variable-length opaque).
Decode a 64-bit unsigned integer.
Decode a 32-bit unsigned integer.
Decode a discriminated union (§4.15). arms maps each discriminant
value to its arm decoder; an unknown discriminant surfaces as
{:error, {:bad_discriminant, n}}.
Decode a variable-length array (§4.13).
Decode a variable-length opaque byte array (§4.10).
Decode void — consumes zero bytes.
Encode a boolean as a 32-bit TRUE(1) / FALSE(0) (§4.4).
Encode an IEEE 754 double-precision float (§4.7).
Encode an enum as a 32-bit signed int (§4.3).
Encode a fixed-length array (§4.12). length must equal the list
length. Every element is encoded with encoder back-to-back — no
array-level length prefix.
Encode a fixed-length opaque byte array (§4.9). The binary's byte
size must equal length; the output is the binary padded with
zeros to a 4-byte boundary.
Encode an IEEE 754 single-precision float (§4.6).
Encode a 64-bit signed integer (hyper, §4.5).
Encode a 32-bit signed integer (§4.1).
Encode an optional value (§4.19): a boolean presence flag followed
by the value when present. nil encodes as false (no body).
Encode a string (§4.11). Identical wire format to variable-length opaque; accepts any binary the caller considers a string (XDR does not mandate an encoding).
Encode a 64-bit unsigned integer (unsigned hyper).
Encode a 32-bit unsigned integer (§4.2).
Encode a discriminated union (§4.15).
Encode a variable-length array (§4.13): a 4-byte count followed by
count elements, no trailing padding (each element handles its
own alignment).
Encode a variable-length opaque byte array (§4.10). A 4-byte length prefix precedes the bytes and its trailing alignment padding.
Encode void (§4.16) — zero bytes. Accepts any argument and returns
<<>>, so it can be plugged into composite encoders uniformly.
Types
A decoder function — takes a binary, returns the parsed value and the remaining bytes (or an error tuple).
An encoder function — takes a value, returns its wire bytes.
Functions
Decode a boolean.
Decode an IEEE 754 double-precision float.
Decode an enum (same wire format as int).
@spec decode_fixed_array(binary(), non_neg_integer(), decoder()) :: {:ok, [term()], binary()} | {:error, term()}
Decode a fixed-length array (§4.12) of length elements using
decoder.
@spec decode_fixed_opaque(binary(), non_neg_integer()) :: {:ok, binary(), binary()} | {:error, term()}
Decode a fixed-length opaque byte array (§4.9).
Decode an IEEE 754 single-precision float.
Decode a 64-bit signed integer (hyper).
Decode a 32-bit signed integer.
Decode an optional value (§4.19).
Decode a string (same wire format as variable-length opaque).
@spec decode_uhyper(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, term()}
Decode a 64-bit unsigned integer.
@spec decode_uint(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, term()}
Decode a 32-bit unsigned integer.
@spec decode_union(binary(), %{required(integer()) => decoder()}) :: {:ok, {integer(), term()}, binary()} | {:error, term()}
Decode a discriminated union (§4.15). arms maps each discriminant
value to its arm decoder; an unknown discriminant surfaces as
{:error, {:bad_discriminant, n}}.
Decode a variable-length array (§4.13).
Decode a variable-length opaque byte array (§4.10).
Decode void — consumes zero bytes.
Encode a boolean as a 32-bit TRUE(1) / FALSE(0) (§4.4).
Encode an IEEE 754 double-precision float (§4.7).
Encode an enum as a 32-bit signed int (§4.3).
@spec encode_fixed_array([term()], non_neg_integer(), encoder()) :: binary()
Encode a fixed-length array (§4.12). length must equal the list
length. Every element is encoded with encoder back-to-back — no
array-level length prefix.
@spec encode_fixed_opaque(binary(), non_neg_integer()) :: binary()
Encode a fixed-length opaque byte array (§4.9). The binary's byte
size must equal length; the output is the binary padded with
zeros to a 4-byte boundary.
Encode an IEEE 754 single-precision float (§4.6).
Encode a 64-bit signed integer (hyper, §4.5).
Encode a 32-bit signed integer (§4.1).
Encode an optional value (§4.19): a boolean presence flag followed
by the value when present. nil encodes as false (no body).
Encode a string (§4.11). Identical wire format to variable-length opaque; accepts any binary the caller considers a string (XDR does not mandate an encoding).
@spec encode_uhyper(non_neg_integer()) :: binary()
Encode a 64-bit unsigned integer (unsigned hyper).
@spec encode_uint(non_neg_integer()) :: binary()
Encode a 32-bit unsigned integer (§4.2).
Encode a discriminated union (§4.15).
arms maps each possible discriminant value to the encoder for its
arm body. The discriminant is encoded as a 32-bit signed integer
(per the most common NFSv3 usage); pass a pre-encoded discriminant
via encode_union_with/3 if your protocol uses a different type.
The void arm (§4.16) encodes as no body — use &XDR.encode_void/1
in the arm map.
Encode a variable-length array (§4.13): a 4-byte count followed by
count elements, no trailing padding (each element handles its
own alignment).
Encode a variable-length opaque byte array (§4.10). A 4-byte length prefix precedes the bytes and its trailing alignment padding.
Encode void (§4.16) — zero bytes. Accepts any argument and returns
<<>>, so it can be plugged into composite encoders uniformly.