commit 3e210a9

shar  ·  2026-06-24 00:36:19 +0000 UTC
parent 3e210a9
wire protocol: binary message parser/serializer with unit tests
5 files changed,  +544, -0
+11, -0
 1@@ -0,0 +1,11 @@
 2+Copyright 2026 Shar (contact@sharflare.dev)
 3+
 4+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 5+
 6+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 7+
 8+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 9+
10+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11+
12+
+3, -0
1@@ -0,0 +1,3 @@
2+# Altaica
3+wayland but for the web
4+really WIP but somewhat lovingly commented when i can remember to comment.
+16, -0
 1@@ -0,0 +1,16 @@
 2+const std = @import("std");
 3+
 4+pub fn build(b: *std.Build) void {
 5+    const target = b.standardTargetOptions(.{});
 6+    const optimize = b.standardOptimizeOption(.{});
 7+
 8+    const wire_mod = b.createModule(.{
 9+        .root_source_file = b.path("compositor/src/wire.zig"),
10+        .target = target,
11+        .optimize = optimize,
12+    });
13+
14+    const wire_test = b.addTest(.{ .root_module = wire_mod });
15+    const test_step = b.step("test", "Run wire protocol unit tests");
16+    test_step.dependOn(&b.addRunArtifact(wire_test).step);
17+}
+7, -0
1@@ -0,0 +1,7 @@
2+.{
3+    .name = .wire,
4+    .version = "0.0.1",
5+    .minimum_zig_version = "0.16.0",
6+    .fingerprint = 0xb07b39351ac564a0,
7+    .paths = .{"compositor/src/wire.zig"},
8+}
+507, -0
  1@@ -0,0 +1,507 @@
  2+const std = @import("std");
  3+
  4+/// Header of every Wayland wire message (8 bytes).
  5+pub const Header = struct {
  6+    object_id: u32,
  7+    opcode: u16,
  8+    size: u16,
  9+
 10+    pub fn decode(data: []const u8) Header {
 11+        return .{
 12+            .object_id = std.mem.readInt(u32, data[0..4], .little),
 13+            .size = std.mem.readInt(u16, data[4..6], .little),
 14+            .opcode = std.mem.readInt(u16, data[6..8], .little),
 15+        };
 16+    }
 17+
 18+    pub fn encode(self: Header, buf: []u8) void {
 19+        std.mem.writeInt(u32, buf[0..4], self.object_id, .little);
 20+        std.mem.writeInt(u16, buf[4..6], self.size, .little);
 21+        std.mem.writeInt(u16, buf[6..8], self.opcode, .little);
 22+    }
 23+};
 24+
 25+/// A single argument in the Wayland wire protocol.
 26+pub const Arg = union(enum) {
 27+    int: i32,
 28+    uint: u32,
 29+    @"fixed": i32,
 30+    string: ?[]const u8,
 31+    object: u32,
 32+    new_id: u32,
 33+    array: []const u8,
 34+    fd: u32,
 35+};
 36+
 37+/// Argument type tag matching the wire format.
 38+pub const ArgType = enum {
 39+    int,
 40+    uint,
 41+    @"fixed",
 42+    string,
 43+    object,
 44+    new_id,
 45+    array,
 46+    fd,
 47+};
 48+
 49+pub const DecodeError = error{
 50+    BufferTooSmall,
 51+    InvalidString,
 52+};
 53+
 54+/// Compute padding needed to reach the next 4-byte boundary.
 55+pub fn pad(n: usize) usize {
 56+    return (4 - (@as(u32, @truncate(n)) & 3)) & 3;
 57+}
 58+
 59+/// Return the encoded wire size of a single argument.
 60+pub fn argEncodedSize(arg: Arg) usize {
 61+    return switch (arg) {
 62+        .int, .uint, .@"fixed", .object, .new_id, .fd => 4,
 63+        .string => |s| {
 64+            if (s) |bytes| {
 65+                const len: usize = bytes.len + 1;
 66+                return 4 + len + pad(len);
 67+            }
 68+            return 4;
 69+        },
 70+        .array => |bytes| {
 71+            return 4 + bytes.len + pad(bytes.len);
 72+        },
 73+    };
 74+}
 75+
 76+/// Return the total wire size for a header + args sequence.
 77+pub fn messageSize(header: Header, args: []const Arg) u16 {
 78+    _ = header;
 79+    var total: u16 = 8;
 80+    for (args) |arg| {
 81+        total +|= @as(u16, @truncate(argEncodedSize(arg)));
 82+    }
 83+    return total;
 84+}
 85+
 86+fn readU32(data: []const u8, offset: *usize) DecodeError!u32 {
 87+    if (offset.* + 4 > data.len) return error.BufferTooSmall;
 88+    const val = std.mem.readInt(u32, data[offset.*..][0..4], .little);
 89+    offset.* += 4;
 90+    return val;
 91+}
 92+
 93+/// Decode a single argument starting at `offset.*`, advancing past it.
 94+pub fn readArg(data: []const u8, offset: *usize, arg_type: ArgType) DecodeError!Arg {
 95+    return switch (arg_type) {
 96+        .int => Arg{ .int = @as(i32, @bitCast(try readU32(data, offset))) },
 97+        .uint => Arg{ .uint = try readU32(data, offset) },
 98+        .@"fixed" => Arg{ .@"fixed" = @as(i32, @bitCast(try readU32(data, offset))) },
 99+        .object => Arg{ .object = try readU32(data, offset) },
100+        .new_id => Arg{ .new_id = try readU32(data, offset) },
101+        .fd => Arg{ .fd = try readU32(data, offset) },
102+        .string => {
103+            const len = try readU32(data, offset);
104+            if (len == 0) return Arg{ .string = null };
105+            if (offset.* + len > data.len) return error.BufferTooSmall;
106+            if (data[offset.* + len - 1] != 0) return error.InvalidString;
107+            const slice = data[offset.* .. offset.* + len - 1];
108+            offset.* += len;
109+            offset.* += pad(len);
110+            return Arg{ .string = slice };
111+        },
112+        .array => {
113+            const len = try readU32(data, offset);
114+            if (offset.* + len > data.len) return error.BufferTooSmall;
115+            const slice = data[offset.* .. offset.* + len];
116+            offset.* += len;
117+            offset.* += pad(len);
118+            return Arg{ .array = slice };
119+        },
120+    };
121+}
122+
123+/// Read past a single argument (skip decoding its value).
124+pub fn skipArg(data: []const u8, offset: *usize, arg_type: ArgType) DecodeError!void {
125+    _ = try readArg(data, offset, arg_type);
126+}
127+
128+/// Write a single argument into `buf` at `offset.*`, advancing the offset.
129+/// The caller must ensure `buf` has enough space; use `argEncodedSize` to
130+/// compute the required capacity ahead of time.
131+pub fn writeArg(buf: []u8, offset: *usize, arg: Arg) void {
132+    switch (arg) {
133+        .int => |val| {
134+            std.mem.writeInt(i32, buf[offset.*..][0..4], val, .little);
135+            offset.* += 4;
136+        },
137+        .uint => |val| {
138+            std.mem.writeInt(u32, buf[offset.*..][0..4], val, .little);
139+            offset.* += 4;
140+        },
141+        .@"fixed" => |val| {
142+            std.mem.writeInt(i32, buf[offset.*..][0..4], val, .little);
143+            offset.* += 4;
144+        },
145+        .object => |val| {
146+            std.mem.writeInt(u32, buf[offset.*..][0..4], val, .little);
147+            offset.* += 4;
148+        },
149+        .new_id => |val| {
150+            std.mem.writeInt(u32, buf[offset.*..][0..4], val, .little);
151+            offset.* += 4;
152+        },
153+        .fd => |val| {
154+            std.mem.writeInt(u32, buf[offset.*..][0..4], val, .little);
155+            offset.* += 4;
156+        },
157+        .string => |maybe_s| {
158+            if (maybe_s) |s| {
159+                const len: u32 = @as(u32, @truncate(s.len)) + 1;
160+                std.mem.writeInt(u32, buf[offset.*..][0..4], len, .little);
161+                offset.* += 4;
162+                @memcpy(buf[offset.*..][0..s.len], s);
163+                offset.* += s.len;
164+                buf[offset.*] = 0;
165+                offset.* += 1;
166+                const p = pad(len);
167+                @memset(buf[offset.*..][0..p], 0);
168+                offset.* += p;
169+            } else {
170+                std.mem.writeInt(u32, buf[offset.*..][0..4], 0, .little);
171+                offset.* += 4;
172+            }
173+        },
174+        .array => |bytes| {
175+            const len: u32 = @as(u32, @truncate(bytes.len));
176+            std.mem.writeInt(u32, buf[offset.*..][0..4], len, .little);
177+            offset.* += 4;
178+            @memcpy(buf[offset.*..][0..bytes.len], bytes);
179+            offset.* += bytes.len;
180+            const p = pad(bytes.len);
181+            @memset(buf[offset.*..][0..p], 0);
182+            offset.* += p;
183+        },
184+    }
185+}
186+
187+// --- Tests ---
188+
189+test "header roundtrip" {
190+    const hdr = Header{
191+        .object_id = 0xdeadbeef,
192+        .opcode = 0x0a0b,
193+        .size = 64,
194+    };
195+    var buf: [8]u8 = undefined;
196+    hdr.encode(&buf);
197+    const decoded = Header.decode(&buf);
198+    try std.testing.expectEqual(hdr.object_id, decoded.object_id);
199+    try std.testing.expectEqual(hdr.opcode, decoded.opcode);
200+    try std.testing.expectEqual(hdr.size, decoded.size);
201+}
202+
203+test "int arg roundtrip" {
204+    var buf: [4]u8 = undefined;
205+    var off: usize = 0;
206+    writeArg(&buf, &off, Arg{ .int = -42 });
207+    try std.testing.expectEqual(@as(usize, 4), off);
208+
209+    off = 0;
210+    const arg = try readArg(&buf, &off, .int);
211+    try std.testing.expectEqual(@as(i32, -42), arg.int);
212+}
213+
214+test "uint arg roundtrip" {
215+    var buf: [4]u8 = undefined;
216+    var off: usize = 0;
217+    writeArg(&buf, &off, Arg{ .uint = 0xff00ff00 });
218+    try std.testing.expectEqual(@as(usize, 4), off);
219+
220+    off = 0;
221+    const arg = try readArg(&buf, &off, .uint);
222+    try std.testing.expectEqual(@as(u32, 0xff00ff00), arg.uint);
223+}
224+
225+test "fixed arg roundtrip" {
226+    var buf: [4]u8 = undefined;
227+    var off: usize = 0;
228+    writeArg(&buf, &off, Arg{ .@"fixed" = 0x12345678 });
229+    try std.testing.expectEqual(@as(usize, 4), off);
230+
231+    off = 0;
232+    const arg = try readArg(&buf, &off, .@"fixed");
233+    try std.testing.expectEqual(@as(i32, 0x12345678), arg.@"fixed");
234+}
235+
236+test "object arg roundtrip" {
237+    var buf: [4]u8 = undefined;
238+    var off: usize = 0;
239+    writeArg(&buf, &off, Arg{ .object = 42 });
240+    try std.testing.expectEqual(@as(usize, 4), off);
241+
242+    off = 0;
243+    const arg = try readArg(&buf, &off, .object);
244+    try std.testing.expectEqual(@as(u32, 42), arg.object);
245+}
246+
247+test "new_id arg roundtrip" {
248+    var buf: [4]u8 = undefined;
249+    var off: usize = 0;
250+    writeArg(&buf, &off, Arg{ .new_id = 7 });
251+    try std.testing.expectEqual(@as(usize, 4), off);
252+
253+    off = 0;
254+    const arg = try readArg(&buf, &off, .new_id);
255+    try std.testing.expectEqual(@as(u32, 7), arg.new_id);
256+}
257+
258+test "fd arg roundtrip" {
259+    var buf: [4]u8 = undefined;
260+    var off: usize = 0;
261+    writeArg(&buf, &off, Arg{ .fd = 0x1234 });
262+    try std.testing.expectEqual(@as(usize, 4), off);
263+
264+    off = 0;
265+    const arg = try readArg(&buf, &off, .fd);
266+    try std.testing.expectEqual(@as(u32, 0x1234), arg.fd);
267+}
268+
269+test "null string roundtrip" {
270+    var buf: [4]u8 = undefined;
271+    var off: usize = 0;
272+    writeArg(&buf, &off, Arg{ .string = null });
273+    try std.testing.expectEqual(@as(usize, 4), off);
274+
275+    off = 0;
276+    const arg = try readArg(&buf, &off, .string);
277+    try std.testing.expect(arg.string == null);
278+}
279+
280+test "empty string roundtrip" {
281+    var buf: [8]u8 = undefined;
282+    // empty string "" has len=1 (just null terminator) + 3 pad = 8 total
283+    var off: usize = 0;
284+    writeArg(&buf, &off, Arg{ .string = "" });
285+    try std.testing.expectEqual(@as(usize, 8), off);
286+
287+    off = 0;
288+    const arg = try readArg(&buf, &off, .string);
289+    try std.testing.expectEqualSlices(u8, "", arg.string.?);
290+}
291+
292+test "non-empty string roundtrip" {
293+    const input = "hello";
294+    const wire_size = 4 + input.len + 1 + pad(input.len + 1);
295+    var buf: [32]u8 = undefined;
296+    var off: usize = 0;
297+    writeArg(&buf, &off, Arg{ .string = input });
298+    try std.testing.expectEqual(wire_size, off);
299+
300+    off = 0;
301+    const arg = try readArg(&buf, &off, .string);
302+    try std.testing.expectEqualSlices(u8, input, arg.string.?);
303+}
304+
305+test "string with padding" {
306+    const input = "abcdef"; // 6 chars -> wire: 4+7+1=12 (padded to 12 already, but 7 -> pad=1)
307+    // len = 7 (6 + null), 7 % 4 = 3, pad = 1, total = 4 + 7 + 1 = 12
308+    var buf: [12]u8 = undefined;
309+    var off: usize = 0;
310+    writeArg(&buf, &off, Arg{ .string = input });
311+    try std.testing.expectEqual(@as(usize, 12), off);
312+
313+    off = 0;
314+    const arg = try readArg(&buf, &off, .string);
315+    try std.testing.expectEqualSlices(u8, input, arg.string.?);
316+}
317+
318+test "string 3 chars -- no padding needed" {
319+    const input = "abc"; // len=3, wire: 4+4+0=8 (3+1=4, 4%4=0)
320+    var buf: [8]u8 = undefined;
321+    var off: usize = 0;
322+    writeArg(&buf, &off, Arg{ .string = input });
323+    try std.testing.expectEqual(@as(usize, 8), off);
324+
325+    off = 0;
326+    const arg = try readArg(&buf, &off, .string);
327+    try std.testing.expectEqualSlices(u8, input, arg.string.?);
328+}
329+
330+test "empty array roundtrip" {
331+    var buf: [4]u8 = undefined;
332+    var off: usize = 0;
333+    writeArg(&buf, &off, Arg{ .array = &[_]u8{} });
334+
335+    off = 0;
336+    const arg = try readArg(&buf, &off, .array);
337+    try std.testing.expectEqual(@as(usize, 0), arg.array.len);
338+}
339+
340+test "non-empty array roundtrip" {
341+    const input = &[_]u8{ 0xde, 0xad, 0xbe, 0xef };
342+    var buf: [12]u8 = undefined;
343+    var off: usize = 0;
344+    writeArg(&buf, &off, Arg{ .array = input });
345+    // 4 + 4 + 0 (4-byte aligned) = 8
346+    try std.testing.expectEqual(@as(usize, 8), off);
347+
348+    off = 0;
349+    const arg = try readArg(&buf, &off, .array);
350+    try std.testing.expectEqualSlices(u8, input, arg.array);
351+}
352+
353+test "array with padding" {
354+    const input = &[_]u8{ 0x01, 0x02, 0x03 }; // 3 bytes -> pad=1
355+    var buf: [8]u8 = undefined;
356+    var off: usize = 0;
357+    writeArg(&buf, &off, Arg{ .array = input });
358+    try std.testing.expectEqual(@as(usize, 8), off);
359+
360+    off = 0;
361+    const arg = try readArg(&buf, &off, .array);
362+    try std.testing.expectEqualSlices(u8, input, arg.array);
363+}
364+
365+test "buffer too small for fixed-size arg" {
366+    const data = &[_]u8{ 0x00, 0x00 };
367+    var off: usize = 0;
368+    try std.testing.expectError(error.BufferTooSmall, readArg(data, &off, .uint));
369+}
370+
371+test "buffer too small for string length" {
372+    const data = &[_]u8{ 0x00, 0x00 };
373+    var off: usize = 0;
374+    try std.testing.expectError(error.BufferTooSmall, readArg(data, &off, .string));
375+}
376+
377+test "buffer too small for string data" {
378+    var buf: [8]u8 = undefined;
379+    std.mem.writeInt(u32, buf[0..4], @as(u32, 100), .little); // claims 100 bytes follow
380+    var off: usize = 0;
381+    try std.testing.expectError(error.BufferTooSmall, readArg(&buf, &off, .string));
382+}
383+
384+test "invalid string missing null terminator" {
385+    var buf: [8]u8 = undefined;
386+    std.mem.writeInt(u32, buf[0..4], @as(u32, 4), .little); // len=4
387+    buf[4] = 'a';
388+    buf[5] = 'b';
389+    buf[6] = 'c';
390+    buf[7] = 'd'; // no null
391+    var off: usize = 0;
392+    try std.testing.expectError(error.InvalidString, readArg(&buf, &off, .string));
393+}
394+
395+test "multiple args in sequence" {
396+    var buf: [32]u8 = undefined;
397+    var off: usize = 0;
398+
399+    writeArg(&buf, &off, Arg{ .uint = 1 });
400+    writeArg(&buf, &off, Arg{ .uint = 2 });
401+    writeArg(&buf, &off, Arg{ .string = "three" });
402+    writeArg(&buf, &off, Arg{ .int = -4 });
403+
404+    off = 0;
405+    const a1 = try readArg(&buf, &off, .uint);
406+    try std.testing.expectEqual(@as(u32, 1), a1.uint);
407+
408+    const a2 = try readArg(&buf, &off, .uint);
409+    try std.testing.expectEqual(@as(u32, 2), a2.uint);
410+
411+    const a3 = try readArg(&buf, &off, .string);
412+    try std.testing.expectEqualSlices(u8, "three", a3.string.?);
413+
414+    const a4 = try readArg(&buf, &off, .int);
415+    try std.testing.expectEqual(@as(i32, -4), a4.int);
416+}
417+
418+test "skip arg advances offset" {
419+    var buf: [32]u8 = undefined;
420+    var off: usize = 0;
421+    writeArg(&buf, &off, Arg{ .uint = 0xaa });
422+    writeArg(&buf, &off, Arg{ .uint = 0xbb });
423+
424+    off = 0;
425+    try skipArg(&buf, &off, .uint);
426+    const arg = try readArg(&buf, &off, .uint);
427+    try std.testing.expectEqual(@as(u32, 0xbb), arg.uint);
428+}
429+
430+test "full message roundtrip" {
431+    const hdr = Header{
432+        .object_id = 2,
433+        .opcode = 0,
434+        .size = 0, // will be filled in
435+    };
436+    const args = [_]Arg{
437+        Arg{ .uint = 42 },
438+        Arg{ .string = "surface" },
439+        Arg{ .new_id = 3 },
440+    };
441+
442+    const total_size = messageSize(hdr, &args);
443+    const hdr_final = Header{ .object_id = 2, .opcode = 0, .size = total_size };
444+
445+    var buf: [64]u8 = undefined;
446+    hdr_final.encode(&buf);
447+
448+    var off: usize = 8;
449+    for (&args) |arg| writeArg(&buf, &off, arg);
450+    try std.testing.expectEqual(@as(usize, total_size), off);
451+
452+    // Decode
453+    const hdr2 = Header.decode(&buf);
454+    try std.testing.expectEqual(@as(u32, 2), hdr2.object_id);
455+    try std.testing.expectEqual(@as(u16, 0), hdr2.opcode);
456+    try std.testing.expectEqual(total_size, hdr2.size);
457+
458+    off = 8;
459+    const r1 = try readArg(&buf, &off, .uint);
460+    try std.testing.expectEqual(@as(u32, 42), r1.uint);
461+    const r2 = try readArg(&buf, &off, .string);
462+    try std.testing.expectEqualSlices(u8, "surface", r2.string.?);
463+    const r3 = try readArg(&buf, &off, .new_id);
464+    try std.testing.expectEqual(@as(u32, 3), r3.new_id);
465+}
466+
467+test "message size calculation" {
468+    const hdr = Header{ .object_id = 1, .opcode = 0, .size = 0 };
469+    const args = [_]Arg{
470+        Arg{ .int = 10 },
471+        Arg{ .uint = 20 },
472+        Arg{ .object = 30 },
473+        Arg{ .new_id = 40 },
474+        Arg{ .fd = 50 },
475+        Arg{ .@"fixed" = 60 },
476+    };
477+    // 6 fixed-size args x 4 bytes + 8 byte header = 32
478+    try std.testing.expectEqual(@as(u16, 32), messageSize(hdr, &args));
479+}
480+
481+test "pad function correctness" {
482+    try std.testing.expectEqual(@as(usize, 0), pad(0));
483+    try std.testing.expectEqual(@as(usize, 3), pad(1));
484+    try std.testing.expectEqual(@as(usize, 2), pad(2));
485+    try std.testing.expectEqual(@as(usize, 1), pad(3));
486+    try std.testing.expectEqual(@as(usize, 0), pad(4));
487+    try std.testing.expectEqual(@as(usize, 3), pad(5));
488+    try std.testing.expectEqual(@as(usize, 2), pad(6));
489+    try std.testing.expectEqual(@as(usize, 1), pad(7));
490+    try std.testing.expectEqual(@as(usize, 0), pad(8));
491+}
492+
493+test "arg encoded size correctness" {
494+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .int = 0 }));
495+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .uint = 0 }));
496+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .@"fixed" = 0 }));
497+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .object = 0 }));
498+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .new_id = 0 }));
499+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .fd = 0 }));
500+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .string = null }));
501+    try std.testing.expectEqual(@as(usize, 8), argEncodedSize(Arg{ .string = "" }));
502+    try std.testing.expectEqual(@as(usize, 4 + 4 + 0), argEncodedSize(Arg{ .string = "abc" }));
503+    try std.testing.expectEqual(@as(usize, 8), argEncodedSize(Arg{ .string = "ab" }));
504+    // "ab" -> len=3 (2+null), pad(3)=1, total=4+3+1=8. But 4+3+1=8 = 8.
505+    try std.testing.expectEqual(@as(usize, 8), argEncodedSize(Arg{ .string = "ab" }));
506+    try std.testing.expectEqual(@as(usize, 4), argEncodedSize(Arg{ .array = &[_]u8{} }));
507+    try std.testing.expectEqual(@as(usize, 8), argEncodedSize(Arg{ .array = &[_]u8{ 1, 2, 3, 4 } }));
508+}