@xterm/addon-image 0.7.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2020, 2023 The xterm.js authors. All rights reserved.
4
+ * @license MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SixelHandler = void 0;
8
+ const Colors_1 = require("sixel/lib/Colors");
9
+ const ImageRenderer_1 = require("./ImageRenderer");
10
+ const Decoder_1 = require("sixel/lib/Decoder");
11
+ // always free decoder ressources after decoding if it exceeds this limit
12
+ const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
13
+ // custom default palette: VT340 (lower 16 colors) + ANSI256 (up to 256) + zeroed (up to 4096)
14
+ const DEFAULT_PALETTE = Colors_1.PALETTE_ANSI_256;
15
+ DEFAULT_PALETTE.set(Colors_1.PALETTE_VT340_COLOR);
16
+ class SixelHandler {
17
+ constructor(_opts, _storage, _coreTerminal) {
18
+ this._opts = _opts;
19
+ this._storage = _storage;
20
+ this._coreTerminal = _coreTerminal;
21
+ this._size = 0;
22
+ this._aborted = false;
23
+ (0, Decoder_1.DecoderAsync)({
24
+ memoryLimit: this._opts.pixelLimit * 4,
25
+ palette: DEFAULT_PALETTE,
26
+ paletteLimit: this._opts.sixelPaletteLimit
27
+ }).then(d => this._dec = d);
28
+ }
29
+ reset() {
30
+ /**
31
+ * reset sixel decoder to defaults:
32
+ * - release all memory
33
+ * - nullify palette (4096)
34
+ * - apply default palette (256)
35
+ */
36
+ if (this._dec) {
37
+ this._dec.release();
38
+ // FIXME: missing interface on decoder to nullify full palette
39
+ this._dec._palette.fill(0);
40
+ this._dec.init(0, DEFAULT_PALETTE, this._opts.sixelPaletteLimit);
41
+ }
42
+ }
43
+ hook(params) {
44
+ var _a;
45
+ this._size = 0;
46
+ this._aborted = false;
47
+ if (this._dec) {
48
+ const fillColor = params.params[1] === 1 ? 0 : extractActiveBg(this._coreTerminal._core._inputHandler._curAttrData, (_a = this._coreTerminal._core._themeService) === null || _a === void 0 ? void 0 : _a.colors);
49
+ this._dec.init(fillColor, null, this._opts.sixelPaletteLimit);
50
+ }
51
+ }
52
+ put(data, start, end) {
53
+ if (this._aborted || !this._dec) {
54
+ return;
55
+ }
56
+ this._size += end - start;
57
+ if (this._size > this._opts.sixelSizeLimit) {
58
+ console.warn(`SIXEL: too much data, aborting`);
59
+ this._aborted = true;
60
+ this._dec.release();
61
+ return;
62
+ }
63
+ try {
64
+ this._dec.decode(data, start, end);
65
+ }
66
+ catch (e) {
67
+ console.warn(`SIXEL: error while decoding image - ${e}`);
68
+ this._aborted = true;
69
+ this._dec.release();
70
+ }
71
+ }
72
+ unhook(success) {
73
+ var _a;
74
+ if (this._aborted || !success || !this._dec) {
75
+ return true;
76
+ }
77
+ const width = this._dec.width;
78
+ const height = this._dec.height;
79
+ // partial fix for https://github.com/jerch/xterm-addon-image/issues/37
80
+ if (!width || !height) {
81
+ if (height) {
82
+ this._storage.advanceCursor(height);
83
+ }
84
+ return true;
85
+ }
86
+ const canvas = ImageRenderer_1.ImageRenderer.createCanvas(undefined, width, height);
87
+ (_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
88
+ if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
89
+ this._dec.release();
90
+ }
91
+ this._storage.addImage(canvas);
92
+ return true;
93
+ }
94
+ }
95
+ exports.SixelHandler = SixelHandler;
96
+ /**
97
+ * Some helpers to extract current terminal colors.
98
+ */
99
+ // get currently active background color from terminal
100
+ // also respect INVERSE setting
101
+ function extractActiveBg(attr, colors) {
102
+ let bg = 0;
103
+ if (!colors) {
104
+ // FIXME: theme service is prolly not available yet,
105
+ // happens if .open() was not called yet (bug in core?)
106
+ return bg;
107
+ }
108
+ if (attr.isInverse()) {
109
+ if (attr.isFgDefault()) {
110
+ bg = convertLe(colors.foreground.rgba);
111
+ }
112
+ else if (attr.isFgRGB()) {
113
+ const t = attr.constructor.toColorRGB(attr.getFgColor());
114
+ bg = (0, Colors_1.toRGBA8888)(...t);
115
+ }
116
+ else {
117
+ bg = convertLe(colors.ansi[attr.getFgColor()].rgba);
118
+ }
119
+ }
120
+ else {
121
+ if (attr.isBgDefault()) {
122
+ bg = convertLe(colors.background.rgba);
123
+ }
124
+ else if (attr.isBgRGB()) {
125
+ const t = attr.constructor.toColorRGB(attr.getBgColor());
126
+ bg = (0, Colors_1.toRGBA8888)(...t);
127
+ }
128
+ else {
129
+ bg = convertLe(colors.ansi[attr.getBgColor()].rgba);
130
+ }
131
+ }
132
+ return bg;
133
+ }
134
+ // rgba values on the color managers are always in BE, thus convert to LE
135
+ function convertLe(color) {
136
+ if (Colors_1.BIG_ENDIAN)
137
+ return color;
138
+ return (color & 0xFF) << 24 | (color >>> 8 & 0xFF) << 16 | (color >>> 16 & 0xFF) << 8 | color >>> 24 & 0xFF;
139
+ }
140
+ //# sourceMappingURL=SixelHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SixelHandler.js","sourceRoot":"","sources":["../src/SixelHandler.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@xterm/addon-image",
3
+ "version": "0.7.0-beta.1",
4
+ "author": {
5
+ "name": "The xterm.js authors",
6
+ "url": "https://xtermjs.org/"
7
+ },
8
+ "main": "lib/addon-image.js",
9
+ "types": "typings/addon-image.d.ts",
10
+ "repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "terminal",
14
+ "image",
15
+ "sixel",
16
+ "xterm",
17
+ "xterm.js"
18
+ ],
19
+ "scripts": {
20
+ "prepackage": "../../node_modules/.bin/tsc -p .",
21
+ "package": "../../node_modules/.bin/webpack",
22
+ "prepublishOnly": "npm run package"
23
+ },
24
+ "peerDependencies": {
25
+ "@xterm/xterm": "^5.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "sixel": "^0.16.0",
29
+ "xterm-wasm-parts": "^0.1.0"
30
+ }
31
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+ import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
6
+ import { ImageRenderer } from './ImageRenderer';
7
+ import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
8
+ import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
9
+ import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
10
+ import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
11
+
12
+
13
+ // eslint-disable-next-line
14
+ declare const Buffer: any;
15
+
16
+ // limit hold memory in base64 decoder
17
+ const KEEP_DATA = 4194304;
18
+
19
+ // default IIP header values
20
+ const DEFAULT_HEADER: IHeaderFields = {
21
+ name: 'Unnamed file',
22
+ size: 0,
23
+ width: 'auto',
24
+ height: 'auto',
25
+ preserveAspectRatio: 1,
26
+ inline: 0
27
+ };
28
+
29
+
30
+ export class IIPHandler implements IOscHandler, IResetHandler {
31
+ private _aborted = false;
32
+ private _hp = new HeaderParser();
33
+ private _header: IHeaderFields = DEFAULT_HEADER;
34
+ private _dec = new Base64Decoder(KEEP_DATA);
35
+ private _metrics = UNSUPPORTED_TYPE;
36
+
37
+ constructor(
38
+ private readonly _opts: IImageAddonOptions,
39
+ private readonly _renderer: ImageRenderer,
40
+ private readonly _storage: ImageStorage,
41
+ private readonly _coreTerminal: ITerminalExt
42
+ ) {}
43
+
44
+ public reset(): void {}
45
+
46
+ public start(): void {
47
+ this._aborted = false;
48
+ this._header = DEFAULT_HEADER;
49
+ this._metrics = UNSUPPORTED_TYPE;
50
+ this._hp.reset();
51
+ }
52
+
53
+ public put(data: Uint32Array, start: number, end: number): void {
54
+ if (this._aborted) return;
55
+
56
+ if (this._hp.state === HeaderState.END) {
57
+ if (this._dec.put(data, start, end)) {
58
+ this._dec.release();
59
+ this._aborted = true;
60
+ }
61
+ } else {
62
+ const dataPos = this._hp.parse(data, start, end);
63
+ if (dataPos === -1) {
64
+ this._aborted = true;
65
+ return;
66
+ }
67
+ if (dataPos > 0) {
68
+ this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
69
+ if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
70
+ this._aborted = true;
71
+ return;
72
+ }
73
+ this._dec.init(this._header.size);
74
+ if (this._dec.put(data, dataPos, end)) {
75
+ this._dec.release();
76
+ this._aborted = true;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ public end(success: boolean): boolean | Promise<boolean> {
83
+ if (this._aborted) return true;
84
+
85
+ let w = 0;
86
+ let h = 0;
87
+
88
+ // early exit condition chain
89
+ let cond: number | boolean = true;
90
+ if (cond = success) {
91
+ if (cond = !this._dec.end()) {
92
+ this._metrics = imageType(this._dec.data8);
93
+ if (cond = this._metrics.mime !== 'unsupported') {
94
+ w = this._metrics.width;
95
+ h = this._metrics.height;
96
+ if (cond = w && h && w * h < this._opts.pixelLimit) {
97
+ [w, h] = this._resize(w, h).map(Math.floor);
98
+ cond = w && h && w * h < this._opts.pixelLimit;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ if (!cond) {
104
+ this._dec.release();
105
+ return true;
106
+ }
107
+
108
+ const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
109
+ this._dec.release();
110
+
111
+ if (!window.createImageBitmap) {
112
+ const url = URL.createObjectURL(blob);
113
+ const img = new Image();
114
+ return new Promise<boolean>(r => {
115
+ img.addEventListener('load', () => {
116
+ URL.revokeObjectURL(url);
117
+ const canvas = ImageRenderer.createCanvas(window.document, w, h);
118
+ canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
119
+ this._storage.addImage(canvas);
120
+ r(true);
121
+ });
122
+ img.src = url;
123
+ // sanity measure to avoid terminal blocking from dangling promise
124
+ // happens from corrupt data (onload never gets fired)
125
+ setTimeout(() => r(true), 1000);
126
+ });
127
+ }
128
+ return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
129
+ .then(bm => {
130
+ this._storage.addImage(bm);
131
+ return true;
132
+ });
133
+ }
134
+
135
+ private _resize(w: number, h: number): [number, number] {
136
+ const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
137
+ const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
138
+ const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
139
+ const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
140
+
141
+ const rw = this._dim(this._header.width!, width, cw);
142
+ const rh = this._dim(this._header.height!, height, ch);
143
+ if (!rw && !rh) {
144
+ const wf = width / w; // TODO: should this respect initial cursor offset?
145
+ const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
146
+ const f = Math.min(wf, hf);
147
+ return f < 1 ? [w * f, h * f] : [w, h];
148
+ }
149
+ return !rw
150
+ ? [w * rh / h, rh]
151
+ : this._header.preserveAspectRatio || !rw || !rh
152
+ ? [rw, h * rw / w] : [rw, rh];
153
+ }
154
+
155
+ private _dim(s: string, total: number, cdim: number): number {
156
+ if (s === 'auto') return 0;
157
+ if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
158
+ if (s.endsWith('px')) return parseInt(s.slice(0, -2));
159
+ return parseInt(s) * cdim;
160
+ }
161
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { assert } from 'chai';
7
+ import { HeaderParser, HeaderState, IHeaderFields } from './IIPHeaderParser';
8
+
9
+
10
+ const CASES: [string, IHeaderFields][] = [
11
+ ['File=size=123456;name=dGVzdA==:', {name: 'test', size: 123456}],
12
+ ['File=size=123456;name=dGVzdA:', {name: 'test', size: 123456}],
13
+ // utf-8 encoding in name
14
+ ['File=size=123456;name=w7xtbMOkdXTDnw==:', {name: 'ümläutß', size: 123456}],
15
+ ['File=size=123456;name=w7xtbMOkdXTDnw:', {name: 'ümläutß', size: 123456}],
16
+ // full header spec
17
+ [
18
+ 'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
19
+ {
20
+ inline: 1,
21
+ width: '10px',
22
+ height: '20%',
23
+ preserveAspectRatio: 1,
24
+ size: 123456,
25
+ name: 'ümläutß'
26
+ }
27
+ ],
28
+ [
29
+ 'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
30
+ {
31
+ inline: 1,
32
+ width: 'auto',
33
+ height: '20',
34
+ preserveAspectRatio: 1,
35
+ size: 123456,
36
+ name: 'ümläutß'
37
+ }
38
+ ]
39
+ ];
40
+
41
+ function fromBs(bs: string): Uint32Array {
42
+ const r = new Uint32Array(bs.length);
43
+ for (let i = 0; i < r.length; ++i) r[i] = bs.charCodeAt(i);
44
+ return r;
45
+ }
46
+
47
+ describe('IIPHeaderParser', () => {
48
+ it('at once', () => {
49
+ const hp = new HeaderParser();
50
+ for (const example of CASES) {
51
+ hp.reset();
52
+ const inp = fromBs(example[0]);
53
+ const res = hp.parse(inp, 0, inp.length);
54
+ assert.strictEqual(res, inp.length);
55
+ assert.strictEqual(hp.state, HeaderState.END);
56
+ assert.deepEqual(hp.fields, example[1]);
57
+ }
58
+ });
59
+ it('bytewise', () => {
60
+ const hp = new HeaderParser();
61
+ for (const example of CASES) {
62
+ hp.reset();
63
+ const inp = fromBs(example[0]);
64
+ let pos = 0;
65
+ let res = -2;
66
+ while (res === -2 && pos < inp.length) {
67
+ res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
68
+ }
69
+ assert.strictEqual(res, 1);
70
+ assert.strictEqual(hp.state, HeaderState.END);
71
+ assert.deepEqual(hp.fields, example[1]);
72
+ }
73
+ });
74
+ it('no File= starter', () => {
75
+ const hp = new HeaderParser();
76
+ let inp = fromBs('size=123456;name=dGVzdA==:');
77
+ let res = hp.parse(inp, 0, inp.length);
78
+ assert.strictEqual(res, -1);
79
+ hp.reset();
80
+ inp = fromBs(CASES[0][0]);
81
+ res = hp.parse(inp, 0, inp.length);
82
+ assert.strictEqual(res, inp.length);
83
+ assert.strictEqual(hp.state, HeaderState.END);
84
+ assert.deepEqual(hp.fields, CASES[0][1]);
85
+ });
86
+ it('empty key - error', () => {
87
+ const hp = new HeaderParser();
88
+ let inp = fromBs('File=size=123456;=dGVzdA==:');
89
+ let res = hp.parse(inp, 0, inp.length);
90
+ assert.strictEqual(res, -1);
91
+ hp.reset();
92
+ inp = fromBs(CASES[0][0]);
93
+ res = hp.parse(inp, 0, inp.length);
94
+ assert.strictEqual(res, inp.length);
95
+ assert.strictEqual(hp.state, HeaderState.END);
96
+ assert.deepEqual(hp.fields, CASES[0][1]);
97
+ });
98
+ it('empty size value - set to 0', () => {
99
+ const hp = new HeaderParser();
100
+ let inp = fromBs('File=size=;name=dGVzdA==:');
101
+ let res = hp.parse(inp, 0, inp.length);
102
+ assert.strictEqual(res, inp.length);
103
+ assert.strictEqual(hp.state, HeaderState.END);
104
+ assert.deepEqual(hp.fields, {name: 'test', size: 0});
105
+ hp.reset();
106
+ inp = fromBs(CASES[0][0]);
107
+ res = hp.parse(inp, 0, inp.length);
108
+ assert.strictEqual(res, inp.length);
109
+ assert.strictEqual(hp.state, HeaderState.END);
110
+ assert.deepEqual(hp.fields, CASES[0][1]);
111
+ });
112
+ it('empty name value - set to empty string', () => {
113
+ const hp = new HeaderParser();
114
+ let inp = fromBs('File=size=123456;name=:');
115
+ let res = hp.parse(inp, 0, inp.length);
116
+ assert.strictEqual(res, inp.length);
117
+ assert.strictEqual(hp.state, HeaderState.END);
118
+ assert.deepEqual(hp.fields, {name: '', size: 123456});
119
+ hp.reset();
120
+ inp = fromBs(CASES[0][0]);
121
+ res = hp.parse(inp, 0, inp.length);
122
+ assert.strictEqual(res, inp.length);
123
+ assert.strictEqual(hp.state, HeaderState.END);
124
+ assert.deepEqual(hp.fields, CASES[0][1]);
125
+ });
126
+ it('empty size value - error', () => {
127
+ const hp = new HeaderParser();
128
+ let inp = fromBs('File=inline=1;width=;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:');
129
+ let res = hp.parse(inp, 0, inp.length);
130
+ assert.strictEqual(res, -1);
131
+ hp.reset();
132
+ inp = fromBs(CASES[0][0]);
133
+ res = hp.parse(inp, 0, inp.length);
134
+ assert.strictEqual(res, inp.length);
135
+ assert.strictEqual(hp.state, HeaderState.END);
136
+ assert.deepEqual(hp.fields, CASES[0][1]);
137
+ });
138
+ });
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ // eslint-disable-next-line
7
+ declare const Buffer: any;
8
+
9
+
10
+ export interface IHeaderFields {
11
+ // base-64 encoded filename. Defaults to "Unnamed file".
12
+ name: string;
13
+ // File size in bytes. The file transfer will be canceled if this size is exceeded.
14
+ size: number;
15
+ /**
16
+ * Optional width and height to render:
17
+ * - N: N character cells.
18
+ * - Npx: N pixels.
19
+ * - N%: N percent of the session's width or height.
20
+ * - auto: The image's inherent size will be used to determine an appropriate dimension.
21
+ */
22
+ width?: string;
23
+ height?: string;
24
+ // Optional, defaults to 1 respecting aspect ratio (width takes precedence).
25
+ preserveAspectRatio?: number;
26
+ // Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
27
+ // (download not supported).
28
+ inline?: number;
29
+ }
30
+
31
+ export const enum HeaderState {
32
+ START = 0,
33
+ ABORT = 1,
34
+ KEY = 2,
35
+ VALUE = 3,
36
+ END = 4
37
+ }
38
+
39
+ // field value decoders
40
+
41
+ // ASCII bytes to string
42
+ function toStr(data: Uint32Array): string {
43
+ let s = '';
44
+ for (let i = 0; i < data.length; ++i) {
45
+ s += String.fromCharCode(data[i]);
46
+ }
47
+ return s;
48
+ }
49
+
50
+ // digits to integer
51
+ function toInt(data: Uint32Array): number {
52
+ let v = 0;
53
+ for (let i = 0; i < data.length; ++i) {
54
+ if (data[i] < 48 || data[i] > 57) {
55
+ throw new Error('illegal char');
56
+ }
57
+ v = v * 10 + data[i] - 48;
58
+ }
59
+ return v;
60
+ }
61
+
62
+ // check for correct size entry
63
+ function toSize(data: Uint32Array): string {
64
+ const v = toStr(data);
65
+ if (!v.match(/^((auto)|(\d+?((px)|(%)){0,1}))$/)) {
66
+ throw new Error('illegal size');
67
+ }
68
+ return v;
69
+ }
70
+
71
+ // name is base64 encoded utf-8
72
+ function toName(data: Uint32Array): string {
73
+ if (typeof Buffer !== 'undefined') {
74
+ return Buffer.from(toStr(data), 'base64').toString();
75
+ }
76
+ const bs = atob(toStr(data));
77
+ const b = new Uint8Array(bs.length);
78
+ for (let i = 0; i < b.length; ++i) {
79
+ b[i] = bs.charCodeAt(i);
80
+ }
81
+ return new TextDecoder().decode(b);
82
+ }
83
+
84
+ const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
85
+ inline: toInt,
86
+ size: toInt,
87
+ name: toName,
88
+ width: toSize,
89
+ height: toSize,
90
+ preserveAspectRatio: toInt
91
+ };
92
+
93
+
94
+ const FILE_MARKER = [70, 105, 108, 101];
95
+ const MAX_FIELDCHARS = 1024;
96
+
97
+
98
+ export class HeaderParser {
99
+ public state: HeaderState = HeaderState.START;
100
+ private _buffer = new Uint32Array(MAX_FIELDCHARS);
101
+ private _position = 0;
102
+ private _key = '';
103
+ public fields: {[key: string]: any} = {};
104
+
105
+ public reset(): void {
106
+ this._buffer.fill(0);
107
+ this.state = HeaderState.START;
108
+ this._position = 0;
109
+ this.fields = {};
110
+ this._key = '';
111
+ }
112
+
113
+ public parse(data: Uint32Array, start: number, end: number): number {
114
+ let state = this.state;
115
+ let pos = this._position;
116
+ const buffer = this._buffer;
117
+ if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
118
+ if (state === HeaderState.START && pos > 6) return -1;
119
+ for (let i = start; i < end; ++i) {
120
+ const c = data[i];
121
+ switch (c) {
122
+ case 59: // ;
123
+ if (!this._storeValue(pos)) return this._a();
124
+ state = HeaderState.KEY;
125
+ pos = 0;
126
+ break;
127
+ case 61: // =
128
+ if (state === HeaderState.START) {
129
+ for (let k = 0; k < FILE_MARKER.length; ++k) {
130
+ if (buffer[k] !== FILE_MARKER[k]) return this._a();
131
+ }
132
+ state = HeaderState.KEY;
133
+ pos = 0;
134
+ } else if (state === HeaderState.KEY) {
135
+ if (!this._storeKey(pos)) return this._a();
136
+ state = HeaderState.VALUE;
137
+ pos = 0;
138
+ } else if (state === HeaderState.VALUE) {
139
+ if (pos >= MAX_FIELDCHARS) return this._a();
140
+ buffer[pos++] = c;
141
+ }
142
+ break;
143
+ case 58: // :
144
+ if (state === HeaderState.VALUE) {
145
+ if (!this._storeValue(pos)) return this._a();
146
+ }
147
+ this.state = HeaderState.END;
148
+ return i + 1;
149
+ default:
150
+ if (pos >= MAX_FIELDCHARS) return this._a();
151
+ buffer[pos++] = c;
152
+ }
153
+ }
154
+ this.state = state;
155
+ this._position = pos;
156
+ return -2;
157
+ }
158
+
159
+ private _a(): number {
160
+ this.state = HeaderState.ABORT;
161
+ return -1;
162
+ }
163
+
164
+ private _storeKey(pos: number): boolean {
165
+ const k = toStr(this._buffer.subarray(0, pos));
166
+ if (k) {
167
+ this._key = k;
168
+ this.fields[k] = null;
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ private _storeValue(pos: number): boolean {
175
+ if (this._key) {
176
+ try {
177
+ const v = this._buffer.slice(0, pos);
178
+ this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
179
+ } catch (e) {
180
+ return false;
181
+ }
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+ }