diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index b9a5ae4e..fa6feb5c 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; @@ -86,6 +86,9 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public mockOscParser(): void { this._oscParser = oscPutParser; } + public identifier(id: IFunctionIdentifier): number { + return this._identifier(id); + } } // test object to collect parser actions and compare them with expected values @@ -1552,5 +1555,110 @@ describe('EscapeSequenceParser', function (): void { chai.expect(errorState).eql(null); }); }); + describe('function identifiers', () => { + describe('registration limits', () => { + it('prefix range 0x3c .. 0x3f, one byte', () => { + for (let i = 0x3c; i <= 0x3f; ++i) { + const c = String.fromCharCode(i); + chai.expect(parser.identToString(parser.identifier({prefix: c, final: 'z'}))).eql(c + 'z'); + } + chai.assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); + chai.assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); + chai.assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported'); + }); + it('intermediates range 0x20 .. 0x2f, up to two bytes', () => { + for (let i = 0x20; i <= 0x2f; ++i) { + const c = String.fromCharCode(i); + chai.expect(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'}))).eql(c + c + 'z'); + } + chai.assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); + chai.assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); + chai.assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported'); + }); + it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => { + for (let i = 0x40; i <= 0x7e; ++i) { + const c = String.fromCharCode(i); + chai.expect(parser.identToString(parser.identifier({final: c}))).eql(c); + } + chai.assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126'); + chai.assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126'); + chai.assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte'); + }); + it('final ESC range 0x30 .. 0x7e, one byte', () => { + for (let i = 0x30; i <= 0x7e; ++i) { + const c = String.fromCharCode(i); + let handler: IDisposable | undefined; + chai.assert.doesNotThrow(() => { handler = parser.addEscHandler({final: c}, () => {}); }, 'final must be in range 48 .. 126'); + if (handler) handler.dispose(); + } + chai.assert.throws(() => { parser.addEscHandler({final: '\x2f'}, () => {}); }, 'final must be in range 48 .. 126'); + chai.assert.throws(() => { parser.addEscHandler({final: '\x7f'}, () => {}); }, 'final must be in range 48 .. 126'); + }); + it('id calculation - should stacking prefix -> intermediate -> final', () => { + chai.expect(parser.identToString(parser.identifier({final: 'z'}))).eql('z'); + chai.expect(parser.identToString(parser.identifier({prefix: '?', final: 'z'}))).eql('?z'); + chai.expect(parser.identToString(parser.identifier({intermediates: '!', final: 'z'}))).eql('!z'); + chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'}))).eql('?!z'); + chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'}))).eql('?!!z'); + }); + }); + describe('identifier invocation', () => { + it('ESC', () => { + const callstack: string[] = []; + const h1 = parser.addEscHandler({final: 'z'}, () => { callstack.push('z'); }); + const h2 = parser.addEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); }); + const h3 = parser.addEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); }); + parse(parser, '\x1bz\x1b!z\x1b!!z'); + h1.dispose(); + h2.dispose(); + h3.dispose(); + parse(parser, '\x1bz\x1b!z\x1b!!z'); + chai.expect(callstack).eql(['z', '!z', '!!z']); + }); + it('CSI', () => { + const callstack: any[] = []; + const h1 = parser.addCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); }); + const h2 = parser.addCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); }); + const h3 = parser.addCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); }); + const h4 = parser.addCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); }); + const h5 = parser.addCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); }); + const h6 = parser.addCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); }); + parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z'); + h1.dispose(); + h2.dispose(); + h3.dispose(); + h4.dispose(); + h5.dispose(); + h6.dispose(); + parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z'); + chai.expect(callstack).eql([['z', [1, 0]], ['!z', [1, 0]], ['!!z', [1, 0]], ['?z', [1, 0]], ['?!z', [1, 0]], ['?!!z', [1, 0]]]); + }); + it('DCS', () => { + const callstack: any[] = []; + const h1 = parser.addDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); })); + const h2 = parser.addDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); })); + const h3 = parser.addDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); })); + const h4 = parser.addDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); })); + const h5 = parser.addDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); })); + const h6 = parser.addDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); })); + parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\'); + h1.dispose(); + h2.dispose(); + h3.dispose(); + h4.dispose(); + h5.dispose(); + h6.dispose(); + parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\'); + chai.expect(callstack).eql([ + ['z', [1, 0], 'AB'], + ['!z', [1, 0], 'AB'], + ['!!z', [1, 0], 'AB'], + ['?z', [1, 0], 'AB'], + ['?!z', [1, 0], 'AB'], + ['?!!z', [1, 0], 'AB'] + ]); + }); + }); + }); // TODO: error conditions and error recovery (not implemented yet in parser) }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index c789e9b4..bc0b620e 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -281,7 +281,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.setEscHandler({final: '\\'}, () => {}); } - private _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { + protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { let res = 0; if (id.prefix) { if (id.prefix.length > 1) { @@ -558,6 +558,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP i--; break; case ParserAction.COLLECT: + collect <<= 8; collect |= code; break; case ParserAction.ESC_DISPATCH: