Eric Bower
·
2026-03-23
socket.zig
1const std = @import("std");
2const posix = std.posix;
3
4pub fn getSeshPrefix() []const u8 {
5 return std.posix.getenv("ZMX_SESSION_PREFIX") orelse "";
6}
7
8pub fn getSeshNameFromEnv() []const u8 {
9 return std.posix.getenv("ZMX_SESSION") orelse "";
10}
11
12pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
13 const prefix = getSeshPrefix();
14 if (prefix.len == 0 and sesh.len == 0) {
15 return error.SessionNameRequired;
16 }
17 const full = try std.fmt.allocPrint(alloc, "{s}{s}", .{ prefix, sesh });
18 // Session names become filenames under socket_dir. Rejecting path
19 // separators and dot-dot prevents socket creation and stale-socket
20 // deletion from operating outside that directory.
21 if (std.mem.indexOfScalar(u8, full, '/') != null or
22 std.mem.indexOfScalar(u8, full, 0) != null or
23 std.mem.eql(u8, full, ".") or std.mem.eql(u8, full, ".."))
24 {
25 alloc.free(full);
26 return error.InvalidSessionName;
27 }
28 return full;
29}
30
31pub fn sessionConnect(sesh: []const u8) !i32 {
32 var unix_addr = try std.net.Address.initUnix(sesh);
33 const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
34 errdefer posix.close(socket_fd);
35 try posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen());
36 return socket_fd;
37}
38
39pub fn cleanupStaleSocket(dir: std.fs.Dir, session_name: []const u8) void {
40 std.log.warn("stale socket found, cleaning up session={s}", .{session_name});
41 dir.deleteFile(session_name) catch |err| {
42 std.log.warn("failed to delete stale socket err={s}", .{@errorName(err)});
43 };
44}
45
46pub fn sessionExists(dir: std.fs.Dir, name: []const u8) !bool {
47 const stat = dir.statFile(name) catch |err| switch (err) {
48 error.FileNotFound => return false,
49 else => return err,
50 };
51 if (stat.kind != .unix_domain_socket) {
52 return error.FileNotUnixSocket;
53 }
54 return true;
55}
56
57pub fn createSocket(fname: []const u8) !i32 {
58 // AF.UNIX: Unix domain socket for local IPC with client processes
59 // SOCK.STREAM: Reliable, bidirectional communication
60 // SOCK.NONBLOCK: Set socket to non-blocking
61 const fd = try posix.socket(
62 posix.AF.UNIX,
63 posix.SOCK.STREAM | posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC,
64 0,
65 );
66 errdefer posix.close(fd);
67
68 var unix_addr = try std.net.Address.initUnix(fname);
69 try posix.bind(fd, &unix_addr.any, unix_addr.getOsSockLen());
70 try posix.listen(fd, 128);
71 return fd;
72}
73
74/// Maximum number of usable bytes in a Unix domain socket path.
75/// Derived from the platform's sockaddr_un.path field, minus 1 for the
76/// required null terminator.
77pub const max_socket_path_len: usize = @typeInfo(
78 @TypeOf(@as(posix.sockaddr.un, undefined).path),
79).array.len - 1;
80
81pub fn getSocketPath(
82 alloc: std.mem.Allocator,
83 socket_dir: []const u8,
84 session_name: []const u8,
85) error{ NameTooLong, OutOfMemory }![]const u8 {
86 const dir = socket_dir;
87 const path_len = dir.len + 1 + session_name.len;
88 if (path_len > max_socket_path_len) return error.NameTooLong;
89 const fname = try alloc.alloc(u8, path_len);
90 @memcpy(fname[0..dir.len], dir);
91 @memcpy(fname[dir.len .. dir.len + 1], "/");
92 @memcpy(fname[dir.len + 1 ..], session_name);
93 return fname;
94}
95
96pub fn printSessionNameTooLong(session_name: []const u8, socket_dir: []const u8) void {
97 var buf: [4096]u8 = undefined;
98 var w = std.fs.File.stderr().writer(&buf);
99 if (maxSessionNameLen(socket_dir)) |max_len| {
100 w.interface.print(
101 "error: session name is too long ({d} bytes, max {d} for socket directory \"{s}\")\n",
102 .{ session_name.len, max_len, socket_dir },
103 ) catch {};
104 } else {
105 w.interface.print(
106 "error: socket directory path is too long (\"{s}\")\n",
107 .{socket_dir},
108 ) catch {};
109 }
110 w.interface.flush() catch {};
111}
112
113/// Returns the maximum session name length for a given socket directory,
114/// or null if the socket directory itself is already too long.
115pub fn maxSessionNameLen(socket_dir: []const u8) ?usize {
116 // path = socket_dir + "/" + session_name
117 const overhead = socket_dir.len + 1;
118 if (overhead >= max_socket_path_len) return null;
119 return max_socket_path_len - overhead;
120}
121
122test "max_socket_path_len matches platform sockaddr_un" {
123 const path_field_len = @typeInfo(
124 @TypeOf(@as(posix.sockaddr.un, undefined).path),
125 ).array.len;
126 try std.testing.expectEqual(path_field_len - 1, max_socket_path_len);
127 try std.testing.expect(max_socket_path_len > 0);
128}
129
130test "getSocketPath succeeds for paths within limit" {
131 const alloc = std.testing.allocator;
132 const result = try getSocketPath(alloc, "/tmp/zmx", "mysession");
133 defer alloc.free(result);
134 try std.testing.expectEqualStrings("/tmp/zmx/mysession", result);
135}
136
137test "getSocketPath returns NameTooLong when path exceeds limit" {
138 const alloc = std.testing.allocator;
139 const dir = [_]u8{'d'} ** (max_socket_path_len - 2);
140 const dir_slice: []const u8 = &dir;
141
142 const ok = try getSocketPath(alloc, dir_slice, "x");
143 defer alloc.free(ok);
144 try std.testing.expectEqual(max_socket_path_len, ok.len);
145
146 const err = getSocketPath(alloc, dir_slice, "xx");
147 try std.testing.expectError(error.NameTooLong, err);
148}
149
150test "getSocketPath returns NameTooLong for empty dir with oversized name" {
151 const alloc = std.testing.allocator;
152 const name = [_]u8{'n'} ** (max_socket_path_len);
153 const name_slice: []const u8 = &name;
154 const err = getSocketPath(alloc, "", name_slice);
155 try std.testing.expectError(error.NameTooLong, err);
156}
157
158test "maxSessionNameLen computes correct dynamic limit" {
159 const short_dir = "/tmp/zmx";
160 const short_max = maxSessionNameLen(short_dir).?;
161 try std.testing.expectEqual(max_socket_path_len - short_dir.len - 1, short_max);
162
163 const full_dir = [_]u8{'f'} ** max_socket_path_len;
164 const full_dir_slice: []const u8 = &full_dir;
165 try std.testing.expectEqual(@as(?usize, null), maxSessionNameLen(full_dir_slice));
166
167 const tight_dir = [_]u8{'t'} ** (max_socket_path_len - 2);
168 const tight_dir_slice: []const u8 = &tight_dir;
169 try std.testing.expectEqual(@as(?usize, 1), maxSessionNameLen(tight_dir_slice));
170}
171
172test "getSocketPath boundary: name fills exactly to limit" {
173 const alloc = std.testing.allocator;
174 const dir = "/tmp/zmx";
175 const max_name_len = maxSessionNameLen(dir).?;
176
177 const name_at_limit = try alloc.alloc(u8, max_name_len);
178 defer alloc.free(name_at_limit);
179 @memset(name_at_limit, 'a');
180
181 const path = try getSocketPath(alloc, dir, name_at_limit);
182 defer alloc.free(path);
183 try std.testing.expectEqual(max_socket_path_len, path.len);
184
185 const name_over_limit = try alloc.alloc(u8, max_name_len + 1);
186 defer alloc.free(name_over_limit);
187 @memset(name_over_limit, 'b');
188
189 try std.testing.expectError(error.NameTooLong, getSocketPath(alloc, dir, name_over_limit));
190}