config: more robust handling of font-family overwrite for CLI args

Fixes #7481

We unfortunately don't have a great way to unit test this since our
logic relies on argv and I'm too lazy to extract it out right now.

The core issue is that we previously detected if font-families changed
by comparing lengths. This doesn't work because the CLI args can reset
and add families back to a lesser length. This caused an integer
overflow.

We can fix this by not being clever and introducing the overwrite logic
directly into the config type. I unit tested that.
This commit is contained in:
Mitchell Hashimoto
2025-05-30 14:15:20 -07:00
parent 2ad86cde69
commit 8be5a78585

View File

@@ -2722,19 +2722,18 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// can replay if we are discarding the default files.
const replay_len_start = self._replay_steps.items.len;
// Keep track of font families because if they are set from the CLI
// then we clear the previously set values. This avoids a UX oddity
// where on the CLI you have to specify `font-family=""` to clear the
// font families before setting a new one.
// font-family settings set via the CLI overwrite any prior values
// rather than append. This avoids a UX oddity where you have to
// specify `font-family=""` to clear the font families.
const fields = &[_][]const u8{
"font-family",
"font-family-bold",
"font-family-italic",
"font-family-bold-italic",
};
var counter: [fields.len]usize = undefined;
inline for (fields, 0..) |field, i| {
counter[i] = @field(self, field).list.items.len;
inline for (fields) |field| @field(self, field).overwrite_next = true;
defer {
inline for (fields) |field| @field(self, field).overwrite_next = false;
}
// Initialize our CLI iterator.
@@ -2759,28 +2758,6 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
try new_config.loadIter(alloc_gpa, &it);
self.deinit();
self.* = new_config;
} else {
// If any of our font family settings were changed, then we
// replace the entire list with the new list.
inline for (fields, 0..) |field, i| {
const v = &@field(self, field);
// The list can be empty if it was reset, i.e. --font-family=""
if (v.list.items.len > 0) {
const len = v.list.items.len - counter[i];
if (len > 0) {
// Note: we don't have to worry about freeing the memory
// that we overwrite or cut off here because its all in
// an arena.
v.list.replaceRangeAssumeCapacity(
0,
len,
v.list.items[counter[i]..],
);
v.list.items.len = len;
}
}
}
}
// Any paths referenced from the CLI are relative to the current working
@@ -4172,6 +4149,11 @@ pub const RepeatableString = struct {
// Allocator for the list is the arena for the parent config.
list: std.ArrayListUnmanaged([:0]const u8) = .{},
// If true, then the next value will clear the list and start over
// rather than append. This is a bit of a hack but is here to make
// the font-family set of configurations work with CLI parsing.
overwrite_next: bool = false,
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
const value = input orelse return error.ValueRequired;
@@ -4181,6 +4163,12 @@ pub const RepeatableString = struct {
return;
}
// If we're overwriting then we clear before appending
if (self.overwrite_next) {
self.list.clearRetainingCapacity();
self.overwrite_next = false;
}
const copy = try alloc.dupeZ(u8, value);
try self.list.append(alloc, copy);
}
@@ -4247,6 +4235,24 @@ pub const RepeatableString = struct {
try testing.expectEqual(@as(usize, 0), list.list.items.len);
}
test "parseCLI overwrite" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: Self = .{};
try list.parseCLI(alloc, "A");
// Set our overwrite flag
list.overwrite_next = true;
try list.parseCLI(alloc, "B");
try testing.expectEqual(@as(usize, 1), list.list.items.len);
try list.parseCLI(alloc, "C");
try testing.expectEqual(@as(usize, 2), list.list.items.len);
}
test "formatConfig empty" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);