lib/wayland/src/connection.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const wayland = @import("root.zig");
4 const wayland_options = @import("wayland_options");
5
6 pub const Environment = struct {
7 wayland_socket: ?[]const u8 = null,
8 wayland_display: ?[]const u8 = null,
9 xdg_runtime_dir: ?[]const u8 = null,
10
11 pub fn current() Environment {
12 return .{
13 .wayland_socket = sys.env.getConstant("WAYLAND_SOCKET"),
14 .wayland_display = sys.env.getConstant("WAYLAND_DISPLAY"),
15 .xdg_runtime_dir = sys.env.getConstant("XDG_RUNTIME_DIR"),
16 };
17 }
18 };
19
20 pub const ProcessEnvironmentLimits = struct {
21 retained_entry_count: usize = wayland_options.process_environment_retained_entry_count,
22 };
23
24 pub const ProcessEnvironmentCapacityError = error{
25 CapacityOverflow,
26 };
27
28 pub const ProcessEnvironmentCapacity = struct {
29 retained_entry_count: usize,
30 source_entry_count: usize,
31 generation_entry_count: usize,
32 generation_bytes: usize,
33 generation_owner_bytes: usize,
34 total_static_bytes: usize,
35
36 pub fn derive(limits: ProcessEnvironmentLimits) ProcessEnvironmentCapacityError!ProcessEnvironmentCapacity {
37 const source_entry_count = std.math.add(
38 usize,
39 limits.retained_entry_count,
40 1,
41 ) catch return error.CapacityOverflow;
42 const generation_bytes = std.math.mul(
43 usize,
44 source_entry_count,
45 @sizeOf(?[*:0]const u8),
46 ) catch return error.CapacityOverflow;
47 const generation_owner_bytes = @sizeOf(sys.env.GenerationStorage);
48 const total_static_bytes = std.math.add(
49 usize,
50 generation_bytes,
51 generation_owner_bytes,
52 ) catch return error.CapacityOverflow;
53 return .{
54 .retained_entry_count = limits.retained_entry_count,
55 .source_entry_count = source_entry_count,
56 .generation_entry_count = source_entry_count,
57 .generation_bytes = generation_bytes,
58 .generation_owner_bytes = generation_owner_bytes,
59 .total_static_bytes = total_static_bytes,
60 };
61 }
62 };
63
64 pub const process_environment_capacity = ProcessEnvironmentCapacity.derive(.{}) catch |err| {
65 @compileError("invalid Wayland process environment capacity: " ++ @errorName(err));
66 };
67
68 var process_environment_generation_entries: [process_environment_capacity.generation_entry_count]?[*:0]const u8 = undefined;
69 var process_environment_generation =
70 sys.env.GenerationStorage.init(&process_environment_generation_entries);
71
72 pub const Error = std.mem.Allocator.Error || sys.net.StreamConnectError || sys.fd.FlagError || error{
73 InvalidInheritedDescriptor,
74 MissingRuntimeDirectory,
75 InvalidRuntimeDirectory,
76 ProcessEnvironmentCapacityExceeded,
77 ProcessEnvironmentGenerationConsumed,
78 InvalidDisplayName,
79 SocketPathTooLong,
80 };
81
82 const test_capacity = wayland.TransportCapacity.derive(.{}) catch unreachable;
83
84 const Endpoint = union(enum) {
85 inherited: sys.fd.Descriptor,
86 path: []const u8,
87 };
88
89 const InheritedSocketClaim = struct {
90 descriptor: ?sys.fd.Descriptor = null,
91
92 fn validate(self: *@This(), descriptor_text: []const u8) bool {
93 const descriptor = std.fmt.parseInt(sys.fd.Descriptor, descriptor_text, 10) catch return false;
94 if (descriptor < 0) return false;
95 sys.fd.setCloseOnExec(descriptor) catch return false;
96 self.descriptor = descriptor;
97 return true;
98 }
99 };
100
101 pub fn connect(
102 session_allocator: std.mem.Allocator,
103 capacity: wayland.transport.Capacity,
104 ) Error!wayland.Transport {
105 return connectProcessEnvironmentStorage(
106 session_allocator,
107 null,
108 capacity,
109 &process_environment_generation,
110 );
111 }
112
113 pub fn connectNamed(
114 session_allocator: std.mem.Allocator,
115 display_name: []const u8,
116 capacity: wayland.transport.Capacity,
117 ) Error!wayland.Transport {
118 return connectProcessEnvironmentStorage(
119 session_allocator,
120 display_name,
121 capacity,
122 &process_environment_generation,
123 );
124 }
125
126 fn connectProcessEnvironmentStorage(
127 session_allocator: std.mem.Allocator,
128 display_name: ?[]const u8,
129 capacity: wayland.transport.Capacity,
130 generation: *sys.env.GenerationStorage,
131 ) Error!wayland.Transport {
132 var storage = try wayland.transport.Storage.init(session_allocator, capacity);
133 errdefer storage.deinit();
134 var claim: InheritedSocketClaim = .{};
135 const inherited = sys.env.claimIf(
136 generation,
137 "WAYLAND_SOCKET",
138 &claim,
139 InheritedSocketClaim.validate,
140 ) catch |err| switch (err) {
141 error.ConditionRejected => return error.InvalidInheritedDescriptor,
142 error.GenerationStorageAlreadyUsed => return error.ProcessEnvironmentGenerationConsumed,
143 error.GenerationStorageTooSmall => return error.ProcessEnvironmentCapacityExceeded,
144 };
145 if (inherited) return storage.attach(claim.descriptor.?);
146 var environment = Environment.current();
147 environment.wayland_socket = null;
148 return connectNamedWithEnvironmentStorage(&storage, display_name, environment);
149 }
150
151 pub fn connectNamedWithEnvironment(
152 session_allocator: std.mem.Allocator,
153 display_name: ?[]const u8,
154 environment: Environment,
155 capacity: wayland.transport.Capacity,
156 ) Error!wayland.Transport {
157 var storage = try wayland.transport.Storage.init(session_allocator, capacity);
158 errdefer storage.deinit();
159 return connectNamedWithEnvironmentStorage(&storage, display_name, environment);
160 }
161
162 fn connectNamedWithEnvironmentStorage(
163 storage: *wayland.transport.Storage,
164 display_name: ?[]const u8,
165 environment: Environment,
166 ) Error!wayland.Transport {
167 var path_buffer: [sys.net.unix_path_capacity - 1]u8 = undefined;
168 return switch (try resolve(display_name, environment, &path_buffer)) {
169 .inherited => |descriptor| storage.attach(descriptor),
170 .path => |path| connectPath(storage, path),
171 };
172 }
173
174 fn connectPath(
175 storage: *wayland.transport.Storage,
176 path: []const u8,
177 ) Error!wayland.Transport {
178 const address = sys.net.Address.initUnix(path) catch return error.SocketPathTooLong;
179 const stream = try sys.net.connectStream(address);
180 return storage.attach(stream.handle);
181 }
182
183 fn resolve(
184 display_name: ?[]const u8,
185 environment: Environment,
186 path_buffer: *[sys.net.unix_path_capacity - 1]u8,
187 ) Error!Endpoint {
188 if (environment.wayland_socket) |descriptor_text| {
189 const descriptor = std.fmt.parseInt(sys.fd.Descriptor, descriptor_text, 10) catch {
190 return error.InvalidInheritedDescriptor;
191 };
192 if (descriptor < 0) return error.InvalidInheritedDescriptor;
193 return .{ .inherited = descriptor };
194 }
195
196 const name = display_name orelse environment.wayland_display orelse "wayland-0";
197 if (std.mem.indexOfScalar(u8, name, 0) != null) return error.InvalidDisplayName;
198 if (name.len != 0 and name[0] == '/') {
199 if (name.len >= sys.net.unix_path_capacity) return error.SocketPathTooLong;
200 return .{ .path = name };
201 }
202
203 const runtime_dir = environment.xdg_runtime_dir orelse return error.MissingRuntimeDirectory;
204 if (runtime_dir.len == 0 or runtime_dir[0] != '/' or std.mem.indexOfScalar(u8, runtime_dir, 0) != null) {
205 return error.InvalidRuntimeDirectory;
206 }
207 const separator_and_name_len = std.math.add(usize, name.len, 1) catch {
208 return error.SocketPathTooLong;
209 };
210 const path_len = std.math.add(usize, runtime_dir.len, separator_and_name_len) catch {
211 return error.SocketPathTooLong;
212 };
213 if (path_len >= sys.net.unix_path_capacity) return error.SocketPathTooLong;
214 @memcpy(path_buffer[0..runtime_dir.len], runtime_dir);
215 path_buffer[runtime_dir.len] = '/';
216 @memcpy(path_buffer[runtime_dir.len + 1 .. path_len], name);
217 return .{ .path = path_buffer[0..path_len] };
218 }
219
220 test "display endpoint resolution follows the Wayland environment contract" {
221 var buffer: [sys.net.unix_path_capacity - 1]u8 = undefined;
222
223 const fallback = try resolve(null, .{ .xdg_runtime_dir = "/run/user/1000" }, &buffer);
224 try std.testing.expectEqualStrings("/run/user/1000/wayland-0", fallback.path);
225
226 const selected = try resolve(null, .{
227 .wayland_display = "wayland-7",
228 .xdg_runtime_dir = "/runtime",
229 }, &buffer);
230 try std.testing.expectEqualStrings("/runtime/wayland-7", selected.path);
231
232 const absolute = try resolve("/tmp/custom-wayland", .{}, &buffer);
233 try std.testing.expectEqualStrings("/tmp/custom-wayland", absolute.path);
234
235 const inherited = try resolve("ignored", .{
236 .wayland_socket = "42",
237 .wayland_display = "also-ignored",
238 }, &buffer);
239 try std.testing.expectEqual(@as(sys.fd.Descriptor, 42), inherited.inherited);
240 }
241
242 test "invalid display endpoints fail before opening a socket" {
243 var buffer: [sys.net.unix_path_capacity - 1]u8 = undefined;
244 try std.testing.expectError(error.MissingRuntimeDirectory, resolve(null, .{}, &buffer));
245 try std.testing.expectError(
246 error.InvalidRuntimeDirectory,
247 resolve(null, .{ .xdg_runtime_dir = "relative" }, &buffer),
248 );
249 try std.testing.expectError(
250 error.InvalidInheritedDescriptor,
251 resolve(null, .{ .wayland_socket = "4x" }, &buffer),
252 );
253 try std.testing.expectError(
254 error.InvalidInheritedDescriptor,
255 resolve(null, .{ .wayland_socket = "-1" }, &buffer),
256 );
257
258 var long_name: [sys.net.unix_path_capacity]u8 = @splat('w');
259 long_name[0] = '/';
260 try std.testing.expectError(error.SocketPathTooLong, resolve(&long_name, .{}, &buffer));
261 }
262
263 test "connection acquires transport storage before endpoint effects" {
264 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
265 .fail_index = 0,
266 });
267 try std.testing.expectError(
268 error.OutOfMemory,
269 connectNamedWithEnvironment(failing.allocator(), null, .{}, test_capacity),
270 );
271 }
272
273 test "process environment capacity derives exact literal-static generation storage" {
274 const capacity = try ProcessEnvironmentCapacity.derive(.{
275 .retained_entry_count = 3,
276 });
277 try std.testing.expectEqual(@as(usize, 3), capacity.retained_entry_count);
278 try std.testing.expectEqual(@as(usize, 4), capacity.source_entry_count);
279 try std.testing.expectEqual(@as(usize, 4), capacity.generation_entry_count);
280 try std.testing.expectEqual(
281 4 * @sizeOf(?[*:0]const u8),
282 capacity.generation_bytes,
283 );
284 try std.testing.expectEqual(
285 @sizeOf(sys.env.GenerationStorage),
286 capacity.generation_owner_bytes,
287 );
288 try std.testing.expectEqual(
289 capacity.generation_bytes + capacity.generation_owner_bytes,
290 capacity.total_static_bytes,
291 );
292 try std.testing.expectEqual(
293 process_environment_capacity.generation_entry_count,
294 process_environment_generation_entries.len,
295 );
296 try std.testing.expectEqual(
297 process_environment_capacity.total_static_bytes,
298 @sizeOf(@TypeOf(process_environment_generation_entries)) +
299 @sizeOf(@TypeOf(process_environment_generation)),
300 );
301 }
302
303 test "process environment capacity rejects arithmetic overflow" {
304 try std.testing.expectError(
305 error.CapacityOverflow,
306 ProcessEnvironmentCapacity.derive(.{
307 .retained_entry_count = std.math.maxInt(usize),
308 }),
309 );
310 }
311
312 test "inherited Wayland socket ownership transfers to the transport" {
313 const sockets = sys.fd.socketPairUnixStream(.{ .close_on_exec = true }) catch |err| switch (err) {
314 error.UnsupportedPlatform => return error.SkipZigTest,
315 else => return err,
316 };
317 var inherited_owned = true;
318 defer if (inherited_owned) sys.fd.close(sockets[0]);
319 defer sys.fd.close(sockets[1]);
320
321 var descriptor_text: [32]u8 = undefined;
322 const value = try std.fmt.bufPrint(&descriptor_text, "{d}", .{sockets[0]});
323 var transport = try connectNamedWithEnvironment(std.testing.allocator, "ignored", .{
324 .wayland_socket = value,
325 }, test_capacity);
326 inherited_owned = false;
327 defer transport.deinit();
328
329 try transport.queueOwned(2, 3, &.{}, &.{});
330 try std.testing.expectEqual(wayland.stream.FlushStatus.drained, try transport.flush());
331 var message: [wayland.wire.header_size]u8 = undefined;
332 try std.testing.expectEqual(message.len, try sys.fd.read(sockets[1], &message));
333 try std.testing.expectEqual(@as(u32, 2), (try wayland.wire.decode(&message)).object_id);
334 }
335
336 test "process inherited socket claim allocates only transport storage" {
337 if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
338 if (comptime @import("builtin").os.tag != .linux) return error.SkipZigTest;
339
340 const sockets = try sys.fd.socketPairUnixStream(.{ .close_on_exec = false });
341 var inherited_owned = true;
342 defer if (inherited_owned) sys.fd.close(sockets[0]);
343 defer sys.fd.close(sockets[1]);
344
345 var descriptor_text: [32]u8 = undefined;
346 const value = try std.fmt.bufPrint(&descriptor_text, "{d}", .{sockets[0]});
347 var map = sys.env.Map.init(std.testing.allocator);
348 defer map.deinit();
349 try map.put("WAYLAND_SOCKET", value);
350 const block = try map.createPosixBlock(std.testing.allocator, .{});
351 defer block.deinit(std.testing.allocator);
352 const previous = sys.env.current();
353 sys.env.installProcessEnvironment(.{ .block = block });
354 defer sys.env.installProcessEnvironment(previous);
355
356 var generation_entries: [1]?[*:0]const u8 = undefined;
357 var generation = sys.env.GenerationStorage.init(&generation_entries);
358 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{
359 .fail_index = 4,
360 });
361 var transport = try connectProcessEnvironmentStorage(
362 failing.allocator(),
363 null,
364 test_capacity,
365 &generation,
366 );
367 inherited_owned = false;
368 defer transport.deinit();
369 try std.testing.expectEqual(@as(usize, 4), failing.allocations);
370 try std.testing.expect(generation.used);
371 try std.testing.expect(sys.env.getConstant("WAYLAND_SOCKET") == null);
372
373 try std.testing.expect(try sys.fd.closeOnExec(transport.descriptor));
374 try std.testing.expect(try sys.fd.nonBlocking(transport.descriptor));
375 }
376
377 test "process environment max plus one rejects before descriptor effects" {
378 if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
379 if (comptime @import("builtin").os.tag != .linux) return error.SkipZigTest;
380
381 const sockets = try sys.fd.socketPairUnixStream(.{ .close_on_exec = false });
382 defer sys.fd.close(sockets[0]);
383 defer sys.fd.close(sockets[1]);
384
385 var descriptor_text: [32]u8 = undefined;
386 const value = try std.fmt.bufPrint(&descriptor_text, "{d}", .{sockets[0]});
387 var map = sys.env.Map.init(std.testing.allocator);
388 defer map.deinit();
389 try map.put("WAYLAND_SOCKET", value);
390 try map.put("TINY_WAYLAND_RETAIN", "visible");
391 const block = try map.createPosixBlock(std.testing.allocator, .{});
392 defer block.deinit(std.testing.allocator);
393 const previous = sys.env.current();
394 sys.env.installProcessEnvironment(.{ .block = block });
395 defer sys.env.installProcessEnvironment(previous);
396
397 var generation_entries: [1]?[*:0]const u8 = undefined;
398 var generation = sys.env.GenerationStorage.init(&generation_entries);
399 try std.testing.expectError(
400 error.ProcessEnvironmentCapacityExceeded,
401 connectProcessEnvironmentStorage(
402 std.testing.allocator,
403 null,
404 test_capacity,
405 &generation,
406 ),
407 );
408 try std.testing.expect(!generation.used);
409 try std.testing.expectEqualStrings(value, sys.env.getConstant("WAYLAND_SOCKET").?);
410 try std.testing.expectEqualStrings(
411 "visible",
412 sys.env.getConstant("TINY_WAYLAND_RETAIN").?,
413 );
414
415 try std.testing.expect(!try sys.fd.closeOnExec(sockets[0]));
416 }
417
418 test "invalid process inherited socket text remains installed" {
419 if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
420
421 var map = sys.env.Map.init(std.testing.allocator);
422 defer map.deinit();
423 try map.put("WAYLAND_SOCKET", "4x");
424 const block = try map.createPosixBlock(std.testing.allocator, .{});
425 defer block.deinit(std.testing.allocator);
426 const previous = sys.env.current();
427 sys.env.installProcessEnvironment(.{ .block = block });
428 defer sys.env.installProcessEnvironment(previous);
429
430 var generation_entries: [1]?[*:0]const u8 = undefined;
431 var generation = sys.env.GenerationStorage.init(&generation_entries);
432 try std.testing.expectError(
433 error.InvalidInheritedDescriptor,
434 connectProcessEnvironmentStorage(
435 std.testing.allocator,
436 null,
437 test_capacity,
438 &generation,
439 ),
440 );
441 try std.testing.expectEqualStrings("4x", sys.env.getConstant("WAYLAND_SOCKET").?);
442
443 var inherited = try sys.env.createMap(std.testing.allocator);
444 defer inherited.deinit();
445 try std.testing.expectEqualStrings("4x", inherited.get("WAYLAND_SOCKET").?);
446 }
447
448 test "closed process inherited socket remains installed" {
449 if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
450 if (comptime @import("builtin").os.tag != .linux) return error.SkipZigTest;
451
452 const sockets = try sys.fd.socketPairUnixStream(.{ .close_on_exec = false });
453 sys.fd.close(sockets[0]);
454 defer sys.fd.close(sockets[1]);
455
456 var descriptor_text: [32]u8 = undefined;
457 const value = try std.fmt.bufPrint(&descriptor_text, "{d}", .{sockets[0]});
458 var map = sys.env.Map.init(std.testing.allocator);
459 defer map.deinit();
460 try map.put("WAYLAND_SOCKET", value);
461 const block = try map.createPosixBlock(std.testing.allocator, .{});
462 defer block.deinit(std.testing.allocator);
463 const previous = sys.env.current();
464 sys.env.installProcessEnvironment(.{ .block = block });
465 defer sys.env.installProcessEnvironment(previous);
466
467 var generation_entries: [1]?[*:0]const u8 = undefined;
468 var generation = sys.env.GenerationStorage.init(&generation_entries);
469 try std.testing.expectError(
470 error.InvalidInheritedDescriptor,
471 connectProcessEnvironmentStorage(
472 std.testing.allocator,
473 null,
474 test_capacity,
475 &generation,
476 ),
477 );
478 try std.testing.expectEqualStrings(value, sys.env.getConstant("WAYLAND_SOCKET").?);
479
480 var inherited = try sys.env.createMap(std.testing.allocator);
481 defer inherited.deinit();
482 try std.testing.expectEqualStrings(value, inherited.get("WAYLAND_SOCKET").?);
483 }
484
485 const InheritedSocketClaimWorkerFixture = struct {
486 generation: *sys.env.GenerationStorage,
487 connected: bool = false,
488 failed: bool = false,
489
490 fn run(self: *@This()) void {
491 var transport = connectProcessEnvironmentStorage(
492 std.testing.allocator,
493 null,
494 test_capacity,
495 self.generation,
496 ) catch |err| switch (err) {
497 error.MissingRuntimeDirectory => return,
498 else => {
499 self.failed = true;
500 return;
501 },
502 };
503 self.connected = true;
504 transport.deinit();
505 }
506 };
507
508 test "concurrent inherited socket claims transfer exactly once" {
509 if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
510 if (comptime @import("builtin").os.tag != .linux) return error.SkipZigTest;
511
512 const sockets = try sys.fd.socketPairUnixStream(.{ .close_on_exec = false });
513 var inherited_owned = true;
514 defer if (inherited_owned) sys.fd.close(sockets[0]);
515 defer sys.fd.close(sockets[1]);
516
517 var descriptor_text: [32]u8 = undefined;
518 const value = try std.fmt.bufPrint(&descriptor_text, "{d}", .{sockets[0]});
519 var map = sys.env.Map.init(std.testing.allocator);
520 defer map.deinit();
521 try map.put("WAYLAND_SOCKET", value);
522 const block = try map.createPosixBlock(std.testing.allocator, .{});
523 defer block.deinit(std.testing.allocator);
524 const previous = sys.env.current();
525 sys.env.installProcessEnvironment(.{ .block = block });
526 defer sys.env.installProcessEnvironment(previous);
527
528 var generation_entries: [1]?[*:0]const u8 = undefined;
529 var generation = sys.env.GenerationStorage.init(&generation_entries);
530 var workers: [8]InheritedSocketClaimWorkerFixture = undefined;
531 for (&workers) |*worker| worker.* = .{ .generation = &generation };
532 var threads: [workers.len]std.Thread = undefined;
533 for (&threads, &workers) |*thread, *worker| {
534 thread.* = try std.Thread.spawn(.{}, InheritedSocketClaimWorkerFixture.run, .{worker});
535 }
536 for (&threads) |*thread| thread.join();
537
538 var connected: usize = 0;
539 for (workers) |worker| {
540 try std.testing.expect(!worker.failed);
541 if (worker.connected) connected += 1;
542 }
543 try std.testing.expectEqual(@as(usize, 1), connected);
544 inherited_owned = false;
545 try std.testing.expect(sys.env.getConstant("WAYLAND_SOCKET") == null);
546 }
547
548 test "display name connects through the runtime directory" {
549 var tmp = std.testing.tmpDir(.{});
550 defer tmp.cleanup();
551
552 var runtime_buffer: [std.fs.max_path_bytes]u8 = undefined;
553 const runtime_len = try tmp.dir.realPath(std.Options.debug_io, &runtime_buffer);
554 const runtime_dir = runtime_buffer[0..runtime_len];
555 var socket_path_buffer: [sys.net.unix_path_capacity - 1]u8 = undefined;
556 const socket_path = try std.fmt.bufPrint(&socket_path_buffer, "{s}/wayland-test", .{runtime_dir});
557
558 var server = try sys.net.Address.listen(try sys.net.Address.initUnix(socket_path), .{});
559 defer server.deinit();
560
561 var transport = try connectNamedWithEnvironment(std.testing.allocator, "wayland-test", .{
562 .xdg_runtime_dir = runtime_dir,
563 }, test_capacity);
564 defer transport.deinit();
565 const accepted = try server.accept();
566 defer accepted.stream.close();
567
568 try transport.queueOwned(8, 5, &.{}, &.{});
569 try std.testing.expectEqual(wayland.stream.FlushStatus.drained, try transport.flush());
570 var message: [wayland.wire.header_size]u8 = undefined;
571 try std.testing.expectEqual(message.len, try accepted.stream.read(&message));
572 const header = try wayland.wire.decode(&message);
573 try std.testing.expectEqual(@as(u32, 8), header.object_id);
574 try std.testing.expectEqual(@as(u16, 5), header.opcode);
575 }