Sign in

    moonxdr

    Bounded XDR schema interpreter, binary interchange and ONC RPC wire toolkit in pure MoonBit

    xdr
    onc-rpc
    rfc4506
    serialization
    schema
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    15 hours ago
    Downloads
    3

    #MoonXDR

    Pure MoonBit schema-driven XDR interchange and ONC RPCv2 wire tools.

    Module: kekeshuo/moonxdr@0.1.0 · MIT · no external runtime dependencies beyond MoonBit core.

    #Why

    XDR defines more than big-endian integers. Applications also need declaration parsing, bounded arrays, discriminated unions, recursive optional values, exact 64-bit numbers, strict padding and procedure payload composition. MoonXDR supplies these as one reusable interpreter instead of generating C stubs or providing an NFS-specific data reader. It is an original implementation of the supported subset of RFC 4506 / RFC 5531, not a port of rpcgen, CPython or go-xdr. It is not a full RPC client/server.

    #Install and import

    moon add kekeshuo/moonxdr@0.1.0

    Add "kekeshuo/moonxdr" @xdr to your package's imports. A calling function must allow raise or catch XdrError::Invalid(offset, message).

    let schema = @xdr.parse_schema("struct Item { unsigned int id; string title<32>; };")
    let item = @xdr.VStruct([("id", @xdr.VUInt(7U)), ("title", @xdr.VString("hello"))])
    let wire = @xdr.encode(schema, "Item", item)
    let restored = @xdr.decode(schema, "Item", wire)
    assert_true(@xdr.encode(schema, "Item", restored) == wire)

    The generated pkg.generated.mbti is the complete public API. Reader/Writer provide scalar codecs; parse_schema, encode, decode provide typed runtime interchange; from_json/to_json, from_hex/to_hex support fixture tools. RpcCall/RpcReply, AuthSys and RecordDecoder compose transport-independent RPC.

    #Three executable workflows

    moon run examples/schema --deny-warn moon run examples/rpc --deny-warn moon run examples/interchange --deny-warn

    1. Protocol/data producers: parse .x declarations, encode a bounded sensor record (exact unsigned64, doubles, enum), decode it, assert identical bytes.
    2. Legacy protocol tooling: encode a procedure argument, attach an AUTH_SYS assertion, produce an RPC CALL, fragment into 7-byte pieces, feed bytewise, reconstruct and inspect the call, then decode a schema-driven RPC REPLY. This is an in-memory wire workflow, not a network service or NFS implementation.
    3. Fixture/interchange authors: validate JSON containing signed/unsigned64 extremes, opaque hex and an optional value; write XDR, read it back, output JSON without converting 64-bit values through floating point.

    All examples assert results and run on all four CI targets.

    #Supported declarations and wire rules

    • const, typedef, named/inline enum, struct, discriminated union; decimal, C octal and hexadecimal signed32 constants; previously declared constants.
    • int, unsigned int, hyper, unsigned hyper, bool, float, double, bounded ASCII string, fixed/variable opaque, fixed/variable arrays and pointers.
    • Forward type references, recursive pointers/variable arrays; mandatory cycles fail.
    • Integer arithmetic preserves all 64 bits; IEEE scalar binary codecs retain special values. JSON deliberately rejects non-finite numbers. Padding must be zero; boolean/discriminant/enum values and trailing bytes are checked.
    • Struct values are ordered (field_name, Value) arrays in schema order. VUnion(tag, payload) omits source arm names; VOptional(None/Some(...)) is explicit.
    • Union labels use signed32 constants. For unsigned discriminants the same 32-bit pattern is used; positive label literals above INT32_MAX are not accepted.
    • Explicit declarations may have larger bounds than the operation budget, but runtime budgets still apply. An omitted < > bound is limits.max_items, not UINT32_MAX. type_names() uses MoonBit shortlex string order, not alphabetical order.

    #JSON convention (project-defined, not an RFC encoding)

    XDRJSON
    int / uintchecked integral number (integer lexical form when parsed)
    hyper / unsigned hyperexact decimal string
    enuminput name or signed32 number; numeric output
    opaqueeven-length hexadecimal string, no whitespace
    optionalnull or {"some": VALUE}; wrapper preserves nested optionals
    struct / arrayexact-key object / array
    union{"tag": NUMBER, "value": VALUE}
    voidnull

    Parsed integer lexemes with fractions or exponent notation are rejected, including 1.0 and 1e0; this avoids silent truncation in the underlying JSON conversion. Float/double JSON conversion is numeric and does not preserve negative-zero spelling or NaN payloads. Use binary scalar APIs when bit-level floating representation matters.

    #Safety and operational limits

    Defaults: 1 MiB input/output/record bytes; 65,536 aggregate value items; depth 64; 65,536 schema tokens. Overrides are validated: max 64 MiB, 1,048,576 items/tokens, depth 128. Schema finite-value analysis has a work budget of 4 × schema token limit. It may reject unusually large otherwise valid type graphs. See SECURITY.md.

    A RecordDecoder accepts arbitrary chunk boundaries and multiple records, including zero-length fragments. Each feed must fit max_bytes; each record has byte and fragment-count limits; one feed has an output-record count limit. Failed feed/finish poisons the instance permanently; discard that stream and instance. A failing feed returns no earlier records from that feed. Framed output includes 4 bytes per fragment and must fit the writer budget. No sockets or timeouts are provided.

    #Verify

    Pinned compiler: v0.10.14+7d59c7ec9 (official development toolchain).

    moon info && moon fmt moon fmt --check moon check --deny-warn moon build --deny-warn moon test --deny-warn python scripts/reference_vectors.py --check python scripts/audit.py

    CI repeats strict check/build/test and all examples on wasm-gc, wasm, js, native using a clean Ubuntu runner. Windows local checks cover the first three; native validation is delegated to the actual CI job, not claimed as a local pass. 26 tests include 64 independent Python struct vectors (384 scalar fields), exact RPC and composite vectors, every split of a fragmented stream, hostile lengths, truncation, overflow and allocation/depth/work limits. The same 64 scalar vectors were also compared byte-for-byte with CPython 3.12 xdrlib (PSF); this is bounded compatibility evidence, not full RFC certification or a performance benchmark.

    #Boundaries

    No quadruple precision, C preprocessor/expressions, rpcgen code generation, RPC program declarations, sockets, rpcbind, NFS operations, credentials verification, RPCSEC_GSS, retransmission, timeouts or authentication policy. No competition acceptance claim. See architecture, references, AI disclosure, selection evidence.

    XdrError

    pub suberror XdrError {
    Invalid(Int, String)
    }

    AcceptStatus

    pub(all) enum AcceptStatus {
    Success(Array[Byte])
    ProgramUnavailable
    ProgramMismatch(UInt, UInt)
    ProcedureUnavailable
    GarbageArguments
    SystemError
    }

    Arm

    pub(all) struct Arm {
    labels : Array[Int]
    ty : Type
    }

    AuthSys

    pub(all) struct AuthSys {
    stamp : UInt
    machine : String
    uid : UInt
    gid : UInt
    groups : Array[UInt]
    }

    AUTH_SYS is an unauthenticated assertion, not proof of identity.

    AuthSys::from_auth

    fn AuthSys::from_auth(auth : OpaqueAuth) -> AuthSys raise XdrError

    AuthSys::to_auth

    fn AuthSys::to_auth(self : AuthSys) -> OpaqueAuth raise XdrError

    Field

    pub(all) struct Field {
    name : String
    ty : Type
    }

    Limits

    pub(all) struct Limits {
    max_bytes : Int
    max_items : Int
    max_depth : Int
    max_schema_tokens : Int
    }

    All limits are per operation, never trusted from wire lengths.

    Limits::default

    fn Limits::default() -> Limits

    Limits::validate

    fn Limits::validate(self : Limits) -> Unit raise XdrError

    OpaqueAuth

    pub(all) struct OpaqueAuth {
    flavor : UInt
    body : Array[Byte]
    }

    Wire authentication only; these bytes do not establish a trusted identity.

    OpaqueAuth::none

    fn OpaqueAuth::none() -> OpaqueAuth

    Parser

    pub struct Parser {
    tokens : Array[Token]
    cursor : Int
    schema : Schema
    }

    Parser::add_constant

    fn Parser::add_constant(self : Parser, name : String, value : Int) -> Unit raise XdrError

    Parser::add_type

    fn Parser::add_type(self : Parser, name : String, ty : Type) -> Unit raise XdrError

    Parser::arm_type

    fn Parser::arm_type(self : Parser, depth : Int) -> Type raise XdrError

    Parser::base_type

    fn Parser::base_type(self : Parser, depth : Int) -> Type raise XdrError

    Parser::bound

    fn Parser::bound(self : Parser, closing : String) -> Int raise XdrError

    Parser::compound

    fn Parser::compound(self : Parser, kind : String, depth : Int) -> Type raise XdrError

    Parser::declaration

    fn Parser::declaration(self : Parser, depth : Int) -> Field raise XdrError

    Parser::expect

    fn Parser::expect(self : Parser, expected : String) -> Unit raise XdrError

    Parser::identifier

    fn Parser::identifier(self : Parser) -> String raise XdrError

    Parser::new

    fn Parser::new(source : String, limits? : Limits) -> Parser raise XdrError

    Parser::offset

    fn Parser::offset(self : Parser) -> Int

    Parser::peek

    fn Parser::peek(self : Parser) -> String

    Parser::take

    fn Parser::take(self : Parser) -> String raise XdrError

    Parser::value

    fn Parser::value(self : Parser) -> Int raise XdrError

    Reader

    pub struct Reader {
    data : Array[Byte]
    pos : Int
    limits : Limits
    }

    Reader::finish

    fn Reader::finish(self : Reader) -> Unit raise XdrError

    Reader::new

    fn Reader::new(data : Array[Byte], limits? : Limits) -> Reader raise XdrError

    Reader::position

    fn Reader::position(self : Reader) -> Int

    Reader::read_auth

    fn Reader::read_auth(self : Reader) -> OpaqueAuth raise XdrError

    Reader::read_bool

    fn Reader::read_bool(self : Reader) -> Bool raise XdrError

    Reader::read_double

    fn Reader::read_double(self : Reader) -> Double raise XdrError

    Reader::read_fixed

    fn Reader::read_fixed(self : Reader, length : Int) -> Array[Byte] raise XdrError

    Reader::read_float

    fn Reader::read_float(self : Reader) -> Float raise XdrError

    Reader::read_hyper

    fn Reader::read_hyper(self : Reader) -> Int64 raise XdrError

    Reader::read_int

    fn Reader::read_int(self : Reader) -> Int raise XdrError

    Reader::read_length

    fn Reader::read_length(self : Reader, bound : Int) -> Int raise XdrError

    Reader::read_opaque

    fn Reader::read_opaque(self : Reader, bound? : Int) -> Array[Byte] raise XdrError

    Reader::read_raw

    fn Reader::read_raw(self : Reader, count : Int) -> Array[Byte] raise XdrError

    Reader::read_string

    fn Reader::read_string(self : Reader, bound? : Int) -> String raise XdrError

    Reader::read_uhyper

    fn Reader::read_uhyper(self : Reader) -> UInt64 raise XdrError

    Reader::read_uint

    fn Reader::read_uint(self : Reader) -> UInt raise XdrError

    Reader::remaining

    fn Reader::remaining(self : Reader) -> Int

    RecordDecoder

    pub struct RecordDecoder {
    limits : Limits
    record : Array[Byte]
    marker : UInt
    header_bytes : Int
    body_left : Int
    last : Bool
    fragments : Int
    poisoned : Bool
    }

    Incremental, owned record assembly. A malformed feed permanently poisons this decoder. feed returns records only on success; discard the stream on error.

    RecordDecoder::feed

    fn RecordDecoder::feed(self : RecordDecoder, chunk : Array[Byte]) -> Array[Array[Byte]] raise XdrError

    RecordDecoder::finish

    fn RecordDecoder::finish(self : RecordDecoder) -> Unit raise XdrError

    End of transport: incomplete headers, bodies and non-final records are errors.

    RecordDecoder::new

    fn RecordDecoder::new(limits? : Limits) -> RecordDecoder raise XdrError

    RejectStatus

    pub(all) enum RejectStatus {
    RpcMismatch(UInt, UInt)
    AuthError(UInt)
    }

    ReplyBody

    pub(all) enum ReplyBody {
    Accepted(OpaqueAuth, AcceptStatus)
    Rejected(RejectStatus)
    }

    RpcCall

    pub(all) struct RpcCall {
    xid : UInt
    program : UInt
    version : UInt
    procedure : UInt
    credential : OpaqueAuth
    verifier : OpaqueAuth
    arguments : Array[Byte]
    }

    RpcReply

    pub(all) struct RpcReply {
    xid : UInt
    body : ReplyBody
    }

    Schema

    pub struct Schema {
    types : Map[String, Type]
    constants : Map[String, Int]
    limits : Limits
    }

    Schema::constant

    fn Schema::constant(self : Schema, name : String) -> Int?

    Schema::empty

    fn Schema::empty(limits? : Limits) -> Schema raise XdrError

    Schema::lookup

    fn Schema::lookup(self : Schema, name : String) -> Type raise XdrError

    Schema::resolve

    fn Schema::resolve(self : Schema, ty : Type) -> Type raise XdrError

    Schema::type_names

    fn Schema::type_names(self : Schema) -> Array[String]

    Schema::validate

    fn Schema::validate(self : Schema) -> Unit raise XdrError

    Fixed-point inhabitation detects mandatory recursive cycles while permitting pointers.

    Token

    pub(all) struct Token {
    text : String
    offset : Int
    }

    Type

    pub(all) enum Type {
    TVoid
    TInt
    TUInt
    THyper
    TUHyper
    TBool
    TFloat
    TDouble
    TString(Int)
    TOpaque(Int, Bool)
    TArray(Type, Int, Bool)
    TOptional(Type)
    TRef(String)
    TEnum(Array[(String, Int)])
    TStruct(Array[Field])
    TUnion(Type, Array[Arm], Type?)
    }

    RFC 4506 types. Bounds are nonnegative and checked before allocation.

    Value

    pub(all) enum Value {
    VVoid
    VInt(Int)
    VUInt(UInt)
    VHyper(Int64)
    VUHyper(UInt64)
    VBool(Bool)
    VFloat(Float)
    VDouble(Double)
    VString(String)
    VOpaque(Array[Byte])
    VArray(Array[Value])
    VOptional(Value?)
    VEnum(Int)
    VStruct(Array[(String, Value)])
    VUnion(Int, Value)
    }

    Values preserve signed/unsigned 64-bit and IEEE representations without JSON rounding.

    Writer

    pub struct Writer {
    data : Array[Byte]
    limits : Limits
    }

    Writer::bytes

    fn Writer::bytes(self : Writer) -> Array[Byte]

    Writer::length

    fn Writer::length(self : Writer) -> Int

    Writer::new

    fn Writer::new(limits? : Limits) -> Writer raise XdrError

    Writer::write_auth

    fn Writer::write_auth(self : Writer, auth : OpaqueAuth) -> Unit raise XdrError

    Writer::write_bool

    fn Writer::write_bool(self : Writer, b : Bool) -> Unit raise XdrError

    Writer::write_double

    fn Writer::write_double(self : Writer, d : Double) -> Unit raise XdrError

    Writer::write_fixed

    fn Writer::write_fixed(self : Writer, bytes : Array[Byte]) -> Unit raise XdrError

    Writer::write_float

    fn Writer::write_float(self : Writer, f : Float) -> Unit raise XdrError

    Writer::write_hyper

    fn Writer::write_hyper(self : Writer, n : Int64) -> Unit raise XdrError

    Writer::write_int

    fn Writer::write_int(self : Writer, n : Int) -> Unit raise XdrError

    Writer::write_opaque

    fn Writer::write_opaque(self : Writer, bytes : Array[Byte], bound? : Int) -> Unit raise XdrError

    Writer::write_raw

    fn Writer::write_raw(self : Writer, bytes : Array[Byte]) -> Unit raise XdrError

    Writer::write_string

    fn Writer::write_string(self : Writer, text : String, bound? : Int) -> Unit raise XdrError

    RFC 4506 strings are ASCII; arbitrary bytes use opaque instead.

    Writer::write_uhyper

    fn Writer::write_uhyper(self : Writer, n : UInt64) -> Unit raise XdrError

    Writer::write_uint

    fn Writer::write_uint(self : Writer, n : UInt) -> Unit raise XdrError

    decode

    fn decode(schema : Schema, type_name : String, bytes : Array[Byte]) -> Value raise XdrError

    Decode exactly one datum; trailing bytes are rejected, not silently discarded.

    decode_call

    fn decode_call(bytes : Array[Byte], limits? : Limits) -> RpcCall raise XdrError

    decode_reply

    fn decode_reply(bytes : Array[Byte], limits? : Limits) -> RpcReply raise XdrError

    encode

    fn encode(schema : Schema, type_name : String, value : Value) -> Array[Byte] raise XdrError

    encode_call

    fn encode_call(call : RpcCall, limits? : Limits) -> Array[Byte] raise XdrError

    encode_reply

    fn encode_reply(reply : RpcReply, limits? : Limits) -> Array[Byte] raise XdrError

    fragment_record

    fn fragment_record(record : Array[Byte], fragment_size : Int, limits? : Limits) -> Array[Byte] raise XdrError

    RFC 5531 record markers; fragment size need not be XDR-word aligned. The returned framed buffer, including headers, must fit max_bytes.

    from_hex

    fn from_hex(text : String, limits? : Limits) -> Array[Byte] raise XdrError

    from_json

    fn from_json(schema : Schema, type_name : String, json : Json) -> Value raise XdrError

    Local JSON convention, not an RFC-defined JSON wire format.

    ident_continue

    fn ident_continue(c : Char) -> Bool

    ident_start

    fn ident_start(c : Char) -> Bool

    parse_constant_number

    fn parse_constant_number(text : String, offset? : Int) -> Int raise XdrError

    Constant syntax follows C decimal, octal and hexadecimal, within signed 32-bit.

    parse_schema

    fn parse_schema(source : String, limits? : Limits) -> Schema raise XdrError

    parse_signed_decimal

    fn parse_signed_decimal(text : String) -> Int64 raise XdrError

    parse_unsigned_decimal

    fn parse_unsigned_decimal(text : String) -> UInt64 raise XdrError

    Exact decimal conversion used for JSON hyper integers; never passes through Double.

    to_hex

    fn to_hex(bytes : Array[Byte]) -> String

    to_json

    fn to_json(schema : Schema, type_name : String, value : Value) -> Json raise XdrError

    Binary validation precedes JSON output, including ASCII, collection and total byte limits.

    tokenize

    fn tokenize(source : String, limits? : Limits) -> Array[Token] raise XdrError

    Lexer preserves source character offsets and rejects unsupported directives.

    union_arm

    fn union_arm(arms : Array[Arm], fallback : Type?, tag : Int) -> Type raise XdrError

    validate_tag

    fn validate_tag(tag : Type, value : Int) -> Unit raise XdrError