Tahr.XDR (tahr v0.1.0)

Copy Markdown View Source

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-float bit 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>/1 and decode_<type>/1 as 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}
    end
  • Composite helpers. encode_array/3, decode_var_array/2, encode_optional/2, decode_union/3 etc. 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

decoder()

@type decoder() :: (binary() -> {:ok, term(), binary()} | {:error, term()})

A decoder function — takes a binary, returns the parsed value and the remaining bytes (or an error tuple).

encoder()

@type encoder() :: (term() -> binary())

An encoder function — takes a value, returns its wire bytes.

Functions

decode_bool(arg1)

@spec decode_bool(binary()) :: {:ok, boolean(), binary()} | {:error, term()}

Decode a boolean.

decode_double(arg1)

@spec decode_double(binary()) :: {:ok, float(), binary()} | {:error, term()}

Decode an IEEE 754 double-precision float.

decode_enum(binary)

@spec decode_enum(binary()) :: {:ok, integer(), binary()} | {:error, term()}

Decode an enum (same wire format as int).

decode_fixed_array(binary, length, decoder)

@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.

decode_fixed_opaque(binary, length)

@spec decode_fixed_opaque(binary(), non_neg_integer()) ::
  {:ok, binary(), binary()} | {:error, term()}

Decode a fixed-length opaque byte array (§4.9).

decode_float(arg1)

@spec decode_float(binary()) :: {:ok, float(), binary()} | {:error, term()}

Decode an IEEE 754 single-precision float.

decode_hyper(arg1)

@spec decode_hyper(binary()) :: {:ok, integer(), binary()} | {:error, term()}

Decode a 64-bit signed integer (hyper).

decode_int(arg1)

@spec decode_int(binary()) :: {:ok, integer(), binary()} | {:error, term()}

Decode a 32-bit signed integer.

decode_optional(binary, decoder)

@spec decode_optional(binary(), decoder()) ::
  {:ok, term() | nil, binary()} | {:error, term()}

Decode an optional value (§4.19).

decode_string(binary)

@spec decode_string(binary()) :: {:ok, binary(), binary()} | {:error, term()}

Decode a string (same wire format as variable-length opaque).

decode_uhyper(arg1)

@spec decode_uhyper(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, term()}

Decode a 64-bit unsigned integer.

decode_uint(arg1)

@spec decode_uint(binary()) :: {:ok, non_neg_integer(), binary()} | {:error, term()}

Decode a 32-bit unsigned integer.

decode_union(binary, arms)

@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_var_array(binary, decoder)

@spec decode_var_array(binary(), decoder()) ::
  {:ok, [term()], binary()} | {:error, term()}

Decode a variable-length array (§4.13).

decode_var_opaque(binary)

@spec decode_var_opaque(binary()) :: {:ok, binary(), binary()} | {:error, term()}

Decode a variable-length opaque byte array (§4.10).

decode_void(binary)

@spec decode_void(binary()) :: {:ok, :void, binary()}

Decode void — consumes zero bytes.

encode_bool(bool)

@spec encode_bool(boolean()) :: binary()

Encode a boolean as a 32-bit TRUE(1) / FALSE(0) (§4.4).

encode_double(f)

@spec encode_double(float()) :: binary()

Encode an IEEE 754 double-precision float (§4.7).

encode_enum(n)

@spec encode_enum(integer()) :: binary()

Encode an enum as a 32-bit signed int (§4.3).

encode_fixed_array(list, length, encoder)

@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.

encode_fixed_opaque(binary, length)

@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_float(f)

@spec encode_float(float()) :: binary()

Encode an IEEE 754 single-precision float (§4.6).

encode_hyper(n)

@spec encode_hyper(integer()) :: binary()

Encode a 64-bit signed integer (hyper, §4.5).

encode_int(n)

@spec encode_int(integer()) :: binary()

Encode a 32-bit signed integer (§4.1).

encode_optional(value, encoder)

@spec encode_optional(term() | nil, encoder()) :: binary()

Encode an optional value (§4.19): a boolean presence flag followed by the value when present. nil encodes as false (no body).

encode_string(s)

@spec encode_string(binary()) :: binary()

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_uhyper(n)

@spec encode_uhyper(non_neg_integer()) :: binary()

Encode a 64-bit unsigned integer (unsigned hyper).

encode_uint(n)

@spec encode_uint(non_neg_integer()) :: binary()

Encode a 32-bit unsigned integer (§4.2).

encode_union(discriminant, value, arms)

@spec encode_union(integer(), term(), %{required(integer()) => encoder()}) :: binary()

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_var_array(list, encoder)

@spec encode_var_array([term()], encoder()) :: binary()

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_var_opaque(binary)

@spec encode_var_opaque(binary()) :: binary()

Encode a variable-length opaque byte array (§4.10). A 4-byte length prefix precedes the bytes and its trailing alignment padding.

encode_void(_)

@spec encode_void(any()) :: binary()

Encode void (§4.16) — zero bytes. Accepts any argument and returns <<>>, so it can be plugged into composite encoders uniformly.