Something went wrong. Try again.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100//! VT-series parser for escape and control sequences.//!//! This is implemented directly as the state machine described on//! vt100.net: https://vt100.net/emu/dec_ansi_parserconst Parser = @This();
const std = @import("std");const testing = std.testing;const table = @import("parse_table.zig").table;const osc = @import("osc.zig");
const log = std.log.scoped(.parser);
/// States for the state machinepub const State = enum { ground, escape, escape_intermediate, csi_entry, csi_intermediate, csi_param, csi_ignore, dcs_entry, dcs_param, dcs_intermediate, dcs_passthrough, dcs_ignore, osc_string, sos_pm_apc_string,};
/// Transition action is an action that can be taken during a state/// transition. This is more of an internal action, not one used by/// end users, typically.pub const TransitionAction = enum { none, ignore, print, execute, collect, param, esc_dispatch, csi_dispatch, put, osc_put, apc_put,};
/// Action is the action that a caller of the parser is expected to/// take as a result of some input character.pub const Action = union(enum) { pub const Tag = std.meta.FieldEnum(Action);
/// Draw character to the screen. This is a unicode codepoint. print: u21,
/// Execute the C0 or C1 function. execute: u8,
/// Execute the CSI command. Note that pointers within this /// structure are only valid until the next call to "next". csi_dispatch: CSI,
/// Execute the ESC command. esc_dispatch: ESC,
/// Execute the OSC command. osc_dispatch: osc.Command,
/// DCS-related events. dcs_hook: DCS, dcs_put: u8, dcs_unhook: void,
/// APC data apc_start: void, apc_put: u8, apc_end: void,
pub const CSI = struct { intermediates: []u8, params: []u16, params_sep: SepList, final: u8,
/// The list of separators used for CSI params. The value of the /// bit can be mapped to Sep. The index of this bit set specifies /// the separator AFTER that param. For example: 0;4:3 would have /// index 1 set. pub const SepList = std.StaticBitSet(MAX_PARAMS);
/// The separator used for CSI params. pub const Sep = enum(u1) { semicolon = 0, colon = 1 };
// Implement formatter for logging pub fn format( self: CSI, writer: *std.Io.Writer, ) !void { try writer.print("ESC [ {s} {any} {c}", .{ self.intermediates, self.params, self.final, }); } };
pub const ESC = struct { intermediates: []u8, final: u8,
// Implement formatter for logging pub fn format( self: ESC, writer: *std.Io.Writer, ) !void { try writer.print("ESC {s} {c}", .{ self.intermediates, self.final, }); } };
pub const DCS = struct { intermediates: []const u8 = "", params: []const u16 = &.{}, final: u8,
pub const C = extern struct { intermediates: [*]const u8, intermediates_len: usize, params: [*]const u16, params_len: usize, final: u8, }; };
// Implement formatter for logging. This is mostly copied from the // std.fmt implementation, but we modify it slightly so that we can // print out custom formats for some of our primitives. pub fn format( self: Action, writer: *std.Io.Writer, ) !void { const T = Action; const info = @typeInfo(T).@"union";
try writer.writeAll(@typeName(T)); if (info.tag_type) |TagType| { try writer.writeAll("{ ."); try writer.writeAll(@tagName(@as(TagType, self))); try writer.writeAll(" = ");
inline for (info.fields) |u_field| { // If this is the active field... if (self == @field(TagType, u_field.name)) { const value = @field(self, u_field.name); switch (@TypeOf(value)) { // Unicode u21 => try writer.print("'{u}' (U+{X})", .{ value, value }),
// Byte u8 => try writer.print("0x{x}", .{value}),
// Note: we don't do ASCII (u8) because there are a lot // of invisible characters we don't want to handle right // now.
// All others do the default behavior else => try writer.printValue( "any", .{}, @field(self, u_field.name), 3, ), } } }
try writer.writeAll(" }"); } else { try format(writer, "@{x}", .{@intFromPtr(&self)}); } }};
/// Maximum number of intermediate characters during parsing. This is/// 4 because we also use the intermediates array for UTF8 decoding which/// can be at most 4 bytes.pub const MAX_INTERMEDIATE = 4;
/// Maximum number of CSI parameters. This is arbitrary. Practically, the/// only CSI command that uses more than 3 parameters is the SGR command/// which can be infinitely long. 24 is a reasonable limit based on empirical/// data. This used to be 16 but Kakoune has a SGR command that uses 17/// parameters.////// We could in the future make this the static limit and then allocate after/// but that's a lot more work and practically its so rare to exceed this/// number. I implore TUI authors to not use more than this number of CSI/// params, but I suspect we'll introduce a slow path with heap allocation/// one day.pub const MAX_PARAMS = 24;
/// Current state of the state machinestate: State,
/// Intermediate tracking.intermediates: [MAX_INTERMEDIATE]u8,intermediates_idx: u8,
/// Param tracking, buildingparams: [MAX_PARAMS]u16,params_sep: Action.CSI.SepList,params_idx: u8,param_acc: u16,param_acc_idx: u8,
/// Parser for OSC sequencesosc_parser: osc.Parser,
pub fn init() Parser { var result: Parser = .{ .state = .ground, .intermediates_idx = 0, .params_sep = .initEmpty(), .params_idx = 0, .param_acc = 0, .param_acc_idx = 0, .osc_parser = .init(null),
.intermediates = undefined, .params = undefined, }; if (std.valgrind.runningOnValgrind() > 0) { // Initialize our undefined fields so Valgrind can catch it. // https://github.com/ziglang/zig/issues/19148 result.intermediates = undefined; result.params = undefined; } return result;}
pub fn deinit(self: *Parser) void { self.osc_parser.deinit();}
/// Next consumes the next character c and returns the actions to execute./// Up to 3 actions may need to be executed -- in order -- representing/// the state exit, transition, and entry actions.pub fn next(self: *Parser, c: u8) [3]?Action { const effect = table[c][@intFromEnum(self.state)];
// log.info("next: {x}", .{c});
const next_state = effect.state; const action = effect.action;
// After generating the actions, we set our next state. defer self.state = next_state;
// When going from one state to another, the actions take place in this order: // // 1. exit action from old state // 2. transition action // 3. entry action to new state return [3]?Action{ // Exit depends on current state if (self.state == next_state) null else switch (self.state) { .osc_string => if (self.osc_parser.end(c)) |cmd| Action{ .osc_dispatch = cmd.* } else null, .dcs_passthrough => Action{ .dcs_unhook = {} }, .sos_pm_apc_string => Action{ .apc_end = {} }, else => null, },
self.doAction(action, c),
// Entry depends on new state if (self.state == next_state) null else switch (next_state) { .escape, .dcs_entry, .csi_entry => clear: { self.clear(); break :clear null; }, .osc_string => osc_string: { self.osc_parser.reset(); break :osc_string null; }, .dcs_passthrough => dcs_hook: { // Ignore too many parameters if (self.params_idx >= MAX_PARAMS) break :dcs_hook null; // Finalize parameters if (self.param_acc_idx > 0) { self.params[self.params_idx] = self.param_acc; self.params_idx += 1; } break :dcs_hook .{ .dcs_hook = .{ .intermediates = self.intermediates[0..self.intermediates_idx], .params = self.params[0..self.params_idx], .final = c, }, }; }, .sos_pm_apc_string => Action{ .apc_start = {} }, else => null, }, };}
pub inline fn collect(self: *Parser, c: u8) void { if (self.intermediates_idx >= MAX_INTERMEDIATE) { @branchHint(.cold); log.warn("invalid intermediates count", .{}); return; }
self.intermediates[self.intermediates_idx] = c; self.intermediates_idx += 1;}
inline fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { return switch (action) { .none, .ignore => null, .print => Action{ .print = c }, .execute => Action{ .execute = c }, .collect => collect: { self.collect(c); break :collect null; }, .param => param: { // Semicolon separates parameters. If we encounter a semicolon // we need to store and move on to the next parameter. if (c == ';' or c == ':') { // Ignore too many parameters if (self.params_idx >= MAX_PARAMS) break :param null;
// Set param final value self.params[self.params_idx] = self.param_acc; if (c == ':') self.params_sep.set(self.params_idx); self.params_idx += 1;
// Reset current param value to 0 self.param_acc = 0; self.param_acc_idx = 0; break :param null; }
// A numeric value. Add it to our accumulator. self.param_acc *|= 10; self.param_acc +|= c - '0';
// Increment our accumulator index. If we overflow then // we're out of bounds and we exit immediately. self.param_acc_idx, const overflow = @addWithOverflow(self.param_acc_idx, 1); if (overflow > 0) break :param null;
// The client is expected to perform no action. break :param null; }, .osc_put => osc_put: { @call(.always_inline, osc.Parser.next, .{ &self.osc_parser, c }); break :osc_put null; }, .csi_dispatch => csi_dispatch: { // Ignore too many parameters if (self.params_idx >= MAX_PARAMS) break :csi_dispatch null;
// Finalize parameters if we have one if (self.param_acc_idx > 0) { self.params[self.params_idx] = self.param_acc; self.params_idx += 1; }
const result: Action = .{ .csi_dispatch = .{ .intermediates = self.intermediates[0..self.intermediates_idx], .params = self.params[0..self.params_idx], .params_sep = self.params_sep, .final = c, }, };
// We only allow colon or mixed separators for the 'm' command. if (c != 'm' and self.params_sep.count() > 0) { @branchHint(.cold); log.warn( "CSI colon or mixed separators only allowed for 'm' command, got: {f}", .{result}, ); break :csi_dispatch null; }
break :csi_dispatch result; }, .esc_dispatch => Action{ .esc_dispatch = .{ .intermediates = self.intermediates[0..self.intermediates_idx], .final = c, }, }, .put => Action{ .dcs_put = c }, .apc_put => Action{ .apc_put = c }, };}
pub inline fn clear(self: *Parser) void { self.intermediates_idx = 0; self.params_idx = 0; self.params_sep = .initEmpty(); self.param_acc = 0; self.param_acc_idx = 0;}
test { var p = init(); _ = p.next(0x9E); try testing.expect(p.state == .sos_pm_apc_string); _ = p.next(0x9C); try testing.expect(p.state == .ground);
{ const a = p.next('a'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .print); try testing.expect(a[2] == null); }
{ const a = p.next(0x19); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .execute); try testing.expect(a[2] == null); }}
test "esc: ESC ( B" { var p = init(); _ = p.next(0x1B); _ = p.next('(');
{ const a = p.next('B'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .esc_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.esc_dispatch; try testing.expect(d.final == 'B'); try testing.expect(d.intermediates.len == 1); try testing.expect(d.intermediates[0] == '('); }}
test "csi: ESC [ H" { var p = init(); _ = p.next(0x1B); _ = p.next(0x5B);
{ const a = p.next(0x48); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 0x48); try testing.expect(d.params.len == 0); }}
test "csi: ESC [ 1 ; 4 H" { var p = init(); _ = p.next(0x1B); _ = p.next(0x5B); _ = p.next(0x31); // 1 _ = p.next(0x3B); // ; _ = p.next(0x34); // 4
{ const a = p.next(0x48); // H try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'H'); try testing.expect(d.params.len == 2); try testing.expectEqual(@as(u16, 1), d.params[0]); try testing.expectEqual(@as(u16, 4), d.params[1]); }}
test "csi: SGR ESC [ 38 : 2 m" { var p = init(); _ = p.next(0x1B); _ = p.next('['); _ = p.next('3'); _ = p.next('8'); _ = p.next(':'); _ = p.next('2');
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'm'); try testing.expect(d.params.len == 2); try testing.expectEqual(@as(u16, 38), d.params[0]); try testing.expect(d.params_sep.isSet(0)); try testing.expectEqual(@as(u16, 2), d.params[1]); try testing.expect(!d.params_sep.isSet(1)); }}
test "csi: SGR colon followed by semicolon" { var p = init(); _ = p.next(0x1B); for ("[48:2") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null); }
_ = p.next(0x1B); _ = p.next('['); { const a = p.next('H'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null); }}
test "csi: SGR mixed colon and semicolon" { var p = init(); _ = p.next(0x1B); for ("[38:5:1;48:5:0") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null); }}
test "csi: SGR ESC [ 48 : 2 m" { var p = init(); _ = p.next(0x1B); for ("[48:2:240:143:104") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'm'); try testing.expect(d.params.len == 5); try testing.expectEqual(@as(u16, 48), d.params[0]); try testing.expect(d.params_sep.isSet(0)); try testing.expectEqual(@as(u16, 2), d.params[1]); try testing.expect(d.params_sep.isSet(1)); try testing.expectEqual(@as(u16, 240), d.params[2]); try testing.expect(d.params_sep.isSet(2)); try testing.expectEqual(@as(u16, 143), d.params[3]); try testing.expect(d.params_sep.isSet(3)); try testing.expectEqual(@as(u16, 104), d.params[4]); try testing.expect(!d.params_sep.isSet(4)); }}
test "csi: SGR ESC [4:3m colon" { var p = init(); _ = p.next(0x1B); _ = p.next('['); _ = p.next('4'); _ = p.next(':'); _ = p.next('3');
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'm'); try testing.expect(d.params.len == 2); try testing.expectEqual(@as(u16, 4), d.params[0]); try testing.expect(d.params_sep.isSet(0)); try testing.expectEqual(@as(u16, 3), d.params[1]); try testing.expect(!d.params_sep.isSet(1)); }}
test "csi: SGR with many blank and colon" { var p = init(); _ = p.next(0x1B); for ("[58:2::240:143:104") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'm'); try testing.expect(d.params.len == 6); try testing.expectEqual(@as(u16, 58), d.params[0]); try testing.expect(d.params_sep.isSet(0)); try testing.expectEqual(@as(u16, 2), d.params[1]); try testing.expect(d.params_sep.isSet(1)); try testing.expectEqual(@as(u16, 0), d.params[2]); try testing.expect(d.params_sep.isSet(2)); try testing.expectEqual(@as(u16, 240), d.params[3]); try testing.expect(d.params_sep.isSet(3)); try testing.expectEqual(@as(u16, 143), d.params[4]); try testing.expect(d.params_sep.isSet(4)); try testing.expectEqual(@as(u16, 104), d.params[5]); try testing.expect(!d.params_sep.isSet(5)); }}
// This is from a Kakoune actual SGR sequence.test "csi: SGR mixed colon and semicolon with blank" { var p = init(); _ = p.next(0x1B); for ("[;4:3;38;2;175;175;215;58:2::190:80:70") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'm'); try testing.expectEqual(14, d.params.len); try testing.expectEqual(@as(u16, 0), d.params[0]); try testing.expect(!d.params_sep.isSet(0)); try testing.expectEqual(@as(u16, 4), d.params[1]); try testing.expect(d.params_sep.isSet(1)); try testing.expectEqual(@as(u16, 3), d.params[2]); try testing.expect(!d.params_sep.isSet(2)); try testing.expectEqual(@as(u16, 38), d.params[3]); try testing.expect(!d.params_sep.isSet(3)); try testing.expectEqual(@as(u16, 2), d.params[4]); try testing.expect(!d.params_sep.isSet(4)); try testing.expectEqual(@as(u16, 175), d.params[5]); try testing.expect(!d.params_sep.isSet(5)); try testing.expectEqual(@as(u16, 175), d.params[6]); try testing.expect(!d.params_sep.isSet(6)); try testing.expectEqual(@as(u16, 215), d.params[7]); try testing.expect(!d.params_sep.isSet(7)); try testing.expectEqual(@as(u16, 58), d.params[8]); try testing.expect(d.params_sep.isSet(8)); try testing.expectEqual(@as(u16, 2), d.params[9]); try testing.expect(d.params_sep.isSet(9)); try testing.expectEqual(@as(u16, 0), d.params[10]); try testing.expect(d.params_sep.isSet(10)); try testing.expectEqual(@as(u16, 190), d.params[11]); try testing.expect(d.params_sep.isSet(11)); try testing.expectEqual(@as(u16, 80), d.params[12]); try testing.expect(d.params_sep.isSet(12)); try testing.expectEqual(@as(u16, 70), d.params[13]); try testing.expect(!d.params_sep.isSet(13)); }}
// This is from a Kakoune actual SGR sequence also.test "csi: SGR mixed colon and semicolon setting underline, bg, fg" { var p = init(); _ = p.next(0x1B); for ("[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('m'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'm'); try testing.expectEqual(17, d.params.len); try testing.expectEqual(@as(u16, 4), d.params[0]); try testing.expect(d.params_sep.isSet(0)); try testing.expectEqual(@as(u16, 3), d.params[1]); try testing.expect(!d.params_sep.isSet(1)); try testing.expectEqual(@as(u16, 38), d.params[2]); try testing.expect(!d.params_sep.isSet(2)); try testing.expectEqual(@as(u16, 2), d.params[3]); try testing.expect(!d.params_sep.isSet(3)); try testing.expectEqual(@as(u16, 51), d.params[4]); try testing.expect(!d.params_sep.isSet(4)); try testing.expectEqual(@as(u16, 51), d.params[5]); try testing.expect(!d.params_sep.isSet(5)); try testing.expectEqual(@as(u16, 51), d.params[6]); try testing.expect(!d.params_sep.isSet(6)); try testing.expectEqual(@as(u16, 48), d.params[7]); try testing.expect(!d.params_sep.isSet(7)); try testing.expectEqual(@as(u16, 2), d.params[8]); try testing.expect(!d.params_sep.isSet(8)); try testing.expectEqual(@as(u16, 170), d.params[9]); try testing.expect(!d.params_sep.isSet(9)); try testing.expectEqual(@as(u16, 170), d.params[10]); try testing.expect(!d.params_sep.isSet(10)); try testing.expectEqual(@as(u16, 170), d.params[11]); try testing.expect(!d.params_sep.isSet(11)); try testing.expectEqual(@as(u16, 58), d.params[12]); try testing.expect(!d.params_sep.isSet(12)); try testing.expectEqual(@as(u16, 2), d.params[13]); try testing.expect(!d.params_sep.isSet(13)); try testing.expectEqual(@as(u16, 255), d.params[14]); try testing.expect(!d.params_sep.isSet(14)); try testing.expectEqual(@as(u16, 97), d.params[15]); try testing.expect(!d.params_sep.isSet(15)); try testing.expectEqual(@as(u16, 136), d.params[16]); try testing.expect(!d.params_sep.isSet(16)); }}
test "csi: colon for non-m final" { var p = init(); _ = p.next(0x1B); for ("[38:2h") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
try testing.expect(p.state == .ground);}
test "csi: request mode decrqm" { var p = init(); _ = p.next(0x1B); for ("[?2026$") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('p'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'p'); try testing.expectEqual(@as(usize, 2), d.intermediates.len); try testing.expectEqual(@as(usize, 1), d.params.len); try testing.expectEqual(@as(u16, '?'), d.intermediates[0]); try testing.expectEqual(@as(u16, '$'), d.intermediates[1]); try testing.expectEqual(@as(u16, 2026), d.params[0]); }}
test "csi: change cursor" { var p = init(); _ = p.next(0x1B); for ("[3 ") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('q'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch; try testing.expect(d.final == 'q'); try testing.expectEqual(@as(usize, 1), d.intermediates.len); try testing.expectEqual(@as(usize, 1), d.params.len); try testing.expectEqual(@as(u16, ' '), d.intermediates[0]); try testing.expectEqual(@as(u16, 3), d.params[0]); }}
test "osc: change window title" { var p = init(); _ = p.next(0x1B); _ = p.next(']'); _ = p.next('0'); _ = p.next(';'); _ = p.next('a'); _ = p.next('b'); _ = p.next('c');
{ const a = p.next(0x07); // BEL try testing.expect(p.state == .ground); try testing.expect(a[0].? == .osc_dispatch); try testing.expect(a[1] == null); try testing.expect(a[2] == null);
const cmd = a[0].?.osc_dispatch; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("abc", cmd.change_window_title); }}
test "osc: change window title (end in esc)" { var p = init(); _ = p.next(0x1B); _ = p.next(']'); _ = p.next('0'); _ = p.next(';'); _ = p.next('a'); _ = p.next('b'); _ = p.next('c');
{ const a = p.next(0x1B); _ = p.next('\\'); try testing.expect(p.state == .ground); try testing.expect(a[0].? == .osc_dispatch); try testing.expect(a[1] == null); try testing.expect(a[2] == null);
const cmd = a[0].?.osc_dispatch; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("abc", cmd.change_window_title); }}
// https://github.com/darrenstarr/VtNetCore/pull/14// Saw this on HN, decided to add a test case because why not.test "osc: 112 incomplete sequence" { var p: Parser = init(); defer p.deinit(); p.osc_parser.alloc = std.testing.allocator;
_ = p.next(0x1B); _ = p.next(']'); _ = p.next('1'); _ = p.next('1'); _ = p.next('2');
{ const a = p.next(0x07); try testing.expect(p.state == .ground); try testing.expect(a[0].? == .osc_dispatch); try testing.expect(a[1] == null); try testing.expect(a[2] == null);
const cmd = a[0].?.osc_dispatch; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.op == .osc_112); try testing.expect(cmd.color_operation.requests.count() == 1); var it = cmd.color_operation.requests.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( osc.color.Request{ .reset = .{ .dynamic = .cursor } }, op.*, ); } try std.testing.expect(it.next() == null); }}
test "osc: 104 empty" { var p: Parser = init(); defer p.deinit(); p.osc_parser.alloc = std.testing.allocator;
_ = p.next(0x1B); _ = p.next(']'); _ = p.next('1'); _ = p.next('0'); _ = p.next('4');
{ const a = p.next(0x07); try testing.expect(p.state == .ground); try testing.expect(a[0].? == .osc_dispatch); try testing.expect(a[1] == null); try testing.expect(a[2] == null);
const cmd = a[0].?.osc_dispatch; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.op == .osc_104); try testing.expect(cmd.color_operation.requests.count() == 1); var it = cmd.color_operation.requests.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset_palette); } try std.testing.expect(it.next() == null); }}
test "csi: too many params" { var p = init(); _ = p.next(0x1B); _ = p.next('['); for (0..100) |_| { _ = p.next('1'); _ = p.next(';'); } _ = p.next('1');
{ const a = p.next('C'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }}
test "csi: sgr with up to our max parameters" { for (1..MAX_PARAMS + 1) |max| { var p = init(); _ = p.next(0x1B); _ = p.next('[');
for (0..max - 1) |_| { _ = p.next('1'); _ = p.next(';'); } _ = p.next('2');
{ const a = p.next('H'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1].? == .csi_dispatch); try testing.expect(a[2] == null);
const csi = a[1].?.csi_dispatch; try testing.expectEqual(@as(usize, max), csi.params.len); try testing.expectEqual(@as(u16, 2), csi.params[max - 1]); } }}
test "csi: sgr beyond our max drops it" { // Has to be +2 for the loops below const max = MAX_PARAMS + 2;
var p = init(); _ = p.next(0x1B); _ = p.next('[');
for (0..max - 1) |_| { _ = p.next('1'); _ = p.next(';'); } _ = p.next('2');
{ const a = p.next('H'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }}
test "dcs: XTGETTCAP" { var p = init(); _ = p.next(0x1B); for ("P+") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('q'); try testing.expect(p.state == .dcs_passthrough); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2].? == .dcs_hook);
const hook = a[2].?.dcs_hook; try testing.expectEqualSlices(u8, &[_]u8{'+'}, hook.intermediates); try testing.expectEqualSlices(u16, &[_]u16{}, hook.params); try testing.expectEqual('q', hook.final); }}
test "dcs: params" { var p = init(); _ = p.next(0x1B); for ("P1000") |c| { const a = p.next(c); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null); }
{ const a = p.next('p'); try testing.expect(p.state == .dcs_passthrough); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2].? == .dcs_hook);
const hook = a[2].?.dcs_hook; try testing.expectEqualSlices(u16, &[_]u16{1000}, hook.params); try testing.expectEqual('p', hook.final); }}
test "dcs: too many params" { // Regression test for a crash found by fuzzing (afl). When a DCS // sequence has more than MAX_PARAMS parameters and param_acc_idx > 0, // entering dcs_passthrough wrote to params[params_idx] without a // bounds check, causing an out-of-bounds access. var p = init(); _ = p.next(0x1B); // ESC _ = p.next('P'); // DCS entry
// Feed a digit then MAX_PARAMS semicolons to fill all param slots. _ = p.next('6'); for (0..MAX_PARAMS) |_| { _ = p.next(';'); } // Feed another digit so param_acc_idx > 0 while params_idx == MAX_PARAMS. _ = p.next('7');
// A final byte triggers entry to dcs_passthrough. The DCS should // be dropped entirely, consistent with how CSI handles overflow. const a = p.next('p'); try testing.expect(a[0] == null); try testing.expect(a[1] == null); try testing.expect(a[2] == null);}