a73x

2ae8fe2e

refactor: mux parses to a transport or an error, like muxa and muxweb

a73x   2026-08-27 08:08

Commit message
refactor: mux parses to a transport or an error, like muxa and muxweb

The four arms that were not transports carried no payload and existed
only to be switched on twice — once for the -A preflight, once to act.
As errors, main answers all four in the catch and the preflight switch
becomes exhaustive over the three transports: a fourth spelling added
later can no longer skip the agent check through an `else`.

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