mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
fix chunked transfer without repeated i = key
This commit is contained in:
@@ -63,6 +63,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
// Storage related states
|
||||
|
||||
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
|
||||
/**
|
||||
* Tracks the pending key of the most recently started chunked upload.
|
||||
* Per spec, subsequent chunks only need m= (and optionally q=), without i=.
|
||||
* When a chunk arrives with no i=, this key is used to find the pending upload.
|
||||
*/
|
||||
private _lastPendingKey: number | undefined;
|
||||
private _nextImageId = 1;
|
||||
/** Maps Kitty protocol image ID → ImageStorage internal ID for deletion/lookup. */
|
||||
private _kittyIdToStorageId: Map<number, number> = new Map();
|
||||
@@ -91,6 +97,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
this._lastPendingKey = undefined;
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
@@ -168,8 +175,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
if (this._aborted) return;
|
||||
|
||||
// Check size limit (compare encoded bytes against pre-calculated limit)
|
||||
// Include cumulative size from pending transmission for multi-chunk images
|
||||
const pendingKey = this._parsedCommand?.id ?? 0;
|
||||
// Include cumulative size from pending transmission for multi-chunk images.
|
||||
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
|
||||
const pendingKey = this._parsedCommand?.id ?? this._lastPendingKey ?? 0;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
|
||||
this._totalEncodedSize += end - start;
|
||||
@@ -182,6 +190,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
this._activeDecoder = null;
|
||||
if (pending) {
|
||||
this._pendingTransmissions.delete(pendingKey);
|
||||
if (this._lastPendingKey === pendingKey) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
}
|
||||
this._aborted = true;
|
||||
return;
|
||||
@@ -203,6 +214,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
this._decodeError = true;
|
||||
if (pending) {
|
||||
this._pendingTransmissions.delete(pendingKey);
|
||||
if (this._lastPendingKey === pendingKey) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +243,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
return this._handleDelete(cmd);
|
||||
}
|
||||
|
||||
const pendingKey = cmd.id ?? 0;
|
||||
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
|
||||
const pendingKey = cmd.id ?? this._lastPendingKey ?? 0;
|
||||
const isMoreComing = cmd.more === 1;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
|
||||
@@ -246,11 +261,17 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
decodeError: this._decodeError
|
||||
});
|
||||
}
|
||||
this._lastPendingKey = pendingKey;
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Final chunk received — clear the last pending key
|
||||
if (pending) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
|
||||
let decodeError = this._decodeError;
|
||||
let finalCmd = cmd;
|
||||
let decoder = this._activeDecoder;
|
||||
@@ -486,6 +507,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
this._lastPendingKey = undefined;
|
||||
this._deleteAll();
|
||||
break;
|
||||
case 'i':
|
||||
@@ -496,6 +518,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
if (pending) {
|
||||
pending.decoder.release();
|
||||
this._pendingTransmissions.delete(cmd.id);
|
||||
if (this._lastPendingKey === cmd.id) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
}
|
||||
this._deleteById(cmd.id);
|
||||
}
|
||||
|
||||
@@ -225,6 +225,107 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
(window as any).smallLimitAddon.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('chunked a=T works when subsequent chunks omit i= (spec pattern)', async () => {
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=400,m=1;${part1}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await getImageStorageLength(), 0);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gm=0;${part2}\x1b\\`);
|
||||
await timeout(100);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
});
|
||||
|
||||
test('chunked a=t works when subsequent chunks omit i= (spec pattern)', async () => {
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=401,m=1;${part1}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Gm=0;${part2}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(401)`), true);
|
||||
});
|
||||
|
||||
test('chunked data without i= on subsequent chunks is assembled correctly', async () => {
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=402,m=1;${part1}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Gm=0;${part2}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const storedData = await ctx.page.evaluate(async () => {
|
||||
const blob = (window as any).imageAddon._handlers.get('kitty').images.get(402).data;
|
||||
const buffer = await blob.arrayBuffer();
|
||||
return Array.from(new Uint8Array(buffer));
|
||||
});
|
||||
deepStrictEqual(storedData, KITTY_BLACK_1X1_BYTES);
|
||||
});
|
||||
|
||||
test('three-chunk transfer with only m= on middle and last chunks', async () => {
|
||||
const third = Math.floor(KITTY_BLACK_1X1_BASE64.length / 3);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, third);
|
||||
const part2End = third + Math.floor((KITTY_BLACK_1X1_BASE64.length - third) / 2);
|
||||
const alignedPart2End = part2End - (part2End - third) % 4 + third;
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(third, alignedPart2End);
|
||||
const part3 = KITTY_BLACK_1X1_BASE64.substring(alignedPart2End);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=403,m=1;${part1}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Gm=1;${part2}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Gm=0;${part3}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const storedData = await ctx.page.evaluate(async () => {
|
||||
const blob = (window as any).imageAddon._handlers.get('kitty').images.get(403).data;
|
||||
const buffer = await blob.arrayBuffer();
|
||||
return Array.from(new Uint8Array(buffer));
|
||||
});
|
||||
deepStrictEqual(storedData, KITTY_BLACK_1X1_BYTES);
|
||||
});
|
||||
|
||||
test('chunked a=T without i= on any chunk works (no response)', async () => {
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,m=1;${part1}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await getImageStorageLength(), 0);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gm=0;${part2}\x1b\\`);
|
||||
await timeout(100);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
});
|
||||
|
||||
test('chunked transfer responds OK on final chunk when i= on first only', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=405,m=1;${part1}\x1b\\`);
|
||||
await timeout(50);
|
||||
|
||||
let response: string = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '');
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gm=0;${part2}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=405;OK\x1b\\');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Delete commands', () => {
|
||||
@@ -749,8 +850,6 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
strictEqual(response, '\x1b_Gi=204;OK\x1b\\');
|
||||
});
|
||||
|
||||
// TODO: When file-based transmission mediums (t=f, t=t, t=s) are supported,
|
||||
// update these tests to verify successful transmission instead of EINVAL.
|
||||
test('transmit rejects t=f with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
@@ -803,8 +902,6 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
strictEqual(response, '');
|
||||
});
|
||||
|
||||
// TODO: When file-based transmission mediums (t=f, t=t, t=s) are supported,
|
||||
// update these tests to verify successful transmit+display instead of EINVAL.
|
||||
test('transmit+display rejects t=f with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
|
||||
Reference in New Issue
Block a user