e923ab8ccb1a

flake/channel/profile/pkg dispatchers reject an unrecognized subcommand with subcommand help and a non-zero exit instead of silently falling off the if-chain and exiting 0; adds a profile_help table (none existed before) and a shared subcommandKnown helper (NINA-012)

time
2026-07-02 13:48 UTC · 3 months ago
machine
koh2
agent
claude-sonnet-5
session
fable audit fix — commands.zig
parent
4f0092888eb7
1 file src/commands.zig+51 −0
src/commands.zig+51 −0
@@ -1793,6 +1793,19 @@17931793     try std.testing.expect(!serviceVerbAllowed("starting")); // no substring match on "start"17941794 }17951795 1796+// True when `sub` matches one of the verbs advertised in a dispatcher's help1797+// table, ignoring any "<arg>"/"[opt]" suffix (e.g. "add <url> [name]" -> "add").1798+// Shared by flake/channel/profile/pkg so an unrecognized subcommand (a typo1799+// like `nina flake updtae`) fails loudly instead of falling off the end of the1800+// if-chain and exiting 0 (NINA-012).1801+fn subcommandKnown(help_rows: []const [2][]const u8, sub: []const u8) bool {1802+    for (help_rows) |row| {1803+        const verb = if (std.mem.indexOfScalar(u8, row[0], ' ')) |i| row[0][0..i] else row[0];1804+        if (std.mem.eql(u8, verb, sub)) return true;1805+    }1806+    return false;1807+}1808+17961809 // --- Flake commands ---17971810 17981811 const flake_help = [_][2][]const u8{@@ -1807,6 +1820,10 @@18071820 18081821 pub fn flake(ctx: Ctx) !void {18091822     if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("flake", &flake_help, null);1823+    if (!subcommandKnown(&flake_help, ctx.sub)) {1824+        output.subcommandHelp("flake", &flake_help, null);1825+        return error.Internal;1826+    }18101827     if (std.mem.eql(u8, ctx.sub, "update")) {18111828         output.flakeUpdateStart();18121829         const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;@@ -1931,6 +1948,10 @@19311948 19321949 pub fn channel(ctx: Ctx) !void {19331950     if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("channel", &channel_help, null);1951+    if (!subcommandKnown(&channel_help, ctx.sub)) {1952+        output.subcommandHelp("channel", &channel_help, null);1953+        return error.Internal;1954+    }19341955     // On a flake-managed system, nix-channel operations generally don't apply.19351956     if (flakeSystem(ctx)) output.flakeChannelNote();19361957     if (std.mem.eql(u8, ctx.sub, "list")) {@@ -1960,7 +1981,19 @@19601981 19611982 // --- Profile commands ---19621983 1984+const profile_help = [_][2][]const u8{1985+    .{ "list", "list profile packages" },1986+    .{ "install <attr>", "install a package into your profile" },1987+    .{ "remove <attr>", "remove a package from your profile" },1988+    .{ "upgrade", "upgrade all profile packages" },1989+};1990+19631991 pub fn profile(ctx: Ctx) !void {1992+    if (std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("profile", &profile_help, null);1993+    if (ctx.sub.len > 0 and !subcommandKnown(&profile_help, ctx.sub)) {1994+        output.subcommandHelp("profile", &profile_help, null);1995+        return error.Internal;1996+    }19641997     if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {19651998         output.profileListHeader();19661999         const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;@@ -2006,6 +2039,10 @@20062039 20072040 pub fn pkg(ctx: Ctx) !void {20082041     if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("pkg", &pkg_help, null);2042+    if (!subcommandKnown(&pkg_help, ctx.sub)) {2043+        output.subcommandHelp("pkg", &pkg_help, null);2044+        return error.Internal;2045+    }20092046     const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;20102047     if (std.mem.eql(u8, ctx.sub, "why")) {20112048         if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;@@ -2031,6 +2068,20 @@20312068         if (try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;20322069         return;20332070     }2071+}2072+2073+test "subcommandKnown rejects a garbage subcommand for each of flake/channel/profile/pkg" {2074+    try std.testing.expect(subcommandKnown(&flake_help, "update"));2075+    try std.testing.expect(!subcommandKnown(&flake_help, "updtae"));2076+2077+    try std.testing.expect(subcommandKnown(&channel_help, "add"));2078+    try std.testing.expect(!subcommandKnown(&channel_help, "addd"));2079+2080+    try std.testing.expect(subcommandKnown(&profile_help, "install"));2081+    try std.testing.expect(!subcommandKnown(&profile_help, "instal"));2082+2083+    try std.testing.expect(subcommandKnown(&pkg_help, "closure"));2084+    try std.testing.expect(!subcommandKnown(&pkg_help, "closures"));20342085 }20352086 20362087 // Top-level alias for `nina pkg why <pkg>` — surfaces what pulled a package in

view file