Skip to content

Commit

Permalink
Fix a regression that broke writing to the clipboard using the OSC 55…
Browse files Browse the repository at this point in the history
…22 protocol

Fixes #7858
  • Loading branch information
kovidgoyal committed Sep 15, 2024
1 parent 7537fa0 commit 4811533
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 7 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Detailed list of changes

- Fix a regression when tinting of background images was introduced that caused window borders to have :opt:`background_opacity` applied to them (:iss:`7850`)

- Fix a regression that broke writing to the clipboard using the OSC 5522 protocol (:iss:`7858`)

0.36.2 [2024-09-06]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
6 changes: 4 additions & 2 deletions kitty/clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ def add_base64_data(self, data: Union[str, bytes], mime: str = 'text/plain') ->

def flush_base64_data(self) -> None:
if self.currently_writing_mime:
if self.decoder.needs_more_data():
log_error('Received incomplete data for clipboard')
self.decoder.reset()
start = self.mime_map[self.currently_writing_mime][0]
self.mime_map[self.currently_writing_mime] = MimePos(start, self.tempfile.tell() - start)
Expand All @@ -291,8 +293,8 @@ def write_base64_data(self, b: bytes) -> None:
if not self.max_size_exceeded:
try:
decoded = self.decoder.decode(b)
except ValueError:
log_error('Clipboard write request has invalid data, ignoring this chunk of data')
except ValueError as e:
log_error(f'Clipboard write request has invalid data, ignoring this chunk of data. Error: {e}')
self.decoder.reset()
decoded = b''
if decoded:
Expand Down
37 changes: 33 additions & 4 deletions kitty/data-types.c
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,15 @@ base64_decode_into(PyObject UNUSED *self, PyObject *args) {
typedef struct StreamingBase64Decoder {
PyObject_HEAD
struct base64_state state;
bool add_trailing_bytes;
bool add_trailing_bytes, needs_more_data;
} StreamingBase64Decoder;

static void
StreamingBase64Decoder_reset_(StreamingBase64Decoder *self) {
base64_stream_decode_init(&self->state, 0);
self->needs_more_data = false;
}

static int
StreamingBase64Decoder_init(PyObject *s, PyObject *args, PyObject *kwds UNUSED) {
if (PyTuple_GET_SIZE(args)) { PyErr_SetString(PyExc_TypeError, "constructor takes no arguments"); return -1; }
Expand All @@ -156,10 +162,17 @@ StreamingBase64Decoder_decode(StreamingBase64Decoder *self, PyObject *a) {
size_t sz = required_buffer_size_for_base64_decode(data.len);
RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, sz));
if (!ans) return NULL;
if (!base64_stream_decode(&self->state, data.buf, data.len, PyBytes_AS_STRING(ans), &sz)) {
int ret;
Py_BEGIN_ALLOW_THREADS
ret = base64_stream_decode(&self->state, data.buf, data.len, PyBytes_AS_STRING(ans), &sz);
Py_END_ALLOW_THREADS;
if (!ret) {
StreamingBase64Decoder_reset_(self);
PyErr_SetString(PyExc_ValueError, "Invalid base64 input data");
return NULL;
}
if (self->state.eof) StreamingBase64Decoder_reset_(self);
else self->needs_more_data = self->state.carry != 0 || self->state.bytes != 0;
if (_PyBytes_Resize(&ans, sz) != 0) return NULL;
return Py_NewRef(ans);
}
Expand All @@ -175,19 +188,30 @@ StreamingBase64Decoder_decode_into(StreamingBase64Decoder *self, PyObject *const
if (!src.buf || !src.len) return PyLong_FromLong(0);
size_t sz = required_buffer_size_for_base64_decode(src.len);
if ((Py_ssize_t)sz > data.len) { PyErr_SetString(PyExc_BufferError, "output buffer too small"); return NULL; }
if (!base64_stream_decode(&self->state, src.buf, src.len, data.buf, &sz)) {
int ret;
Py_BEGIN_ALLOW_THREADS
ret = base64_stream_decode(&self->state, src.buf, src.len, data.buf, &sz);
Py_END_ALLOW_THREADS
if (!ret) {
StreamingBase64Decoder_reset_(self);
PyErr_SetString(PyExc_ValueError, "Invalid base64 input data");
return NULL;
}
if (self->state.eof) StreamingBase64Decoder_reset_(self); else self->needs_more_data = true;
return PyLong_FromSize_t(sz);
}

static PyObject*
StreamingBase64Decoder_reset(StreamingBase64Decoder *self, PyObject *args UNUSED) {
base64_stream_decode_init(&self->state, 0);
StreamingBase64Decoder_reset_(self);
Py_RETURN_NONE;
}

static PyObject*
StreamingBase64Decoder_needs_more_data(StreamingBase64Decoder *self, PyObject *args UNUSED) {
return Py_NewRef(self->needs_more_data ? Py_True : Py_False);
}

static PyTypeObject StreamingBase64Decoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "kitty.fast_data_types.StreamingBase64Decoder",
Expand All @@ -198,6 +222,7 @@ static PyTypeObject StreamingBase64Decoder_Type = {
{"decode", (PyCFunction)StreamingBase64Decoder_decode, METH_O, ""},
{"decode_into", (PyCFunction)(void(*)(void))StreamingBase64Decoder_decode_into, METH_FASTCALL, ""},
{"reset", (PyCFunction)StreamingBase64Decoder_reset, METH_NOARGS, ""},
{"needs_more_data", (PyCFunction)StreamingBase64Decoder_needs_more_data, METH_NOARGS, ""},
{NULL, NULL, 0, NULL},
},
.tp_new = PyType_GenericNew,
Expand Down Expand Up @@ -225,7 +250,9 @@ StreamingBase64Encoder_encode(StreamingBase64Decoder *self, PyObject *a) {
size_t sz = required_buffer_size_for_base64_encode(data.len);
RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, sz));
if (!ans) return NULL;
Py_BEGIN_ALLOW_THREADS
base64_stream_encode(&self->state, data.buf, data.len, PyBytes_AS_STRING(ans), &sz);
Py_END_ALLOW_THREADS
if (_PyBytes_Resize(&ans, sz) != 0) return NULL;
return Py_NewRef(ans);
}
Expand All @@ -241,7 +268,9 @@ StreamingBase64Encoder_encode_into(StreamingBase64Decoder *self, PyObject *const
if (!src.buf || !src.len) return PyLong_FromLong(0);
size_t sz = required_buffer_size_for_base64_encode(src.len);
if ((Py_ssize_t)sz > data.len) { PyErr_SetString(PyExc_BufferError, "output buffer too small"); return NULL; }
Py_BEGIN_ALLOW_THREADS
base64_stream_encode(&self->state, src.buf, src.len, data.buf, &sz);
Py_END_ALLOW_THREADS
return PyLong_FromSize_t(sz);
}

