diff --git a/src/common/parser/Params.test.ts b/src/common/parser/Params.test.ts new file mode 100644 index 000000000..564b0d565 --- /dev/null +++ b/src/common/parser/Params.test.ts @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { Params } from 'common/parser/Params'; + +/** `Params` parser shim */ +function parse(params: Params, s: string): void { + params.reset(); + params.addParam(0); + let isSub = false; + for (let i = 0; i < s.length; ++i) { + let code = s.charCodeAt(i); + do { + switch (code) { + case 0x3b: + params.addParam(0); + isSub = false; + break; + case 0x3a: + params.addSubParam(-1); + isSub = true; + break; + default: // 0x30 - 0x39 + if (isSub) params.addSubParamDigit(code - 48); + else params.addParamDigit(code - 48); + } + } while (++i < s.length && (code = s.charCodeAt(i)) > 0x2f && code < 0x3c); + } +} + + +describe('Params', () => { + it('should respect ctor args', () => { + const params = new Params(12, 23); + assert.equal(params.params.length, 12); + assert.equal(params.subParams.length, 23); + assert.deepEqual(params.toArray(), []); + }); + it('addParam', () => { + const params = new Params(); + 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 Params(); + 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 Params(); + 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 Params(); + 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: (number | number[])[] = []; + 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('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 Int16Array([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 Int16Array([2, 3]), 2: new Int16Array([-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]]); + }); + }); +}); diff --git a/src/common/parser/Params.ts b/src/common/parser/Params.ts new file mode 100644 index 000000000..f9031b77f --- /dev/null +++ b/src/common/parser/Params.ts @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { StringToUtf32 } from 'common/input/TextDecoder'; + +// TODO: move to Types +interface IParamsConstructor { + new(maxLength: number, maxSubParamsLength: number): IParams; + fromArray(values: (number | number[])[]): IParams; +} + +interface IParams { + /** from ctor */ + maxLength: number; + maxSubParamsLength: number; + + /** param values and its length */ + params: Int16Array; + length: number; + + /** sub params and its length */ + subParams: Int16Array; + subParamsLength: number; + + /** methods */ + clone(): IParams; + toArray(): (number | number[])[]; + reset(): void; + addParam(value: number): void; + addSubParam(value: number): void; + hasSubParams(idx: number): boolean; + getSubParams(idx: number): Int16Array | null; + getSubParamsAll(): {[idx: number]: Int16Array}; +} + +/** + * Params storage class. + * This type is used by the parser to acuumulate sequence parameters and sub parameters + * and transmit them to the input handler actions. + * Note: The params object for the handler actions is borrowed from the parser + * and will be lost after the handler exits. Use either `toArray` or `clone` to get + * a stable copy of the data. + */ +export class Params implements IParams { + // params store and length + public params: Int16Array; + public length: number; + + // sub params store and length + public subParams: Int16Array; + public subParamsLength: number; + + // sub params offsets from param: param idx --> [start, end] offset + private _subParamsIdx: Uint16Array; + private _rejectDigits: boolean; + private _rejectSubDigits: boolean; + + /** + * Create a `Params` type from JS array representation. + */ + public static fromArray(values: (number | number[])[]): Params { + const params = new Params(); + if (!values.length) { + return params; + } + // skip leading sub params + for (let i = (values[0] instanceof Array) ? 1 : 0; i < values.length; ++i) { + const value = values[i]; + if (value instanceof Array) { + 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) { + // precondition: subparams cannot be more than 256 + if (maxSubParamsLength > 256) { + throw new Error('maxSubParamsLength must not be greater than 256'); + } + this.params = new Int16Array(maxLength); + this.length = 0; + this.subParams = new Int16Array(maxSubParamsLength); + this.subParamsLength = 0; + this._subParamsIdx = new Uint16Array(maxLength); + this._rejectDigits = false; + this._rejectSubDigits = false; + } + + /** + * Clone object to its own copy. + */ + 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); + 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(): (number | number[])[] { + const res: (number | number[])[] = []; + 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; + } + + /** + * 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 { + if (this.length >= this.maxLength) { + this._rejectDigits = true; + return; + } + this._subParamsIdx[this.length] = this.subParamsLength << 8 | this.subParamsLength; + this.params[this.length++] = value; + } + + /** + * Add a sub parameter value. + * The sub parameter is automatically associated with the last parameter value. + * If there is no parameter yet the sub parameter is ingored. + * `Params` only stores up to `subParamsLength` sub parameters, any later + * sub parameter will be ignored. + */ + public addSubParam(value: number): void { + if (!this.length || this.subParamsLength >= this.maxSubParamsLength) { + this._rejectSubDigits = true; + return; + } + this.subParams[this.subParamsLength++] = 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): Int16Array | 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 kown sub parameters as {idx: subparams} mapping. + * Note: The values are not borrowed, thus it is safe to hold + * them without copying. + */ + public getSubParamsAll(): {[idx: number]: Int16Array} { + const result: {[idx: number]: Int16Array} = {}; + 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. + * Do not use this method directly, consider using `addParam` instead. + */ + public addParamDigit(value: number): void { + if (this._rejectDigits) { + return; + } + this.params[this.length - 1] = this.params[this.length - 1] * 10 + value; + } + + /** + * Add a single digit value to current sub parameter. + * This is used by the parser to account digits on a char by char basis. + * Do not use this method directly, consider using `addSubParam` instead. + */ + public addSubParamDigit(value: number): void { + if (!this.subParamsLength || this._rejectDigits || this._rejectSubDigits) { + return; + } + if (this.subParams[this.subParamsLength - 1] === -1) { + this.subParams[this.subParamsLength - 1] = value; + } else { + this.subParams[this.subParamsLength - 1] = this.subParams[this.subParamsLength - 1] * 10 + value; + } + } +}