From b4d57525ea85aee604669b23948ec232700217ad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 7 Mar 2026 17:19:23 +0530 Subject: [PATCH] Only base64 encode payload when needed --- docs/dnd-protocol.rst | 22 +++++++++++---------- gen/apc_parsers.py | 2 +- kitty/dnd.c | 40 +++++++++++++++++++++------------------ kitty/parse-dnd-command.h | 17 +++++------------ kitty/state.h | 1 + 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 9ee8173a2..548ee7516 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -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)``. 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, -whose meaning depends on the metadata. +The final part of the escape code is the payload data, whose meaning depends on the metadata. -The payload must be no more than 4096 bytes encoded bytes. 4096 is the limit to -be applied after encoding. When the payload is larger than 4096 base64 encoded +The payload must be no more than 4096 bytes. When the payload is larger than 4096 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 -their metadata. Each chunk must have a payload of no more than 4096 base64 -encoded bytes without trailing padding, except the last chunk which may -optionally have trailing padding. Only the first chunk is guaranteed to have -metadata other than the ``m`` key. Subsequent chunks may optionally omit all +their metadata. Each chunk must have a payload of no more than 4096 bytes. +Only the first chunk is guaranteed to have 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 progress it is a protocol error to for the sending side to 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 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 ----------------- @@ -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 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:: OSC _dnd_code ; t=R ; POSIX error name ST diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index aea0a7826..22fdaf90e 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -342,7 +342,7 @@ def parsers() -> None: } text = generate( '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') diff --git a/kitty/dnd.c b/kitty/dnd.c index aa8b9e0c0..a0e71159b 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -99,7 +99,7 @@ string_arrays_cmp(const char **a, size_t an, const char **b, size_t bn) { } 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; char buf[4096 + 1024]; 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; return 1; } + const size_t limit = as_base64 ? 3072 : 4096; while (offset < data_sz) { size_t chunk = data_sz - offset; + if (chunk > limit) chunk = limit; size_t p = header_sz; - buf[p++] = offset + 3072 >= data_sz ? '0' : '1'; - buf[p++] = ';'; - size_t b64_len = sizeof(buf) - p; - base64_encode8((const uint8_t*)data + offset, chunk, (uint8_t*)buf + p, &b64_len, false); - p += b64_len; + const bool is_last = offset + chunk >= data_sz; + buf[p++] = is_last ? '0' : '1'; buf[p++] = ';'; + if (as_base64) { + size_t b64_len = sizeof(buf) - p; + 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++] = '\\'; bool 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) { while (pending->count) { 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 -= written; @@ -162,16 +169,16 @@ flush_pending_payloads(id_type timer_id UNUSED, void *x) { } 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; - 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)) { ensure_space_for(pending, items, PendingEntry, pending->count + 1, capacity, 32, true); char *buf = malloc(header_sz + data_sz - offset); if (!buf) fatal("Out of memory"); memcpy(buf, header, header_sz); memcpy(buf + header_sz, data, data_sz - offset); 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(); } @@ -215,11 +222,11 @@ drop_move_on_child(Window *w, const char** mimes, size_t num_mimes, bool is_drop if (n < 0) break; 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 { 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 drop_send_error(Window *w, int error_code) { char buf[128]; - uint8_t ebuf[32]; - size_t b64_len = sizeof(ebuf); 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); - 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 @@ -328,7 +332,7 @@ drop_dispatch_data(Window *w, const char *data, ssize_t sz) { else { char buf[128]; 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) { 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); - 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); } } diff --git a/kitty/parse-dnd-command.h b/kitty/parse-dnd-command.h index 652922a0e..32902667f 100644 --- a/kitty/parse-dnd-command.h +++ b/kitty/parse-dnd-command.h @@ -7,7 +7,7 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, const size_t parser_buf_pos) { unsigned int pos = 0; - + size_t payload_start = 0; enum PARSER_STATES { KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE, PAYLOAD }; enum PARSER_STATES state = KEY, value_state = FLAG; DnDCommand g = {0}; @@ -180,15 +180,8 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, case PAYLOAD: { sz = parser_buf_pos - pos; - g.payload_sz = MAX(BUF_EXTRA, sz); - if (!base64_decode8(parser_buf + pos, sz, parser_buf, &g.payload_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; - } + payload_start = pos; + g.payload_sz = sz; pos = parser_buf_pos; } 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", (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); } diff --git a/kitty/state.h b/kitty/state.h index 75ea00350..5e7ab8598 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -207,6 +207,7 @@ typedef struct WindowBarData { typedef struct PendingEntry { char *buf; size_t header_sz; size_t data_sz; + bool as_base64; } PendingEntry; typedef struct PendingData {