263 lines
7.3 KiB
TypeScript
263 lines
7.3 KiB
TypeScript
/**
|
|
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
// eslint-disable-next-line
|
|
declare const Buffer: any;
|
|
|
|
export const enum HeaderState {
|
|
START = 0,
|
|
ABORT = 1,
|
|
KEY = 2,
|
|
VALUE = 3,
|
|
END = 4
|
|
}
|
|
|
|
export const enum SequenceType {
|
|
INVALID = 0,
|
|
FILE = 1,
|
|
MULTIPARTFILE = 2,
|
|
FILEPART = 3,
|
|
FILEEND = 4,
|
|
REPORTCELLSIZE = 5
|
|
}
|
|
|
|
export interface IHeaderFields {
|
|
[key: string]: number | string | Uint32Array | null | undefined;
|
|
// sequence type
|
|
type: SequenceType;
|
|
// base-64 encoded filename. Defaults to "Unnamed file".
|
|
name: string;
|
|
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
|
size: number;
|
|
/**
|
|
* Optional width and height to render:
|
|
* - N: N character cells.
|
|
* - Npx: N pixels.
|
|
* - N%: N percent of the session's width or height.
|
|
* - auto: The image's inherent size will be used to determine an appropriate dimension.
|
|
*/
|
|
width?: string;
|
|
height?: string;
|
|
// Optional, defaults to 1 respecting aspect ratio (width takes precedence).
|
|
preserveAspectRatio?: number;
|
|
// Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
|
|
// (download not supported).
|
|
inline?: number;
|
|
}
|
|
|
|
// field value decoders
|
|
|
|
// ASCII bytes to string
|
|
function toStr(data: Uint32Array): string {
|
|
let s = '';
|
|
for (let i = 0; i < data.length; ++i) {
|
|
s += String.fromCharCode(data[i]);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// digits to integer
|
|
function toInt(data: Uint32Array): number {
|
|
let v = 0;
|
|
for (let i = 0; i < data.length; ++i) {
|
|
if (data[i] < 48 || data[i] > 57) {
|
|
throw new Error('illegal char');
|
|
}
|
|
v = v * 10 + data[i] - 48;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
// check for correct size entry
|
|
function toSize(data: Uint32Array): string {
|
|
const v = toStr(data);
|
|
if (!v.match(/^((auto)|(\d+?((px)|(%)){0,1}))$/)) {
|
|
throw new Error('illegal size');
|
|
}
|
|
return v;
|
|
}
|
|
|
|
// name is base64 encoded utf-8
|
|
function toName(data: Uint32Array): string {
|
|
if (typeof Buffer !== 'undefined') {
|
|
return Buffer.from(toStr(data), 'base64').toString();
|
|
}
|
|
const bs = atob(toStr(data));
|
|
const b = new Uint8Array(bs.length);
|
|
for (let i = 0; i < b.length; ++i) {
|
|
b[i] = bs.charCodeAt(i);
|
|
}
|
|
return new TextDecoder().decode(b);
|
|
}
|
|
|
|
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
|
|
inline: toInt,
|
|
size: toInt,
|
|
name: toName,
|
|
width: toSize,
|
|
height: toSize,
|
|
preserveAspectRatio: toInt
|
|
};
|
|
|
|
|
|
// sequence type markers
|
|
// File
|
|
const FILE_MARKER = [70, 105, 108, 101];
|
|
// MultipartFile
|
|
const MULTIPARTFILE_MARKER = [77, 117, 108, 116, 105, 112, 97, 114, 116, 70, 105, 108, 101];
|
|
// FilePart
|
|
const FILEPART_MARKER = [70, 105, 108, 101, 80, 97, 114, 116];
|
|
// FileEnd
|
|
const FILEEND_MARKER = [70, 105, 108, 101, 69, 110, 100];
|
|
// ReportCellSize
|
|
const REPORTCELLSIZE_MARKER = [82, 101, 112, 111, 114, 116, 67, 101, 108, 108, 83, 105, 122, 101];
|
|
|
|
// max allowed chars for sequence header
|
|
const MAX_FIELDCHARS = 1024;
|
|
|
|
|
|
export class HeaderParser {
|
|
public state: HeaderState = HeaderState.START;
|
|
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
|
private _position = 0;
|
|
private _key = '';
|
|
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
|
|
|
|
public reset(): void {
|
|
this._buffer.fill(0);
|
|
this.state = HeaderState.START;
|
|
this._position = 0;
|
|
this.fields = {};
|
|
this._key = '';
|
|
}
|
|
|
|
public end(): number {
|
|
if (this.state === HeaderState.START) {
|
|
if (this._position === FILEEND_MARKER.length) {
|
|
for (let k = 0; k < FILEEND_MARKER.length; ++k) {
|
|
if (this._buffer[k] !== FILEEND_MARKER[k]) return this._a();
|
|
}
|
|
this.fields['type'] = SequenceType.FILEEND;
|
|
this.state = HeaderState.END;
|
|
return 0;
|
|
}
|
|
if (this._position === REPORTCELLSIZE_MARKER.length) {
|
|
for (let k = 0; k < REPORTCELLSIZE_MARKER.length; ++k) {
|
|
if (this._buffer[k] !== REPORTCELLSIZE_MARKER[k]) return this._a();
|
|
}
|
|
this.fields['type'] = SequenceType.REPORTCELLSIZE;
|
|
this.state = HeaderState.END;
|
|
return 0;
|
|
}
|
|
return this._a();
|
|
}
|
|
if (this.state === HeaderState.END) return 0;
|
|
if (this.state === HeaderState.VALUE
|
|
&& this.fields.type === SequenceType.MULTIPARTFILE
|
|
) {
|
|
if (!this._storeValue(this._position)) return this._a();
|
|
this.state = HeaderState.END;
|
|
return 0;
|
|
}
|
|
return this._a();
|
|
}
|
|
|
|
public parse(data: Uint32Array, start: number, end: number): number {
|
|
let state = this.state;
|
|
let pos = this._position;
|
|
const buffer = this._buffer;
|
|
if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
|
|
if (state === HeaderState.START && pos > 14) return -1;
|
|
for (let i = start; i < end; ++i) {
|
|
const c = data[i];
|
|
switch (c) {
|
|
case 59: // ;
|
|
if (!this._storeValue(pos)) return this._a();
|
|
state = HeaderState.KEY;
|
|
pos = 0;
|
|
break;
|
|
case 61: // =
|
|
if (state === HeaderState.START) {
|
|
if (buffer[0] === 70) {
|
|
// 'File' or 'FilePart'
|
|
let k = 0;
|
|
for (; k < FILE_MARKER.length; ++k) {
|
|
if (buffer[k] !== FILE_MARKER[k]) return this._a();
|
|
}
|
|
this.fields['type'] = SequenceType.FILE;
|
|
if (pos === FILEPART_MARKER.length) {
|
|
for (; k < FILEPART_MARKER.length; ++k) {
|
|
if (buffer[k] !== FILEPART_MARKER[k]) return this._a();
|
|
}
|
|
this.fields['type'] = SequenceType.FILEPART;
|
|
this.state = HeaderState.END;
|
|
return i + 1;
|
|
}
|
|
} else if (buffer[0] === 77) {
|
|
// 'MultipartFile'
|
|
for (let k = 0; k < MULTIPARTFILE_MARKER.length; ++k) {
|
|
if (buffer[k] !== MULTIPARTFILE_MARKER[k]) return this._a();
|
|
}
|
|
this.fields['type'] = SequenceType.MULTIPARTFILE;
|
|
} else {
|
|
return this._a();
|
|
}
|
|
state = HeaderState.KEY;
|
|
pos = 0;
|
|
} else if (state === HeaderState.KEY) {
|
|
if (!this._storeKey(pos)) return this._a();
|
|
state = HeaderState.VALUE;
|
|
pos = 0;
|
|
} else if (state === HeaderState.VALUE) {
|
|
if (pos >= MAX_FIELDCHARS) return this._a();
|
|
buffer[pos++] = c;
|
|
}
|
|
break;
|
|
case 58: // :
|
|
if (state === HeaderState.VALUE) {
|
|
if (!this._storeValue(pos)) return this._a();
|
|
}
|
|
this.state = HeaderState.END;
|
|
return i + 1;
|
|
default:
|
|
if (pos >= MAX_FIELDCHARS) return this._a();
|
|
buffer[pos++] = c;
|
|
}
|
|
}
|
|
this.state = state;
|
|
this._position = pos;
|
|
return -2;
|
|
}
|
|
|
|
private _a(): number {
|
|
this.fields.type = SequenceType.INVALID;
|
|
this.state = HeaderState.ABORT;
|
|
return -1;
|
|
}
|
|
|
|
private _storeKey(pos: number): boolean {
|
|
const k = toStr(this._buffer.subarray(0, pos));
|
|
if (k) {
|
|
this._key = k;
|
|
this.fields[k] = null;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private _storeValue(pos: number): boolean {
|
|
if (this._key) {
|
|
try {
|
|
const v = this._buffer.slice(0, pos);
|
|
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
|
|
} catch {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|