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