d1572590
refactor: spell the host grammar once, free the key path once
a73x 2026-09-01 13:23
Commit message
src/cli/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -473,16 +473,11 @@ fn run(alloc: std.mem.Allocator, o: DaemonArguments, sock_path: []const u8) !u8 | |||
| 473 | }; | 473 | }; |
| 474 | // Resolve `--key`, then `MUX_KEY_FILE`, then the default path, matching | 474 | // Resolve `--key`, then `MUX_KEY_FILE`, then the default path, matching |
| 475 | // direct QUIC clients. Only QUIC-enabled startup requires a key. | 475 | // direct QUIC clients. Only QUIC-enabled startup requires a key. |
| 476 | var key_owned: ?[]const u8 = null; | 476 | const key_res = try xdg.resolveKeyPath(alloc, xdg.pickKey(o.key, std.posix.getenv(xdg.key_env))); |
| 477 | defer if (key_owned) |p| alloc.free(p); | 477 | defer key_res.deinit(alloc); |
| 478 | const key_path = switch (try xdg.resolveKeyPath(alloc, xdg.pickKey(o.key, std.posix.getenv(xdg.key_env)))) { | 478 | const key_path = switch (key_res) { |
| 479 | .given => |g| g, | 479 | .given, .default => |p| p, |
| 480 | .default => |p| blk: { | ||
| 481 | key_owned = p; | ||
| 482 | break :blk p; | ||
| 483 | }, | ||
| 484 | .missing => |p| { | 480 | .missing => |p| { |
| 485 | defer alloc.free(p); | ||
| 486 | std.debug.print( | 481 | std.debug.print( |
| 487 | "mux d: no key: pass --key, set MUX_KEY_FILE, or run `mux d keygen` (default {s})\n", | 482 | "mux d: no key: pass --key, set MUX_KEY_FILE, or run `mux d keygen` (default {s})\n", |
| 488 | .{p}, | 483 | .{p}, |
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -176,8 +176,8 @@ const ClientArguments = struct { | |||
| 176 | /// target so `parseArgs` can reject multiple transports. An empty | 176 | /// target so `parseArgs` can reject multiple transports. An empty |
| 177 | /// `quic://` target is invalid immediately. | 177 | /// `quic://` target is invalid immediately. |
| 178 | pub fn positional(self: *ClientArguments, word: []const u8) bool { | 178 | pub fn positional(self: *ClientArguments, word: []const u8) bool { |
| 179 | if (std.mem.startsWith(u8, word, "quic://")) { | 179 | if (std.mem.startsWith(u8, word, hosts.quic_prefix)) { |
| 180 | const host_port = word["quic://".len..]; | 180 | const host_port = word[hosts.quic_prefix.len..]; |
| 181 | if (host_port.len == 0) return false; | 181 | if (host_port.len == 0) return false; |
| 182 | self._quic = host_port; | 182 | self._quic = host_port; |
| 183 | } else self._host = word; | 183 | } else self._host = word; |
| @@ -276,10 +276,7 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 276 | switch (parsed) { | 276 | switch (parsed) { |
| 277 | .quic => |q| { | 277 | .quic => |q| { |
| 278 | const res = try xdg.resolveKeyPath(alloc, q.key); | 278 | const res = try xdg.resolveKeyPath(alloc, q.key); |
| 279 | defer switch (res) { | 279 | defer res.deinit(alloc); |
| 280 | .given => {}, | ||
| 281 | .default, .missing => |p| alloc.free(p), | ||
| 282 | }; | ||
| 283 | const key_path = switch (res) { | 280 | const key_path = switch (res) { |
| 284 | .given, .default => |p| p, | 281 | .given, .default => |p| p, |
| 285 | .missing => |p| { | 282 | .missing => |p| { |
| @@ -332,8 +329,8 @@ pub fn main(args: []const [:0]const u8) !u8 { | |||
| 332 | /// Return true when the hosts file includes the local socket but no process is | 329 | /// Return true when the hosts file includes the local socket but no process is |
| 333 | /// currently answering on it. | 330 | /// currently answering on it. |
| 334 | fn localNeedsStart(h: *const hosts.Hosts, sock: []const u8) bool { | 331 | fn localNeedsStart(h: *const hosts.Hosts, sock: []const u8) bool { |
| 335 | var buf: [std.fs.max_path_bytes + "--sock ".len]u8 = undefined; | 332 | var buf: [std.fs.max_path_bytes + hosts.sock_prefix.len]u8 = undefined; |
| 336 | const line = std.fmt.bufPrint(&buf, "--sock {s}", .{sock}) catch return false; | 333 | const line = std.fmt.bufPrint(&buf, hosts.sock_prefix ++ "{s}", .{sock}) catch return false; |
| 337 | return h.has(line) and !sockpath.answers(sock); | 334 | return h.has(line) and !sockpath.answers(sock); |
| 338 | } | 335 | } |
| 339 | 336 | ||
src/cli/muxa.zig
| Old | New | ||
|---|---|---|---|
| @@ -1010,6 +1010,10 @@ fn openQuicConn( | |||
| 1010 | // so every client selects the same credential. | 1010 | // so every client selects the same credential. |
| 1011 | const res = xdg.resolveKeyPath(alloc, xdg.pickKey(o.key, std.posix.getenv(xdg.key_env))) catch |e| | 1011 | const res = xdg.resolveKeyPath(alloc, xdg.pickKey(o.key, std.posix.getenv(xdg.key_env))) catch |e| |
| 1012 | return .{ .exit = fail("quic: cannot resolve a key path", @errorName(e)) }; | 1012 | return .{ .exit = fail("quic: cannot resolve a key path", @errorName(e)) }; |
| 1013 | // `key_path` is read here and nowhere after: `Key.load` copies the bytes | ||
| 1014 | // it needs and the connection keeps no path. Freeing on the way out is a | ||
| 1015 | // no-op under this arena, and keeps the site correct under any allocator. | ||
| 1016 | defer res.deinit(alloc); | ||
| 1013 | const key_path = switch (res) { | 1017 | const key_path = switch (res) { |
| 1014 | .given, .default => |p| p, | 1018 | .given, .default => |p| p, |
| 1015 | // Include the missing path so callers know which credential to create. | 1019 | // Include the missing path so callers know which credential to create. |
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -1046,11 +1046,11 @@ pub fn wallSpelling(out: []u8, target: Target, name: []const u8) SpellingError![ | |||
| 1046 | const written = switch (target) { | 1046 | const written = switch (target) { |
| 1047 | // The sock spelling carries its own flag INSIDE the string — one | 1047 | // The sock spelling carries its own flag INSIDE the string — one |
| 1048 | // argv element per tile is the grammar, not two. | 1048 | // argv element per tile is the grammar, not two. |
| 1049 | .sock => |path| std.fmt.bufPrint(out, "--sock {s}#{s}", .{ path, name }), | 1049 | .sock => |path| std.fmt.bufPrint(out, hosts.sock_prefix ++ "{s}#{s}", .{ path, name }), |
| 1050 | .via => return error.NoSpelling, | 1050 | .via => return error.NoSpelling, |
| 1051 | // `host_port` is what the user typed after `quic://`, port and | 1051 | // `host_port` is what the user typed after the quic prefix, port and |
| 1052 | // all, so it is written back out unexamined. | 1052 | // all, so it is written back out unexamined. |
| 1053 | .quic => |q| std.fmt.bufPrint(out, "quic://{s}#{s}", .{ q.host_port, name }), | 1053 | .quic => |q| std.fmt.bufPrint(out, hosts.quic_prefix ++ "{s}#{s}", .{ q.host_port, name }), |
| 1054 | // The bare-HOST form: the wall re-runs the ssh→QUIC handoff from | 1054 | // The bare-HOST form: the wall re-runs the ssh→QUIC handoff from |
| 1055 | // the host word, exactly as this client did. | 1055 | // the host word, exactly as this client did. |
| 1056 | .hand => |h| std.fmt.bufPrint(out, "{s}#{s}", .{ h.host, name }), | 1056 | .hand => |h| std.fmt.bufPrint(out, "{s}#{s}", .{ h.host, name }), |
| @@ -1058,16 +1058,19 @@ pub fn wallSpelling(out: []u8, target: Target, name: []const u8) SpellingError![ | |||
| 1058 | return written catch error.NoSpace; | 1058 | return written catch error.NoSpace; |
| 1059 | } | 1059 | } |
| 1060 | 1060 | ||
| 1061 | /// The longest spelling this target can produce. Both grammar prefixes | 1061 | /// The longest spelling this target can produce. Derived from the longer of |
| 1062 | /// ("--sock ", "quic://") are seven bytes, so one number covers them. | 1062 | /// the two grammar prefixes rather than from a number: the bare-HOST form |
| 1063 | /// has no prefix at all, so the widest case is always one of those two, and | ||
| 1064 | /// a cap taken from them cannot fall short when a literal is re-spelled. | ||
| 1063 | pub fn spellingCap(target: Target) usize { | 1065 | pub fn spellingCap(target: Target) usize { |
| 1066 | const prefix_max = @max(hosts.sock_prefix.len, hosts.quic_prefix.len); | ||
| 1064 | const operand: usize = switch (target) { | 1067 | const operand: usize = switch (target) { |
| 1065 | .sock => |path| path.len, | 1068 | .sock => |path| path.len, |
| 1066 | .via => 0, | 1069 | .via => 0, |
| 1067 | .quic => |q| q.host_port.len, | 1070 | .quic => |q| q.host_port.len, |
| 1068 | .hand => |h| h.host.len, | 1071 | .hand => |h| h.host.len, |
| 1069 | }; | 1072 | }; |
| 1070 | return "--sock ".len + operand + 1 + proto.session_name_max; | 1073 | return prefix_max + operand + 1 + proto.session_name_max; |
| 1071 | } | 1074 | } |
| 1072 | 1075 | ||
| 1073 | // The grid a birth asks for: `main.DaemonArguments`'s own default, the size `mux d start` | 1076 | // The grid a birth asks for: `main.DaemonArguments`'s own default, the size `mux d start` |
src/client/hosts.zig
| Old | New | ||
|---|---|---|---|
| @@ -29,15 +29,22 @@ pub fn hasBadSpelling(word: []const u8) bool { | |||
| 29 | return false; | 29 | return false; |
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | /// The two prefixes that ARE the grammar. Every writer and every reader — | ||
| 33 | /// argv, the file, the picker, a tile's label — spells them through these, | ||
| 34 | /// so changing a literal (or its length) moves all of them at once instead | ||
| 35 | /// of leaving one site matching a prefix nothing writes any more. | ||
| 36 | pub const sock_prefix = "--sock "; | ||
| 37 | pub const quic_prefix = "quic://"; | ||
| 38 | |||
| 32 | pub fn parse(line: []const u8) ParseError!Spec { | 39 | pub fn parse(line: []const u8) ParseError!Spec { |
| 33 | for (line) |b| if (b < 0x20 or b == 0x7f) return error.BadByte; | 40 | for (line) |b| if (b < 0x20 or b == 0x7f) return error.BadByte; |
| 34 | if (std.mem.indexOfScalar(u8, line, '#') != null) return error.HasSession; | 41 | if (std.mem.indexOfScalar(u8, line, '#') != null) return error.HasSession; |
| 35 | if (std.mem.startsWith(u8, line, "--sock ")) { | 42 | if (std.mem.startsWith(u8, line, sock_prefix)) { |
| 36 | const p = line["--sock ".len..]; | 43 | const p = line[sock_prefix.len..]; |
| 37 | return if (p.len == 0) error.EmptySpec else .{ .sock = p }; | 44 | return if (p.len == 0) error.EmptySpec else .{ .sock = p }; |
| 38 | } | 45 | } |
| 39 | if (std.mem.startsWith(u8, line, "quic://")) { | 46 | if (std.mem.startsWith(u8, line, quic_prefix)) { |
| 40 | const h = line["quic://".len..]; | 47 | const h = line[quic_prefix.len..]; |
| 41 | return if (h.len == 0) error.EmptySpec else .{ .quic = h }; | 48 | return if (h.len == 0) error.EmptySpec else .{ .quic = h }; |
| 42 | } | 49 | } |
| 43 | if (line.len == 0) return error.EmptySpec; | 50 | if (line.len == 0) return error.EmptySpec; |
| @@ -49,7 +56,7 @@ pub const ArgvError = error{ MissingSockPath, FlagLikeTarget } || std.mem.Alloca | |||
| 49 | 56 | ||
| 50 | /// Every mouth's rule: a host starting with a dash is a mistyped flag. | 57 | /// Every mouth's rule: a host starting with a dash is a mistyped flag. |
| 51 | pub fn flagLike(target: []const u8) bool { | 58 | pub fn flagLike(target: []const u8) bool { |
| 52 | return target.len > 0 and target[0] == '-' and !std.mem.startsWith(u8, target, "--sock "); | 59 | return target.len > 0 and target[0] == '-' and !std.mem.startsWith(u8, target, sock_prefix); |
| 53 | } | 60 | } |
| 54 | 61 | ||
| 55 | /// Both `--sock` dialects reach one spelling: one parser, one line format. | 62 | /// Both `--sock` dialects reach one spelling: one parser, one line format. |
| @@ -62,7 +69,7 @@ pub fn spellingFromArgv( | |||
| 62 | // A trailing `--sock` names no path: a usage mistake, reported as | 69 | // A trailing `--sock` names no path: a usage mistake, reported as |
| 63 | // one rather than read off the end of argv. | 70 | // one rather than read off the end of argv. |
| 64 | if (i + 1 >= args.len) return error.MissingSockPath; | 71 | if (i + 1 >= args.len) return error.MissingSockPath; |
| 65 | return .{ .spelling = try std.fmt.allocPrint(alloc, "--sock {s}", .{args[i + 1]}), .consumed = 2 }; | 72 | return .{ .spelling = try std.fmt.allocPrint(alloc, sock_prefix ++ "{s}", .{args[i + 1]}), .consumed = 2 }; |
| 66 | } | 73 | } |
| 67 | // A wall takes hosts, and no host starts with a dash. Left to fall through, | 74 | // A wall takes hosts, and no host starts with a dash. Left to fall through, |
| 68 | // `mux hosts add -A box` becomes a host named `-A` that fails to resolve far | 75 | // `mux hosts add -A box` becomes a host named `-A` that fails to resolve far |
src/tui/wall_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -406,9 +406,9 @@ pub fn applyHostList(w: Wall, hi: usize) void { | |||
| 406 | /// `mux hosts` prints have to read like the one that would have named it. | 406 | /// `mux hosts` prints have to read like the one that would have named it. |
| 407 | pub fn hostSpelling(alloc: std.mem.Allocator, target: client.Target) ![]const u8 { | 407 | pub fn hostSpelling(alloc: std.mem.Allocator, target: client.Target) ![]const u8 { |
| 408 | return switch (target) { | 408 | return switch (target) { |
| 409 | .sock => |p| try std.fmt.allocPrint(alloc, "--sock {s}", .{p}), | 409 | .sock => |p| try std.fmt.allocPrint(alloc, hosts.sock_prefix ++ "{s}", .{p}), |
| 410 | .hand => |h| try alloc.dupe(u8, h.host), | 410 | .hand => |h| try alloc.dupe(u8, h.host), |
| 411 | .quic => |q| try std.fmt.allocPrint(alloc, "quic://{s}", .{q.host_port}), | 411 | .quic => |q| try std.fmt.allocPrint(alloc, hosts.quic_prefix ++ "{s}", .{q.host_port}), |
| 412 | // `--via` has no form in that grammar — an arbitrary command is not | 412 | // `--via` has no form in that grammar — an arbitrary command is not |
| 413 | // an address — so the label is honest and is not a spelling. | 413 | // an address — so the label is honest and is not a spelling. |
| 414 | .via => |c| try std.fmt.allocPrint(alloc, "--via {s}", .{c}), | 414 | .via => |c| try std.fmt.allocPrint(alloc, "--via {s}", .{c}), |
src/xdg.zig
| Old | New | ||
|---|---|---|---|
| @@ -25,6 +25,22 @@ pub const KeyResolution = union(enum) { | |||
| 25 | /// (the agent answers in JSON) and both need the path; the hub folds | 25 | /// (the agent answers in JSON) and both need the path; the hub folds |
| 26 | /// it into a bare MissingKey. | 26 | /// it into a bare MissingKey. |
| 27 | missing: []const u8, | 27 | missing: []const u8, |
| 28 | |||
| 29 | /// Release what this resolution OWNS: nothing for `.given`, which | ||
| 30 | /// borrows from argv or the environment, and the path for the two | ||
| 31 | /// XDG-derived arms. The borrow/own split is stated once here so a | ||
| 32 | /// caller never re-derives which arm allocated and frees the wrong one. | ||
| 33 | /// | ||
| 34 | /// Only for callers that keep the path no longer than the resolution. | ||
| 35 | /// A caller that hands the `.default` path onward as owned memory — | ||
| 36 | /// `client.Target.fromSpec` stores it in the target it returns — must | ||
| 37 | /// take that arm apart itself instead. | ||
| 38 | pub fn deinit(self: KeyResolution, alloc: std.mem.Allocator) void { | ||
| 39 | switch (self) { | ||
| 40 | .given => {}, | ||
| 41 | .default, .missing => |p| alloc.free(p), | ||
| 42 | } | ||
| 43 | } | ||
| 28 | }; | 44 | }; |
| 29 | 45 | ||
| 30 | /// ONE owner: a drift here would mean two binaries disagreeing about which | 46 | /// ONE owner: a drift here would mean two binaries disagreeing about which |