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)``.
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

View File

@@ -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')

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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 {