fix OSC parsing

This commit is contained in:
Jörg Breitbart
2019-07-25 20:38:14 +02:00
parent d8364d749c
commit f626e1c93d
9 changed files with 561 additions and 110 deletions
+8 -6
View File
@@ -21,6 +21,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { IAttributeData, IDisposable } from 'common/Types';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services';
import { ISelectionService } from 'browser/services/Services';
import { OscHandlerFactory } from 'common/parser/OscParser';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -152,9 +153,10 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.setExecuteHandlerFallback((code: number) => {
this._logService.debug('Unknown EXECUTE code: ', { code });
});
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
this._logService.debug('Unknown OSC code: ', { identifier, data });
});
this._parser.setOscHandlerFallback((identifier, action, data) => {
this._logService.debug('Unknown OSC code: ', { identifier, action, data });
}
);
/**
* print handler
@@ -224,10 +226,10 @@ export class InputHandler extends Disposable implements IInputHandler {
* OSC handler
*/
// 0 - icon name + title
this._parser.setOscHandler(0, (data) => this.setTitle(data));
this._parser.setOscHandler(0, new OscHandlerFactory((data: string) => this.setTitle(data)));
// 1 - icon name
// 2 - title
this._parser.setOscHandler(2, (data) => this.setTitle(data));
this._parser.setOscHandler(2, new OscHandlerFactory((data: string) => this.setTitle(data)));
// 3 - set property X in the form "prop=value"
// 4 - Change Color Number
// 5 - Change Special Color Number
@@ -485,7 +487,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Forward addOscHandler from parser.
*/
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._parser.addOscHandler(ident, callback);
return this._parser.addOscHandler(ident, new OscHandlerFactory(callback));
}
/**
+10
View File
@@ -43,3 +43,13 @@ export const enum ParserAction {
DCS_PUT = 13,
DCS_UNHOOK = 14
}
/**
* Internal states of OscParser.
*/
export const enum OscState {
START = 0,
ID = 1,
PAYLOAD = 2,
ABORT = 3
}
+67 -29
View File
@@ -3,12 +3,14 @@
* @license MIT
*/
import { IDcsHandler, IParsingState, IParams, ParamsArray } from 'common/parser/Types';
import { IDcsHandler, IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types';
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
import * as chai from 'chai';
import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
import { ParserState } from 'common/parser/Constants';
import { Params } from 'common/parser/Params';
import { OscHandlerFactory } from 'common/parser/OscParser';
import { IDisposable } from 'common/Types';
function r(a: number, b: number): string[] {
@@ -20,13 +22,45 @@ function r(a: number, b: number): string[] {
return arr;
}
class MockOscPutParser implements IOscParser {
private _fallback: OscFallbackHandler = () => {};
public data = '';
public reset(): void {
this.data = '';
}
public put(data: Uint32Array, start: number, end: number): void {
this.data += utf32ToString(data, start, end);
}
public dispose(): void { }
public start(): void { }
public end(): void {
const id = parseInt(this.data.slice(0, this.data.indexOf(';')));
if (!isNaN(id)) {
this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1));
}
}
addOscHandler(ident: number, handler: IOscHandler): IDisposable {
throw new Error('not implemented');
}
setOscHandler(ident: number, handler: IOscHandler): void {
throw new Error('not implemented');
}
clearOscHandler(ident: number): void {
throw new Error('not implemented');
}
setOscHandlerFallback(handler: OscFallbackHandler): void {
this._fallback = handler;
}
}
const oscPutParser = new MockOscPutParser();
// derived parser with access to internal states
class TestEscapeSequenceParser extends EscapeSequenceParser {
public get osc(): string {
return this._osc;
return (this._oscParser as MockOscPutParser).data;
}
public set osc(value: string) {
this._osc = value;
(this._oscParser as MockOscPutParser).data = value;
}
public get params(): ParamsArray {
return this._params.toArray();
@@ -46,6 +80,9 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
public mockActiveDcsHandler(): void {
this._activeDcsHandler = this._dcsHandlerFb;
}
public mockOscParser(): void {
this._oscParser = oscPutParser;
}
}
// test object to collect parser actions and compare them with expected values
@@ -124,6 +161,7 @@ let state: any;
// parser with Uint8Array based transition table
const testParser = new TestEscapeSequenceParser();
testParser.mockOscParser();
testParser.setPrintHandler(testTerminal.print.bind(testTerminal));
testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => {
testTerminal.actionCSI(collect, params, String.fromCharCode(flag));
@@ -134,9 +172,9 @@ testParser.setEscHandlerFallback((collect: string, flag: number) => {
testParser.setExecuteHandlerFallback((code: number) => {
testTerminal.actionExecute(String.fromCharCode(code));
});
testParser.setOscHandlerFallback((identifier: number, data: string) => {
testParser.setOscHandlerFallback((identifier, action, data) => {
if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently
else testTerminal.actionOSC('' + identifier + ';' + data);
else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END
});
testParser.setDcsHandlerFallback(new DcsTest());
@@ -1026,9 +1064,9 @@ describe('EscapeSequenceParser', function (): void {
], null);
});
it('print + OSC(C1) + print', function (): void {
test('abc\x9d123tzf\x9cdefg', [
test('abc\x9d123;tzf\x9cdefg', [
['print', 'abc'],
['osc', '123tzf'],
['osc', '123;tzf'],
['print', 'defg']
], null);
});
@@ -1039,9 +1077,9 @@ describe('EscapeSequenceParser', function (): void {
], null);
});
it('7bit ST should be swallowed', function (): void {
test('abc\x9d123tzf\x1b\\defg', [
test('abc\x9d123;tzf\x1b\\defg', [
['print', 'abc'],
['osc', '123tzf'],
['osc', '123;tzf'],
['print', 'defg']
], null);
});
@@ -1256,9 +1294,9 @@ describe('EscapeSequenceParser', function (): void {
chai.expect(exe).eql(['\n']);
});
it('OSC handler', function (): void {
parser2.setOscHandler(1, function (data: string): void {
parser2.setOscHandler(1, new OscHandlerFactory(function (data: string): void {
osc.push([1, data]);
});
}));
parse(parser2, INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
parser2.clearOscHandler(1);
@@ -1270,16 +1308,16 @@ describe('EscapeSequenceParser', function (): void {
describe('OSC custom handlers', () => {
it('Prevent fallback', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
parse(parser2, INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
});
it('Allow fallback', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; });
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return false; }));
parse(parser2, INPUT);
chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
@@ -1287,9 +1325,9 @@ describe('EscapeSequenceParser', function (): void {
it('Multiple custom handlers fallback once', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; });
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return false; }));
parse(parser2, INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
@@ -1298,9 +1336,9 @@ describe('EscapeSequenceParser', function (): void {
it('Multiple custom handlers no fallback', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; });
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return true; }));
parse(parser2, INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([], 'Should not fallback once');
@@ -1308,16 +1346,16 @@ describe('EscapeSequenceParser', function (): void {
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.setOscHandler(1, () => order.push(1));
parser2.addOscHandler(1, () => { order.push(2); return false; });
parser2.addOscHandler(1, () => { order.push(3); return false; });
parser2.setOscHandler(1, new OscHandlerFactory(() => order.push(1)));
parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(2); return false; }));
parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(3); return false; }));
parse(parser2, '\x1b]1;foo=bar\x1b\\');
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
customHandler.dispose();
parse(parser2, INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
@@ -1325,8 +1363,8 @@ describe('EscapeSequenceParser', function (): void {
});
it('Should not corrupt the parser when dispose is called twice', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
customHandler.dispose();
customHandler.dispose();
parse(parser2, INPUT);
+21 -69
View File
@@ -3,20 +3,13 @@
* @license MIT
*/
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser } from 'common/parser/Types';
import { ParserState, ParserAction } from 'common/parser/Constants';
import { Disposable } from 'common/Lifecycle';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
import { fill } from 'common/TypedArrayUtils';
import { Params } from 'common/parser/Params';
interface IHandlerCollection<T> {
[key: string]: T[];
}
type CsiHandler = (params: IParams, collect: string) => boolean | void;
type OscHandler = (data: string) => boolean | void;
import { OscParser } from 'common/parser/OscParser';
/**
* Table values are generated like this:
@@ -240,7 +233,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
public precedingCodepoint: number;
// buffers over several parse calls
protected _osc: string;
protected _params: Params;
protected _collect: string;
@@ -249,7 +241,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _executeHandlers: any;
protected _csiHandlers: IHandlerCollection<CsiHandler>;
protected _escHandlers: any;
protected _oscHandlers: IHandlerCollection<OscHandler>;
protected _oscParser: IOscParser;
protected _dcsHandlers: any;
protected _activeDcsHandler: IDcsHandler;
protected _errorHandler: (state: IParsingState) => IParsingState;
@@ -259,7 +251,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _executeHandlerFb: (code: number) => void;
protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void;
protected _escHandlerFb: (collect: string, flag: number) => void;
protected _oscHandlerFb: (identifier: number, data: string) => void;
protected _dcsHandlerFb: IDcsHandler;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
@@ -268,7 +259,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
this._params = new Params(); // defaults to 32 storable params/subparams
this._params.addParam(0); // ZDM
this._collect = '';
@@ -279,14 +269,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlerFb = (code: number): void => { };
this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { };
this._escHandlerFb = (collect: string, flag: number): void => { };
this._oscHandlerFb = (identifier: number, data: string): void => { };
this._dcsHandlerFb = new DcsDummy();
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
this._printHandler = this._printHandlerFb;
this._executeHandlers = Object.create(null);
this._csiHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._oscHandlers = Object.create(null);
this._oscParser = new OscParser();
this._dcsHandlers = Object.create(null);
this._activeDcsHandler = this._dcsHandlerFb;
this._errorHandler = this._errorHandlerFb;
@@ -300,6 +289,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._escHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = new DcsDummy();
this._oscParser.dispose();
}
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void {
@@ -355,29 +345,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._escHandlerFb = callback;
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
if (this._oscHandlers[ident] === undefined) {
this._oscHandlers[ident] = [];
}
const handlerList = this._oscHandlers[ident];
handlerList.push(callback);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(callback);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
addOscHandler(ident: number, handler: IOscHandler): IDisposable {
return this._oscParser.addOscHandler(ident, handler);
}
setOscHandler(ident: number, callback: (data: string) => void): void {
this._oscHandlers[ident] = [callback];
setOscHandler(ident: number, handler: IOscHandler): void {
this._oscParser.setOscHandler(ident, handler);
}
clearOscHandler(ident: number): void {
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
this._oscParser.clearOscHandler(ident);
}
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void {
this._oscHandlerFb = callback;
setOscHandlerFallback(handler: OscFallbackHandler): void {
this._oscParser.setOscHandlerFallback(handler);
}
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void {
@@ -404,7 +382,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
reset(): void {
this.currentState = this.initialState;
this._osc = '';
this._oscParser.reset();
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = '';
@@ -430,7 +408,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
let code = 0;
let transition = 0;
let currentState = this.currentState;
let osc = this._osc;
const osc = this._oscParser;
let collect = this._collect;
const params = this._params;
const table: Uint8Array = this.TRANSITIONS.table;
@@ -484,7 +462,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
position: i,
code,
currentState,
osc,
osc: '', // FIXME: what to send here?
collect,
params,
abort: false
@@ -537,7 +515,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this.precedingCodepoint = 0;
break;
case ParserAction.CLEAR:
osc = '';
osc.reset();
params.reset();
params.addParam(0); // ZDM
collect = '';
@@ -562,54 +540,29 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
dcsHandler.unhook();
dcsHandler = this._dcsHandlerFb;
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
osc.reset();
params.reset();
params.addParam(0); // ZDM
collect = '';
this.precedingCodepoint = 0;
break;
case ParserAction.OSC_START:
osc = '';
osc.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 <= 0x9f)) {
osc += utf32ToString(data, i, j);
osc.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.OSC_END:
if (osc && code !== 0x18 && code !== 0x1a) {
// NOTE: OSC subparsing is not part of the original parser
// we do basic identifier parsing here to offer a jump table for OSC as well
const idx = osc.indexOf(';');
if (idx === -1) {
this._oscHandlerFb(-1, osc); // this is an error (malformed OSC)
} else {
// Note: NaN is not handled here
// either catch it with the fallback handler
// or with an explicit NaN OSC handler
const identifier = parseInt(osc.substring(0, idx));
const content = osc.substring(idx + 1);
// Trigger OSC Handler
const handlers = this._oscHandlers[identifier];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
// undefined or true means success and to stop bubbling
if (handlers[j](content) !== false) {
break;
}
}
if (j < 0) {
this._oscHandlerFb(identifier, content);
}
}
}
osc.end(code !== 0x18 && code !== 0x1a);
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
osc.reset();
params.reset();
params.addParam(0); // ZDM
collect = '';
@@ -620,7 +573,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
// save non pushable buffers
this._osc = osc;
this._collect = collect;
this._params = params;
+222
View File
@@ -0,0 +1,222 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { OscParser, OscHandlerFactory } from 'common/parser/OscParser';
import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
import { IOscHandler } from 'common/parser/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) {}
start(): void {
this.output.push([this.msg, this.id, 'START']);
}
put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
end(success: boolean): void | boolean {
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
}
}
describe('OscParser', () => {
let parser: OscParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new OscParser();
parser.setOscHandlerFallback((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.setOscHandler(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.setOscHandler(1234, new TestHandler(1234, reports, 'th'));
parser.clearOscHandler(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.setOscHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.addOscHandler(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.setOscHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.addOscHandler(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.setOscHandler(1234, new TestHandler(1234, reports, 'th1'));
const dispo = parser.addOscHandler(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', () => {
it('should be called once on end(true)', () => {
parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data])));
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.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data])));
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.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data])));
const dispo = parser.addOscHandler(1234, new OscHandlerFactory(data => reports.push(['two', data])));
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.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data])));
parser.addOscHandler(1234, new OscHandlerFactory(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!']]);
});
});
});
+186
View File
@@ -0,0 +1,186 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IOscHandler, IHandlerCollection, OscFallbackHandler } from 'common/parser/Types';
import { OscState } from 'common/parser/Constants';
import { Disposable } from 'common/Lifecycle';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
export class OscParser extends Disposable {
private _state = OscState.START;
private _id = -1;
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
private _handlerFb: OscFallbackHandler = () => { };
addOscHandler(ident: number, handler: IOscHandler): IDisposable {
if (this._handlers[ident] === undefined) {
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);
}
}
};
}
setOscHandler(ident: number, handler: IOscHandler): void {
this._handlers[ident] = [handler];
}
clearOscHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
setOscHandlerFallback(handler: OscFallbackHandler): void {
this._handlerFb = handler;
}
public dispose(): void {
this._handlers = {};
}
public reset(): void {
// cleanup handlers if payload was already sent
if (this._state === OscState.PAYLOAD) {
this.end(false);
}
this._id = -1;
this._state = OscState.START;
}
private _start(): void {
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'START');
} else {
for (let j = handlers.length - 1; j >= 0; j--) {
handlers[j].start();
}
}
}
private _put(data: Uint32Array, start: number, end: number): void {
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = handlers.length - 1; j >= 0; j--) {
handlers[j].put(data, start, end);
}
}
}
private _end(success: boolean): void {
// other than the old code we always have to call .end
// to keep the bubbling we use `success` to indicate
// whether a handler should execute
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'END', success);
} else {
let j = handlers.length - 1;
for (; j >= 0; j--) {
if (handlers[j].end(success) !== false) {
break;
}
}
j--;
// cleanup left over handlers
for (; j >= 0; j--) {
handlers[j].end(false);
}
}
}
public start(): void {
this._id = -1;
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): void {
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();
}
this._end(success);
}
this._id = -1;
this._state = OscState.START;
}
}
/**
* Convenient class to allow attaching string based handler functions
* as OSC handlers.
*/
export class OscHandlerFactory implements IOscHandler {
private _data = '';
constructor(private _handler: (data: string) => any) {}
public start(): void {
this._data = '';
}
public put(data: Uint32Array, start: number, end: number): void {
this._data += utf32ToString(data, start, end);
}
public end(success: boolean): any {
let ret;
if (success) {
ret = this._handler(this._data);
}
this._data = '';
return ret;
}
}
+43 -3
View File
@@ -63,6 +63,12 @@ export interface IParsingState {
abort: boolean;
}
export interface IHandlerCollection<T> {
[key: string]: T[];
}
export type CsiHandler = (params: IParams, collect: string) => boolean | void;
/**
* DCS handler signature for EscapeSequenceParser.
* EscapeSequenceParser handles DCS commands via separate
@@ -90,6 +96,29 @@ export interface IDcsHandler {
unhook(): void;
}
export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
export interface IOscHandler {
/**
* Announces start of this OSC command.
* Prepare needed data structures here.
*/
start(): void;
/**
* Incoming data chunk.
*/
put(data: Uint32Array, start: number, end: number): void;
/**
* End of OSC command. `success` indicates whether the
* command finished normally or got aborted, thus execution
* of the command should depend on `success`.
* To save memory cleanup data structures in `.end`.
*/
end(success: boolean): void | boolean;
}
/**
* EscapeSequenceParser interface.
*/
@@ -123,15 +152,15 @@ export interface IEscapeSequenceParser extends IDisposable {
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
addOscHandler(ident: number, handler: IOscHandler): IDisposable;
setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
setOscHandler(ident: number, callback: (data: string) => void): void;
setOscHandler(ident: number, handler: IOscHandler): void;
clearOscHandler(ident: number): void;
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
setOscHandlerFallback(handler: OscFallbackHandler): void;
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
clearDcsHandler(collectAndFlag: string): void;
@@ -140,3 +169,14 @@ export interface IEscapeSequenceParser extends IDisposable {
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
export interface IOscParser extends IDisposable {
addOscHandler(ident: number, handler: IOscHandler): IDisposable;
setOscHandler(ident: number, handler: IOscHandler): void;
clearOscHandler(ident: number): void;
setOscHandlerFallback(handler: OscFallbackHandler): void;
reset(): void;
start(): void;
put(data: Uint32Array, start: number, end: number): void;
end(success: boolean): void;
}
@@ -7,6 +7,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
import { C0, C1 } from 'common/data/EscapeSequences';
import { IDcsHandler, IParams } from 'common/parser/Types';
import { OscHandlerFactory } from 'common/parser/OscParser';
function toUtf32(s: string): Uint32Array {
@@ -79,8 +80,8 @@ perfContext('Parser throughput - 50MB data', () => {
parser.setExecuteHandler(C1.IND, () => {});
parser.setExecuteHandler(C1.NEL, () => {});
parser.setExecuteHandler(C1.HTS, () => {});
parser.setOscHandler(0, (data) => {});
parser.setOscHandler(2, (data) => {});
parser.setOscHandler(0, new OscHandlerFactory((data) => {}));
parser.setOscHandler(2, new OscHandlerFactory((data) => {}));
parser.setEscHandler('7', () => {});
parser.setEscHandler('8', () => {});
parser.setEscHandler('D', () => {});
+1 -1
View File
@@ -516,7 +516,7 @@ declare module 'xterm' {
addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable;
/**
* (EXPERIMENTAL) Adds a handler for OSC escape sequences.
* Adds a handler for OSC escape sequences.
* @param ident The number (first parameter) of the sequence.
* @param callback The function to handle the escape sequence. The callback
* is called with OSC data string. Return true if the sequence was handled;