guard SGR 38/48 lookahead; parse colon sub-parameters (T-369)

printf '\e[38m' was a RangeError inside Terminal.write — the
extended-color branches indexed params[i+1..i+4] unguarded. An
emulator must never throw on hostile bytes. Both branches now share a
bounds-checked helper that ignores truncated sequences.

Colons were silently dropped mid-CSI, fusing 38:2:255:0:0 into one
bogus parameter; the consumer now records ECMA-48 sub-parameter
links, so ITU T.416 colon-form truecolor/256-color (with or without
the colorspace slot) parses identically to the semicolon form, and a
malformed colon group is dropped whole instead of bleeding into
neighbouring SGR codes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:30:41 +02:00
co-authored by Claude Fable 5
parent 390ab2b64e
commit 34821fbc39
5 changed files with 149 additions and 30 deletions
+50
View File
@@ -676,6 +676,56 @@ void main() {
f.parser.write('\x1b[123m');
expect(f.h.named('unsupportedStyle').first.args, [123]);
});
// T-369: an emulator must never throw on hostile bytes. The old code did
// unguarded params[i+1] lookahead in 38/48 — `\x1b[38m` was a RangeError
// inside Terminal.write.
test('truncated 38/48 sequences are ignored, never throw', () {
final f = _newParser();
for (final s in ['\x1b[38m', '\x1b[48m', '\x1b[38;2m', '\x1b[38;2;255m', '\x1b[38;2;255;10m', '\x1b[38;5m', '\x1b[48;5m', '\x1b[38:2m', '\x1b[38:5m', '\x1b[48:2:255m']) {
f.parser.write(s);
}
expect(f.h.named('setForegroundColorRgb'), isEmpty);
expect(f.h.named('setBackgroundColorRgb'), isEmpty);
expect(f.h.named('setForegroundColor256'), isEmpty);
expect(f.h.named('setBackgroundColor256'), isEmpty);
});
test('colon-form truecolor matches semicolon form (ITU T.416)', () {
final f = _newParser();
f.parser.write('\x1b[38:2:10:20:30m\x1b[48:2:100:150:200m');
expect(f.h.named('setForegroundColorRgb').first.args, [10, 20, 30]);
expect(f.h.named('setBackgroundColorRgb').first.args, [100, 150, 200]);
});
test('colon-form with empty colorspace slot — 38:2::r:g:b', () {
final f = _newParser();
f.parser.write('\x1b[38:2::10:20:30m');
expect(f.h.named('setForegroundColorRgb').first.args, [10, 20, 30]);
});
test('colon-form 256-colour — 38:5:n', () {
final f = _newParser();
f.parser.write('\x1b[38:5:200m\x1b[48:5:42m');
expect(f.h.named('setForegroundColor256').first.args, [200]);
expect(f.h.named('setBackgroundColor256').first.args, [42]);
});
test('a malformed colon group is dropped whole, neighbours still apply', () {
final f = _newParser();
// The bogus 38:2:255 group must not bleed into the following bold.
f.parser.write('\x1b[38:2:255;1m');
expect(f.h.named('setForegroundColorRgb'), isEmpty);
expect(f.h.named('setCursorBold').length, 1);
});
test('extended color followed by more SGR params keeps positions', () {
final f = _newParser();
f.parser.write('\x1b[1;38;2;10;20;30;4m');
expect(f.h.named('setCursorBold').length, 1);
expect(f.h.named('setForegroundColorRgb').first.args, [10, 20, 30]);
expect(f.h.named('setCursorUnderline').length, 1);
});
});
group('EscapeParser — OSC sequences', () {