From 10182cc656a37b1e6a4619a6ac9d8a66c36054f6 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 12 Feb 2026 18:03:19 -0800 Subject: [PATCH] Reject unsupporteed transmission medium for a=t and a=T --- .../src/kitty/KittyGraphicsHandler.ts | 11 +- addons/addon-image/test/KittyGraphics.test.ts | 108 ++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts index 052898ed..100f4853 100644 --- a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts +++ b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts @@ -327,7 +327,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler { switch (action) { case KittyAction.TRANSMIT: { const result = this._handleTransmit(cmd, bytes, decodeError); - if (cmd.id !== undefined) { + // Only send response when _handleTransmit didn't already respond + // (it handles unsupported transmission medium responses internally) + if ((cmd.transmission ?? 'd') === 'd' && cmd.id !== undefined) { if (decodeError) { this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0); } else if (bytes.length > 0) { @@ -364,6 +366,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler { // 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents // 3. For t=d: treat bytes as image data (current behavior) // When implementing, also update _handleQuery to accept these transmission mediums. + const transmission = cmd.transmission ?? 'd'; + if (transmission !== 'd') { + if (cmd.id !== undefined) { + this._sendResponse(cmd.id, 'EINVAL:unsupported transmission medium', cmd.quiet ?? 0); + } + return true; + } if (decodeError || bytes.length === 0) return true; diff --git a/addons/addon-image/test/KittyGraphics.test.ts b/addons/addon-image/test/KittyGraphics.test.ts index 7df3d503..7aa15a78 100644 --- a/addons/addon-image/test/KittyGraphics.test.ts +++ b/addons/addon-image/test/KittyGraphics.test.ts @@ -748,6 +748,114 @@ test.describe('Kitty Graphics Protocol', () => { const response = await ctx.page.evaluate('window.kittyResponse'); 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 = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Gi=300,a=t,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response.startsWith('\x1b_Gi=300;EINVAL:'), true); + }); + + test('transmit rejects t=s with id (EINVAL response)', async () => { + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Gi=301,a=t,t=s,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response.startsWith('\x1b_Gi=301;EINVAL:'), true); + }); + + test('transmit rejects t=t with id (EINVAL response)', async () => { + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Gi=302,a=t,t=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response.startsWith('\x1b_Gi=302;EINVAL:'), true); + }); + + test('transmit rejects t=f without id (no response)', async () => { + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Ga=t,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + 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 = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Gi=310,a=T,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response.startsWith('\x1b_Gi=310;EINVAL:'), true); + }); + + test('transmit+display rejects t=s with id (EINVAL response)', async () => { + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Gi=311,a=T,t=s,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response.startsWith('\x1b_Gi=311;EINVAL:'), true); + }); + + test('transmit+display rejects t=t with id (EINVAL response)', async () => { + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Gi=312,a=T,t=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response.startsWith('\x1b_Gi=312;EINVAL:'), true); + }); + + test('transmit+display rejects t=f without id (no response)', async () => { + await ctx.page.evaluate(() => { + (window as any).kittyResponse = ''; + (window as any).term.onData((data: string) => { (window as any).kittyResponse = data; }); + }); + + await ctx.proxy.write(`\x1b_Ga=T,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await timeout(100); + + const response: string = await ctx.page.evaluate('window.kittyResponse'); + strictEqual(response, ''); + }); }); test.describe('Unimplemented action responses', () => {