794d1fa1
refactor: dead import, dead JSON writer, one socket-path owner
a73x 2026-08-20 08:30
Commit message
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 | ||