Only base64 encode payload when needed

This commit is contained in:
Kovid Goyal
2026-03-07 17:19:23 +05:30
parent 17e941a180
commit b4d57525ea
5 changed files with 41 additions and 41 deletions

View File

@@ -13,17 +13,14 @@ There is one central escape code used for this protocol, which is of the form::
Here, ``OSC`` is the bytes ``ESC ] (0x1b 0x5b)`` and ST is ``ESC \\ (0x1b 0x5c)``. Here, ``OSC`` is the bytes ``ESC ] (0x1b 0x5b)`` and ST is ``ESC \\ (0x1b 0x5c)``.
The ``metadata`` is a colon separated list of ``key=value`` pairs. The ``metadata`` is a colon separated list of ``key=value`` pairs.
The final part of the escape code is the :rfc:`base64 <4648>` encoded payload data, The final part of the escape code is the payload data, whose meaning depends on the metadata.
whose meaning depends on the metadata.
The payload must be no more than 4096 bytes encoded bytes. 4096 is the limit to The payload must be no more than 4096 bytes. When the payload is larger than 4096
be applied after encoding. When the payload is larger than 4096 base64 encoded
bytes, it is chunked up using the ``m`` key. An escape code that has a too long bytes, it is chunked up using the ``m`` key. An escape code that has a too long
payload is transmitted in chunks. All but the last chunk must have ``m=1`` in payload is transmitted in chunks. All but the last chunk must have ``m=1`` in
their metadata. Each chunk must have a payload of no more than 4096 base64 their metadata. Each chunk must have a payload of no more than 4096 bytes.
encoded bytes without trailing padding, except the last chunk which may Only the first chunk is guaranteed to have metadata other than the ``m`` key.
optionally have trailing padding. Only the first chunk is guaranteed to have Subsequent chunks may optionally omit all
metadata other than the ``m`` key. Subsequent chunks may optionally omit all
metadata except the ``m`` and ``i`` keys. While a chunked transfer is in metadata except the ``m`` and ``i`` keys. While a chunked transfer is in
progress it is a protocol error to for the sending side to progress it is a protocol error to for the sending side to
send any protocol related escape codes other than chunked ones. send any protocol related escape codes other than chunked ones.
@@ -31,6 +28,11 @@ send any protocol related escape codes other than chunked ones.
All integer values used in this escape code must be 32-bit signed or unsigned All integer values used in this escape code must be 32-bit signed or unsigned
integers encoded in decimal representation. integers encoded in decimal representation.
When transferring binary data the payload is :rfc:`base64 <4648>` encoded. The
4096 bytes limit applies to *encoded bytes*, that is, it is applied after
encoding. base64 padding bytes are optional and may or may not be present at
the end of the last chunk.
Accepting drops Accepting drops
----------------- -----------------
@@ -105,9 +107,9 @@ Requesting data is done by sending an escape code of the form::
This will request data for the specifid MIME type. The terminal must respond This will request data for the specifid MIME type. The terminal must respond
with a series of escape codes of the form:: with a series of escape codes of the form::
OSC _dnd_code ; t=r ; data ST OSC _dnd_code ; t=r ; base64 encoded data ST
End of data is indicated by an empty payload. If some error occures while End of data is indicated by an empty payload. If some error occurs while
getting the data, the terminal must send an escape code of the form:: getting the data, the terminal must send an escape code of the form::
OSC _dnd_code ; t=R ; POSIX error name ST OSC _dnd_code ; t=R ; POSIX error name ST

View File

@@ -342,7 +342,7 @@ def parsers() -> None:
} }
text = generate( text = generate(
'parse_dnd_code', 'screen_handle_dnd_command', 'dnd_command', keymap, 'DnDCommand', 'parse_dnd_code', 'screen_handle_dnd_command', 'dnd_command', keymap, 'DnDCommand',
payload_is_base64=True, start_parsing_at=0, field_sep=':') payload_is_base64=False, start_parsing_at=0, field_sep=':')
write_header(text, 'kitty/parse-dnd-command.h') write_header(text, 'kitty/parse-dnd-command.h')

View File

