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}