2ae8fe2e
refactor: mux parses to a transport or an error, like muxa and muxweb
a73x 2026-08-27 08:08
Commit message
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -55,10 +55,10 @@ const usage = | |||
| 55 | \\ | 55 | \\ |
| 56 | ; | 56 | ; |
| 57 | 57 | ||
| 58 | /// What the command line asked for. A tagged union rather than a struct of | 58 | /// The transport the command line asked for, one arm per spelling, so the |
| 59 | /// optionals so every answer that is not a transport — the refusals, and the | 59 | /// parse can be tested without a process to exit from. Everything that is |
| 60 | /// two questions about the binary itself — is a result in its own right, and | 60 | /// NOT a transport — the two refusals, and the two questions about the |
| 61 | /// so the parse can be tested without a process to exit from. | 61 | /// binary itself — is an error below. |
| 62 | const ParseResult = union(enum) { | 62 | const ParseResult = union(enum) { |
| 63 | /// At most one of these is set; both null means the default local socket. | 63 | /// At most one of these is set; both null means the default local socket. |
| 64 | /// `session` defaults to "" (empty), the wire-compatible name that puts | 64 | /// `session` defaults to "" (empty), the wire-compatible name that puts |
| @@ -74,18 +74,14 @@ const ParseResult = union(enum) { | |||
| 74 | /// A direct QUIC attach. The key is resolved in main, where the | 74 | /// A direct QUIC attach. The key is resolved in main, where the |
| 75 | /// environment can be consulted. | 75 | /// environment can be consulted. |
| 76 | quic: struct { host_port: []const u8, key: ?[]const u8, idle_ms: u32, session: []const u8 = "", agent: bool = false }, | 76 | quic: struct { host_port: []const u8, key: ?[]const u8, idle_ms: u32, session: []const u8 = "", agent: bool = false }, |
| 77 | /// `--version`: not a transport at all, so it short-circuits the rest of | ||
| 78 | /// the parse rather than being reconciled with it. | ||
| 79 | version, | ||
| 80 | /// `--help`: the usage someone ASKED for, so main writes it to stdout | ||
| 81 | /// and exits 0, unlike every refusal below. | ||
| 82 | help, | ||
| 83 | /// More than one transport named — a request that cannot be honoured | ||
| 84 | /// rather than one to reconcile. | ||
| 85 | conflict, | ||
| 86 | usage_error, | ||
| 87 | }; | 77 | }; |
| 88 | 78 | ||
| 79 | /// Help and Version short-circuit the rest of the parse rather than being | ||
| 80 | /// reconciled with it, and main answers both on stdout with an exit 0 — | ||
| 81 | /// unlike the two refusals. Conflict is more than one transport named: a | ||
| 82 | /// request that cannot be honoured rather than one to reconcile. | ||
| 83 | const ParseError = error{ Usage, Help, Version, Conflict }; | ||
| 84 | |||
| 89 | /// `SSH_AGENTC_REQUEST_IDENTITIES` in the ssh-agent framing: a 4-byte | 85 | /// `SSH_AGENTC_REQUEST_IDENTITIES` in the ssh-agent framing: a 4-byte |
| 90 | /// big-endian length, then the message type. `ssh-add -l` sends exactly | 86 | /// big-endian length, then the message type. `ssh-add -l` sends exactly |
| 91 | /// this, which is why every agent implementation answers it — with an | 87 | /// this, which is why every agent implementation answers it — with an |
| @@ -214,22 +210,22 @@ comptime { | |||
| 214 | cliflags.assertDocumented(Opts, usage, &.{}); | 210 | cliflags.assertDocumented(Opts, usage, &.{}); |
| 215 | } | 211 | } |
| 216 | 212 | ||
| 217 | fn parseArgs(args: []const [:0]const u8, env_key: ?[]const u8) ParseResult { | 213 | fn parseArgs(args: []const [:0]const u8, env_key: ?[]const u8) ParseError!ParseResult { |
| 218 | var o: Opts = .{}; | 214 | var o: Opts = .{}; |
| 219 | switch (cliflags.parse(Opts, &o, args[1..])) { | 215 | switch (cliflags.parse(Opts, &o, args[1..])) { |
| 220 | .ok => {}, | 216 | .ok => {}, |
| 221 | .help => return .help, | 217 | .help => return error.Help, |
| 222 | .version => return .version, | 218 | .version => return error.Version, |
| 223 | .unknown_arg, .missing_value, .bad_value => return .usage_error, | 219 | .unknown_arg, .missing_value, .bad_value => return error.Usage, |
| 224 | } | 220 | } |
| 225 | 221 | ||
| 226 | if (o._conflict) return .conflict; | 222 | if (o._conflict) return error.Conflict; |
| 227 | 223 | ||
| 228 | // Every pairing of the four is two transports for one session. | 224 | // Every pairing of the four is two transports for one session. |
| 229 | const named: u8 = @as(u8, @intFromBool(o.sock != null)) + | 225 | const named: u8 = @as(u8, @intFromBool(o.sock != null)) + |
| 230 | @intFromBool(o.via != null) + @intFromBool(o._host != null) + | 226 | @intFromBool(o.via != null) + @intFromBool(o._host != null) + |
| 231 | @intFromBool(o._quic != null); | 227 | @intFromBool(o._quic != null); |
| 232 | if (named > 1) return .conflict; | 228 | if (named > 1) return error.Conflict; |
| 233 | 229 | ||
| 234 | // Rides every transport below, unlike --key: a session name is not | 230 | // Rides every transport below, unlike --key: a session name is not |
| 235 | // authenticating anything, so there is no "no quic:// means ignore it" | 231 | // authenticating anything, so there is no "no quic:// means ignore it" |
| @@ -272,7 +268,21 @@ pub fn main() !u8 { | |||
| 272 | if (args.len > 1 and std.mem.eql(u8, args[1], "wall")) | 268 | if (args.len > 1 and std.mem.eql(u8, args[1], "wall")) |
| 273 | return wallMain(alloc, args[2..]); | 269 | return wallMain(alloc, args[2..]); |
| 274 | 270 | ||
| 275 | const parsed = parseArgs(args, std.posix.getenv(xdg.key_env)); | 271 | const parsed = parseArgs(args, std.posix.getenv(xdg.key_env)) catch |e| switch (e) { |
| 272 | error.Version => return cliflags.version("mux", build_options.version), | ||
| 273 | error.Help => return cliflags.help(usage), | ||
| 274 | error.Usage => { | ||
| 275 | std.debug.print("{s}", .{usage}); | ||
| 276 | return 2; | ||
| 277 | }, | ||
| 278 | error.Conflict => { | ||
| 279 | std.debug.print( | ||
| 280 | "mux: name one transport: HOST, --sock, --via or quic://\n{s}", | ||
| 281 | .{usage}, | ||
| 282 | ); | ||
| 283 | return 2; | ||
| 284 | }, | ||
| 285 | }; | ||
| 276 | 286 | ||
| 277 | // `-A` is a promise, and a client with no agent behind it cannot keep | 287 | // `-A` is a promise, and a client with no agent behind it cannot keep |
| 278 | // one. Left to attach, it offers anyway — the offer is a declaration, | 288 | // one. Left to attach, it offers anyway — the offer is a declaration, |
| @@ -285,7 +295,6 @@ pub fn main() !u8 { | |||
| 285 | .host => |h| h.agent, | 295 | .host => |h| h.agent, |
| 286 | .quic => |q| q.agent, | 296 | .quic => |q| q.agent, |
| 287 | .attach => |at| at.agent, | 297 | .attach => |at| at.agent, |
| 288 | else => false, | ||
| 289 | }; | 298 | }; |
| 290 | if (wants_agent) { | 299 | if (wants_agent) { |
| 291 | const sock = std.posix.getenv(proto.agent_sock_env) orelse ""; | 300 | const sock = std.posix.getenv(proto.agent_sock_env) orelse ""; |
| @@ -306,19 +315,6 @@ pub fn main() !u8 { | |||
| 306 | } | 315 | } |
| 307 | 316 | ||
| 308 | switch (parsed) { | 317 | switch (parsed) { |
| 309 | .version => return cliflags.version("mux", build_options.version), | ||
| 310 | .help => return cliflags.help(usage), | ||
| 311 | .usage_error => { | ||
| 312 | std.debug.print("{s}", .{usage}); | ||
| 313 | return 2; | ||
| 314 | }, | ||
| 315 | .conflict => { | ||
| 316 | std.debug.print( | ||
| 317 | "mux: name one transport: HOST, --sock, --via or quic://\n{s}", | ||
| 318 | .{usage}, | ||
| 319 | ); | ||
| 320 | return 2; | ||
| 321 | }, | ||
| 322 | .quic => |q| { | 318 | .quic => |q| { |
| 323 | const res = try xdg.resolveKeyPath(alloc, q.key); | 319 | const res = try xdg.resolveKeyPath(alloc, q.key); |
| 324 | defer switch (res) { | 320 | defer switch (res) { |
| @@ -661,36 +657,36 @@ fn wallEdit( | |||
| 661 | } | 657 | } |
| 662 | 658 | ||
| 663 | /// parseArgs takes what argsAlloc produces; the tests must match the type. | 659 | /// parseArgs takes what argsAlloc produces; the tests must match the type. |
| 664 | fn parse(comptime argv: []const [:0]const u8) ParseResult { | 660 | fn parse(comptime argv: []const [:0]const u8) ParseError!ParseResult { |
| 665 | return parseArgs(argv, null); | 661 | return parseArgs(argv, null); |
| 666 | } | 662 | } |
| 667 | 663 | ||
| 668 | /// The same, with `MUX_KEY_FILE` set to `env`. | 664 | /// The same, with `MUX_KEY_FILE` set to `env`. |
| 669 | fn parseEnv(comptime argv: []const [:0]const u8, env: ?[]const u8) ParseResult { | 665 | fn parseEnv(comptime argv: []const [:0]const u8, env: ?[]const u8) ParseError!ParseResult { |
| 670 | return parseArgs(argv, env); | 666 | return parseArgs(argv, env); |
| 671 | } | 667 | } |
| 672 | 668 | ||
| 673 | test "parseArgs: no arguments means the default local socket" { | 669 | test "parseArgs: no arguments means the default local socket" { |
| 674 | const r = parse(&.{"mux"}); | 670 | const r = try parse(&.{"mux"}); |
| 675 | try std.testing.expect(r == .attach); | 671 | try std.testing.expect(r == .attach); |
| 676 | try std.testing.expect(r.attach.sock == null); | 672 | try std.testing.expect(r.attach.sock == null); |
| 677 | try std.testing.expect(r.attach.via == null); | 673 | try std.testing.expect(r.attach.via == null); |
| 678 | } | 674 | } |
| 679 | 675 | ||
| 680 | test "parseArgs: --sock and --via each name their transport" { | 676 | test "parseArgs: --sock and --via each name their transport" { |
| 681 | const s = parse(&.{ "mux", "--sock", "/tmp/x.sock" }); | 677 | const s = try parse(&.{ "mux", "--sock", "/tmp/x.sock" }); |
| 682 | try std.testing.expect(s == .attach); | 678 | try std.testing.expect(s == .attach); |
| 683 | try std.testing.expectEqualStrings("/tmp/x.sock", s.attach.sock.?); | 679 | try std.testing.expectEqualStrings("/tmp/x.sock", s.attach.sock.?); |
| 684 | try std.testing.expect(s.attach.via == null); | 680 | try std.testing.expect(s.attach.via == null); |
| 685 | 681 | ||
| 686 | const v = parse(&.{ "mux", "--via", "ssh box muxd proxy" }); | 682 | const v = try parse(&.{ "mux", "--via", "ssh box muxd proxy" }); |
| 687 | try std.testing.expect(v == .attach); | 683 | try std.testing.expect(v == .attach); |
| 688 | try std.testing.expectEqualStrings("ssh box muxd proxy", v.attach.via.?); | 684 | try std.testing.expectEqualStrings("ssh box muxd proxy", v.attach.via.?); |
| 689 | try std.testing.expect(v.attach.sock == null); | 685 | try std.testing.expect(v.attach.sock == null); |
| 690 | } | 686 | } |
| 691 | 687 | ||
| 692 | test "parseArgs: a bare word is a host to hop to" { | 688 | test "parseArgs: a bare word is a host to hop to" { |
| 693 | const h = parse(&.{ "mux", "vm1" }); | 689 | const h = try parse(&.{ "mux", "vm1" }); |
| 694 | try std.testing.expect(h == .host); | 690 | try std.testing.expect(h == .host); |
| 695 | try std.testing.expectEqualStrings("vm1", h.host.name); | 691 | try std.testing.expectEqualStrings("vm1", h.host.name); |
| 696 | // Spelled out rather than written `client.quic_idle_ms_default` — see | 692 | // Spelled out rather than written `client.quic_idle_ms_default` — see |
| @@ -701,24 +697,24 @@ test "parseArgs: a bare word is a host to hop to" { | |||
| 701 | // The user@host form is just as bare a word; nothing parses inside it, | 697 | // The user@host form is just as bare a word; nothing parses inside it, |
| 702 | // which is what lets ssh's own config (aliases, ports, ProxyJump) keep | 698 | // which is what lets ssh's own config (aliases, ports, ProxyJump) keep |
| 703 | // working untouched. | 699 | // working untouched. |
| 704 | const u = parse(&.{ "mux", "ubuntu@sandbox-9b70e9" }); | 700 | const u = try parse(&.{ "mux", "ubuntu@sandbox-9b70e9" }); |
| 705 | try std.testing.expect(u == .host); | 701 | try std.testing.expect(u == .host); |
| 706 | try std.testing.expectEqualStrings("ubuntu@sandbox-9b70e9", u.host.name); | 702 | try std.testing.expectEqualStrings("ubuntu@sandbox-9b70e9", u.host.name); |
| 707 | } | 703 | } |
| 708 | 704 | ||
| 709 | test "parseArgs: naming two transports is a conflict, however it is spelled" { | 705 | test "parseArgs: naming two transports is a conflict, however it is spelled" { |
| 710 | try std.testing.expect(parse(&.{ "mux", "vm1", "--sock", "/tmp/x.sock" }) == .conflict); | 706 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "vm1", "--sock", "/tmp/x.sock" })); |
| 711 | try std.testing.expect(parse(&.{ "mux", "--sock", "/tmp/x.sock", "vm1" }) == .conflict); | 707 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "--sock", "/tmp/x.sock", "vm1" })); |
| 712 | try std.testing.expect(parse(&.{ "mux", "vm1", "--via", "ssh box muxd proxy" }) == .conflict); | 708 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "vm1", "--via", "ssh box muxd proxy" })); |
| 713 | try std.testing.expect(parse(&.{ "mux", "--sock", "/a", "--via", "c" }) == .conflict); | 709 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "--sock", "/a", "--via", "c" })); |
| 714 | // Two of the same kind is the same ambiguity as two different kinds — | 710 | // Two of the same kind is the same ambiguity as two different kinds — |
| 715 | // for the two spellings that carry no flag. A flag repeated is not | 711 | // for the two spellings that carry no flag. A flag repeated is not |
| 716 | // ambiguous, it is corrected: the last value wins, as everywhere else. | 712 | // ambiguous, it is corrected: the last value wins, as everywhere else. |
| 717 | try std.testing.expect(parse(&.{ "mux", "vm1", "vm2" }) == .conflict); | 713 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "vm1", "vm2" })); |
| 718 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "quic://b:2" }) == .conflict); | 714 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "quic://a:1", "quic://b:2" })); |
| 719 | const s2 = parse(&.{ "mux", "--sock", "/a", "--sock", "/b" }); | 715 | const s2 = try parse(&.{ "mux", "--sock", "/a", "--sock", "/b" }); |
| 720 | try std.testing.expectEqualStrings("/b", s2.attach.sock.?); | 716 | try std.testing.expectEqualStrings("/b", s2.attach.sock.?); |
| 721 | const v2 = parse(&.{ "mux", "--via", "ssh a", "--via", "ssh b" }); | 717 | const v2 = try parse(&.{ "mux", "--via", "ssh a", "--via", "ssh b" }); |
| 722 | try std.testing.expectEqualStrings("ssh b", v2.attach.via.?); | 718 | try std.testing.expectEqualStrings("ssh b", v2.attach.via.?); |
| 723 | } | 719 | } |
| 724 | 720 | ||
| @@ -740,39 +736,39 @@ test "wall: a bad spelling is refused at parse, before any tile is dialed" { | |||
| 740 | } | 736 | } |
| 741 | 737 | ||
| 742 | test "parseArgs: unknown flags and valueless flags are usage errors" { | 738 | test "parseArgs: unknown flags and valueless flags are usage errors" { |
| 743 | try std.testing.expect(parse(&.{ "mux", "--wat" }) == .usage_error); | 739 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "--wat" })); |
| 744 | try std.testing.expect(parse(&.{ "mux", "-x" }) == .usage_error); | 740 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "-x" })); |
| 745 | // A flag whose value is missing must not be mistaken for a bare host. | 741 | // A flag whose value is missing must not be mistaken for a bare host. |
| 746 | // Every value-taking flag has to have a row here: one outcome answers | 742 | // Every value-taking flag has to have a row here: one outcome answers |
| 747 | // for all of them, so a flag added without a row is a flag nobody | 743 | // for all of them, so a flag added without a row is a flag nobody |
| 748 | // actually checked. | 744 | // actually checked. |
| 749 | inline for (.{ "--sock", "--via", "--key", "--quic-idle-ms", "--session" }) |flag| { | 745 | inline for (.{ "--sock", "--via", "--key", "--quic-idle-ms", "--session" }) |flag| { |
| 750 | try std.testing.expect(parse(&.{ "mux", flag }) == .usage_error); | 746 | try std.testing.expectError(error.Usage, parse(&.{ "mux", flag })); |
| 751 | } | 747 | } |
| 752 | } | 748 | } |
| 753 | 749 | ||
| 754 | test "parseArgs: --help is the usage someone asked for, wherever it sits" { | 750 | test "parseArgs: --help is the usage someone asked for, wherever it sits" { |
| 755 | try std.testing.expect(parse(&.{ "mux", "--help" }) == .help); | 751 | try std.testing.expectError(error.Help, parse(&.{ "mux", "--help" })); |
| 756 | try std.testing.expect(parse(&.{ "mux", "-h" }) == .help); | 752 | try std.testing.expectError(error.Help, parse(&.{ "mux", "-h" })); |
| 757 | try std.testing.expect(parse(&.{ "mux", "vm1", "--help" }) == .help); | 753 | try std.testing.expectError(error.Help, parse(&.{ "mux", "vm1", "--help" })); |
| 758 | // Even where a value belongs, and beside a line that would otherwise be | 754 | // Even where a value belongs, and beside a line that would otherwise be |
| 759 | // refused: asking for the usage is not a way to mistype a flag. | 755 | // refused: asking for the usage is not a way to mistype a flag. |
| 760 | try std.testing.expect(parse(&.{ "mux", "--sock", "--help" }) == .help); | 756 | try std.testing.expectError(error.Help, parse(&.{ "mux", "--sock", "--help" })); |
| 761 | try std.testing.expect(parse(&.{ "mux", "--wat", "--help" }) == .help); | 757 | try std.testing.expectError(error.Help, parse(&.{ "mux", "--wat", "--help" })); |
| 762 | } | 758 | } |
| 763 | 759 | ||
| 764 | test "-A rides every transport spelling" { | 760 | test "-A rides every transport spelling" { |
| 765 | try std.testing.expect(parse(&.{ "mux", "-A", "somehost" }).host.agent); | 761 | try std.testing.expect((try parse(&.{ "mux", "-A", "somehost" })).host.agent); |
| 766 | try std.testing.expect(parse(&.{ "mux", "-A", "--sock", "/tmp/x.sock" }).attach.agent); | 762 | try std.testing.expect((try parse(&.{ "mux", "-A", "--sock", "/tmp/x.sock" })).attach.agent); |
| 767 | try std.testing.expect(parse(&.{ "mux", "quic://h:1", "-A" }).quic.agent); | 763 | try std.testing.expect((try parse(&.{ "mux", "quic://h:1", "-A" })).quic.agent); |
| 768 | try std.testing.expect(!parse(&.{ "mux", "somehost" }).host.agent); | 764 | try std.testing.expect(!(try parse(&.{ "mux", "somehost" })).host.agent); |
| 769 | // `-A` is an alias, not a flag of its own, so the field's own spelling | 765 | // `-A` is an alias, not a flag of its own, so the field's own spelling |
| 770 | // has to work too. | 766 | // has to work too. |
| 771 | try std.testing.expect(parse(&.{ "mux", "--agent", "--sock", "/tmp/x.sock" }).attach.agent); | 767 | try std.testing.expect((try parse(&.{ "mux", "--agent", "--sock", "/tmp/x.sock" })).attach.agent); |
| 772 | } | 768 | } |
| 773 | 769 | ||
| 774 | test "parseArgs: quic:// is a transport like any other" { | 770 | test "parseArgs: quic:// is a transport like any other" { |
| 775 | const q = parse(&.{ "mux", "quic://box:4433", "--key", "/k" }); | 771 | const q = try parse(&.{ "mux", "quic://box:4433", "--key", "/k" }); |
| 776 | try std.testing.expect(q == .quic); | 772 | try std.testing.expect(q == .quic); |
| 777 | try std.testing.expectEqualStrings("box:4433", q.quic.host_port); | 773 | try std.testing.expectEqualStrings("box:4433", q.quic.host_port); |
| 778 | try std.testing.expectEqualStrings("/k", q.quic.key.?); | 774 | try std.testing.expectEqualStrings("/k", q.quic.key.?); |
| @@ -783,91 +779,90 @@ test "parseArgs: quic:// is a transport like any other" { | |||
| 783 | 779 | ||
| 784 | // Counted with the rest: naming it alongside another transport is the | 780 | // Counted with the rest: naming it alongside another transport is the |
| 785 | // same ambiguity as any other pairing, whichever order they arrive in. | 781 | // same ambiguity as any other pairing, whichever order they arrive in. |
| 786 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "--key", "/k", "--sock", "/x" }) == .conflict); | 782 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "quic://a:1", "--key", "/k", "--sock", "/x" })); |
| 787 | try std.testing.expect(parse(&.{ "mux", "--sock", "/x", "quic://a:1", "--key", "/k" }) == .conflict); | 783 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "--sock", "/x", "quic://a:1", "--key", "/k" })); |
| 788 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "--key", "/k", "--via", "ssh h" }) == .conflict); | 784 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "quic://a:1", "--key", "/k", "--via", "ssh h" })); |
| 789 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "--key", "/k", "vm1" }) == .conflict); | 785 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "quic://a:1", "--key", "/k", "vm1" })); |
| 790 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "quic://b:2", "--key", "/k" }) == .conflict); | 786 | try std.testing.expectError(error.Conflict, parse(&.{ "mux", "quic://a:1", "quic://b:2", "--key", "/k" })); |
| 791 | 787 | ||
| 792 | // The scheme with nothing after it names no host. | 788 | // The scheme with nothing after it names no host. |
| 793 | try std.testing.expect(parse(&.{ "mux", "quic://", "--key", "/k" }) == .usage_error); | 789 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "quic://", "--key", "/k" })); |
| 794 | } | 790 | } |
| 795 | 791 | ||
| 796 | test "parseArgs: a quic attach without a key defers to main, which resolves it" { | 792 | test "parseArgs: a quic attach without a key defers to main, which resolves it" { |
| 797 | // No --key and no environment: not a refusal any more. main has a | 793 | // No --key and no environment: not a refusal any more. main has a |
| 798 | // default path to try and parse cannot see the filesystem. | 794 | // default path to try and parse cannot see the filesystem. |
| 799 | const q = parse(&.{ "mux", "quic://a:1" }); | 795 | const q = try parse(&.{ "mux", "quic://a:1" }); |
| 800 | try std.testing.expect(q == .quic); | 796 | try std.testing.expect(q == .quic); |
| 801 | try std.testing.expect(q.quic.key == null); | 797 | try std.testing.expect(q.quic.key == null); |
| 802 | 798 | ||
| 803 | // Empty env var means unset, same as an empty --key would be nonsense. | 799 | // Empty env var means unset, same as an empty --key would be nonsense. |
| 804 | const empty_env = parseEnv(&.{ "mux", "quic://a:1" }, ""); | 800 | const empty_env = try parseEnv(&.{ "mux", "quic://a:1" }, ""); |
| 805 | try std.testing.expect(empty_env == .quic); | 801 | try std.testing.expect(empty_env == .quic); |
| 806 | try std.testing.expect(empty_env.quic.key == null); | 802 | try std.testing.expect(empty_env.quic.key == null); |
| 807 | 803 | ||
| 808 | // The environment supplies it when the flag does not... | 804 | // The environment supplies it when the flag does not... |
| 809 | const e = parseEnv(&.{ "mux", "quic://a:1" }, "/env.key"); | 805 | const e = try parseEnv(&.{ "mux", "quic://a:1" }, "/env.key"); |
| 810 | try std.testing.expect(e == .quic); | 806 | try std.testing.expect(e == .quic); |
| 811 | try std.testing.expectEqualStrings("/env.key", e.quic.key.?); | 807 | try std.testing.expectEqualStrings("/env.key", e.quic.key.?); |
| 812 | 808 | ||
| 813 | // ...and the flag wins when both are there, because it is the more | 809 | // ...and the flag wins when both are there, because it is the more |
| 814 | // specific statement of intent. | 810 | // specific statement of intent. |
| 815 | const both = parseEnv(&.{ "mux", "quic://a:1", "--key", "/flag.key" }, "/env.key"); | 811 | const both = try parseEnv(&.{ "mux", "quic://a:1", "--key", "/flag.key" }, "/env.key"); |
| 816 | try std.testing.expectEqualStrings("/flag.key", both.quic.key.?); | 812 | try std.testing.expectEqualStrings("/flag.key", both.quic.key.?); |
| 817 | 813 | ||
| 818 | // A key with no quic:// is ignored rather than refused: MUX_KEY_FILE | 814 | // A key with no quic:// is ignored rather than refused: MUX_KEY_FILE |
| 819 | // exported in a shell must not break an ordinary local attach. | 815 | // exported in a shell must not break an ordinary local attach. |
| 820 | try std.testing.expect(parseEnv(&.{"mux"}, "/env.key") == .attach); | 816 | try std.testing.expect((try parseEnv(&.{"mux"}, "/env.key")) == .attach); |
| 821 | try std.testing.expect(parse(&.{ "mux", "--key", "/k" }) == .attach); | 817 | try std.testing.expect((try parse(&.{ "mux", "--key", "/k" })) == .attach); |
| 822 | try std.testing.expect(parse(&.{ "mux", "--key", "/k", "vm1" }) == .host); | 818 | try std.testing.expect((try parse(&.{ "mux", "--key", "/k", "vm1" })) == .host); |
| 823 | } | 819 | } |
| 824 | 820 | ||
| 825 | test "parseArgs: --quic-idle-ms parses, and refuses what ngtcp2 would invert" { | 821 | test "parseArgs: --quic-idle-ms parses, and refuses what ngtcp2 would invert" { |
| 826 | const t = parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "1500" }); | 822 | const t = try parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "1500" }); |
| 827 | try std.testing.expectEqual(@as(u32, 1500), t.quic.idle_ms); | 823 | try std.testing.expectEqual(@as(u32, 1500), t.quic.idle_ms); |
| 828 | 824 | ||
| 829 | // A bare HOST ends in a QUIC link too, so the flag has to reach it — | 825 | // A bare HOST ends in a QUIC link too, so the flag has to reach it — |
| 830 | // the .host result carried no idle_ms at all and the flag was accepted | 826 | // the .host result carried no idle_ms at all and the flag was accepted |
| 831 | // and then dropped, which is worse than refusing it. muxweb's HOST | 827 | // and then dropped, which is worse than refusing it. muxweb's HOST |
| 832 | // tiles were already right; this is mux catching up. | 828 | // tiles were already right; this is mux catching up. |
| 833 | const h = parse(&.{ "mux", "vm1", "--quic-idle-ms", "1500" }); | 829 | const h = try parse(&.{ "mux", "vm1", "--quic-idle-ms", "1500" }); |
| 834 | try std.testing.expect(h == .host); | 830 | try std.testing.expect(h == .host); |
| 835 | try std.testing.expectEqual(@as(u32, 1500), h.host.idle_ms); | 831 | try std.testing.expectEqual(@as(u32, 1500), h.host.idle_ms); |
| 836 | try std.testing.expect(parse(&.{ "mux", "vm1", "--quic-idle-ms", "0" }) == .usage_error); | 832 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "vm1", "--quic-idle-ms", "0" })); |
| 837 | 833 | ||
| 838 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "0" }) == .usage_error); | 834 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "0" })); |
| 839 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "soon" }) == .usage_error); | 835 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "soon" })); |
| 840 | try std.testing.expect(parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "99999999999" }) == .usage_error); | 836 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "quic://a:1", "--key", "/k", "--quic-idle-ms", "99999999999" })); |
| 841 | // The bare-flag/missing-value case is covered once, for every | 837 | // The bare-flag/missing-value case is covered once, for every |
| 842 | // value-taking flag, by the valueless-flags sweep above. | 838 | // value-taking flag, by the valueless-flags sweep above. |
| 843 | } | 839 | } |
| 844 | 840 | ||
| 845 | test "parseArgs: --version wins wherever it appears" { | 841 | test "parseArgs: --version wins wherever it appears" { |
| 846 | try std.testing.expect(parse(&.{ "mux", "--version" }) == .version); | 842 | try std.testing.expectError(error.Version, parse(&.{ "mux", "--version" })); |
| 847 | try std.testing.expect(parse(&.{ "mux", "--sock", "/x", "--version" }) == .version); | 843 | try std.testing.expectError(error.Version, parse(&.{ "mux", "--sock", "/x", "--version" })); |
| 848 | // Including beside a line that would otherwise be a conflict or a | 844 | // Including beside a line that would otherwise be a conflict or a |
| 849 | // mistake: asking a binary its version must answer whatever else is on | 845 | // mistake: asking a binary its version must answer whatever else is on |
| 850 | // the line. | 846 | // the line. |
| 851 | try std.testing.expect(parse(&.{ "mux", "vm1", "--sock", "/x", "--version" }) == .version); | 847 | try std.testing.expectError(error.Version, parse(&.{ "mux", "vm1", "--sock", "/x", "--version" })); |
| 852 | try std.testing.expect(parse(&.{ "mux", "--wat", "--version" }) == .version); | 848 | try std.testing.expectError(error.Version, parse(&.{ "mux", "--wat", "--version" })); |
| 853 | } | 849 | } |
| 854 | 850 | ||
| 855 | test "parseArgs: --session rides every transport spelling" { | 851 | test "parseArgs: --session rides every transport spelling" { |
| 856 | const s = parse(&.{ "mux", "--session", "b", "--sock", "/tmp/x.sock" }); | 852 | const s = try parse(&.{ "mux", "--session", "b", "--sock", "/tmp/x.sock" }); |
| 857 | try std.testing.expectEqualStrings("b", s.attach.session); | 853 | try std.testing.expectEqualStrings("b", s.attach.session); |
| 858 | const h = parse(&.{ "mux", "somehost", "--session", "b" }); | 854 | const h = try parse(&.{ "mux", "somehost", "--session", "b" }); |
| 859 | try std.testing.expectEqualStrings("b", h.host.session); | 855 | try std.testing.expectEqualStrings("b", h.host.session); |
| 860 | const q = parse(&.{ "mux", "quic://h:1", "--session", "b" }); | 856 | const q = try parse(&.{ "mux", "quic://h:1", "--session", "b" }); |
| 861 | try std.testing.expectEqualStrings("b", q.quic.session); | 857 | try std.testing.expectEqualStrings("b", q.quic.session); |
| 862 | } | 858 | } |
| 863 | 859 | ||
| 864 | test "parseArgs: a bad --session is a usage error, not a wire experiment" { | 860 | test "parseArgs: a bad --session is a usage error, not a wire experiment" { |
| 865 | const r = parse(&.{ "mux", "--session", "has space" }); | 861 | try std.testing.expectError(error.Usage, parse(&.{ "mux", "--session", "has space" })); |
| 866 | try std.testing.expect(r == .usage_error); | ||
| 867 | } | 862 | } |
| 868 | 863 | ||
| 869 | test "parseArgs: no --session means the empty wire name (older-daemon compat)" { | 864 | test "parseArgs: no --session means the empty wire name (older-daemon compat)" { |
| 870 | const s = parse(&.{"mux"}); | 865 | const s = try parse(&.{"mux"}); |
| 871 | try std.testing.expectEqualStrings("", s.attach.session); | 866 | try std.testing.expectEqualStrings("", s.attach.session); |
| 872 | } | 867 | } |
| 873 | 868 | ||