Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 84 additions & 10 deletions src/Parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -243,16 +243,29 @@ inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Resu
// end is the index of the terminating byte(s) (either the last byte of an
// ST or BEL)
const end: usize = blk: {
const esc_result = skipUntilST(input);
if (esc_result.n > 0) break :blk esc_result.n;

// No escape, could be BEL terminated
const bel = std.mem.indexOfScalarPos(u8, input, 2, 0x07) orelse return .{
.event = null,
.n = 0,
};
bel_terminated = true;
break :blk bel + 1;
// Find the first terminator: BEL (0x07) or ST (ESC \), whichever comes
// first. A bare ESC that is not part of an ST begins a new sequence, so
// we must stop rather than consume into it.
var i: usize = 2;
while (i < input.len) : (i += 1) {
switch (input[i]) {
0x07 => {
bel_terminated = true;
break :blk i + 1;
},
0x1b => {
if (i + 1 >= input.len) return .{ .event = null, .n = 0 };
if (input[i + 1] == 0x5c) break :blk i + 2;
// ESC not followed by \: the OSC was never terminated and a
// new escape sequence has begun. Consume the malformed OSC
// prefix and leave the ESC for the next parse call.
return .{ .event = null, .n = i };
},
else => {},
}
}
// No terminator yet: wait for more input.
return .{ .event = null, .n = 0 };
};

// The complete OSC sequence
Expand Down Expand Up @@ -1330,3 +1343,64 @@ test "parse: disambiguate shift + space" {
test {
std.testing.refAllDecls(@This());
}

test "parse: bel-terminated osc color report followed by da1" {
// A BEL-terminated OSC 11 (background color) reply immediately followed by
// a DA1 reply, as they can arrive together in one read(). The OSC must stop
// at the BEL and must not consume into the following sequence.
var parser: Parser = .{};
const input = "\x1b]11;rgb:0000/0000/0000\x07\x1b[?1;2c";
const result = try parser.parse(input, null);
try testing.expectEqual(24, result.n);
switch (result.event.?) {
.color_report => {},
else => try testing.expect(false),
}
// The DA1 that follows must survive intact.
const rest = try parser.parse(input[result.n..], null);
switch (rest.event.?) {
.cap_da1 => {},
else => try testing.expect(false),
}
}

test "parse: bel-terminated osc does not consume the following sequence" {
// An OSC (BEL-terminated) directly followed by a CSI cursor-up. The OSC must
// stop at the BEL and leave the CSI intact.
var parser: Parser = .{};
const input = "\x1b]0;title\x07\x1b[A";
const result = try parser.parse(input, null);
try testing.expectEqual(10, result.n);
const rest = try parser.parse(input[result.n..], null);
switch (rest.event.?) {
.key_press => |key| try testing.expectEqual(Key.up, key.codepoint),
else => try testing.expect(false),
}
}

test "parse: osc interrupted by escape leaves the next sequence intact" {
// Per the DEC parser, ESC is an anywhere transition out of OSC. If it is not
// the ST sequence (ESC \\), the malformed OSC should be discarded up to the
// ESC and the new escape sequence should be parsed next.
var parser: Parser = .{};
const input = "\x1b]0;title\x1b[A";
const result = try parser.parse(input, null);
try testing.expectEqual(9, result.n);
try testing.expectEqual(@as(?Event, null), result.event);

const rest = try parser.parse(input[result.n..], null);
switch (rest.event.?) {
.key_press => |key| try testing.expectEqual(Key.up, key.codepoint),
else => try testing.expect(false),
}
}

test "parse: osc ending with escape waits for a possible ST" {
// If the buffer ends at ESC, it may still become an ST when more bytes
// arrive, so the parser should wait instead of discarding the OSC.
var parser: Parser = .{};
const input = "\x1b]0;title\x1b";
const result = try parser.parse(input, null);
try testing.expectEqual(0, result.n);
try testing.expectEqual(@as(?Event, null), result.event);
}
Loading