a73x

794d1fa1

refactor: dead import, dead JSON writer, one socket-path owner

a73x   2026-08-20 08:30

Commit message
refactor: dead import, dead JSON writer, one socket-path owner

Three things the audit found that were code, not comments.

client.zig imported `proxy` "for ignoreSigpipe only" and never called it —
the call moved to `wallview.runAttach`, which installs the ignore after
opening the transport. The import and the build.zig edge go with it, so
the module graph stops claiming a dependency the code does not have.

webhub.zig's `tilesJson` was superseded by `Hub.json`, which builds the
same shape inline, and had no caller outside its own tests. Its escaping
cases were the only pin on `appendJsonString`, which `Hub.json` still
uses, so they move down onto the helper directly rather than being
deleted with the writer above them: same bytes asserted, one fewer
function to keep in step.

mux_main hand-rolled the default socket path that `sockpath.zig` already
owns, byte for byte — the one binary of three not calling
`defaultSockPath`. The whole point of that function is that two binaries
started with no `--sock` land on the same daemon, which a private copy
can only get right by accident.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

build.zig
Old New
@@ -245,13 +245,11 @@ const mod_table = [_]ModSpec{
245 // state anybody else can see — but the predicting itself is interact's 245 // state anybody else can see — but the predicting itself is interact's
246 // now, along with the rest of the terminal-facing machinery; what stays 246 // now, along with the rest of the terminal-facing machinery; what stays
247 // here is Target/Transport, the attach loop and the session's meanings. 247 // here is Target/Transport, the attach loop and the session's meanings.
248 // It also borrows ignoreSigpipe, which proxy owns — proxy is a leaf, so
249 // this adds no cycle and teaches the proxy nothing.
250 // `wall` is the spelling grammar AND the state file: a grid-claiming 248 // `wall` is the spelling grammar AND the state file: a grid-claiming
251 // attach records its own tile (the wall is attach history), and the 249 // attach records its own tile (the wall is attach history), and the
252 // chord switches that re-dial from inside client.attach have to record 250 // chord switches that re-dial from inside client.attach have to record
253 // theirs too, so the writer cannot live up in mux_main. 251 // theirs too, so the writer cannot live up in mux_main.
254 .{ .name = "client", .path = "src/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "interact", "quic_client", "quic", "handoff", "proxy", "wall" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, 252 .{ .name = "client", .path = "src/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "interact", "quic_client", "quic", "handoff", "wall" }, .test_imports = &.{"testtmp"}, .quic_tests = true },
255 // The daemon entrypoint loads the key and constructs the listener, so 253 // The daemon entrypoint loads the key and constructs the listener, so
256 // it needs quic/quic_server directly rather than through the server. 254 // it needs quic/quic_server directly rather than through the server.
257 // `muxd endpoint` prints the announce line handoff spells; sockpath is 255 // `muxd endpoint` prints the announce line handoff spells; sockpath is
src/client.zig
Old New
@@ -35,8 +35,6 @@ const handoff = @import("handoff");
35 // The wall file: attach history. See `recordTile` for why the writer of a 35 // The wall file: attach history. See `recordTile` for why the writer of a
36 // user's tile is this module and not mux_main. 36 // user's tile is this module and not mux_main.
37 const wall = @import("wall"); 37 const wall = @import("wall");
38 // For ignoreSigpipe only, which proxy.zig owns.
39 const proxy = @import("proxy");
40 38
41 /// A session name held by value. The names a switch travels on are decoded 39 /// A session name held by value. The names a switch travels on are decoded
42 /// out of a frame payload that is freed before the re-dial, so they cannot 40 /// out of a frame payload that is freed before the re-dial, so they cannot
src/mux_main.zig
Old New
@@ -290,10 +290,8 @@ pub fn main() !u8 {
290 ); 290 );
291 const sock_path = if (t.sock) |s| 291 const sock_path = if (t.sock) |s|
292 try alloc.dupe(u8, s) 292 try alloc.dupe(u8, s)
293 else if (std.posix.getenv("XDG_RUNTIME_DIR")) |dir|
294 try std.fmt.allocPrint(alloc, "{s}/muxd.sock", .{dir})
295 else 293 else
296 try std.fmt.allocPrint(alloc, "/tmp/muxd-{d}.sock", .{std.os.linux.getuid()}); 294 try sockpath.defaultSockPath(alloc);
297 defer alloc.free(sock_path); 295 defer alloc.free(sock_path);
298 296
299 // Before the PATH search, before auto-start, before the dial: 297 // Before the PATH search, before auto-start, before the dial:
src/webhub.zig
Old New
@@ -402,8 +402,10 @@ pub const Hub = struct {
402 for (self.tiles.items, 0..) |t, i| { 402 for (self.tiles.items, 0..) |t, i| {
403 if (i > 0) try out.append(alloc, ','); 403 if (i > 0) try out.append(alloc, ',');
404 try out.print(alloc, "{{\"id\":{d},\"label\":", .{t.id}); 404 try out.print(alloc, "{{\"id\":{d},\"label\":", .{t.id});
405 // The same escape road tilesJson uses: a label is the user's 405 // Both strings take the same escape road: a label is the user's
406 // spelling, which is theirs to make unparseable. 406 // spelling, which is theirs to make unparseable, and the session
407 // rides beside it rather than inside it so the page never has to
408 // dig one back out of the other.
407 try appendJsonString(alloc, &out, t.label); 409 try appendJsonString(alloc, &out, t.label);
408 try out.appendSlice(alloc, ",\"session\":"); 410 try out.appendSlice(alloc, ",\"session\":");
409 try appendJsonString(alloc, &out, t.session); 411 try appendJsonString(alloc, &out, t.session);
@@ -887,35 +889,6 @@ fn appendJsonString(alloc: std.mem.Allocator, out: *std.ArrayList(u8), s: []cons
887 try out.append(alloc, '"'); 889 try out.append(alloc, '"');
888 } 890 }
889 891
890 /// `sessions` is parallel to `labels`, index for index — the caller builds
891 /// them in one pass, and /ws/<idx> indexes the same order. The session is a
892 /// field of its own rather than something the page digs back out of the
893 /// label: the label is the user's spelling, which is theirs to make
894 /// unparseable.
895 pub fn tilesJson(
896 alloc: std.mem.Allocator,
897 labels: []const []const u8,
898 sessions: []const []const u8,
899 ) ![]u8 {
900 std.debug.assert(labels.len == sessions.len);
901 var out: std.ArrayList(u8) = .empty;
902 errdefer out.deinit(alloc);
903 try out.append(alloc, '[');
904 for (labels, sessions, 0..) |label, session, i| {
905 if (i > 0) try out.append(alloc, ',');
906 try out.appendSlice(alloc, "{\"label\":");
907 // One helper for both strings: two copies of the escape loop is two
908 // things to keep in step, and the session's bytes reach the page by
909 // exactly the same road the label's do.
910 try appendJsonString(alloc, &out, label);
911 try out.appendSlice(alloc, ",\"session\":");
912 try appendJsonString(alloc, &out, session);
913 try out.append(alloc, '}');
914 }
915 try out.append(alloc, ']');
916 return out.toOwnedSlice(alloc);
917 }
918
919 /// `3,0,2` → ids. Empty, junk, or trailing garbage refuse: the body is 892 /// `3,0,2` → ids. Empty, junk, or trailing garbage refuse: the body is
920 /// machine-written by our own page, so anything malformed is a bug 893 /// machine-written by our own page, so anything malformed is a bug
921 /// worth surfacing, not input to repair. 894 /// worth surfacing, not input to repair.
@@ -1507,67 +1480,33 @@ test "parseIdList: happy path and refusals" {
1507 try std.testing.expectError(error.Bad, parseIdList(alloc, "-1")); 1480 try std.testing.expectError(error.Bad, parseIdList(alloc, "-1"));
1508 } 1481 }
1509 1482
1510 test "tiles json: label/session objects, order preserved" { 1483 test "tiles json: every byte a label or session can carry, escaped" {
1484 // Pinned on the helper rather than on `Hub.json`, because the helper is
1485 // what both strings go through and building a Hub would test the
1486 // brackets instead of the escaping. Label and session share one road on
1487 // purpose: two copies of this loop would be two things to keep in step.
1511 const alloc = std.testing.allocator; 1488 const alloc = std.testing.allocator;
1512 { 1489 const cases = [_]struct { in: []const u8, want: []const u8 }{
1513 const j = try tilesJson(alloc, &.{}, &.{}); 1490 .{ .in = "", .want = "\"\"" },
1514 defer alloc.free(j); 1491 .{ .in = "box2#b", .want = "\"box2#b\"" },
1515 try std.testing.expectEqualStrings("[]", j);
1516 }
1517 {
1518 // Index order IS the tile order — /ws/<idx> indexes the same list.
1519 // The session rides beside the label rather than inside it: the page
1520 // shows one and attaches with the other.
1521 const j = try tilesJson(alloc, &.{ "box1", "box2#b", "box3" }, &.{ "", "b", "c" });
1522 defer alloc.free(j);
1523 try std.testing.expectEqualStrings(
1524 "[{\"label\":\"box1\",\"session\":\"\"}," ++
1525 "{\"label\":\"box2#b\",\"session\":\"b\"}," ++
1526 "{\"label\":\"box3\",\"session\":\"c\"}]",
1527 j,
1528 );
1529 }
1530 {
1531 // A path with a quote in it must not break the page. 1492 // A path with a quote in it must not break the page.
1532 const j = try tilesJson(alloc, &.{"/tmp/we\"ird\\path"}, &.{""}); 1493 .{ .in = "/tmp/we\"ird\\path", .want = "\"/tmp/we\\\"ird\\\\path\"" },
1533 defer alloc.free(j);
1534 try std.testing.expectEqualStrings(
1535 "[{\"label\":\"/tmp/we\\\"ird\\\\path\",\"session\":\"\"}]",
1536 j,
1537 );
1538 }
1539 {
1540 // Control bytes go to \u00XX, including the ones JSON has short 1494 // Control bytes go to \u00XX, including the ones JSON has short
1541 // spellings for — one rule, no table to get wrong. 1495 // spellings for — one rule, no table to get wrong.
1542 const j = try tilesJson(alloc, &.{"a\nb\tc\x00d\x1fe"}, &.{""}); 1496 .{ .in = "a\nb\tc\x00d\x1fe", .want = "\"a\\u000ab\\u0009c\\u0000d\\u001fe\"" },
1543 defer alloc.free(j);
1544 try std.testing.expectEqualStrings(
1545 "[{\"label\":\"a\\u000ab\\u0009c\\u0000d\\u001fe\",\"session\":\"\"}]",
1546 j,
1547 );
1548 }
1549 {
1550 // Bytes above 0x7f pass through: labels are argv, and a UTF-8 1497 // Bytes above 0x7f pass through: labels are argv, and a UTF-8
1551 // hostname stays itself. 1498 // hostname stays itself.
1552 const j = try tilesJson(alloc, &.{ "", "héllo" }, &.{ "", "" }); 1499 .{ .in = "héllo", .want = "\"héllo\"" },
1553 defer alloc.free(j); 1500 // `validSessionName` refuses every one of these, which is exactly
1554 try std.testing.expectEqualStrings( 1501 // why the escaping is pinned here: nothing downstream would catch it
1555 "[{\"label\":\"\",\"session\":\"\"},{\"label\":\"héllo\",\"session\":\"\"}]", 1502 // going wrong.
1556 j, 1503 .{ .in = "we\"ird\\\x01", .want = "\"we\\\"ird\\\\\\u0001\"" },
1557 ); 1504 };
1558 } 1505 for (cases) |c| {
1559 { 1506 var out: std.ArrayList(u8) = .empty;
1560 // The session string gets the SAME escaping as the label, byte for 1507 defer out.deinit(alloc);
1561 // byte — one helper, not two loops that could drift. parseArgs will 1508 try appendJsonString(alloc, &out, c.in);
1562 // never hand these through (validSessionName refuses every one of 1509 try std.testing.expectEqualStrings(c.want, out.items);
1563 // them), which is exactly why the escaping has to be pinned here:
1564 // nothing downstream would catch it going wrong.
1565 const j = try tilesJson(alloc, &.{"t"}, &.{"we\"ird\\\x01"});
1566 defer alloc.free(j);
1567 try std.testing.expectEqualStrings(
1568 "[{\"label\":\"t\",\"session\":\"we\\\"ird\\\\\\u0001\"}]",
1569 j,
1570 );
1571 } 1510 }
1572 } 1511 }
1573 1512