# Binary Protocols and Byte Order

> A struct is not a wire format. Write the fields, and say which end first.

Every binary protocol starts with a fixed-size header, so the receiver can
read a known number of bytes before it knows anything else. Writing one comes
down to two rules that are easy to state and easy to skip.

```zig
const std = @import("std");

/// The header every message on this imaginary protocol starts with. Ten
/// bytes, always, so the receiver can read a fixed amount before it knows
/// anything else. Compare `@sizeOf(Header)`, which is not ten.
const Header = struct {
    magic: u16,
    version: u8,
    kind: Kind,
    length: u32,
    checksum: u16,

    const Kind = enum(u8) { request = 1, response = 2 };
    const magic_value: u16 = 0x5A47; // "ZG"
    const wire_size = 10;

    /// Field by field, endianness stated at every call. This is more typing
    /// than casting the struct's bytes and it is the only version that
    /// survives a different compiler, a different target, or a peer written
    /// in another language.
    fn write(self: Header, w: *std.Io.Writer) !void {
        try w.writeInt(u16, self.magic, .big);
        try w.writeInt(u8, self.version, .big);
        try w.writeInt(u8, @intFromEnum(self.kind), .big);
        try w.writeInt(u32, self.length, .big);
        try w.writeInt(u16, self.checksum, .big);
    }

    fn read(r: *std.Io.Reader) !Header {
        const magic = try r.takeInt(u16, .big);
        if (magic != magic_value) return error.NotOurProtocol;

        const version = try r.takeInt(u8, .big);
        const kind = try r.takeInt(u8, .big);
        return .{
            .magic = magic,
            .version = version,
            // `std.enums.fromInt` returns null rather than an error, and it
            // replaced `std.meta.intToEnum`, which no longer exists. A tag
            // byte off the wire is attacker-controlled, so `@enumFromInt` on
            // it is undefined behaviour, not a shortcut.
            .kind = std.enums.fromInt(Kind, kind) orelse return error.UnknownKind,
            .length = try r.takeInt(u32, .big),
            .checksum = try r.takeInt(u16, .big),
        };
    }
};

pub fn main(init: std.process.Init) !void {
    var buf: [512]u8 = undefined;
    var file_writer = std.Io.File.stdout().writerStreaming(init.io, &buf);
    const out = &file_writer.interface;

    const payload = "hello";
    const header: Header = .{
        .magic = Header.magic_value,
        .version = 1,
        .kind = .request,
        .length = payload.len,
        .checksum = 0xBEEF,
    };

    // The struct in memory is larger than the message on the wire, because
    // the compiler is free to pad and reorder. That gap is exactly why you
    // cannot send a struct: the padding bytes are not yours to define.
    try out.print("@sizeOf(Header) = {d}, on the wire = {d}\n\n", .{
        @sizeOf(Header),
        Header.wire_size,
    });

    var wire: [64]u8 = undefined;
    var w: std.Io.Writer = .fixed(&wire);
    try header.write(&w);
    try w.writeAll(payload);

    try out.print("wire: {x}\n", .{w.buffered()});
    try out.print("      ^^^^ magic  ^^ version  ^^ kind  ^^^^^^^^ length\n\n", .{});

    var r: std.Io.Reader = .fixed(w.buffered());
    const back = try Header.read(&r);
    const body = try r.take(back.length);
    try out.print("decoded: version {d}, {s}, {d} bytes: \"{s}\"\n\n", .{
        back.version,
        @tagName(back.kind),
        back.length,
        body,
    });

    // Big-endian written, little-endian read. Nothing errors: the length
    // is simply wrong, and it is wrong by a factor that makes the receiver
    // try to allocate or wait for 83 million bytes.
    var swapped: std.Io.Reader = .fixed(w.buffered()[4..8]);
    try out.print("length read as big-endian:    {d}\n", .{
        std.mem.readInt(u32, w.buffered()[4..8], .big),
    });
    try out.print("length read as little-endian: {d}\n", .{
        try swapped.takeInt(u32, .little),
    });

    // The other half of a header check. A peer that is not speaking your
    // protocol at all should be rejected on the first two bytes, before
    // any length is trusted enough to act on.
    var junk: std.Io.Reader = .fixed("GET / HTTP/1.1\r\n");
    if (Header.read(&junk)) |_| {
        try out.writeAll("\nan HTTP request into this parser: accepted\n");
    } else |err| {
        try out.print("\nan HTTP request into this parser: {s}\n", .{@errorName(err)});
    }

    try out.flush();
}
```

*Runnable: compiled to WebAssembly and executed by CI against Zig master. (`11-networking.binary-protocols`)*

## A struct is not a wire format

```
@sizeOf(Header) = 12, on the wire = 10
```

The struct is bigger than the message. Zig may pad a struct and may reorder
its fields, and those padding bytes are not yours to define: they are whatever
was in that memory. Send the struct and you have sent two bytes of something
else, and the layout you sent is the one your compiler chose on your target
today.

```zig
// Do not do this.
try socket.writeAll(std.mem.asBytes(&header));
```

`extern struct` fixes the layout and is the right tool for calling C, as
[ABI](https://www.ziglang.in/learn/working-with-c/abi/) covers. It still does not fix byte order, so it is
not enough here.

Write the fields:

```zig
try w.writeInt(u16, self.magic, .big);
try w.writeInt(u8, self.version, .big);
```

More typing, and the only version that survives a different compiler, a
different target, or a peer written in another language.

## Say which end first

```
length read as big-endian:    5
length read as little-endian: 83886080
```

Nothing errored. The length is simply wrong, and wrong in the worst possible
way: a receiver that trusts it will try to wait for, or allocate, 83 megabytes
because a five-byte message arrived.

Network byte order is big-endian by convention, and the convention is worth
following even when both ends are little-endian machines today, because one
day one of them will not be. State the endianness at every call. `readInt` and
`writeInt` require the argument precisely so that you cannot forget.

This is also why the [Byte Order](https://www.ziglang.in/learn/language-basics/integer-rules/) rules matter
more here than anywhere else in a program: inside your process the question
never comes up.

## Reject before you trust

```
an HTTP request into this parser: NotOurProtocol
```

The magic number is checked before any length is read. A port on the internet
receives HTTP requests, TLS handshakes and scanner probes within minutes of
opening, and every one of them will parse as *something* if you let it. Two
bytes of magic turn that into an immediate rejection.

The same applies to the tag byte:

```zig
.kind = std.enums.fromInt(Kind, kind) orelse return error.UnknownKind,
```

A byte off the wire is attacker-controlled. `@enumFromInt` on a value that is
not a valid tag is undefined behaviour, not a shortcut, and `std.enums.fromInt`
is what returns null instead. It replaced `std.meta.intToEnum`, which no longer
exists: if you have that in your code, this is the swap.
