From 5f724c59247b723c9880ea4a6025ca3c6a517f32 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Mon, 2 Feb 2026 20:17:21 -0800 Subject: [PATCH] Reduce MAX_CONTROL_DATA_SIZE + EINVAL for i and I --- .../src/kitty/KittyGraphicsHandler.ts | 16 +++++++++- .../src/kitty/KittyGraphicsTypes.ts | 4 +++ addons/addon-image/test/ImageAddon.test.ts | 30 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts index 028f58d8..f9843644 100644 --- a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts +++ b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts @@ -30,7 +30,7 @@ const BASE64_SHARD_SIZE = 1048576; const DECODED_SHARD_SIZE = 786432; // Maximum control data size -const MAX_CONTROL_DATA_SIZE = 4096; +const MAX_CONTROL_DATA_SIZE = 512; // Semicolon codepoint const SEMICOLON = 0x3B; @@ -211,6 +211,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler { // Parse command to check m=1 and get pending key const cmd = parseKittyCommand(this._parseControlDataString()); + + // Per spec: specifying both i and I is an error + if (cmd.id !== undefined && cmd.imageNumber !== undefined) { + this._sendResponse(cmd.id, 'EINVAL:cannot specify both i and I keys', cmd.quiet ?? 0); + return true; + } + const pendingKey = cmd.id ?? 0; const isMoreComing = cmd.more === 1; const pending = this._pendingTransmissions.get(pendingKey); @@ -381,6 +388,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler { private _handleNoPayloadCommand(): boolean | Promise { const cmd = parseKittyCommand(this._parseControlDataString()); + + // Per spec: specifying both i and I is an error + if (cmd.id !== undefined && cmd.imageNumber !== undefined) { + this._sendResponse(cmd.id, 'EINVAL:cannot specify both i and I keys', cmd.quiet ?? 0); + return true; + } + const action = cmd.action ?? 't'; switch (action) { diff --git a/addons/addon-image/src/kitty/KittyGraphicsTypes.ts b/addons/addon-image/src/kitty/KittyGraphicsTypes.ts index 17ebefc1..ef91bf57 100644 --- a/addons/addon-image/src/kitty/KittyGraphicsTypes.ts +++ b/addons/addon-image/src/kitty/KittyGraphicsTypes.ts @@ -47,6 +47,8 @@ export const enum KittyKey { FORMAT = 'f', // Image ID for referencing stored images ID = 'i', + // Image number (alternative to ID, terminal assigns ID) + IMAGE_NUMBER = 'I', // Source image width in pixels WIDTH = 's', // Source image height in pixels @@ -79,6 +81,7 @@ export interface IKittyCommand { action?: string; format?: number; id?: number; + imageNumber?: number; width?: number; height?: number; x?: number; @@ -148,6 +151,7 @@ export function parseKittyCommand(data: string): IKittyCommand { switch (key) { case KittyKey.FORMAT: cmd.format = numValue; break; case KittyKey.ID: cmd.id = numValue; break; + case KittyKey.IMAGE_NUMBER: cmd.imageNumber = numValue; break; case KittyKey.WIDTH: cmd.width = numValue; break; case KittyKey.HEIGHT: cmd.height = numValue; break; case KittyKey.X_OFFSET: cmd.x = numValue; break; diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts index 923da858..d07990dc 100644 --- a/addons/addon-image/test/ImageAddon.test.ts +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -504,6 +504,36 @@ test.describe('ImageAddon', () => { strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false); }); + + test('responds with EINVAL when both i and I keys are specified', async () => { + let response = ''; + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + // Per spec: "Specifying both i and I keys in any command is an error" + await ctx.proxy.write(`\x1b_Gi=100,I=200,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + response = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response, '\x1b_Gi=100;EINVAL:cannot specify both i and I keys\x1b\\'); + }); + + test('responds with EINVAL for i+I conflict even without payload', async () => { + let response = ''; + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + // Delete command with both i and I (no payload case) + await ctx.proxy.write('\x1b_Gi=101,I=201,a=d\x1b\\'); + await timeout(100); + + response = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response, '\x1b_Gi=101;EINVAL:cannot specify both i and I keys\x1b\\'); + }); }); test.describe('Kitty pixel verification', () => {