Reject unsupporteed transmission medium for a=t and a=T

This commit is contained in:
Anthony Kim
2026-02-12 18:03:19 -08:00
parent 3c7f313c34
commit 10182cc656
2 changed files with 118 additions and 1 deletions
@@ -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;
@@ -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', () => {