diff --git a/src/Parser.zig b/src/Parser.zig index a7980c30..d16f1a21 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -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 @@ -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); +}