mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-26 17:11:40 +00:00
terminal: treat high bytes in DCS strings as payload data
Refs #11216 The dcs_passthrough state only forwarded bytes 0x00-0x7E to the DCS handler. Bytes 0x80-0x9F hit the "anywhere" C1 transitions and exited the string, while 0xA0-0xFF fell through to the default transition and were silently dropped. **This breaks any DCS payload carrying UTF-8. ** A continuation byte in the C1 range terminates or corrupts the string: "Ü" is 0xC3 0x9C, so the 0xC3 is dropped and the 0x9C acts as 8-bit ST, ending the DCS mid-character. Also, a payload byte such as 0x9B (second byte of "Û") transitions to csi_entry, so the remainder of the payload executes as a live control sequence. This is a prerequisite for tmux control mode (#1935), whose %output notifications carry raw UTF-8 pane content. Fix this in the parse table only: override 0x80-0xFF in dcs_passthrough to put and in dcs_ignore to ignore, exactly how osc_string already claims 0x20-0xFF (including 0x9C) as data. This deviates from the vt100.net state machine (https://vt100.net/emu/dec_ansi_parser) deliberately and includes 0x9C: a raw 0x9C is indistinguishable from a UTF-8 continuation byte, and we don't honor 8-bit C1 controls in the ground state either.
This commit is contained in:
@@ -206,6 +206,13 @@ fn genTable() Table {
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
|
||||
// High bytes are ignored payload data, overriding the
|
||||
// "anywhere" C1 transitions. See dcs_passthrough below for more.
|
||||
// In dcs_ignore the additional concern is that a UTF-8 payload in
|
||||
// an ignored DCS could otherwise begin a live sequence mid-string
|
||||
// (e.g. 0x9B => csi_entry).
|
||||
range(&result, 0x80, 0xFF, source, source, .ignore);
|
||||
}
|
||||
|
||||
// dcs_param
|
||||
@@ -241,6 +248,21 @@ fn genTable() Table {
|
||||
range(&result, 0x1C, 0x1F, source, source, .put);
|
||||
range(&result, 0x20, 0x7E, source, source, .put);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// High bytes are payload data, overriding the "anywhere" C1
|
||||
// transitions, matching how osc_string handles them below.
|
||||
// Ghostty is UTF-8 only, and DCS payloads carry UTF-8 text
|
||||
// (e.g. tmux control mode pane content): without this, a
|
||||
// UTF-8 continuation byte in the C1 range terminates or
|
||||
// corrupts the string and 0xA0-0xFF are silently dropped.
|
||||
//
|
||||
// This includes 0x9C (8-bit ST) on purpose: a raw 0x9C is
|
||||
// indistinguishable from a UTF-8 continuation byte ("Ü" is
|
||||
// 0xC3 0x9C), and Ghostty doesn't honor 8-bit C1 controls in
|
||||
// the ground state either (they go through UTF-8 decoding).
|
||||
// DCS strings terminate via 7-bit ST (ESC \) and abort via
|
||||
// CAN/SUB, which are unaffected here.
|
||||
range(&result, 0x80, 0xFF, source, source, .put);
|
||||
}
|
||||
|
||||
// csi_param
|
||||
@@ -386,3 +408,41 @@ test {
|
||||
// that it succeeds in creation.
|
||||
_ = table;
|
||||
}
|
||||
|
||||
test "dcs_passthrough: high bytes are payload data" {
|
||||
// Bytes 0x80-0xFF within a DCS string are payload data, not C1
|
||||
// controls. This includes 0x9C (8-bit ST): a raw 0x9C is
|
||||
// indistinguishable from a UTF-8 continuation byte (e.g. "Ü" is
|
||||
// 0xC3 0x9C) and Ghostty doesn't support 8-bit C1 controls
|
||||
// anywhere else.
|
||||
for (0x80..0x100) |c| {
|
||||
const entry = table[c][@intFromEnum(State.dcs_passthrough)];
|
||||
try std.testing.expectEqual(State.dcs_passthrough, entry.state);
|
||||
try std.testing.expectEqual(Action.put, entry.action);
|
||||
}
|
||||
}
|
||||
|
||||
test "dcs_ignore: high bytes are ignored payload data" {
|
||||
// Same as dcs_passthrough: a UTF-8 payload inside an ignored DCS
|
||||
// must not trigger "anywhere" C1 transitions (e.g. 0x9B beginning
|
||||
// a CSI mid-string).
|
||||
for (0x80..0x100) |c| {
|
||||
const entry = table[c][@intFromEnum(State.dcs_ignore)];
|
||||
try std.testing.expectEqual(State.dcs_ignore, entry.state);
|
||||
try std.testing.expectEqual(Action.ignore, entry.action);
|
||||
}
|
||||
}
|
||||
|
||||
test "dcs_passthrough: ESC, CAN, and SUB still exit" {
|
||||
// 7-bit ST (ESC \) is the DCS terminator and CAN/SUB abort, so
|
||||
// these must continue to leave dcs_passthrough. dcs_unhook is
|
||||
// emitted by the parser on any transition out of dcs_passthrough.
|
||||
const esc = table[0x1B][@intFromEnum(State.dcs_passthrough)];
|
||||
try std.testing.expectEqual(State.escape, esc.state);
|
||||
|
||||
const can = table[0x18][@intFromEnum(State.dcs_passthrough)];
|
||||
try std.testing.expectEqual(State.ground, can.state);
|
||||
|
||||
const sub = table[0x1A][@intFromEnum(State.dcs_passthrough)];
|
||||
try std.testing.expectEqual(State.ground, sub.state);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user