Expand Down
3 changes: 2 additions & 1 deletion kitty/fast_data_types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1719,7 +1719,8 @@ class StreamingBase64Decoder:
def decode(self, data: ReadableBuffer) -> bytes: ...
# decode the specified data, return number of bytes written dest should be as large as src (technically 3/4 src + 2)
def decode_into(self, dest: WriteableBuffer, src: ReadableBuffer) -> int: ...

# whether the data stream decoded so far is complete or not
def needs_more_data(self) -> bool: ...


class StreamingBase64Encodeer:
Expand Down
22 changes: 22 additions & 0 deletions kitty_tests/clipboard.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>

from base64 import standard_b64decode, standard_b64encode

from kitty.clipboard import WriteRequest
from kitty.fast_data_types import StreamingBase64Decoder

from . import BaseTest

Expand Down Expand Up @@ -30,3 +32,23 @@ def t(data, expected):
for x in 'bGlnaHQgd29y':
wr.add_base64_data(x)
self.ae(wr.data_for(), b'light wor')

def test_base64_streaming_decoder(self):
d = StreamingBase64Decoder()
c = standard_b64encode(b'abcdef')
self.ae(b'abcdef', d.decode(c))
self.assertFalse(d.needs_more_data())
a = d.decode(c[:4])
self.assertFalse(d.needs_more_data())
self.ae(b'abcdef', a + d.decode(c[4:]))
self.assertFalse(d.needs_more_data())
a = d.decode(c[:1])
self.assertTrue(d.needs_more_data())
self.ae(b'abcdef', a + d.decode(c[1:4]) + d.decode(c[4:]))
self.assertFalse(d.needs_more_data())
c = standard_b64encode(b'abcd')
self.ae(b'abcd', d.decode(c[:2]) + d.decode(c[2:]))
c1 = standard_b64encode(b'1' * 4096)
c2 = standard_b64encode(b'2' * 4096)
self.ae(standard_b64decode(c1) + standard_b64decode(c2), d.decode(c1) + d.decode(c2))
self.assertFalse(d.needs_more_data())

0 comments on commit 4811533

Please sign in to comment.