@@ -99,7 +99,7 @@ string_arrays_cmp(const char **a, size_t an, const char **b, size_t bn) {
} }
static size_t static size_t
send_payload_to_child(id_type id, const char *header, size_t header_sz, const char *data, const size_t data_sz) { send_payload_to_child(id_type id, const char *header, size_t header_sz, const char *data, const size_t data_sz, bool as_base64) {
size_t offset = 0; size_t offset = 0;
char buf[4096 + 1024]; char buf[4096 + 1024];
memcpy(buf, header, header_sz); memcpy(buf, header, header_sz);
@@ -111,14 +111,21 @@ send_payload_to_child(id_type id, const char *header, size_t header_sz, const ch
if (too_much_data) return 0; if (too_much_data) return 0;
return 1; return 1;
} }
const size_t limit = as_base64 ? 3072 : 4096;
while (offset < data_sz) { while (offset < data_sz) {
size_t chunk = data_sz - offset; size_t chunk = data_sz - offset;
if (chunk > limit) chunk = limit;
size_t p = header_sz; size_t p = header_sz;
buf[p++] = offset + 3072 >= data_sz ? '0' : '1'; const bool is_last = offset + chunk >= data_sz;
buf[p++] = ';'; buf[p++] = is_last ? '0' : '1'; buf[p++] = ';';
size_t b64_len = sizeof(buf) - p; if (as_base64) {
base64_encode8((const uint8_t*)data + offset, chunk, (uint8_t*)buf + p, &b64_len, false); size_t b64_len = sizeof(buf) - p;
p += b64_len; base64_encode8((const uint8_t*)data + offset, chunk, (uint8_t*)buf + p, &b64_len, false);
p += b64_len;
} else {
memcpy(buf + p, data + offset, chunk);
p += chunk;
}
buf[p++] = 0x1b; buf[p++] = '\\'; buf[p++] = 0x1b; buf[p++] = '\\';
bool found, too_much_data; bool found, too_much_data;
schedule_write_to_child_if_possible(id, buf, p, &found, &too_much_data); schedule_write_to_child_if_possible(id, buf, p, &found, &too_much_data);
@@ -133,7 +140,7 @@ static bool
flush_pending(id_type id, PendingData *pending) { flush_pending(id_type id, PendingData *pending) {
while (pending->count) { while (pending->count) {
PendingEntry *e = pending->items; PendingEntry *e = pending->items;
size_t written = send_payload_to_child(id, e->buf, e->header_sz, e->buf + e->header_sz, e->data_sz); size_t written = send_payload_to_child(id, e->buf, e->header_sz, e->buf + e->header_sz, e->data_sz, e->as_base64);
if (written < e->data_sz) { if (written < e->data_sz) {
if (written) { if (written) {
e->data_sz -= written; e->data_sz -= written;
@@ -162,16 +169,16 @@ flush_pending_payloads(id_type timer_id UNUSED, void *x) {
} }
static void static void
queue_payload_to_child(id_type id, PendingData *pending, const char *header, size_t header_sz, const char *data, size_t data_sz) { queue_payload_to_child(id_type id, PendingData *pending, const char *header, size_t header_sz, const char *data, size_t data_sz, bool as_base64) {
size_t offset = 0; size_t offset = 0;
if (flush_pending(id, pending)) offset = send_payload_to_child(id, header, header_sz, data, data_sz); if (flush_pending(id, pending)) offset = send_payload_to_child(id, header, header_sz, data, data_sz, as_base64);
if (offset < data_sz || (!offset && !data_sz)) { if (offset < data_sz || (!offset && !data_sz)) {
ensure_space_for(pending, items, PendingEntry, pending->count + 1, capacity, 32, true); ensure_space_for(pending, items, PendingEntry, pending->count + 1, capacity, 32, true);
char *buf = malloc(header_sz + data_sz - offset); char *buf = malloc(header_sz + data_sz - offset);
if (!buf) fatal("Out of memory"); if (!buf) fatal("Out of memory");
memcpy(buf, header, header_sz); memcpy(buf + header_sz, data, data_sz - offset); memcpy(buf, header, header_sz); memcpy(buf + header_sz, data, data_sz - offset);
PendingEntry *e = &pending->items[pending->count++]; PendingEntry *e = &pending->items[pending->count++];
e->buf = buf; e->header_sz = header_sz; e->data_sz = data_sz - offset; e->buf = buf; e->header_sz = header_sz; e->data_sz = data_sz - offset; e->as_base64 = as_base64;
} }
if (pending->count) check_for_pending_writes(); if (pending->count) check_for_pending_writes();
} }
@@ -215,11 +222,11 @@ drop_move_on_child(Window *w, const char** mimes, size_t num_mimes, bool is_drop
if (n < 0) break; if (n < 0) break;
pos += n; pos += n;
} }
queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, mbuf, pos); queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, mbuf, pos, false);
} }
} else { } else {
buf[header_size++] = 0x1b; buf[header_size++] = '\\'; buf[header_size++] = 0x1b; buf[header_size++] = '\\';
queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, NULL, 0); queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, NULL, 0, false);
} }
} }
@@ -297,12 +304,9 @@ get_errno_name(int err) {
static void static void
drop_send_error(Window *w, int error_code) { drop_send_error(Window *w, int error_code) {
char buf[128]; char buf[128];
uint8_t ebuf[32];
size_t b64_len = sizeof(ebuf);
const char *e = get_errno_name(error_code); const char *e = get_errno_name(error_code);
base64_encode8((const uint8_t*)e, strlen(e), ebuf, &b64_len, false);
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=R", DND_CODE, w->drop.client_id); int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=R", DND_CODE, w->drop.client_id);
queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, (char*)ebuf, b64_len); queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, e, strlen(e), false);
} }
void void
@@ -328,7 +332,7 @@ drop_dispatch_data(Window *w, const char *data, ssize_t sz) {
else { else {
char buf[128]; char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=r", DND_CODE, w->drop.client_id); int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=r", DND_CODE, w->drop.client_id);
queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz); queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz, true);
} }
} }
@@ -340,6 +344,6 @@ drop_left_child(Window *w) {
if (w->drop.wanted) { if (w->drop.wanted) {
char buf[128]; char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=m:x=-1:y=-1", DND_CODE, w->drop.client_id); int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;i=%u:t=m:x=-1:y=-1", DND_CODE, w->drop.client_id);
queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, NULL, 0); queue_payload_to_child(w->id, &w->drop.pending, buf, header_size, NULL, 0, false);
} }
} }

