From abc17c3b950c3d1b85b56eb5b0e12f79ef340a04 Mon Sep 17 00:00:00 2001 From: Travis Cole <11240+kelp@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:45:44 -0700 Subject: [PATCH] Fix id, sleep, yes audit findings id: -z alone now exits 2 (GNU rejects without -u/-g/-G). id: -a is now a no-op (GNU behavior), not an alias for -G. sleep: 'inf'/'infinity' accepted as maxInt(u64) (GNU compat). sleep: error message includes the invalid token name. yes: unknown flag exits 1 (GNU behavior), not 2. yes: error message includes the unrecognized flag name. --- src/common/time.zig | 12 +++++++++--- src/id.zig | 43 +++++++++++++++++++++++++------------------ src/sleep.zig | 33 +++++++++++++++++++++++++++------ src/yes.zig | 43 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 102 insertions(+), 29 deletions(-) diff --git a/src/common/time.zig b/src/common/time.zig index 4b8bdf6..7fdf9ea 100644 --- a/src/common/time.zig +++ b/src/common/time.zig @@ -62,6 +62,11 @@ pub fn parseTimeString(time_str: []const u8) !u64 { return error.InvalidTimeFormat; } + // GNU sleep accepts 'inf' and 'infinity' to mean sleep forever. + if (std.mem.eql(u8, time_str, "inf") or std.mem.eql(u8, time_str, "infinity")) { + return std.math.maxInt(u64); + } + // Find the unit suffix (if any) var number_part = time_str; var unit = TimeUnit.seconds; // default unit @@ -182,12 +187,13 @@ test "parseTimeString - invalid formats" { try testing.expectEqual(@as(u64, @intFromFloat(0.5 * std.time.ns_per_s)), try parseTimeString(".5")); } -test "parseTimeString - reject NaN and Inf" { +test "parseTimeString - reject NaN and Inf (except GNU-compatible inf/infinity)" { try testing.expectError(error.InvalidTimeFormat, parseTimeString("nan")); try testing.expectError(error.InvalidTimeFormat, parseTimeString("NaN")); - try testing.expectError(error.InvalidTimeFormat, parseTimeString("inf")); + // GNU sleep accepts 'inf' and 'infinity' as meaning sleep forever + try testing.expectEqual(std.math.maxInt(u64), try parseTimeString("inf")); try testing.expectError(error.InvalidTimeFormat, parseTimeString("Inf")); - try testing.expectError(error.InvalidTimeFormat, parseTimeString("infinity")); + try testing.expectEqual(std.math.maxInt(u64), try parseTimeString("infinity")); try testing.expectError(error.InvalidTimeFormat, parseTimeString("nans")); try testing.expectError(error.InvalidTimeFormat, parseTimeString("infm")); } diff --git a/src/id.zig b/src/id.zig index e7eb889..f50a6ee 100644 --- a/src/id.zig +++ b/src/id.zig @@ -134,8 +134,8 @@ pub fn runId(allocator: Allocator, args: []const []const u8, stdout_writer: anyt return @intFromEnum(common.ExitCode.misuse); } - // -a is a compatibility alias for -G (show all groups) - const show_groups = parsed.groups or parsed.all; + // GNU id: -a is a no-op (ignored), not an alias for -G + const show_groups = parsed.groups; // -n and -r only make sense with -u, -g, or -G if (parsed.name and !parsed.user and !parsed.group and !show_groups) { @@ -147,7 +147,13 @@ pub fn runId(allocator: Allocator, args: []const []const u8, stdout_writer: anyt return @intFromEnum(common.ExitCode.misuse); } - // Mutually exclusive: -u, -g, -G/-a + // GNU id: -z requires a format flag (-u, -g, or -G) + if (parsed.zero and !parsed.user and !parsed.group and !show_groups) { + common.printErrorWithProgram(allocator, stderr_writer, "id", "option --zero not permitted in default format", .{}); + return @intFromEnum(common.ExitCode.misuse); + } + + // Mutually exclusive: -u, -g, -G const mode_count: u8 = @as(u8, @intFromBool(parsed.user)) + @as(u8, @intFromBool(parsed.group)) + @as(u8, @intFromBool(show_groups)); @@ -303,7 +309,7 @@ pub fn runId(allocator: Allocator, args: []const []const u8, stdout_writer: anyt return @intFromEnum(common.ExitCode.success); } - // Handle -G (all groups) or -a (compatibility alias) + // Handle -G (all groups) if (show_groups) { const group_separator: u8 = if (parsed.zero) 0 else ' '; @@ -1050,32 +1056,33 @@ test "id -p does not contain uid= format" { try testing.expect(std.mem.indexOf(u8, stdout_buffer.items, "uid=") == null); } -test "id -a shows all groups (same as -G)" { +test "id -a is a no-op (GNU behavior), produces default format" { var stdout_a = try std.ArrayList(u8).initCapacity(testing.allocator, 0); defer stdout_a.deinit(testing.allocator); - var stdout_g = try std.ArrayList(u8).initCapacity(testing.allocator, 0); - defer stdout_g.deinit(testing.allocator); + var stdout_default = try std.ArrayList(u8).initCapacity(testing.allocator, 0); + defer stdout_default.deinit(testing.allocator); const args_a = [_][]const u8{"-a"}; const result_a = try runId(testing.allocator, &args_a, stdout_a.writer(testing.allocator), common.null_writer); try testing.expectEqual(@as(u8, 0), result_a); - const args_g = [_][]const u8{"-G"}; - const result_g = try runId(testing.allocator, &args_g, stdout_g.writer(testing.allocator), common.null_writer); - try testing.expectEqual(@as(u8, 0), result_g); + const args_default = [_][]const u8{}; + const result_default = try runId(testing.allocator, &args_default, stdout_default.writer(testing.allocator), common.null_writer); + try testing.expectEqual(@as(u8, 0), result_default); - // -a and -G should produce identical output - try testing.expectEqualStrings(stdout_g.items, stdout_a.items); + // GNU: -a is a no-op, so output should match plain id + try testing.expectEqualStrings(stdout_default.items, stdout_a.items); } -test "id -a with -n shows group names" { - var stdout_buffer = try std.ArrayList(u8).initCapacity(testing.allocator, 0); - defer stdout_buffer.deinit(testing.allocator); +test "id -a with -n is rejected (GNU: -a is no-op, -n alone is invalid)" { + var stderr_buffer = try std.ArrayList(u8).initCapacity(testing.allocator, 0); + defer stderr_buffer.deinit(testing.allocator); const args = [_][]const u8{ "-a", "-n" }; - const result = try runId(testing.allocator, &args, stdout_buffer.writer(testing.allocator), common.null_writer); - try testing.expectEqual(@as(u8, 0), result); - try testing.expect(stdout_buffer.items.len > 1); + const result = try runId(testing.allocator, &args, common.null_writer, stderr_buffer.writer(testing.allocator)); + // -a is no-op, -n without -u/-g/-G is an error + try testing.expectEqual(@as(u8, 2), result); + try testing.expect(stderr_buffer.items.len > 0); } test "id -A prints audit stub and exits 1" { diff --git a/src/sleep.zig b/src/sleep.zig index ef6e0d2..ee3be5a 100644 --- a/src/sleep.zig +++ b/src/sleep.zig @@ -47,6 +47,15 @@ fn parseTotalTime(args: []const []const u8) !u64 { return total_nanos; } +/// Find the first token that fails to parse as a valid time string. +/// Used to include the offending token in error messages (GNU behavior). +fn findBadToken(args: []const []const u8) ?[]const u8 { + for (args) |arg| { + _ = time.parseTimeString(arg) catch return arg; + } + return null; +} + /// Print help message fn printHelp(allocator: std.mem.Allocator, writer: anytype) !void { try common.help.printColorized(allocator, writer, @@ -99,7 +108,7 @@ pub fn runSleep(allocator: std.mem.Allocator, args: []const []const u8, stdout_w return @intFromEnum(common.ExitCode.success); } - // Parse time arguments + // Parse time arguments, tracking which token failed for error reporting const total_nanos = parseTotalTime(parsed_args.positionals) catch |err| { switch (err) { error.MissingTimeArgument => { @@ -108,11 +117,22 @@ pub fn runSleep(allocator: std.mem.Allocator, args: []const []const u8, stdout_w return @intFromEnum(common.ExitCode.misuse); }, error.InvalidTimeFormat => { - common.printErrorWithProgram(allocator, stderr_writer, "sleep", "invalid time interval", .{}); + // Find the offending token for the error message (GNU includes it) + const bad_token = findBadToken(parsed_args.positionals); + if (bad_token) |token| { + common.printErrorWithProgram(allocator, stderr_writer, "sleep", "invalid time interval '{s}'", .{token}); + } else { + common.printErrorWithProgram(allocator, stderr_writer, "sleep", "invalid time interval", .{}); + } return @intFromEnum(common.ExitCode.misuse); }, error.NegativeTime => { - common.printErrorWithProgram(allocator, stderr_writer, "sleep", "invalid time interval", .{}); + const bad_token = findBadToken(parsed_args.positionals); + if (bad_token) |token| { + common.printErrorWithProgram(allocator, stderr_writer, "sleep", "invalid time interval '{s}'", .{token}); + } else { + common.printErrorWithProgram(allocator, stderr_writer, "sleep", "invalid time interval", .{}); + } return @intFromEnum(common.ExitCode.misuse); }, error.TimeOverflow => { @@ -213,12 +233,13 @@ test "parseTimeString - invalid formats" { try testing.expectEqual(@as(u64, @intFromFloat(0.5 * std.time.ns_per_s)), try time.parseTimeString(".5")); } -test "parseTimeString - reject NaN and Inf" { +test "parseTimeString - reject NaN and Inf (except GNU-compatible inf/infinity)" { try testing.expectError(error.InvalidTimeFormat, time.parseTimeString("nan")); try testing.expectError(error.InvalidTimeFormat, time.parseTimeString("NaN")); - try testing.expectError(error.InvalidTimeFormat, time.parseTimeString("inf")); + // GNU sleep accepts 'inf' and 'infinity' as meaning sleep forever + try testing.expectEqual(std.math.maxInt(u64), try time.parseTimeString("inf")); try testing.expectError(error.InvalidTimeFormat, time.parseTimeString("Inf")); - try testing.expectError(error.InvalidTimeFormat, time.parseTimeString("infinity")); + try testing.expectEqual(std.math.maxInt(u64), try time.parseTimeString("infinity")); try testing.expectError(error.InvalidTimeFormat, time.parseTimeString("nans")); try testing.expectError(error.InvalidTimeFormat, time.parseTimeString("infm")); } diff --git a/src/yes.zig b/src/yes.zig index f8d71ae..d87176e 100644 --- a/src/yes.zig +++ b/src/yes.zig @@ -29,8 +29,14 @@ pub fn runYes( const parsed_args = common.argparse.ArgParser.parse(YesArgs, allocator, args) catch |err| { switch (err) { error.UnknownFlag => { - common.printErrorWithProgram(allocator, stderr_writer, "yes", "unrecognized option", .{}); - return @intFromEnum(common.ExitCode.misuse); + // GNU yes exits 1 for unrecognized options and includes the flag name + const bad_flag = findUnknownFlag(args); + if (bad_flag) |flag| { + common.printErrorWithProgram(allocator, stderr_writer, "yes", "unrecognized option '{s}'", .{flag}); + } else { + common.printErrorWithProgram(allocator, stderr_writer, "yes", "unrecognized option", .{}); + } + return @intFromEnum(common.ExitCode.general_error); }, error.MissingValue => { common.printErrorWithProgram(allocator, stderr_writer, "yes", "option requires an argument", .{}); @@ -102,6 +108,39 @@ pub fn runYes( } } +/// Find the first unrecognized flag in args for error reporting. +/// Returns the full flag string (e.g., "--bad-flag") or null. +fn findUnknownFlag(args: []const []const u8) ?[]const u8 { + for (args) |arg| { + if (arg.len > 1 and arg[0] == '-') { + if (std.mem.eql(u8, arg, "--")) break; + // Check if it's a known flag + if (arg.len > 2 and arg[1] == '-') { + // Long flag + const flag_content = arg[2..]; + const flag_name = if (std.mem.indexOfScalar(u8, flag_content, '=')) |eq_pos| + flag_content[0..eq_pos] + else + flag_content; + if (std.mem.eql(u8, flag_name, "help") or + std.mem.eql(u8, flag_name, "version")) + { + continue; + } + return arg; + } else { + // Short flag - check each character + for (arg[1..]) |c| { + if (c != 'h' and c != 'V') { + return arg; + } + } + } + } + } + return null; +} + /// Prints help text for the yes utility. fn printHelp(allocator: std.mem.Allocator, writer: anytype) !void { try common.help.printColorized(allocator, writer,