mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-15 16:13:56 +00:00
terminal/apc: reject malformed glyph clear cp
Glyph clear execution previously treated an unparsable cp option the same as an omitted cp option. That made inputs such as c;cp=zz behave like a bare clear request and remove every glossary registration. Track clear option presence separately from successful decoding. A present but malformed cp now returns a malformed_payload clear failure without mutating the glossary, while an omitted cp still clears all registrations.
This commit is contained in:
@@ -112,6 +112,11 @@ fn clear(
|
||||
error.OutOfNamespace => "out_of_namespace",
|
||||
},
|
||||
} };
|
||||
} else if (clr.has(.cp)) {
|
||||
return .{ .clear = .{
|
||||
.status = .err,
|
||||
.reason = "malformed_payload",
|
||||
} };
|
||||
} else {
|
||||
glossary.clearAndFree(alloc);
|
||||
}
|
||||
@@ -278,6 +283,36 @@ test "execute clear rejects non-PUA" {
|
||||
}, testExecute(alloc, &glossary, &req).?);
|
||||
}
|
||||
|
||||
test "execute clear rejects malformed cp without clearing glossary" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var glossary: Glossary = .empty;
|
||||
defer glossary.deinit(alloc);
|
||||
|
||||
var reg1 = try testParse(alloc, "r;cp=e0a0;AAAAAAAAAAAAAA==");
|
||||
defer reg1.deinit(alloc);
|
||||
_ = testExecute(alloc, &glossary, ®1);
|
||||
|
||||
var reg2 = try testParse(alloc, "r;cp=e0a1;AAAAAAAAAAAAAA==");
|
||||
defer reg2.deinit(alloc);
|
||||
_ = testExecute(alloc, &glossary, ®2);
|
||||
|
||||
for ([_][]const u8{ "c;cp=zz", "c;cp=", "c;cp=200000" }) |data| {
|
||||
var req = try testParse(alloc, data);
|
||||
defer req.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(Response{
|
||||
.clear = .{
|
||||
.status = .err,
|
||||
.reason = "malformed_payload",
|
||||
},
|
||||
}, testExecute(alloc, &glossary, &req).?);
|
||||
try testing.expect(glossary.contains(0xE0A0));
|
||||
try testing.expect(glossary.contains(0xE0A1));
|
||||
}
|
||||
}
|
||||
|
||||
test "execute query reports no coverage" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -350,6 +350,12 @@ pub const Request = union(enum) {
|
||||
.cp => std.fmt.parseInt(u21, value, 16) catch null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Return whether the option is present in the raw option string,
|
||||
/// independent of whether its value can be decoded.
|
||||
pub fn present(comptime self: Option, raw: []const u8) bool {
|
||||
return optionValue(raw, self.key()) != null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Lazily decode a clear option on demand.
|
||||
@@ -357,6 +363,11 @@ pub const Request = union(enum) {
|
||||
return option.read(self.rawOptions());
|
||||
}
|
||||
|
||||
/// Return whether a clear option was provided, even if malformed.
|
||||
pub fn has(self: Clear, comptime option: Option) bool {
|
||||
return option.present(self.rawOptions());
|
||||
}
|
||||
|
||||
/// Return the raw option portion of a valid clear command.
|
||||
fn rawOptions(self: Clear) []const u8 {
|
||||
assert(self.raw.len >= 2);
|
||||
@@ -799,9 +810,23 @@ test "clear command" {
|
||||
defer cmd.deinit(testing.allocator);
|
||||
|
||||
try testing.expect(cmd == .clear);
|
||||
try testing.expect(cmd.clear.has(.cp));
|
||||
try testing.expectEqual(@as(u21, 0xE0A0), cmd.clear.get(.cp).?);
|
||||
}
|
||||
|
||||
test "clear command tracks malformed cp presence" {
|
||||
const testing = std.testing;
|
||||
|
||||
for ([_][]const u8{ "c;cp=zz", "c;cp=", "c;cp=200000" }) |data| {
|
||||
var cmd = try testParse(testing.allocator, data);
|
||||
defer cmd.deinit(testing.allocator);
|
||||
|
||||
try testing.expect(cmd == .clear);
|
||||
try testing.expect(cmd.clear.has(.cp));
|
||||
try testing.expect(cmd.clear.get(.cp) == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "invalid command" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user