View File

@@ -7,7 +7,7 @@
static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
const size_t parser_buf_pos) { const size_t parser_buf_pos) {
unsigned int pos = 0; unsigned int pos = 0;
size_t payload_start = 0;
enum PARSER_STATES { KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE, PAYLOAD }; enum PARSER_STATES { KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE, PAYLOAD };
enum PARSER_STATES state = KEY, value_state = FLAG; enum PARSER_STATES state = KEY, value_state = FLAG;
DnDCommand g = {0}; DnDCommand g = {0};
@@ -180,15 +180,8 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
case PAYLOAD: { case PAYLOAD: {
sz = parser_buf_pos - pos; sz = parser_buf_pos - pos;
g.payload_sz = MAX(BUF_EXTRA, sz); payload_start = pos;
if (!base64_decode8(parser_buf + pos, sz, parser_buf, &g.payload_sz)) { g.payload_sz = sz;
g.payload_sz = MAX(BUF_EXTRA, sz);
REPORT_ERROR("Failed to parse DnDCommand command payload with error: "
" invalid base64 data in chunk of size: %zu with output "
"buffer size: %zu",
sz, g.payload_sz);
return;
}
pos = parser_buf_pos; pos = parser_buf_pos;
} break; } break;
@@ -222,7 +215,7 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
"cell_x", (int)g.cell_x, "cell_y", (int)g.cell_y, "pixel_x", "cell_x", (int)g.cell_x, "cell_y", (int)g.cell_y, "pixel_x",
(int)g.pixel_x, "pixel_y", (int)g.pixel_y, (int)g.pixel_x, "pixel_y", (int)g.pixel_y,
"", (char *)parser_buf, g.payload_sz); "", (char *)parser_buf + payload_start, g.payload_sz);
screen_handle_dnd_command(self->screen, &g, parser_buf); screen_handle_dnd_command(self->screen, &g, parser_buf + payload_start);
} }

View File

@@ -207,6 +207,7 @@ typedef struct WindowBarData {
typedef struct PendingEntry { typedef struct PendingEntry {
char *buf; size_t header_sz; char *buf; size_t header_sz;
size_t data_sz; size_t data_sz;
bool as_base64;
} PendingEntry; } PendingEntry;
typedef struct PendingData { typedef struct PendingData {