Move common primitives to src/common/primitives/

Phase 1 of #5963: reorganize building blocks (buffer, parser, input,
data, and shared utilities) under common/primitives/. Runtime code
(services, public API, CoreTerminal, InputHandler) stays in place.

Update import paths across browser, headless, addons, and tests.
Add TypeScript path mappings so common/* aliases resolve primitives
first, then the common root.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-06-06 18:21:03 +00:00
co-authored by Daniel Imms
parent 3cdbc77386
commit 701c0affd5
187 changed files with 320 additions and 284 deletions
@@ -0,0 +1,487 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { ApcParser, ApcHandler } from './ApcParser';
import { StringToUtf32, utf32ToString } from '../input/TextDecoder';
import { IApcHandler, IFunctionIdentifier } from './Types';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
function identifier(id: IFunctionIdentifier): number {
let res = 0;
if (id.prefix) {
if (id.prefix.length > 1) {
throw new Error('only one byte as prefix supported');
}
res = id.prefix.charCodeAt(0);
if (res < 0x3c || res > 0x3f) {
throw new Error('prefix must be in range 0x3c .. 0x3f');
}
}
if (id.intermediates) {
if (id.intermediates.length > 2) {
throw new Error('only two bytes as intermediates are supported');
}
for (let i = 0; i < id.intermediates.length; ++i) {
const intermediate = id.intermediates.charCodeAt(i);
if (0x20 > intermediate || intermediate > 0x2f) {
throw new Error('intermediate must be in range 0x20 .. 0x2f');
}
res <<= 8;
res |= intermediate;
}
}
if (id.final.length !== 1) {
throw new Error('final must be a single byte');
}
const finalCode = id.final.charCodeAt(0);
if (0x40 > finalCode || finalCode > 0x7e) {
throw new Error('final must be in range 0x40 .. 0x7e');
}
res <<= 8;
res |= finalCode;
return res;
}
class TestHandler implements IApcHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
public start(): void {
this.output.push([this.msg, 'START']);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
public end(success: boolean): boolean {
this.output.push([this.msg, 'END', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
describe('ApcParser', () => {
let parser: ApcParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new ApcParser();
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
});
describe('handler registration', () => {
it('setApcHandler', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from TestHandler
['th', 'START'],
['th', 'PUT', 'Here comes'],
['th', 'PUT', 'the mouse!'],
['th', 'END', true]
]);
});
it('clearApcHandler', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from fallback handler
[identifier({intermediates: '+', final: 'p'}), 'START', undefined],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
[identifier({intermediates: '+', final: 'p'}), 'END', true]
]);
});
it('addApcHandler', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 'START'],
['th1', 'START'],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'END', true],
['th1', 'END', false] // false due being already handled by th2!
]);
});
it('addApcHandler with return false', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 'START'],
['th1', 'START'],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'END', true],
['th1', 'END', true] // true since th2 indicated to keep bubbling
]);
});
it('dispose handlers', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
dispo.dispose();
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th1', 'START'],
['th1', 'PUT', 'Here comes'],
['th1', 'PUT', 'the mouse!'],
['th1', 'END', true]
]);
});
});
describe('ApcHandlerFactory', () => {
const TEST_PAYLOAD_LIMIT = 100;
const CHUNK_SIZE = 10;
let originalPayloadLimit: number;
beforeEach(() => {
const handlerConstructor = ApcHandler as unknown as { _payloadLimit: number };
originalPayloadLimit = handlerConstructor._payloadLimit;
handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
});
afterEach(() => {
const handlerConstructor = ApcHandler as unknown as { _payloadLimit: number };
handlerConstructor._payloadLimit = originalPayloadLimit;
});
it('should be called once on end(true)', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, ['Here comes the mouse!']);
});
it('should not be called on end(false)', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(false);
assert.deepEqual(reports, []);
});
it('should be disposable', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['one', data]); return true; }));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['two', data]); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start(identifier({intermediates: '+', final: 'p'}));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['one', data]); return true; }));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['two', data]); return false; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
it('should work up to payload limit', function(): void {
this.timeout(30000);
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
const data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
parser.put(data, 0, data.length);
}
parser.end(true);
assert.deepEqual(reports, ['A'.repeat(TEST_PAYLOAD_LIMIT)]);
});
it('should abort for payload limit +1', function(): void {
this.timeout(30000);
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, []);
});
});
});
class TestHandlerAsync implements IApcHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
public start(): void {
this.output.push([this.msg, 'START']);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
public async end(success: boolean): Promise<boolean> {
// simple sleep to check in tests whether ordering gets messed up
await Promise.resolve();
this.output.push([this.msg, 'END', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
async function unhookP(parser: ApcParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.end(success, prev)) {
prev = await result;
}
}
describe('ApcParser - async tests', () => {
let parser: ApcParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new ApcParser();
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
});
describe('sync and async mixed', () => {
describe('sync | async | sync', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'START'],
['a1', 'START'],
['s1', 'START'],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'END', true],
['a1', 'END', false], // important: a1 before s1
['s1', 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'START'],
['a1', 'START'],
['s1', 'START'],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'END', true],
['a1', 'END', true], // important: a1 before s1
['s1', 'END', true]
]);
});
});
describe('async | sync | async', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'START'],
['s1', 'START'],
['a1', 'START'],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'END', true],
['s1', 'END', false], // important: s1 between a2 .. a1
['a1', 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'START'],
['s1', 'START'],
['a1', 'START'],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'END', true],
['s1', 'END', true], // important: s1 between a2 .. a1
['a1', 'END', true]
]);
});
});
describe('ApcHandlerFactory', () => {
it('should be called once on end(true)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(data); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, ['Here comes the mouse!']);
});
it('should not be called on end(false)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(data); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, false);
assert.deepEqual(reports, []);
});
it('should be disposable', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['one', data]); return true; }));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['two', data]); return true; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start(identifier({intermediates: '+', final: 'p'}));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['one', data]); return true; }));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['two', data]); return false; }));
parser.start(identifier({intermediates: '+', final: 'p'}));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
});
});
describe('reset', () => {
it('should abort active handlers with end(false) when reset during payload', () => {
const ident = identifier({ intermediates: '+', final: 'p' });
parser.registerHandler(ident, new TestHandler(reports, 'th'));
parser.start(ident);
let data = toUtf32('partial');
parser.put(data, 0, data.length);
parser.reset();
assert.deepEqual(reports, [
['th', 'START'],
['th', 'PUT', 'partial'],
['th', 'END', false]
]);
reports.length = 0;
parser.start(ident);
data = toUtf32('complete');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th', 'START'],
['th', 'PUT', 'complete'],
['th', 'END', true]
]);
});
});
});
+196
View File
@@ -0,0 +1,196 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from './Types';
import { ParserConstants } from './Constants';
import { utf32ToString } from '../input/TextDecoder';
import { IDisposable } from '../../Types';
import { LimitedStringBuilder } from '../StringBuilder';
const EMPTY_HANDLERS: IApcHandler[] = [];
/**
* APC Parser for handling Application Program Command sequences.
* APC sequences use the format: ESC _ <identifier><data> ESC \
*
* Unlike OSC which uses numeric identifiers (e.g., OSC 1337),
* APC uses the first character as the identifier (e.g., 'G' for Kitty graphics).
* The identifier is the character code of the first byte after ESC _.
*/
export class ApcParser implements IApcParser {
private _handlers: IHandlerCollection<IApcHandler> = Object.create(null);
private _active = EMPTY_HANDLERS;
private _ident: number = 0;
private _handlerFb: ApcFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
/**
* Register an APC handler for a specific identifier.
* @param ident The character code of the first byte (e.g., 0x47 for 'G')
* @param handler The handler to register
*/
public registerHandler(ident: number, handler: IApcHandler): IDisposable {
this._handlers[ident] ??= [];
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: ApcFallbackHandlerType): void {
this._handlerFb = handler;
}
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
public reset(): void {
// force cleanup handlers
if (this._active.length) {
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].end(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
public start(ident: number): void {
// always reset leftover handlers
this.reset();
this._ident = ident;
this._active = this._handlers[ident] || EMPTY_HANDLERS;
if (!this._active.length) {
this._handlerFb(this._ident, 'START');
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].start();
}
}
}
public put(data: Uint32Array, start: number, end: number): void {
if (!this._active.length) {
this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].put(data, start, end);
}
}
}
/**
* Indicates end of an APC command.
* Whether the APC got aborted or finished normally
* is indicated by `success`.
*/
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (!this._active.length) {
this._handlerFb(this._ident, 'END', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].end(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers (fallThrough for async)
for (; j >= 0; j--) {
handlerResult = this._active[j].end(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
}
/**
* Convenient class to allow attaching string based handler functions
* as APC handlers.
*/
export class ApcHandler implements IApcHandler {
private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
private _data = new LimitedStringBuilder(ApcHandler._payloadLimit);
private _hitLimit: boolean = false;
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
public start(): void {
this._data.reset();
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
if (this._data.append(utf32ToString(data, start, end))) {
this._hitLimit = true;
}
}
public end(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data.toString());
if (ret instanceof Promise) {
// need to hold data until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._data.reset();
this._hitLimit = false;
return res;
});
}
}
this._data.reset();
this._hitLimit = false;
return ret;
}
}
+68
View File
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* Internal states of EscapeSequenceParser.
*/
export const enum ParserState {
GROUND = 0,
ESCAPE = 1,
ESCAPE_INTERMEDIATE = 2,
CSI_ENTRY = 3,
CSI_PARAM = 4,
CSI_INTERMEDIATE = 5,
CSI_IGNORE = 6,
SOS_PM_STRING = 7,
OSC_STRING = 8,
DCS_ENTRY = 9,
DCS_PARAM = 10,
DCS_IGNORE = 11,
DCS_INTERMEDIATE = 12,
DCS_PASSTHROUGH = 13,
APC_ENTRY = 14,
APC_INTERMEDIATE = 15,
APC_PASSTHROUGH = 16,
// Number of states, meaning LAST_STATE + 1.
STATE_LENGTH = 17
}
/**
* Internal actions of EscapeSequenceParser.
*/
export const enum ParserAction {
IGNORE = 0,
ERROR = 1,
PRINT = 2,
EXECUTE = 3,
OSC_START = 4,
OSC_PUT = 5,
OSC_END = 6,
CSI_DISPATCH = 7,
PARAM = 8,
COLLECT = 9,
ESC_DISPATCH = 10,
CLEAR = 11,
DCS_HOOK = 12,
DCS_PUT = 13,
DCS_UNHOOK = 14,
APC_START = 15,
APC_PUT = 16,
APC_END = 17
}
/**
* Internal states of OscParser.
*/
export const enum OscState {
START = 0,
ID = 1,
PAYLOAD = 2,
ABORT = 3
}
// payload limit for OSC and DCS
export const enum ParserConstants {
PAYLOAD_LIMIT = 10000000
}
@@ -0,0 +1,499 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { DcsParser, DcsHandler } from './DcsParser';
import { IDcsHandler, IParams, IFunctionIdentifier } from './Types';
import { utf32ToString, StringToUtf32 } from '../input/TextDecoder';
import { Params } from './Params';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
function identifier(id: IFunctionIdentifier): number {
let res = 0;
if (id.prefix) {
if (id.prefix.length > 1) {
throw new Error('only one byte as prefix supported');
}
res = id.prefix.charCodeAt(0);
if (res < 0x3c || res > 0x3f) {
throw new Error('prefix must be in range 0x3c .. 0x3f');
}
}
if (id.intermediates) {
if (id.intermediates.length > 2) {
throw new Error('only two bytes as intermediates are supported');
}
for (let i = 0; i < id.intermediates.length; ++i) {
const intermediate = id.intermediates.charCodeAt(i);
if (0x20 > intermediate || intermediate > 0x2f) {
throw new Error('intermediate must be in range 0x20 .. 0x2f');
}
res <<= 8;
res |= intermediate;
}
}
if (id.final.length !== 1) {
throw new Error('final must be a single byte');
}
const finalCode = id.final.charCodeAt(0);
if (0x40 > finalCode || finalCode > 0x7e) {
throw new Error('final must be in range 0x40 .. 0x7e');
}
res <<= 8;
res |= finalCode;
return res;
}
class TestHandler implements IDcsHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
public hook(params: IParams): void {
this.output.push([this.msg, 'HOOK', params.toArray()]);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
public unhook(success: boolean): boolean {
this.output.push([this.msg, 'UNHOOK', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
describe('DcsParser', () => {
let parser: DcsParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new DcsParser();
parser.setHandlerFallback((id, action, data) => {
if (action === 'HOOK') {
data = data.toArray();
}
reports.push([id, action, data]);
});
});
describe('handler registration', () => {
it('setDcsHandler', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
// messages from TestHandler
['th', 'HOOK', [1, 2, 3]],
['th', 'PUT', 'Here comes'],
['th', 'PUT', 'the mouse!'],
['th', 'UNHOOK', true]
]);
});
it('clearDcsHandler', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
// messages from fallback handler
[identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
[identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true]
]);
});
it('addDcsHandler', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th2', 'HOOK', [1, 2, 3]],
['th1', 'HOOK', [1, 2, 3]],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'UNHOOK', true],
['th1', 'UNHOOK', false] // false due being already handled by th2!
]);
});
it('addDcsHandler with return false', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th2', 'HOOK', [1, 2, 3]],
['th1', 'HOOK', [1, 2, 3]],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'UNHOOK', true],
['th1', 'UNHOOK', true] // true since th2 indicated to keep bubbling
]);
});
it('dispose handlers', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th1', 'HOOK', [1, 2, 3]],
['th1', 'PUT', 'Here comes'],
['th1', 'PUT', 'the mouse!'],
['th1', 'UNHOOK', true]
]);
});
});
describe('DcsHandlerFactory', () => {
const TEST_PAYLOAD_LIMIT = 100;
const CHUNK_SIZE = 10;
let originalPayloadLimit: number;
beforeEach(() => {
const handlerConstructor = DcsHandler as unknown as { _payloadLimit: number };
originalPayloadLimit = handlerConstructor._payloadLimit;
handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
});
afterEach(() => {
const handlerConstructor = DcsHandler as unknown as { _payloadLimit: number };
handlerConstructor._payloadLimit = originalPayloadLimit;
});
it('should be called once on end(true)', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
});
it('should not be called on end(false)', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(false);
assert.deepEqual(reports, []);
});
it('should be disposable', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
});
it('should respect return false', () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
});
it('should work up to payload limit', function(): void {
this.timeout(30000);
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
const data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
parser.put(data, 0, data.length);
}
parser.unhook(true);
assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
});
it('should abort for payload limit +1', function(): void {
this.timeout(30000);
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, []);
});
});
});
class TestHandlerAsync implements IDcsHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
public hook(params: IParams): void {
this.output.push([this.msg, 'HOOK', params.toArray()]);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
public async unhook(success: boolean): Promise<boolean> {
// simple sleep to check in tests whether ordering gets messed up
await Promise.resolve();
this.output.push([this.msg, 'UNHOOK', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
async function unhookP(parser: DcsParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.unhook(success, prev)) {
prev = await result;
}
}
describe('DcsParser - async tests', () => {
let parser: DcsParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new DcsParser();
parser.setHandlerFallback((id, action, data) => {
if (action === 'HOOK') {
data = data.toArray();
}
reports.push([id, action, data]);
});
});
describe('sync and async mixed', () => {
describe('sync | async | sync', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'UNHOOK', true],
['a1', 'UNHOOK', false], // important: a1 before s1
['s1', 'UNHOOK', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'UNHOOK', true],
['a1', 'UNHOOK', true], // important: a1 before s1
['s1', 'UNHOOK', true]
]);
});
});
describe('async | sync | async', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'UNHOOK', true],
['s1', 'UNHOOK', false], // important: s1 between a2 .. a1
['a1', 'UNHOOK', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'UNHOOK', true],
['s1', 'UNHOOK', true], // important: s1 between a2 .. a1
['a1', 'UNHOOK', true]
]);
});
});
describe('DcsHandlerFactory', () => {
it('should be called once on end(true)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
});
it('should not be called on end(false)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, false);
assert.deepEqual(reports, []);
});
it('should be disposable', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
});
it('should respect return false', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
});
});
});
describe('reset', () => {
it('should abort active handlers with unhook(false) when reset during payload', () => {
const ident = identifier({ intermediates: '+', final: 'p' });
const params = Params.fromArray([1, 2, 3]);
parser.registerHandler(ident, new TestHandler(reports, 'th'));
parser.hook(ident, params);
let data = toUtf32('partial');
parser.put(data, 0, data.length);
parser.reset();
assert.deepEqual(reports, [
['th', 'HOOK', [1, 2, 3]],
['th', 'PUT', 'partial'],
['th', 'UNHOOK', false]
]);
reports.length = 0;
parser.hook(ident, params);
data = toUtf32('complete');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th', 'HOOK', [1, 2, 3]],
['th', 'PUT', 'complete'],
['th', 'UNHOOK', true]
]);
});
});
});
+191
View File
@@ -0,0 +1,191 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from '../../Types';
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from './Types';
import { utf32ToString } from '../input/TextDecoder';
import { Params } from './Params';
import { ParserConstants } from './Constants';
import { LimitedStringBuilder } from '../StringBuilder';
const EMPTY_HANDLERS: IDcsHandler[] = [];
export class DcsParser implements IDcsParser {
private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null);
private _active: IDcsHandler[] = EMPTY_HANDLERS;
private _ident: number = 0;
private _handlerFb: DcsFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
public registerHandler(ident: number, handler: IDcsHandler): IDisposable {
this._handlers[ident] ??= [];
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: DcsFallbackHandlerType): void {
this._handlerFb = handler;
}
public reset(): void {
// force cleanup leftover handlers
if (this._active.length) {
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].unhook(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
public hook(ident: number, params: IParams): void {
// always reset leftover handlers
this.reset();
this._ident = ident;
this._active = this._handlers[ident] || EMPTY_HANDLERS;
if (!this._active.length) {
this._handlerFb(this._ident, 'HOOK', params);
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].hook(params);
}
}
}
public put(data: Uint32Array, start: number, end: number): void {
if (!this._active.length) {
this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].put(data, start, end);
}
}
}
public unhook(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (!this._active.length) {
this._handlerFb(this._ident, 'UNHOOK', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].unhook(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers (fallThrough for async)
for (; j >= 0; j--) {
handlerResult = this._active[j].unhook(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
}
// predefine empty params as [0] (ZDM)
const EMPTY_PARAMS = new Params();
EMPTY_PARAMS.addParam(0);
/**
* Convenient class to create a DCS handler from a single callback function.
* Note: The payload is currently limited to 50 MB (hardcoded).
*/
export class DcsHandler implements IDcsHandler {
private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
private _data = new LimitedStringBuilder(DcsHandler._payloadLimit);
private _params: IParams = EMPTY_PARAMS;
private _hitLimit: boolean = false;
constructor(private _handler: (data: string, params: IParams) => boolean | Promise<boolean>) { }
public hook(params: IParams): void {
// since we need to preserve params until `unhook`, we have to clone it
// (only borrowed from parser and spans multiple parser states)
// perf optimization:
// clone only, if we have non empty params, otherwise stick with default
this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS;
this._data.reset();
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
if (this._data.append(utf32ToString(data, start, end))) {
this._hitLimit = true;
}
}
public unhook(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data.toString(), this._params);
if (ret instanceof Promise) {
// need to hold data and params until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._params = EMPTY_PARAMS;
this._data.reset();
this._hitLimit = false;
return res;
});
}
}
this._params = EMPTY_PARAMS;
this._data.reset();
this._hitLimit = false;
return ret;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,934 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from './Types';
import { ParserState, ParserAction } from './Constants';
import { Disposable, toDisposable } from '../Lifecycle';
import { IDisposable } from '../../Types';
import { Params } from './Params';
import { OscParser } from './OscParser';
import { DcsParser } from './DcsParser';
import { ApcParser } from './ApcParser';
/**
* VT commands done by the parser
*/
// @vt: #Y ESC CSI "Control Sequence Introducer" "ESC [" "Start of a CSI sequence."
// @vt: #Y ESC OSC "Operating System Command" "ESC ]" "Start of an OSC sequence."
// @vt: #Y ESC DCS "Device Control String" "ESC P" "Start of a DCS sequence."
// @vt: #Y ESC ST "String Terminator" "ESC \\" "Terminator used for string type sequences."
// @vt: #Y ESC PM "Privacy Message" "ESC ^" "Start of a privacy message."
// @vt: #Y ESC APC "Application Program Command" "ESC _" "Start of an APC sequence."
// @vt: #Y C1 CSI "Control Sequence Introducer" "\x9B" "Start of a CSI sequence."
// @vt: #Y C1 OSC "Operating System Command" "\x9D" "Start of an OSC sequence."
// @vt: #Y C1 DCS "Device Control String" "\x90" "Start of a DCS sequence."
// @vt: #Y C1 ST "String Terminator" "\x9C" "Terminator used for string type sequences."
// @vt: #Y C1 PM "Privacy Message" "\x9E" "Start of a privacy message."
// @vt: #Y C1 APC "Application Program Command" "\x9F" "Start of an APC sequence."
// @vt: #Y C0 NUL "Null" "\0, \x00" "NUL is ignored."
// @vt: #Y C0 ESC "Escape" "\e, \x1B" "Start of a sequence. Cancels any other sequence."
/**
* Table values are generated like this:
* index: currentState << TableValue.INDEX_STATE_SHIFT | charCode
* value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState
*/
const enum TableAccess {
TRANSITION_ACTION_SHIFT = 8,
TRANSITION_STATE_MASK = 255,
INDEX_STATE_SHIFT = 8
}
/**
* Transition table for EscapeSequenceParser.
*/
export class TransitionTable {
public table: Uint16Array;
constructor(length: number) {
this.table = new Uint16Array(length);
}
/**
* Set default transition.
* @param action default action
* @param next default next state
*/
public setDefault(action: ParserAction, next: ParserState): void {
this.table.fill(action << TableAccess.TRANSITION_ACTION_SHIFT | next);
}
/**
* Add a transition to the transition table.
* @param code input character code
* @param state current parser state
* @param action parser action to be done
* @param next next parser state
*/
public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void {
this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;
}
/**
* Add transitions for multiple input character codes.
* @param codes input character code array
* @param state current parser state
* @param action parser action to be done
* @param next next parser state
*/
public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void {
for (let i = 0; i < codes.length; i++) {
this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;
}
}
}
// Pseudo-character placeholder for printable non-ascii characters (unicode).
const NON_ASCII_PRINTABLE = 0xA0;
/**
* VT500 compatible transition table.
* Taken from https://vt100.net/emu/dec_ansi_parser.
*/
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
// table size:
// (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1
const table: TransitionTable = new TransitionTable(4257);
// range macro for byte
const BYTE_VALUES = 256;
const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i);
const r = (start: number, end: number): number[] => blueprint.slice(start, end);
// Default definitions.
const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded
const EXECUTABLES = r(0x00, 0x18);
EXECUTABLES.push(0x19);
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH);
// set default transition
table.setDefault(ParserAction.ERROR, ParserState.GROUND);
// printables
table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
// global anywhere rules
for (const state of states) {
table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);
table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM
table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
}
// rules for executables and 7f
table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);
table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);
table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);
table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);
table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);
table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);
table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);
table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);
table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);
table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);
table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);
// osc
table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);
table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);
table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
// sos/pm
table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);
table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
// apc
table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);
table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);
table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.APC_INTERMEDIATE, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);
table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);
// 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);
table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);
table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);
table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
// esc_intermediate
table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
// dcs entry
table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);
table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);
table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
// special handling of unicode chars
table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
return table;
})();
/**
* EscapeSequenceParser.
* This class implements the ANSI/DEC compatible parser described by
* Paul Williams (https://vt100.net/emu/dec_ansi_parser).
*
* To implement custom ANSI compliant escape sequences it is not needed to
* alter this parser, instead consider registering a custom handler.
* For non ANSI compliant sequences change the transition table with
* the optional `transitions` constructor argument and
* reimplement the `parse` method.
*
* This parser is currently hardcoded to operate in ZDM (Zero Default Mode)
* as suggested by the original parser, thus empty parameters are set to 0.
* This is not in line with the latest ECMA-48 specification
* (ZDM was part of the early specs and got completely removed later on).
*
* Other than the original parser from vt100.net this parser supports
* sub parameters in digital parameters separated by colons. Empty sub parameters
* are set to -1 (no ZDM for sub parameters).
*
* About prefix and intermediate bytes:
* This parser follows the assumptions of the vt100.net parser with these restrictions:
* - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f
* - max. two intermediates are respected, byte range 0x20 .. 0x2f
* Note that this is not in line with ECMA-48 which does not limit either of those.
* Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently
* there are no known sequences that follow the broader definition of the specification.
*
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
public initialState: number;
public currentState: number;
public precedingJoinState: number; // UnicodeJoinProperties
// buffers over several parse calls
protected _params: Params;
protected _collect: number;
// handler lookup containers
protected _printHandler: PrintHandlerType;
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
// fast path for EXE bytes < 0x18
protected _executeHandlersArr: (ExecuteHandlerType | undefined)[];
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
protected _escHandlers: IHandlerCollection<EscHandlerType>;
protected readonly _oscParser: IOscParser;
protected readonly _dcsParser: IDcsParser;
protected readonly _apcParser: IApcParser;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
protected _printHandlerFb: PrintFallbackHandlerType;
protected _executeHandlerFb: ExecuteFallbackHandlerType;
protected _csiHandlerFb: CsiFallbackHandlerType;
protected _escHandlerFb: EscFallbackHandlerType;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
// parser stack save for async handler support
protected _parseStack: IParserStackState = {
state: ParserStackType.NONE,
handlers: [],
handlerPos: 0,
transition: 0,
chunkPos: 0
};
constructor(
protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE
) {
super();
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._params = new Params(); // defaults to 32 storable params/subparams
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingJoinState = 0;
// set default fallback handlers and handler lookup containers
this._printHandlerFb = (data, start, end): void => { };
this._executeHandlerFb = (code: number): void => { };
this._csiHandlerFb = (ident: number, params: IParams): void => { };
this._escHandlerFb = (ident: number): void => { };
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
this._printHandler = this._printHandlerFb;
this._executeHandlers = Object.create(null);
this._executeHandlersArr = new Array(0x18).fill(undefined);
this._csiHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._register(toDisposable(() => {
this._csiHandlers = Object.create(null);
this._executeHandlers = Object.create(null);
this._executeHandlersArr = new Array(0x18).fill(undefined);
this._escHandlers = Object.create(null);
}));
this._oscParser = this._register(new OscParser());
this._dcsParser = this._register(new DcsParser());
this._apcParser = this._register(new ApcParser());
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
this.registerEscHandler({ final: '\\' }, () => true);
}
protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {
let res = 0;
if (id.prefix) {
if (id.prefix.length > 1) {
throw new Error('only one byte as prefix supported');
}
res = id.prefix.charCodeAt(0);
if (res < 0x3c || res > 0x3f) {
throw new Error('prefix must be in range 0x3c .. 0x3f');
}
}
if (id.intermediates) {
if (id.intermediates.length > 2) {
throw new Error('only two bytes as intermediates are supported');
}
for (let i = 0; i < id.intermediates.length; ++i) {
const intermediate = id.intermediates.charCodeAt(i);
if (0x20 > intermediate || intermediate > 0x2f) {
throw new Error('intermediate must be in range 0x20 .. 0x2f');
}
res <<= 8;
res |= intermediate;
}
}
if (id.final.length !== 1) {
throw new Error('final must be a single byte');
}
const finalCode = id.final.charCodeAt(0);
if (finalRange[0] > finalCode || finalCode > finalRange[1]) {
throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);
}
res <<= 8;
res |= finalCode;
return res;
}
public identToString(ident: number): string {
const res: string[] = [];
while (ident) {
res.push(String.fromCharCode(ident & 0xFF));
ident >>= 8;
}
return res.reverse().join('');
}
public setPrintHandler(handler: PrintHandlerType): void {
this._printHandler = handler;
}
public clearPrintHandler(): void {
this._printHandler = this._printHandlerFb;
}
public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {
const ident = this._identifier(id, [0x30, 0x7e]);
this._escHandlers[ident] ??= [];
const handlerList = this._escHandlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public clearEscHandler(id: IFunctionIdentifier): void {
if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];
}
public setEscHandlerFallback(handler: EscFallbackHandlerType): void {
this._escHandlerFb = handler;
}
public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {
const code = flag.charCodeAt(0);
this._executeHandlers[code] = handler;
if (code < 0x18) this._executeHandlersArr[code] = handler;
}
public clearExecuteHandler(flag: string): void {
const code = flag.charCodeAt(0);
if (this._executeHandlers[code]) delete this._executeHandlers[code];
if (code < 0x18) this._executeHandlersArr[code] = undefined;
}
public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {
this._executeHandlerFb = handler;
}
public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {
const ident = this._identifier(id);
this._csiHandlers[ident] ??= [];
const handlerList = this._csiHandlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public clearCsiHandler(id: IFunctionIdentifier): void {
if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];
}
public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {
this._csiHandlerFb = callback;
}
public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {
return this._dcsParser.registerHandler(this._identifier(id), handler);
}
public clearDcsHandler(id: IFunctionIdentifier): void {
this._dcsParser.clearHandler(this._identifier(id));
}
public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {
this._dcsParser.setHandlerFallback(handler);
}
public registerOscHandler(ident: number, handler: IOscHandler): IDisposable {
return this._oscParser.registerHandler(ident, handler);
}
public clearOscHandler(ident: number): void {
this._oscParser.clearHandler(ident);
}
public setOscHandlerFallback(handler: OscFallbackHandlerType): void {
this._oscParser.setHandlerFallback(handler);
}
public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {
id.prefix = undefined; // APC does not support prefix byte
return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);
}
public clearApcHandler(id: IFunctionIdentifier): void {
id.prefix = undefined; // APC does not support prefix byte
this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));
}
public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {
this._apcParser.setHandlerFallback(handler);
}
public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
this._errorHandler = callback;
}
public clearErrorHandler(): void {
this._errorHandler = this._errorHandlerFb;
}
/**
* Reset parser to initial values.
*
* This can also be used to lift the improper continuation error condition
* when dealing with async handlers. Use this only as a last resort to silence
* that error when the terminal has no pending data to be processed. Note that
* the interrupted async handler might continue its work in the future messing
* up the terminal state even further.
*/
public reset(): void {
this.currentState = this.initialState;
this._oscParser.reset();
this._dcsParser.reset();
this._apcParser.reset();
this._params.resetZdm();
this._collect = 0;
this.precedingJoinState = 0;
// abort pending continuation from async handler
// Here the RESET type indicates, that the next parse call will
// ignore any saved stack, instead continues sync with next codepoint from GROUND
if (this._parseStack.state !== ParserStackType.NONE) {
this._parseStack.state = ParserStackType.RESET;
this._parseStack.handlers = []; // also release handlers ref
}
}
/**
* Async parse support.
*/
protected _preserveStack(
state: ParserStackType,
handlers: ResumableHandlersType,
handlerPos: number,
transition: number,
chunkPos: number
): void {
this._parseStack.state = state;
this._parseStack.handlers = handlers;
this._parseStack.handlerPos = handlerPos;
this._parseStack.transition = transition;
this._parseStack.chunkPos = chunkPos;
}
/**
* Parse UTF32 codepoints in `data` up to `length`.
*
* Note: For several actions with high data load the parsing is optimized
* by using local read ahead loops with hardcoded conditions to
* avoid costly table lookups. Make sure that any change of table values
* will be reflected in the loop conditions as well and vice versa.
* Affected states/actions:
* - GROUND:PRINT
* - CSI_PARAM:PARAM
* - DCS_PARAM:PARAM
* - OSC_STRING:OSC_PUT
* - DCS_PASSTHROUGH:DCS_PUT
*
* Additionally the following fast paths exist before the table lookup:
* - EXE bytes < 0x18 in non-payload states (avoids table lookup entirely)
* - 7-bit CSI sequences without intermediates (ESC [ params final)
*
* Note on asynchronous handler support:
* Any handler returning a promise will be treated as asynchronous.
* To keep the in-band blocking working for async handlers, `parse` pauses execution,
* creates a stack save and returns the promise to the caller.
* For proper continuation of the paused state it is important
* to await the promise resolving. On resolve the parse must be repeated
* with the same chunk of data and the resolved value in `promiseResult`
* until no promise is returned.
*
* Important: With only sync handlers defined, parsing is completely synchronous as well.
* As soon as an async handler is involved, synchronous parsing is not possible anymore.
*
* Boilerplate for proper parsing of multiple chunks with async handlers:
*
* ```typescript
* async function parseMultipleChunks(chunks: Uint32Array[]): Promise<void> {
* for (const chunk of chunks) {
* let result: void | Promise<boolean>;
* let prev: boolean | undefined;
* while (result = parser.parse(chunk, chunk.length, prev)) {
* prev = await result;
* }
* }
* // finished parsing all chunks...
* }
* ```
*/
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
let code: number;
let transition: number;
let start = 0;
let handlerResult: void | boolean | Promise<boolean>;
// resume from async handler
if (this._parseStack.state) {
// allow sync parser reset even in continuation mode
// Note: can be used to recover parser from improper continuation error below
if (this._parseStack.state === ParserStackType.RESET) {
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND
} else {
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
/**
* Reject further parsing on improper continuation after pausing. This is a really bad
* condition with screwed up execution order and prolly messed up terminal state,
* therefore we exit hard with an exception and reject any further parsing.
*
* Note: With `Terminal.write` usage this exception should never occur, as the top level
* calls are guaranteed to handle async conditions properly. If you ever encounter this
* exception in your terminal integration it indicates, that you injected data chunks to
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
* continuation of a running async handler.
*
* It is possible to get rid of this error by calling `reset`. But dont rely on that, as
* the pending async handler still might mess up the terminal later. Instead fix the
* faulty async handling, so this error will not be thrown anymore.
*/
this._parseStack.state = ParserStackType.FAIL;
throw new Error('improper continuation due to previous async handler, giving up parsing');
}
// we have to resume the old handler loop if:
// - return value of the promise was `false`
// - handlers are not exhausted yet
const handlers = this._parseStack.handlers;
let handlerPos = this._parseStack.handlerPos - 1;
switch (this._parseStack.state) {
case ParserStackType.CSI:
if (promiseResult === false && handlerPos > -1) {
for (; handlerPos >= 0; handlerPos--) {
handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._parseStack.handlerPos = handlerPos;
return handlerResult;
}
}
}
this._parseStack.handlers = [];
break;
case ParserStackType.ESC:
if (promiseResult === false && handlerPos > -1) {
for (; handlerPos >= 0; handlerPos--) {
handlerResult = (handlers as EscHandlerType[])[handlerPos]();
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._parseStack.handlerPos = handlerPos;
return handlerResult;
}
}
}
this._parseStack.handlers = [];
break;
case ParserStackType.DCS:
code = data[this._parseStack.chunkPos];
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.resetZdm();
this._collect = 0;
break;
case ParserStackType.OSC:
code = data[this._parseStack.chunkPos];
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.resetZdm();
this._collect = 0;
break;
case ParserStackType.APC:
code = data[this._parseStack.chunkPos];
handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.resetZdm();
this._collect = 0;
break;
}
// cleanup before continuing with the main sync loop
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1;
this.precedingJoinState = 0;
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
}
}
// continue with main sync loop
// process input string
for (let i = start; i < length; ++i) {
code = data[i];
// EXE fast-path: common control bytes (0x00-0x17) in non-payload states
if (code < 0x18 && this.currentState <= ParserState.CSI_IGNORE) {
(this._executeHandlersArr[code] ?? this._executeHandlerFb)(code);
this.precedingJoinState = 0;
continue;
}
// CSI fast-path: collapse ESC [ into a single entry, parse params+final in a tight loop
if (code === 0x1b
&& this.currentState < ParserState.OSC_STRING
&& i + 2 < length && data[i + 1] === 0x5b
) {
this._params.resetZdm();
this._collect = 0;
let k = i + 2;
let ch = data[k];
if (ch >= 0x3c && ch <= 0x3f) {
this._collect = ch;
k++;
}
let csiDone = false;
for (; k < length; k++) {
ch = data[k];
if (ch >= 0x30 && ch <= 0x39) {
this._params.addDigit(ch - 48);
} else if (ch === 0x3b) {
this._params.addParam(0);
} else if (ch === 0x3a) {
this._params.addSubParam(-1);
} else if (ch >= 0x40 && ch <= 0x7e) {
const handlers = this._csiHandlers[this._collect << 8 | ch];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
handlerResult = handlers[j](this._params);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
transition = ParserAction.CSI_DISPATCH << TableAccess.TRANSITION_ACTION_SHIFT | ParserState.GROUND;
this._preserveStack(ParserStackType.CSI, handlers, j, transition, k);
return handlerResult;
}
}
if (j < 0) {
this._csiHandlerFb(this._collect << 8 | ch, this._params);
}
this.precedingJoinState = 0;
i = k;
this.currentState = ParserState.GROUND;
csiDone = true;
break;
} else {
break;
}
}
if (!csiDone) {
i = k - 1;
this.currentState = ParserState.CSI_PARAM;
}
continue;
}
// normal transition & action lookup
transition = this._transitions.table[
this.currentState << TableAccess.INDEX_STATE_SHIFT |
(code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)
];
switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {
case ParserAction.PRINT:
// Note: 0x20 (SP) is included, 0x7F (DEL) is excluded
let c = i;
const l4 = length - 4;
while (c < l4
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
) {}
if (c >= l4) {
while (c < length && data[c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)) {
c++;
}
}
this._printHandler(data, i, c);
i = c - 1;
break;
case ParserAction.EXECUTE:
if (this._executeHandlers[code]) this._executeHandlers[code]();
else this._executeHandlerFb(code);
this.precedingJoinState = 0;
break;
case ParserAction.IGNORE:
break;
case ParserAction.ERROR:
const inject: IParsingState = this._errorHandler(
{
position: i,
code,
currentState: this.currentState,
collect: this._collect,
params: this._params,
abort: false
});
if (inject.abort) return;
// inject values: currently not implemented
break;
case ParserAction.CSI_DISPATCH:
// Trigger CSI Handler
const handlers = this._csiHandlers[this._collect << 8 | code];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
// true means success and to stop bubbling
// a promise indicates an async handler that needs to finish before progressing
handlerResult = handlers[j](this._params);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);
return handlerResult;
}
}
if (j < 0) {
this._csiHandlerFb(this._collect << 8 | code, this._params);
}
this.precedingJoinState = 0;
break;
case ParserAction.PARAM:
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
do {
switch (code) {
case 0x3b:
this._params.addParam(0); // ZDM
break;
case 0x3a:
this._params.addSubParam(-1);
break;
default: // 0x30 - 0x39
this._params.addDigit(code - 48);
}
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
i--;
break;
case ParserAction.COLLECT:
this._collect <<= 8;
this._collect |= code;
break;
case ParserAction.ESC_DISPATCH:
const handlersEsc = this._escHandlers[this._collect << 8 | code];
let jj = handlersEsc ? handlersEsc.length - 1 : -1;
for (; jj >= 0; jj--) {
// true means success and to stop bubbling
// a promise indicates an async handler that needs to finish before progressing
handlerResult = handlersEsc[jj]();
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);
return handlerResult;
}
}
if (jj < 0) {
this._escHandlerFb(this._collect << 8 | code);
}
this.precedingJoinState = 0;
break;
case ParserAction.CLEAR:
this._params.resetZdm();
this._collect = 0;
break;
case ParserAction.DCS_HOOK:
this._dcsParser.hook(this._collect << 8 | code, this._params);
break;
case ParserAction.DCS_PUT:
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
// unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
for (let j = i + 1; ; ++j) {
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
this._dcsParser.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.DCS_UNHOOK:
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.DCS, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
this._params.resetZdm();
this._collect = 0;
this.precedingJoinState = 0;
break;
case ParserAction.OSC_START:
this._oscParser.start();
break;
case ParserAction.OSC_PUT:
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
for (let j = i + 1; ; j++) {
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
this._oscParser.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.OSC_END:
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.OSC, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
this._params.resetZdm();
this._collect = 0;
this.precedingJoinState = 0;
break;
case ParserAction.APC_START:
this._apcParser.start(this._collect << 8 | code);
break;
case ParserAction.APC_PUT:
// inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c
// allowed: 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE
for (let j = i + 1; ; ++j) {
if (j < length && (
(data[j] >= 0x20 && data[j] < 0x7f) || (data[j] >= 0x08 && data[j] < 0x0e) || data[j] >= NON_ASCII_PRINTABLE
)) continue;
this._apcParser.put(data, i, j);
i = j - 1;
break;
}
break;
case ParserAction.APC_END:
handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.APC, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
this._params.resetZdm();
this._collect = 0;
this.precedingJoinState = 0;
break;
}
this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;
}
}
}
@@ -0,0 +1,491 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { OscParser, OscHandler } from './OscParser';
import { StringToUtf32, utf32ToString } from '../input/TextDecoder';
import { IOscHandler } from './Types';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
class TestHandler implements IOscHandler {
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
public start(): void {
this.output.push([this.msg, this.id, 'START']);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
public end(success: boolean): boolean {
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
describe('OscParser', () => {
let parser: OscParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new OscParser();
parser.setHandlerFallback((id, action, data) => {
reports.push([id, action, data]);
});
});
describe('identifier parsing', () => {
it('no report for illegal ids', () => {
const data = toUtf32('hello world!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, []);
});
it('no payload', () => {
parser.start();
let data = toUtf32('12');
parser.put(data, 0, data.length);
data = toUtf32('34');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]);
});
it('with payload', () => {
parser.start();
let data = toUtf32('12');
parser.put(data, 0, data.length);
data = toUtf32('34');
parser.put(data, 0, data.length);
data = toUtf32(';h');
parser.put(data, 0, data.length);
data = toUtf32('ello');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
[1234, 'START', undefined],
[1234, 'PUT', 'h'],
[1234, 'PUT', 'ello'],
[1234, 'END', true]
]);
});
});
describe('handler registration', () => {
it('setOscHandler', () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from TestHandler
['th', 1234, 'START'],
['th', 1234, 'PUT', 'Here comes'],
['th', 1234, 'PUT', 'the mouse!'],
['th', 1234, 'END', true]
]);
});
it('clearOscHandler', () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
parser.clearHandler(1234);
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from fallback handler
[1234, 'START', undefined],
[1234, 'PUT', 'Here comes'],
[1234, 'PUT', 'the mouse!'],
[1234, 'END', true]
]);
});
it('addOscHandler', () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 'th2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 1234, 'START'],
['th1', 1234, 'START'],
['th2', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'Here comes'],
['th2', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'PUT', 'the mouse!'],
['th2', 1234, 'END', true],
['th1', 1234, 'END', false] // false due being already handled by th2!
]);
});
it('addOscHandler with return false', () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 1234, 'START'],
['th1', 1234, 'START'],
['th2', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'Here comes'],
['th2', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'PUT', 'the mouse!'],
['th2', 1234, 'END', true],
['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling
]);
});
it('dispose handlers', () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
const dispo = parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true));
dispo.dispose();
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th1', 1234, 'START'],
['th1', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'END', true]
]);
});
});
describe('OscHandlerFactory', () => {
const TEST_PAYLOAD_LIMIT = 100;
const CHUNK_SIZE = 10;
let originalPayloadLimit: number;
beforeEach(() => {
const handlerConstructor = OscHandler as unknown as { _payloadLimit: number };
originalPayloadLimit = handlerConstructor._payloadLimit;
handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
});
afterEach(() => {
const handlerConstructor = OscHandler as unknown as { _payloadLimit: number };
handlerConstructor._payloadLimit = originalPayloadLimit;
});
it('should be called once on end(true)', () => {
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
});
it('should not be called on end(false)', () => {
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(false);
assert.deepEqual(reports, []);
});
it('should be disposable', () => {
parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
const dispo = parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start();
data = toUtf32('1234;some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', () => {
parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
it('should work up to payload limit', function(): void {
this.timeout(30000);
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
parser.put(data, 0, data.length);
}
parser.end(true);
assert.deepEqual(reports, [[1234, 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
});
it('should abort for payload limit +1', function(): void {
this.timeout(30000);
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, []);
});
});
});
class TestHandlerAsync implements IOscHandler {
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
public start(): void {
this.output.push([this.msg, this.id, 'START']);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
public async end(success: boolean): Promise<boolean> {
await Promise.resolve();
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
async function endP(parser: OscParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.end(success, prev)) {
prev = await result;
}
}
describe('OscParser - async tests', () => {
let parser: OscParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new OscParser();
parser.setHandlerFallback((id, action, data) => {
reports.push([id, action, data]);
});
});
describe('sync and async mixed', () => {
describe('sync | async | sync', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 's1'));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 's2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', false],
['s1', 1234, 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true));
parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', true],
['s1', 1234, 'END', true]
]);
});
});
describe('async | sync | async', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1'));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', false],
['s1', 1234, 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true));
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', true],
['s1', 1234, 'END', true]
]);
});
});
describe('OscHandlerFactory', () => {
it('should be called once on end(true)', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
await endP(parser, true);
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
});
it('should not be called on end(false)', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, false);
assert.deepEqual(reports, []);
});
it('should be disposable', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start();
data = toUtf32('1234;some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
});
});
describe('reset', () => {
it('should abort active handlers with end(false) when reset during payload', () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
parser.start();
let data = toUtf32('1234;partial');
parser.put(data, 0, data.length);
parser.reset();
assert.deepEqual(reports, [
['th', 1234, 'START'],
['th', 1234, 'PUT', 'partial'],
['th', 1234, 'END', false]
]);
reports.length = 0;
parser.start();
data = toUtf32('1234;complete');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th', 1234, 'START'],
['th', 1234, 'PUT', 'complete'],
['th', 1234, 'END', true]
]);
});
});
});
+237
View File
@@ -0,0 +1,237 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from './Types';
import { OscState, ParserConstants } from './Constants';
import { utf32ToString } from '../input/TextDecoder';
import { IDisposable } from '../../Types';
import { LimitedStringBuilder } from '../StringBuilder';
const EMPTY_HANDLERS: IOscHandler[] = [];
export class OscParser implements IOscParser {
private _state = OscState.START;
private _active = EMPTY_HANDLERS;
private _id = -1;
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
private _handlerFb: OscFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
public registerHandler(ident: number, handler: IOscHandler): IDisposable {
this._handlers[ident] ??= [];
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: OscFallbackHandlerType): void {
this._handlerFb = handler;
}
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
public reset(): void {
// force cleanup handlers if payload was already sent
if (this._state === OscState.PAYLOAD) {
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].end(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._id = -1;
this._state = OscState.START;
}
private _start(): void {
this._active = this._handlers[this._id] || EMPTY_HANDLERS;
if (!this._active.length) {
this._handlerFb(this._id, 'START');
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].start();
}
}
}
private _put(data: Uint32Array, start: number, end: number): void {
if (!this._active.length) {
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].put(data, start, end);
}
}
}
public start(): void {
// always reset leftover handlers
this.reset();
this._state = OscState.ID;
}
/**
* Put data to current OSC command.
* Expects the identifier of the OSC command in the form
* OSC id ; payload ST/BEL
* Payload chunks are not further processed and get
* directly passed to the handlers.
*/
public put(data: Uint32Array, start: number, end: number): void {
if (this._state === OscState.ABORT) {
return;
}
if (this._state === OscState.ID) {
while (start < end) {
const code = data[start++];
if (code === 0x3b) {
this._state = OscState.PAYLOAD;
this._start();
break;
}
if (code < 0x30 || 0x39 < code) {
this._state = OscState.ABORT;
return;
}
if (this._id === -1) {
this._id = 0;
}
this._id = this._id * 10 + code - 48;
}
}
if (this._state === OscState.PAYLOAD && end - start > 0) {
this._put(data, start, end);
}
}
/**
* Indicates end of an OSC command.
* Whether the OSC got aborted or finished normally
* is indicated by `success`.
*/
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (this._state === OscState.START) {
return;
}
// do nothing if command was faulty
if (this._state !== OscState.ABORT) {
// if we are still in ID state and get an early end
// means that the command has no payload thus we still have
// to announce START and send END right after
if (this._state === OscState.ID) {
this._start();
}
if (!this._active.length) {
this._handlerFb(this._id, 'END', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].end(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers
// we always have to call .end for proper cleanup,
// here we use `success` to indicate whether a handler should execute
for (; j >= 0; j--) {
handlerResult = this._active[j].end(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
}
this._active = EMPTY_HANDLERS;
this._id = -1;
this._state = OscState.START;
}
}
/**
* Convenient class to allow attaching string based handler functions
* as OSC handlers.
*/
export class OscHandler implements IOscHandler {
private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
private _data = new LimitedStringBuilder(OscHandler._payloadLimit);
private _hitLimit: boolean = false;
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
public start(): void {
this._data.reset();
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
if (this._data.append(utf32ToString(data, start, end))) {
this._hitLimit = true;
}
}
public end(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data.toString());
if (ret instanceof Promise) {
// need to hold data until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._data.reset();
this._hitLimit = false;
return res;
});
}
}
this._data.reset();
this._hitLimit = false;
return ret;
}
}
+237
View File
@@ -0,0 +1,237 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { Params } from './Params';
import { ParamsArray } from './Types';
class TestParams extends Params {
public get subParams(): Int32Array {
return this._subParams;
}
public get subParamsLength(): number {
return this._subParamsLength;
}
}
/** `Params` parser shim */
function parse(params: Params, s: string | string[]): void {
params.reset();
params.addParam(0);
if (typeof s === 'string') {
s = [s];
}
for (const chunk of s) {
for (let i = 0; i < chunk.length; ++i) {
let code = chunk.charCodeAt(i);
do {
switch (code) {
case 0x3b:
params.addParam(0);
break;
case 0x3a:
params.addSubParam(-1);
break;
default: // 0x30 - 0x39
params.addDigit(code - 48);
}
} while (++i < s.length && (code = chunk.charCodeAt(i)) > 0x2f && code < 0x3c);
i--;
}
}
}
describe('Params', () => {
it('should respect ctor args', () => {
const params = new TestParams(12, 23);
assert.equal(params.params.length, 12);
assert.equal(params.subParams.length, 23);
assert.deepEqual(params.toArray(), []);
});
it('addParam', () => {
const params = new TestParams();
params.addParam(1);
assert.equal(params.length, 1);
assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1]);
assert.deepEqual(params.toArray(), [1]);
params.addParam(23);
assert.equal(params.length, 2);
assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1, 23]);
assert.deepEqual(params.toArray(), [1, 23]);
assert.equal(params.subParamsLength, 0);
});
it('addSubParam', () => {
const params = new TestParams();
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
assert.equal(params.length, 1);
assert.equal(params.subParamsLength, 2);
assert.deepEqual(params.toArray(), [1, [2, 3]]);
params.addParam(12345);
params.addSubParam(-1);
assert.equal(params.length, 2);
assert.equal(params.subParamsLength, 3);
assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]);
});
it('should not add sub params without previous param', () => {
const params = new TestParams();
params.addSubParam(2);
params.addSubParam(3);
assert.equal(params.length, 0);
assert.equal(params.subParamsLength, 0);
assert.deepEqual(params.toArray(), []);
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
assert.equal(params.length, 1);
assert.equal(params.subParamsLength, 2);
assert.deepEqual(params.toArray(), [1, [2, 3]]);
});
it('reset', () => {
const params = new TestParams();
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
params.addParam(12345);
params.addSubParam(-1);
params.reset();
assert.equal(params.length, 0);
assert.equal(params.subParamsLength, 0);
assert.deepEqual(params.toArray(), []);
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
params.addParam(12345);
params.addSubParam(-1);
assert.equal(params.length, 2);
assert.equal(params.subParamsLength, 3);
assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]);
});
it('Params.fromArray --> toArray', () => {
let data: ParamsArray = [];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [1, [2, 3], 12345, [-1]];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [38, 2, 50, 100, 150];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [38, 2, 50, 100, [150]];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [38, [2, 50, 100, 150]];
assert.deepEqual(Params.fromArray(data).toArray(), data);
// strip empty sub params
data = [38, [2, 50, 100, 150], 5, [], 6];
assert.deepEqual(Params.fromArray(data).toArray(), [38, [2, 50, 100, 150], 5, 6]);
});
it('clone', () => {
const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6, 1, [2, 3], 12345, [-1]]);
assert.deepEqual(params.clone(), params);
});
it('hasSubParams / getSubParams', () => {
const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6]);
assert.equal(params.hasSubParams(0), true);
assert.deepEqual(params.getSubParams(0), new Int32Array([2, 50, 100, 150]));
assert.equal(params.hasSubParams(1), false);
assert.deepEqual(params.getSubParams(1), null);
assert.equal(params.hasSubParams(2), false);
assert.deepEqual(params.getSubParams(2), null);
});
it('getSubParamsAll', () => {
const params = Params.fromArray([1, [2, 3], 7, 12345, [-1]]);
assert.deepEqual(params.getSubParamsAll(), {0: new Int32Array([2, 3]), 2: new Int32Array([-1])});
});
describe('parse tests', () => {
it('param defaults to 0 (ZDM - zero default mode)', () => {
const params = new Params();
parse(params, '');
assert.deepEqual(params.toArray(), [0]);
});
it('sub param defaults to -1', () => {
const params = new Params();
parse(params, ':');
assert.deepEqual(params.toArray(), [0, [-1]]);
});
it('should correctly reset on new sequence', () => {
const params = new Params();
parse(params, '1;2;3');
assert.deepEqual(params.toArray(), [1, 2, 3]);
parse(params, '4');
assert.deepEqual(params.toArray(), [4]);
parse(params, '4::123:5;6;7');
assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]);
parse(params, '');
assert.deepEqual(params.toArray(), [0]);
});
it('should handle length restrictions correctly', () => {
// restrict to 3 params and 3 sub params
const params = new Params(3, 3);
parse(params, '1;2;3');
assert.deepEqual(params.toArray(), [1, 2, 3]);
parse(params, '4');
assert.deepEqual(params.toArray(), [4]);
parse(params, '4::123:5;6;7');
assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]);
parse(params, '');
assert.deepEqual(params.toArray(), [0]);
// overlong params
parse(params, '1;2;3;4;5;6;7');
assert.deepEqual(params.toArray(), [1, 2, 3]);
// overlong sub params
parse(params, '4;38:2::50:100:150;48:5:22');
assert.deepEqual(params.toArray(), [4, 38, [2, -1, 50], 48]);
});
it('typical sequences', () => {
const params = new Params();
// SGR with semicolon syntax
parse(params, '0;4;38;2;50;100;150;48;5;22');
assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, 100, 150, 48, 5, 22]);
// SGR mixed style (partly wrong)
parse(params, '0;4;38;2;50:100:150;48;5:22');
assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, [100, 150], 48, 5, [22]]);
// SGR colon style
parse(params, '0;4;38:2::50:100:150;48:5:22');
assert.deepEqual(params.toArray(), [0, 4, 38, [2, -1, 50, 100, 150], 48, [5, 22]]);
});
});
describe('should not overflow to negative', () => {
it('reject params lesser -1', () => {
const params = new Params();
params.addParam(-1);
assert.throws(() => params.addParam(-2), 'values less than -1 are not allowed');
});
it('reject subparams lesser -1', () => {
const params = new Params();
params.addParam(-1);
params.addSubParam(-1);
assert.throws(() => params.addSubParam(-2), 'values less than -1 are not allowed');
assert.deepEqual(params.toArray(), [-1, [-1]]);
});
it('clamp parsed params', () => {
const params = new Params();
parse(params, '2147483648');
assert.deepEqual(params.toArray(), [0x7FFFFFFF]);
});
it('clamp parsed subparams', () => {
const params = new Params();
parse(params, ':2147483648');
assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]);
});
});
describe('issue 2389', () => {
it('should cancel subdigits if beyond params limit', () => {
const params = new Params();
parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::');
assert.deepEqual(params.toArray(), [
0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
0, 0, 0, 0, 0, 0, 0, 0, 0, 20,
0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32]);
});
it('should carry forward isSub state', () => {
const params = new Params();
parse(params, ['1:22:33', '44']);
assert.deepEqual(params.toArray(), [1, [22, 3344]]);
});
});
});
+248
View File
@@ -0,0 +1,248 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IParams, ParamsArray } from './Types';
const enum Constants {
/**
* Max value supported for a single param/subparam (clamped to positive int32 range)
*/
MAX_VALUE = 0x7FFFFFFF,
/**
* Max allowed subparams for a single sequence (hardcoded limitation)
*/
MAX_SUBPARAMS = 256
}
/**
* Params storage class.
* This type is used by the parser to accumulate sequence parameters and sub parameters
* and transmit them to the input handler actions.
*
* NOTES:
* - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy
* - never read beyond `params.length - 1` (likely to contain arbitrary data)
* - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params
* - hardcoded limitations:
* - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that)
* - max. 256 sub params possible
* - negative values are not allowed beside -1 (placeholder for default value)
*
* About ZDM (Zero Default Mode):
* ZDM is not orchestrated by this class. If the parser is in ZDM,
* it should add 0 for empty params, otherwise -1. This does not apply
* to subparams, empty subparams should always be added with -1.
*/
export class Params implements IParams {
// params store and length
public params: Int32Array;
public length: number;
// sub params store and length
protected _subParams: Int32Array;
protected _subParamsLength: number;
// sub params offsets from param: param idx --> [start, end] offset
private _subParamsIdx: Uint16Array;
private _rejectDigits: boolean;
private _rejectSubDigits: boolean;
private _digitIsSub: boolean;
/**
* Create a `Params` type from JS array representation.
*/
public static fromArray(values: ParamsArray): Params {
const params = new Params();
if (!values.length) {
return params;
}
// skip leading sub params
for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) {
const value = values[i];
if (Array.isArray(value)) {
for (let k = 0; k < value.length; ++k) {
params.addSubParam(value[k]);
}
} else {
params.addParam(value);
}
}
return params;
}
/**
* @param maxLength max length of storable parameters
* @param maxSubParamsLength max length of storable sub parameters
*/
constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {
if (maxSubParamsLength > Constants.MAX_SUBPARAMS) {
throw new Error('maxSubParamsLength must not be greater than 256');
}
this.params = new Int32Array(maxLength);
this.length = 0;
this._subParams = new Int32Array(maxSubParamsLength);
this._subParamsLength = 0;
this._subParamsIdx = new Uint16Array(maxLength);
this._rejectDigits = false;
this._rejectSubDigits = false;
this._digitIsSub = false;
}
/**
* Clone object.
*/
public clone(): Params {
const newParams = new Params(this.maxLength, this.maxSubParamsLength);
newParams.params.set(this.params);
newParams.length = this.length;
newParams._subParams.set(this._subParams);
newParams._subParamsLength = this._subParamsLength;
newParams._subParamsIdx.set(this._subParamsIdx);
newParams._rejectDigits = this._rejectDigits;
newParams._rejectSubDigits = this._rejectSubDigits;
newParams._digitIsSub = this._digitIsSub;
return newParams;
}
/**
* Get a JS array representation of the current parameters and sub parameters.
* The array is structured as follows:
* sequence: "1;2:3:4;5::6"
* array : [1, 2, [3, 4], 5, [-1, 6]]
*/
public toArray(): ParamsArray {
const res: ParamsArray = [];
for (let i = 0; i < this.length; ++i) {
res.push(this.params[i]);
const start = this._subParamsIdx[i] >> 8;
const end = this._subParamsIdx[i] & 0xFF;
if (end - start > 0) {
res.push(Array.prototype.slice.call(this._subParams, start, end));
}
}
return res;
}
/**
* Reset to initial empty state.
*/
public reset(): void {
this.length = 0;
this._subParamsLength = 0;
this._rejectDigits = false;
this._rejectSubDigits = false;
this._digitIsSub = false;
}
/**
* Reset and add 0 as first param (ZDM).
*/
public resetZdm(): void {
this.length = 1;
this._subParamsLength = 0;
this._rejectDigits = false;
this._rejectSubDigits = false;
this._digitIsSub = false;
this._subParamsIdx[0] = 0;
this.params[0] = 0;
}
/**
* Add a parameter value.
* `Params` only stores up to `maxLength` parameters, any later
* parameter will be ignored.
* Note: VT devices only stored up to 16 values, xterm seems to
* store up to 30.
*/
public addParam(value: number): void {
this._digitIsSub = false;
if (this.length >= this.maxLength) {
this._rejectDigits = true;
return;
}
if (value < -1) {
throw new Error('values less than -1 are not allowed');
}
this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;
this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
}
/**
* Add a sub parameter value.
* The sub parameter is automatically associated with the last parameter value.
* Thus it is not possible to add a subparameter without any parameter added yet.
* `Params` only stores up to `maxSubParamsLength` sub parameters, any later
* sub parameter will be ignored.
*/
public addSubParam(value: number): void {
this._digitIsSub = true;
if (!this.length) {
return;
}
if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {
this._rejectSubDigits = true;
return;
}
if (value < -1) {
throw new Error('values less than -1 are not allowed');
}
this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
this._subParamsIdx[this.length - 1]++;
}
/**
* Whether parameter at index `idx` has sub parameters.
*/
public hasSubParams(idx: number): boolean {
return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0);
}
/**
* Return sub parameters for parameter at index `idx`.
* Note: The values are borrowed, thus you need to copy
* the values if you need to hold them in nonlocal scope.
*/
public getSubParams(idx: number): Int32Array | null {
const start = this._subParamsIdx[idx] >> 8;
const end = this._subParamsIdx[idx] & 0xFF;
if (end - start > 0) {
return this._subParams.subarray(start, end);
}
return null;
}
/**
* Return all sub parameters as {idx: subparams} mapping.
* Note: The values are not borrowed.
*/
public getSubParamsAll(): {[idx: number]: Int32Array} {
const result: {[idx: number]: Int32Array} = {};
for (let i = 0; i < this.length; ++i) {
const start = this._subParamsIdx[i] >> 8;
const end = this._subParamsIdx[i] & 0xFF;
if (end - start > 0) {
result[i] = this._subParams.slice(start, end);
}
}
return result;
}
/**
* Add a single digit value to current parameter.
* This is used by the parser to account digits on a char by char basis.
*/
public addDigit(value: number): void {
let length;
if (this._rejectDigits
|| !(length = this._digitIsSub ? this._subParamsLength : this.length)
|| (this._digitIsSub && this._rejectSubDigits)
) {
return;
}
const store = this._digitIsSub ? this._subParams : this.params;
const cur = store[length - 1];
store[length - 1] = ~cur ? Math.min(cur * 10 + value, Constants.MAX_VALUE) : value;
}
}
+285
View File
@@ -0,0 +1,285 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable, IParams, ParamsArray } from '../../Types';
import { ParserState } from './Constants';
export type { IParams, ParamsArray };
/** Params constructor type. */
export interface IParamsConstructor {
new(maxLength: number, maxSubParamsLength: number): IParams;
/** create params from ParamsArray */
fromArray(values: ParamsArray): IParams;
}
/**
* Internal state of EscapeSequenceParser.
* Used as argument of the error handler to allow
* introspection at runtime on parse errors.
* Return it with altered values to recover from
* faulty states (not yet supported).
* Set `abort` to `true` to abort the current parsing.
*/
export interface IParsingState {
// position in parse string
position: number;
// actual character code
code: number;
// current parser state
currentState: ParserState;
// collect buffer with intermediate characters
collect: number;
// params buffer
params: IParams;
// should abort (default: false)
abort: boolean;
}
/**
* Command handler interfaces.
*/
/**
* CSI handler types.
* Note: `params` is borrowed.
*/
export type CsiHandlerType = (params: IParams) => boolean | Promise<boolean>;
export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
/**
* DCS handler types.
*/
export interface IDcsHandler {
/**
* Called when a DCS command starts.
* Prepare needed data structures here.
* Note: `params` is borrowed.
*/
hook(params: IParams): void;
/**
* Incoming payload chunk.
* Note: `params` is borrowed.
*/
put(data: Uint32Array, start: number, end: number): void;
/**
* End of DCS command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
unhook(success: boolean): boolean | Promise<boolean>;
}
export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
/**
* ESC handler types.
*/
export type EscHandlerType = () => boolean | Promise<boolean>;
export type EscFallbackHandlerType = (identifier: number) => void;
/**
* EXECUTE handler types.
*/
export type ExecuteHandlerType = (ident?: number) => boolean;
export type ExecuteFallbackHandlerType = (ident: number) => void;
/**
* OSC handler types.
*/
export interface IOscHandler {
/**
* Announces start of this OSC command.
* Prepare needed data structures here.
*/
start(): void;
/**
* Incoming data chunk.
* Note: Data is borrowed.
*/
put(data: Uint32Array, start: number, end: number): void;
/**
* End of OSC command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
end(success: boolean): boolean | Promise<boolean>;
}
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
/**
* APC handler types.
*/
export interface IApcHandler {
/**
* Announces start of this APC command.
* Prepare needed data structures here.
*/
start(): void;
/**
* Incoming data chunk.
*/
put(data: Uint32Array, start: number, end: number): void;
/**
* End of APC command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
end(success: boolean): boolean | Promise<boolean>;
}
export type ApcFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
/**
* PRINT handler types.
*/
export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void;
export type PrintFallbackHandlerType = PrintHandlerType;
/**
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser extends IDisposable {
/**
* Preceding grapheme-join-state.
* Used for joining grapheme clusters across calls to `print`.
* Also used by REP to check if repeating a character is allowed.
* It gets reset by the parser for any valid sequence besides text.
*/
precedingJoinState: number; // More specifically: UnicodeJoinProperties
/**
* Reset the parser to its initial state (handlers are kept).
*/
reset(): void;
/**
* Parse UTF32 codepoints in `data` up to `length`.
* @param data The data to parse.
*/
parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean>;
/**
* Get string from numercial function identifier `ident`.
* Useful in fallback handlers which expose the low level
* numcerical function identifier for debugging purposes.
* Note: A full back translation to `IFunctionIdentifier`
* is not implemented.
*/
identToString(ident: number): string;
setPrintHandler(handler: PrintHandlerType): void;
clearPrintHandler(): void;
registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable;
clearEscHandler(id: IFunctionIdentifier): void;
setEscHandlerFallback(handler: EscFallbackHandlerType): void;
setExecuteHandler(flag: string, handler: ExecuteHandlerType): void;
clearExecuteHandler(flag: string): void;
setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void;
registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable;
clearCsiHandler(id: IFunctionIdentifier): void;
setCsiHandlerFallback(callback: CsiFallbackHandlerType): void;
registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable;
clearDcsHandler(id: IFunctionIdentifier): void;
setDcsHandlerFallback(handler: DcsFallbackHandlerType): void;
registerOscHandler(ident: number, handler: IOscHandler): IDisposable;
clearOscHandler(ident: number): void;
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable;
clearApcHandler(id: IFunctionIdentifier): void;
setApcHandlerFallback(handler: ApcFallbackHandlerType): void;
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
/**
* Subparser interfaces.
* The subparsers are instantiated in `EscapeSequenceParser` and
* called during `EscapeSequenceParser.parse`.
*/
export interface ISubParser<T, U> extends IDisposable {
reset(): void;
registerHandler(ident: number, handler: T): IDisposable;
clearHandler(ident: number): void;
setHandlerFallback(handler: U): void;
put(data: Uint32Array, start: number, end: number): void;
}
export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> {
start(): void;
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> {
hook(ident: number, params: IParams): void;
unhook(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
export interface IApcParser extends ISubParser<IApcHandler, ApcFallbackHandlerType> {
start(ident: number): void;
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
/**
* Interface to denote a specific ESC, CSI or DCS handler slot.
* The values are used to create an integer representation during handler
* registration before passed to the subparsers as `ident`.
* The integer translation is made to allow a faster handler access
* in `EscapeSequenceParser.parse`.
*/
export interface IFunctionIdentifier {
prefix?: string;
intermediates?: string;
final: string;
}
export interface IHandlerCollection<T> {
[key: string]: T[];
}
/**
* Types for async parser support.
*/
// type of saved stack state in parser
export const enum ParserStackType {
NONE = 0,
FAIL,
RESET,
CSI,
ESC,
OSC,
DCS,
APC
}
// aggregate of resumable handler lists
export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[];
// saved stack state of the parser
export interface IParserStackState {
state: ParserStackType;
handlers: ResumableHandlersType;
handlerPos: number;
transition: number;
chunkPos: number;
}
// saved stack state of subparser (OSC and DCS)
export interface ISubParserStackState {
paused: boolean;
loopPosition: number;
fallThrough: boolean;
}