diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 0f1d63cc0d..135cc393e2 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -6,6 +6,7 @@ import { ParserState, IDcsHandler, IParsingState } from './Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from './EscapeSequenceParser'; import * as chai from 'chai'; +import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; function r(a: number, b: number): string[] { let c = b - a; @@ -50,8 +51,12 @@ const testTerminal: any = { compare: function (value: any): void { chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here }, - print: function (data: string, start: number, end: number): void { - this.calls.push(['print', data.substring(start, end)]); + print: function (data: Uint32Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += stringFromCodePoint(data[i]); + } + this.calls.push(['print', s]); }, actionOSC: function (s: string): void { this.calls.push(['osc', s]); @@ -68,8 +73,12 @@ const testTerminal: any = { actionDCSHook: function (collect: string, params: number[], flag: string): void { this.calls.push(['dcs hook', collect, params, flag]); }, - actionDCSPrint: function (data: string, start: number, end: number): void { - this.calls.push(['dcs put', data.substring(start, end)]); + actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += stringFromCodePoint(data[i]); + } + this.calls.push(['dcs put', s]); }, actionDCSUnhook: function (): void { this.calls.push(['dcs unhook']); @@ -81,7 +90,7 @@ class DcsTest implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); } - put(data: string, start: number, end: number): void { + put(data: Uint32Array, start: number, end: number): void { testTerminal.actionDCSPrint(data, start, end); } unhook(): void { @@ -155,6 +164,12 @@ interface IRun { parser: TestEscapeSequenceParser; } +// translate string based parse calls into typed array based +function parse(parser: TestEscapeSequenceParser, data: string): void { + const container = new Uint32Array(data.length); + const decoder = new StringToUtf32(); + parser.parse(container, decoder.decode(data, container)); +} describe('EscapeSequenceParser', function (): void { let parser: TestEscapeSequenceParser | null = null; @@ -205,12 +220,12 @@ describe('EscapeSequenceParser', function (): void { it('state GROUND execute action', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.GROUND; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -223,7 +238,7 @@ describe('EscapeSequenceParser', function (): void { const printables = r(0x20, 0x7f); // NOTE: DEL excluded for (let i = 0; i < printables.length; ++i) { parser.currentState = ParserState.GROUND; - parser.parse(printables[i]); + parse(parser, printables[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['print', printables[i]]]); parser.reset(); @@ -245,13 +260,13 @@ describe('EscapeSequenceParser', function (): void { for (state in states) { for (let i = 0; i < exes.length; ++i) { parser.currentState = state; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare((state in exceptions ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } - parser.parse('\x9c'); + parse(parser, '\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -265,7 +280,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [23]; parser.collect = '#'; - parser.parse('\x1b'); + parse(parser, '\x1b'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -276,12 +291,12 @@ describe('EscapeSequenceParser', function (): void { it('state ESCAPE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.ESCAPE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -292,7 +307,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); testTerminal.compare([]); parser.reset(); @@ -301,13 +316,13 @@ describe('EscapeSequenceParser', function (): void { it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { parser.reset(); testTerminal.clear(); - const dispatches = r(0x30, 0x50); - dispatches.concat(r(0x51, 0x58)); - dispatches.concat(['\x59', '\x5a', '\x5c']); - dispatches.concat(r(0x60, 0x7f)); + let dispatches = r(0x30, 0x50); + dispatches = dispatches.concat(r(0x51, 0x58)); + dispatches = dispatches.concat(['\x59', '\x5a']); // excluded \x5c + dispatches = dispatches.concat(r(0x60, 0x7f)); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.ESCAPE; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['esc', '', dispatches[i]]]); parser.reset(); @@ -319,7 +334,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -328,12 +343,12 @@ describe('EscapeSequenceParser', function (): void { it('state ESCAPE_INTERMEDIATE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -344,7 +359,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([]); parser.reset(); @@ -355,7 +370,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -367,7 +382,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x30, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); // '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]); @@ -382,7 +397,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [123]; parser.collect = '#'; - parser.parse('['); + parse(parser, '['); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -394,7 +409,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [123]; parser.collect = '#'; - parser.parse('\x9b'); + parse(parser, '\x9b'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -405,12 +420,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_ENTRY execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -421,7 +436,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); testTerminal.compare([]); parser.reset(); @@ -432,7 +447,7 @@ describe('EscapeSequenceParser', function (): void { const dispatches = r(0x40, 0x7f); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0], dispatches[i]]]); parser.reset(); @@ -445,19 +460,19 @@ describe('EscapeSequenceParser', function (): void { const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -466,12 +481,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_PARAM execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -483,13 +498,13 @@ describe('EscapeSequenceParser', function (): void { const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -498,7 +513,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); testTerminal.compare([]); parser.reset(); @@ -510,7 +525,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); @@ -522,7 +537,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -533,7 +548,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -542,12 +557,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_INTERMEDIATE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -559,7 +574,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -569,7 +584,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); testTerminal.compare([]); parser.reset(); @@ -581,7 +596,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); @@ -591,7 +606,7 @@ describe('EscapeSequenceParser', function (): void { it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { parser.reset(); parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x3a'); + parse(parser, '\x3a'); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); parser.reset(); }); @@ -600,7 +615,7 @@ describe('EscapeSequenceParser', function (): void { const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x3b' + chars[i]); + parse(parser, '\x3b' + chars[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -611,7 +626,7 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(chars[i]); + parse(parser, chars[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); chai.expect(parser.params).eql([0]); parser.reset(); @@ -620,12 +635,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_IGNORE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -635,11 +650,11 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_IGNORE ignore', function (): void { parser.reset(); testTerminal.clear(); - const ignored = r(0x20, 0x40); - ignored.concat(['\x7f']); + let ignored = r(0x20, 0x40); + ignored = ignored.concat(['\x7f']); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); @@ -652,7 +667,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -664,7 +679,7 @@ describe('EscapeSequenceParser', function (): void { // C0 let initializers = ['\x58', '\x5e', '\x5f']; for (let i = 0; i < initializers.length; ++i) { - parser.parse('\x1b' + initializers[i]); + parse(parser, '\x1b' + initializers[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -673,7 +688,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = state; initializers = ['\x98', '\x9e', '\x9f']; for (let i = 0; i < initializers.length; ++i) { - parser.parse(initializers[i]); + parse(parser, initializers[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -681,13 +696,13 @@ describe('EscapeSequenceParser', function (): void { }); it('state SOS_PM_APC_STRING ignore rules', function (): void { parser.reset(); - const ignored = r(0x00, 0x18); - ignored.concat(['\x19']); - ignored.concat(r(0x1c, 0x20)); - ignored.concat(r(0x20, 0x80)); + let ignored = r(0x00, 0x18); + ignored = ignored.concat(['\x19']); + ignored = ignored.concat(r(0x1c, 0x20)); + ignored = ignored.concat(r(0x20, 0x80)); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.SOS_PM_APC_STRING; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -695,13 +710,13 @@ describe('EscapeSequenceParser', function (): void { it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { parser.reset(); // C0 - parser.parse('\x1b]'); + parse(parser, '\x1b]'); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.parse('\x9d'); + parse(parser, '\x9d'); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); parser.reset(); } @@ -714,7 +729,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.OSC_STRING; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); chai.expect(parser.osc).equal(''); parser.reset(); @@ -725,7 +740,7 @@ describe('EscapeSequenceParser', function (): void { const puts = r(0x20, 0x80); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.OSC_STRING; - parser.parse(puts[i]); + parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); chai.expect(parser.osc).equal(puts[i]); parser.reset(); @@ -734,13 +749,13 @@ describe('EscapeSequenceParser', function (): void { it('state DCS_ENTRY', function (): void { parser.reset(); // C0 - parser.parse('\x1bP'); + parse(parser, '\x1bP'); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.parse('\x90'); + parse(parser, '\x90'); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); } @@ -753,7 +768,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); } @@ -764,19 +779,19 @@ describe('EscapeSequenceParser', function (): void { const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.DCS_ENTRY; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -790,7 +805,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); parser.reset(); } @@ -800,13 +815,13 @@ describe('EscapeSequenceParser', function (): void { const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.DCS_PARAM; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -814,7 +829,7 @@ describe('EscapeSequenceParser', function (): void { it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { parser.reset(); parser.currentState = ParserState.DCS_ENTRY; - parser.parse('\x3a'); + parse(parser, '\x3a'); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); }); @@ -823,7 +838,7 @@ describe('EscapeSequenceParser', function (): void { const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse('\x3b' + chars[i]); + parse(parser, '\x3b' + chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -834,21 +849,21 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(chars[i]); + parse(parser, chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); } }); it('state DCS_IGNORE ignore rules', function (): void { parser.reset(); - const ignored = [ + let ignored = [ '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - ignored.concat(r(0x20, 0x80)); + ignored = ignored.concat(r(0x20, 0x80)); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_IGNORE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); } @@ -858,7 +873,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -869,7 +884,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -883,7 +898,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); parser.reset(); } @@ -893,7 +908,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -904,7 +919,7 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse('\x20' + chars[i]); + parse(parser, '\x20' + chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); chai.expect(parser.collect).equal('\x20'); parser.reset(); @@ -916,7 +931,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -929,7 +944,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -942,7 +957,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -952,14 +967,14 @@ describe('EscapeSequenceParser', function (): void { it('state DCS_PASSTHROUGH put action', function (): void { parser.reset(); testTerminal.clear(); - const puts = r(0x00, 0x18); - puts.concat(['\x19']); - puts.concat(r(0x1c, 0x20)); - puts.concat(r(0x20, 0x7f)); + let puts = r(0x00, 0x18); + puts = puts.concat(['\x19']); + puts = puts.concat(r(0x1c, 0x20)); + puts = puts.concat(r(0x20, 0x7f)); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.DCS_PASSTHROUGH; parser.mockActiveDcsHandler(); - parser.parse(puts[i]); + parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs put', puts[i]]]); parser.reset(); @@ -970,7 +985,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([]); parser.reset(); @@ -989,7 +1004,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); } - parser.parse(s); + parse(parser, s); testTerminal.compare(value); }; }); @@ -1067,7 +1082,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_IGNORE; - parser.parse('€öäü'); + parse(parser, '€öäü'); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); @@ -1077,7 +1092,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_IGNORE; - parser.parse('€öäü'); + parse(parser, '€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); testTerminal.compare([]); parser.reset(); @@ -1087,7 +1102,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.parse('\x901;2;3+$a€öäü'); + parse(parser, '\x901;2;3+$a€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); parser.reset(); @@ -1097,7 +1112,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.GROUND; - parser.parse('\x9c'); + parse(parser, '\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -1127,15 +1142,17 @@ describe('EscapeSequenceParser', function (): void { clearAccu(); }); it('print handler', function (): void { - parser2.setPrintHandler(function (data: string, start: number, end: number): void { - print += data.substring(start, end); + parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { + for (let i = start; i < end; ++i) { + print += stringFromCodePoint(data[i]); + } }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(print).equal('hello world!$>'); parser2.clearPrintHandler(); parser2.clearPrintHandler(); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(print).equal(''); }); it('ESC handler', function (): void { @@ -1145,28 +1162,28 @@ describe('EscapeSequenceParser', function (): void { parser2.setEscHandler('E', function (): void { esc.push('E'); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql(['%G', 'E']); parser2.clearEscHandler('%G'); parser2.clearEscHandler('%G'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql(['E']); parser2.clearEscHandler('E'); clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql([]); }); it('CSI handler', function (): void { parser2.setCsiHandler('m', function (params: number[], collect: string): void { csi.push(['m', params, collect]); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); parser2.clearCsiHandler('m'); parser2.clearCsiHandler('m'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([]); }); describe('CSI custom handlers', () => { @@ -1174,7 +1191,7 @@ describe('EscapeSequenceParser', function (): void { const csiCustom: [string, number[], string][] = []; parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); @@ -1182,7 +1199,7 @@ describe('EscapeSequenceParser', function (): void { const csiCustom: [string, number[], string][] = []; parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); @@ -1192,7 +1209,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1203,7 +1220,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([], 'Should not fallback once'); chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1213,7 +1230,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', () => order.push(1)); parser2.addCsiHandler('m', () => { order.push(2); return false; }); parser2.addCsiHandler('m', () => { order.push(3); return false; }); - parser2.parse('\x1b[0m'); + parse(parser2, '\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { @@ -1221,7 +1238,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1231,7 +1248,7 @@ describe('EscapeSequenceParser', function (): void { const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); customHandler.dispose(); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1243,24 +1260,24 @@ describe('EscapeSequenceParser', function (): void { parser2.setExecuteHandler('\r', function (): void { exe.push('\r'); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(exe).eql(['\r', '\n']); parser2.clearExecuteHandler('\r'); parser2.clearExecuteHandler('\r'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(exe).eql(['\n']); }); it('OSC handler', function (): void { parser2.setOscHandler(1, function (data: string): void { osc.push([1, data]); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); parser2.clearOscHandler(1); parser2.clearOscHandler(1); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([]); }); describe('OSC custom handlers', () => { @@ -1268,7 +1285,7 @@ describe('EscapeSequenceParser', function (): void { const oscCustom: [number, string][] = []; parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); @@ -1276,7 +1293,7 @@ describe('EscapeSequenceParser', function (): void { const oscCustom: [number, string][] = []; parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); @@ -1286,7 +1303,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); chai.expect(oscCustom2).eql([[1, 'foo=bar']]); @@ -1297,7 +1314,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([], 'Should not fallback once'); chai.expect(oscCustom2).eql([[1, 'foo=bar']]); @@ -1307,7 +1324,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, () => order.push(1)); parser2.addOscHandler(1, () => { order.push(2); return false; }); parser2.addOscHandler(1, () => { order.push(3); return false; }); - parser2.parse('\x1b]1;foo=bar\x1b\\'); + parse(parser2, '\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { @@ -1315,7 +1332,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, data => osc.push([1, data])); const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1325,7 +1342,7 @@ describe('EscapeSequenceParser', function (): void { const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); customHandler.dispose(); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1335,15 +1352,19 @@ describe('EscapeSequenceParser', function (): void { hook: function (collect: string, params: number[], flag: number): void { dcs.push(['hook', collect, params, flag]); }, - put: function (data: string, start: number, end: number): void { - dcs.push(['put', data.substring(start, end)]); + put: function (data: Uint32Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += stringFromCodePoint(data[i]); + } + dcs.push(['put', s]); }, unhook: function (): void { dcs.push(['unhook']); } }); - parser2.parse('\x1bP1;2;3+pabc'); - parser2.parse(';de\x9c'); + parse(parser2, '\x1bP1;2;3+pabc'); + parse(parser2, ';de\x9c'); chai.expect(dcs).eql([ ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], ['put', 'abc'], ['put', ';de'], @@ -1352,8 +1373,8 @@ describe('EscapeSequenceParser', function (): void { parser2.clearDcsHandler('+p'); parser2.clearDcsHandler('+p'); // should not throw clearAccu(); - parser2.parse('\x1bP1;2;3+pabc'); - parser2.parse(';de\x9c'); + parse(parser2, '\x1bP1;2;3+pabc'); + parse(parser2, ';de\x9c'); chai.expect(dcs).eql([]); }); it('ERROR handler', function (): void { @@ -1362,7 +1383,7 @@ describe('EscapeSequenceParser', function (): void { errorState = state; return state; }); - parser2.parse('\x1b[1;2;€;3m'); // faulty escape sequence + parse(parser2, '\x1b[1;2;€;3m'); // faulty escape sequence chai.expect(errorState).eql({ position: 6, code: '€'.charCodeAt(0), @@ -1377,7 +1398,7 @@ describe('EscapeSequenceParser', function (): void { parser2.clearErrorHandler(); parser2.clearErrorHandler(); // should not throw errorState = null; - parser2.parse('\x1b[1;2;a;3m'); + parse(parser2, '\x1b[1;2;a;3m'); chai.expect(errorState).eql(null); }); }); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 6d3de0e0ed..7b65624d23 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -6,6 +6,7 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +import { utf32ToString } from './core/input/TextDecoder'; interface IHandlerCollection { [key: string]: T[]; @@ -134,6 +135,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); + table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // csi entries table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); @@ -202,7 +204,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { */ class DcsDummy implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { } - put(data: string, start: number, end: number): void { } + put(data: Uint32Array, start: number, end: number): void { } unhook(): void { } } @@ -228,7 +230,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _collect: string; // handler lookup containers - protected _printHandler: (data: string, start: number, end: number) => void; + protected _printHandler: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlers: any; protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; @@ -238,7 +240,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers - protected _printHandlerFb: (data: string, start: number, end: number) => void; + protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlerFb: (code: number) => void; protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; @@ -294,7 +296,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = null; } - setPrintHandler(callback: (data: string, start: number, end: number) => void): void { + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { this._printHandler = callback; } clearPrintHandler(): void { @@ -397,7 +399,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._activeDcsHandler = null; } - parse(data: string): void { + parse(data: Uint32Array, length: number): void { let code = 0; let transition = 0; let error = false; @@ -412,15 +414,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let callback: Function | null = null; // process input string - const l = data.length; - for (let i = 0; i < l; ++i) { - code = data.charCodeAt(i); + for (let i = 0; i < length; ++i) { + code = data[i]; // shortcut for most chars (print action) if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) { print = (~print) ? print : i; do i++; - while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80); + while (i < length && data[i] > 0x1f && data[i] < 0x80); i--; continue; } @@ -563,10 +564,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.OSC_PUT: for (let j = i + 1; ; j++) { - if (j >= l - || (code = data.charCodeAt(j)) < 0x20 + if (j >= length + || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { - osc += data.substring(i, j); + osc += utf32ToString(data, i, j); i = j - 1; break; } @@ -610,9 +611,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // push leftover pushable buffers to terminal if (currentState === ParserState.GROUND && ~print) { - this._printHandler(data, print, data.length); + this._printHandler(data, print, length); } else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) { - dcsHandler.put(data, dcs, data.length); + dcsHandler.put(data, dcs, length); } // save non pushable buffers diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index ac3fbc8b3b..a7963a2c4b 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -337,7 +337,9 @@ describe('InputHandler', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); const inputHandler = new InputHandler(term); - inputHandler.print(String.fromCharCode(0x200B), 0, 1); + const container = new Uint32Array(10); + container[0] = 0x200B; + inputHandler.print(container, 0, 1); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 1b3907fd23..2f53cfcbbe 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,6 +14,8 @@ import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +import { concat } from './common/TypedArrayUtils'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder'; /** * Map collect to glevel. Used in `selectCharset`. @@ -32,21 +34,22 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) */ class DECRQSS implements IDcsHandler { - private _data: string; + private _data: Uint32Array = new Uint32Array(0); constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { - // reset data - this._data = ''; + this._data = new Uint32Array(0); } - put(data: string, start: number, end: number): void { - this._data += data.substring(start, end); + put(data: Uint32Array, start: number, end: number): void { + this._data = concat(this._data, data.subarray(start, end)); } unhook(): void { - switch (this._data) { + const data = utf32ToString(this._data); + this._data = new Uint32Array(0); + switch (data) { // valid: DCS 1 $ r Pt ST (xterm) case '"q': // DECSCA return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`); @@ -66,7 +69,7 @@ class DECRQSS implements IDcsHandler { return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); default: // invalid: DCS 0 $ r Pt ST (xterm) - this._terminal.error('Unknown DCS $q %s', this._data); + this._terminal.error('Unknown DCS $q %s', data); this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`); } } @@ -78,11 +81,17 @@ class DECRQSS implements IDcsHandler { * not supported */ - /** - * DCS + p Pt ST (xterm) - * Set Terminfo Data - * not supported - */ +/** + * DCS + q Pt ST (xterm) + * Request Terminfo String + * not implemented + */ + +/** + * DCS + p Pt ST (xterm) + * Set Terminfo Data + * not supported + */ @@ -94,7 +103,8 @@ class DECRQSS implements IDcsHandler { * each function's header comment. */ export class InputHandler extends Disposable implements IInputHandler { - private _surrogateFirst: string; + private _parseBuffer: Uint32Array = new Uint32Array(4096); + private _stringDecoder: StringToUtf32 = new StringToUtf32(); constructor( protected _terminal: IInputHandlingTerminal, @@ -104,8 +114,6 @@ export class InputHandler extends Disposable implements IInputHandler { this.register(this._parser); - this._surrogateFirst = ''; - /** * custom fallback handlers */ @@ -290,13 +298,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.log('data: ' + data); } - // apply leftover surrogate high from last write - if (this._surrogateFirst) { - data = this._surrogateFirst + data; - this._surrogateFirst = ''; + if (this._parseBuffer.length < data.length) { + this._parseBuffer = new Uint32Array(data.length); } - - this._parser.parse(data); + for (let i = 0; i < data.length; ++i) { + this._parseBuffer[i] = data.charCodeAt(i); + } + this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer)); buffer = this._terminal.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { @@ -304,9 +312,9 @@ export class InputHandler extends Disposable implements IInputHandler { } } - public print(data: string, start: number, end: number): void { - let char: string; + public print(data: Uint32Array, start: number, end: number): void { let code: number; + let char: string; let chWidth: number; const buffer: IBuffer = this._terminal.buffer; const charset: ICharset = this._terminal.charset; @@ -318,41 +326,23 @@ export class InputHandler extends Disposable implements IInputHandler { let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._terminal.updateRange(buffer.y); - for (let stringPosition = start; stringPosition < end; ++stringPosition) { - char = data.charAt(stringPosition); - code = data.charCodeAt(stringPosition); - - // surrogate pair handling - if (0xD800 <= code && code <= 0xDBFF) { - if (++stringPosition >= end) { - // end of input: - // handle pairs as true UTF-16 and wait for the second part - // since we expect the input comming from a stream there is - // a small chance that the surrogate pair got split - // therefore we dont process the first char here, instead - // it gets added as first char to the next processed chunk - this._surrogateFirst = char; - continue; - } - const second = data.charCodeAt(stringPosition); - // if the second part is in surrogate pair range create the high codepoint - // otherwise fall back to UCS-2 behavior (handle codepoints independently) - if (0xDC00 <= second && second <= 0xDFFF) { - code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; - char += data.charAt(stringPosition); - } else { - stringPosition--; - } - } + for (let pos = start; pos < end; ++pos) { + code = data[pos]; + char = stringFromCodePoint(code); // calculate print space // expensive call, therefore we save width in line buffer chWidth = wcwidth(code); // get charset replacement character - if (charset) { - char = charset[char] || char; - code = char.charCodeAt(0); + // charset are only defined for ASCII, therefore we only + // search for an replacement char if code < 127 + if (code < 127 && charset) { + const ch = charset[char]; + if (ch) { + code = ch.charCodeAt(0); + char = ch; + } } if (screenReaderMode) { diff --git a/src/Types.ts b/src/Types.ts index 31e423391c..cc8ff00a28 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -111,7 +111,7 @@ export interface ICompositionHelper { */ export interface IInputHandler { parse(data: string): void; - print(data: string, start: number, end: number): void; + print(data: Uint32Array, start: number, end: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; @@ -452,18 +452,26 @@ export interface IParsingState { * DCS handler signature for EscapeSequenceParser. * EscapeSequenceParser handles DCS commands via separate * subparsers that get hook/unhooked and can handle -* arbitrary amount of print data. +* arbitrary amount of data. +* * On entering a DSC sequence `hook` is called by * `EscapeSequenceParser`. Use it to initialize or reset * states needed to handle the current DCS sequence. +* Note: A DCS parser is only instantiated once, therefore +* you cannot rely on the ctor to reinitialize state. +* * EscapeSequenceParser will call `put` several times if the -* parsed string got splitted, therefore you might have to collect -* `data` until `unhook` is called. `unhook` marks the end -* of the current DCS sequence. +* parsed data got split, therefore you might have to collect +* `data` until `unhook` is called. +* Note: `data` is borrowed, if you cannot process the data +* in chunks you have to copy it, doing otherwise will lead to +* data losses or corruption. +* +* `unhook` marks the end of the current DCS sequence. */ export interface IDcsHandler { hook(collect: string, params: number[], flag: number): void; - put(data: string, start: number, end: number): void; + put(data: Uint32Array, start: number, end: number): void; unhook(): void; } @@ -480,9 +488,9 @@ export interface IEscapeSequenceParser extends IDisposable { * Parse string `data`. * @param data The data to parse. */ - parse(data: string): void; + parse(data: Uint32Array, length: number): void; - setPrintHandler(callback: (data: string, start: number, end: number) => void): void; + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; clearPrintHandler(): void; setExecuteHandler(flag: string, callback: () => void): void; diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts index 69a62abc70..99b0fd8285 100644 --- a/src/common/TypedArrayUtils.test.ts +++ b/src/common/TypedArrayUtils.test.ts @@ -3,21 +3,20 @@ * @license MIT */ import { assert } from 'chai'; -import { fillFallback } from './TypedArrayUtils'; +import { fillFallback, concat } from './TypedArrayUtils'; type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; -describe('polyfill conformance tests', function(): void { - - function deepEquals(a: TypedArray, b: TypedArray): void { - assert.equal(a.length, b.length); - for (let i = 0; i < a.length; ++i) { - assert.equal(a[i], b[i]); - } +function deepEquals(a: TypedArray, b: TypedArray): void { + assert.equal(a.length, b.length); + for (let i = 0; i < a.length; ++i) { + assert.equal(a[i], b[i]); } +} +describe('polyfill conformance tests', function(): void { describe('TypedArray.fill', function(): void { it('should work with all typed array types', function(): void { const u81 = new Uint8Array(5); @@ -87,3 +86,12 @@ describe('polyfill conformance tests', function(): void { }); }); }); + +describe('typed array convenience functions', () => { + it('concat', () => { + const a = new Uint8Array([1, 2, 3, 4, 5]); + const b = new Uint8Array([6, 7, 8, 9, 0]); + const merged = concat(a, b); + deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); + }); +}); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 6e1a3630ba..5469983549 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -3,15 +3,15 @@ * @license MIT */ +export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + + /** * polyfill for TypedArray.fill * This is needed to support .fill in all safari versions and IE 11. */ - -type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray - | Int8Array | Int16Array | Int32Array - | Float32Array | Float64Array; - export function fill(array: T, value: number, start?: number, end?: number): T { // all modern engines that support .fill if (array.fill) { @@ -39,3 +39,14 @@ export function fillFallback(array: T, value: number, star } return array; } + +/** + * Concat two typed arrays `a` and `b`. + * Returns a new typed array. + */ +export function concat(a: T, b: T): T { + const result = new (a.constructor as any)(a.length + b.length); + result.set(a); + result.set(b, a.length); + return result; +} diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts new file mode 100644 index 0000000000..12f3099abf --- /dev/null +++ b/src/core/input/TextDecoder.test.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './TextDecoder'; + +describe('text encodings', () => { + it('stringFromCodePoint/utf32ToString', () => { + const s = 'abcdefg'; + const data = new Uint32Array(s.length); + for (let i = 0; i < s.length; ++i) { + data[i] = s.charCodeAt(i); + assert.equal(stringFromCodePoint(data[i]), s[i]); + } + assert.equal(utf32ToString(data), s); + }); + + describe('StringToUtf32 Decoder', () => { + describe('full codepoint test', () => { + it('0..65535', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 0; i < 65536; ++i) { + // skip surrogate pairs + if (i >= 0xD800 && i <= 0xDFFF) { + continue; + } + const length = decoder.decode(String.fromCharCode(i), target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), String.fromCharCode(i)); + decoder.clear(); + } + }); + it('65536..0x10FFFF (surrogates)', function(): void { + this.timeout(20000); + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 65536; i < 0x10FFFF; ++i) { + const codePoint = i - 0x10000; + const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + const length = decoder.decode(s, target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), s); + decoder.clear(); + } + }); + }); + + describe('stream handling', () => { + it('surrogates mixed advance by 1', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + const input = 'Ä€𝄞Ö𝄞€Ü𝄞€'; + let decoded = ''; + for (let i = 0; i < input.length; ++i) { + const written = decoder.decode(input[i], target); + decoded += utf32ToString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); + }); + }); +}); diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts new file mode 100644 index 0000000000..1ff0094009 --- /dev/null +++ b/src/core/input/TextDecoder.ts @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. + * To keep the decoder in line with JS strings it handles single surrogates as UCS2. + */ +export class StringToUtf32 { + private _interim: number = 0; + + /** + * Clears interim and resets decoder to clean state. + */ + public clear(): void { + this._interim = 0; + } + + /** + * Decode JS string to UTF32 codepoints. + * The methods assumes stream input and will store partly transmitted + * surrogate pairs and decode them with the next data chunk. + * Note: The method does no bound checks for target, therefore make sure + * the provided input data does not exceed the size of `target`. + * Returns the number of written codepoints in `target`. + */ + decode(input: string, target: Uint32Array): number { + const length = input.length; + + if (!length) { + return 0; + } + + let size = 0; + let startPos = 0; + + // handle leftover surrogate high + if (this._interim) { + const second = input.charCodeAt(startPos++); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = this._interim; + target[size++] = second; + } + this._interim = 0; + } + + for (let i = startPos; i < length; ++i) { + const code = input.charCodeAt(i); + // surrogate pair first + if (0xD800 <= code && code <= 0xDBFF) { + if (++i >= length) { + this._interim = code; + return size; + } + const second = input.charCodeAt(i); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = code; + target[size++] = second; + } + continue; + } + target[size++] = code; + } + return size; + } +} + +/** + * Convert UTF32 codepoint into JS string. + */ +export function stringFromCodePoint(codePoint: number): string { + if (codePoint > 0xFFFF) { + // UTF32 to UTF16 conversion (see comments in utf32ToString) + codePoint -= 0x10000; + return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + } + return String.fromCharCode(codePoint); +} + +/** + * Convert UTF32 char codes into JS string. + * Basically the same as `stringFromCodePoint` but for multiple codepoints + * in a loop (which is a lot faster). + */ +export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string { + let result = ''; + for (let i = start; i < end; ++i) { + let codepoint = data[i]; + if (codepoint > 0xFFFF) { + // JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair + // conversion rules: + // - subtract 0x10000 from code point, leaving a 20 bit number + // - add high 10 bits to 0xD800 --> first surrogate + // - add low 10 bits to 0xDC00 --> second surrogate + codepoint -= 0x10000; + result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00); + } else { + result += String.fromCharCode(codepoint); + } + } + return result; +}