Fix 4 bugs in t=k implementation and add test infrastructure

Bug 1: remote_items allocation used ds.num_mimes instead of mi.num_uris
Bug 2: Off-by-one in uri_item_idx > mi.num_uris (should be >=)
Bug 3: Off-by-one in entry_num > parent->children_sz (should be >=)
Bug 4: DRAG_SOURCE_DROPPED state never set in drag_notify

Also add dnd_test_force_drag_dropped() helper and make
notify_drag_data_ready() succeed in test mode.

Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/9da0bff7-6a1a-490f-a4c5-8cb328e056ce

Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-13 15:21:20 +00:00 committed by GitHub
parent 6bd41c94a2
commit 2018f8b134
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 41 additions and 5 deletions

View file

@ -170,6 +170,11 @@ dnd_set_test_write_func(PyObject *func, size_t mime_list_size_cap, size_t presen
REMOTE_DRAG_LIMIT = remote_drag_limit ? remote_drag_limit : DEFAULT_REMOTE_DRAG_LIMIT;
}
bool
dnd_is_test_mode(void) {
return g_dnd_test_write_func != NULL;
}
static int
string_arrays_cmp(const char **a, size_t an, const char **b, size_t bn) {
if (an != bn) return (int)an - (int)bn;
@ -1438,7 +1443,7 @@ drag_notify(Window *w, DragNotifyType type) {
default:
sz += snprintf(buf + sz, sizeof(buf) - sz, "o=1"); break;
} break;
case DRAG_NOTIFY_DROPPED: break;
case DRAG_NOTIFY_DROPPED: ds.state = DRAG_SOURCE_DROPPED; break;
case DRAG_NOTIFY_FINISHED:
sz += snprintf(buf + sz, sizeof(buf) - sz, "y=%d", global_state.drag_source.was_canceled ? 1 : 0); break;
}
@ -1780,9 +1785,9 @@ toplevel_data_for_drag(
bool has_more, const uint8_t *payload, size_t payload_sz
) {
if (!mi.remote_items) {
mi.remote_items = calloc(ds.num_mimes, sizeof(mi.remote_items[0]));
mi.remote_items = calloc(mi.num_uris, sizeof(mi.remote_items[0]));
if (!mi.remote_items) abrt(ENOMEM);
mi.num_remote_items = ds.num_mimes;
mi.num_remote_items = mi.num_uris;
}
if (!mi.base_dir_for_remote_items) {
int fd;
@ -1796,7 +1801,7 @@ toplevel_data_for_drag(
ri->started = true;
ri->type = item_type;
base64_init_stream_decoder(&ri->base64_state);
if (uri_item_idx > mi.num_uris) abrt(EINVAL);
if (uri_item_idx >= mi.num_uris) abrt(EINVAL);
const char *uri = mi.uri_list[uri_item_idx];
char *fname = sanitized_filename_from_url(uri);
if (!fname) abrt(EINVAL);
@ -1860,7 +1865,7 @@ subdir_data_for_drag(
parent->fd_plus_one = fd + 1;
}
}
if (entry_num > parent->children_sz) abrt(EINVAL);
if (entry_num >= parent->children_sz) abrt(EINVAL);
DragRemoteItem *ri = parent->children + entry_num;
if (!ri->started) {
ri->started = true;

View file

@ -22,6 +22,7 @@ size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_m
void drop_dispatch_data(Window *w, const char *mime_type, const char *data, ssize_t sz);
void drop_finish(Window *w);
void dnd_set_test_write_func(PyObject*, size_t, size_t, size_t);
bool dnd_is_test_mode(void);
typedef enum { DRAG_NOTIFY_ACCEPTED, DRAG_NOTIFY_ACTION_CHANGED, DRAG_NOTIFY_DROPPED, DRAG_NOTIFY_FINISHED } DragNotifyType;

View file

@ -1052,6 +1052,34 @@ dnd_test_fake_drop_data(PyObject *self UNUSED, PyObject *args) {
Py_RETURN_NONE;
}
static PyObject *
dnd_test_force_drag_dropped(PyObject *self UNUSED, PyObject *args) {
// Force the drag source state to DROPPED for testing purposes.
// This simulates what would happen after start_window_drag() succeeds
// and the drop target receives the data.
unsigned long long window_id;
if (!PyArg_ParseTuple(args, "K", &window_id)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (w->drag_source.state != DRAG_SOURCE_BEING_BUILT) {
PyErr_SetString(PyExc_ValueError, "Drag source state is not BEING_BUILT");
return NULL;
}
// Simulate what drag_start does on success, without calling start_window_drag
for (size_t i = 0; i < w->drag_source.num_mimes; i++) {
free(w->drag_source.items[i].optional_data);
w->drag_source.items[i].optional_data = NULL;
w->drag_source.items[i].data_size = 0;
w->drag_source.items[i].data_capacity = 0;
w->drag_source.items[i].data_decode_initialized = false;
}
for (size_t i = 0; i < arraysz(w->drag_source.images); i++) {
if (w->drag_source.images[i].data) free(w->drag_source.images[i].data);
zero_at_ptr(w->drag_source.images + i);
}
w->drag_source.state = DRAG_SOURCE_DROPPED;
Py_RETURN_NONE;
}
// }}}
static void
@ -1150,6 +1178,7 @@ drag_source_callback(GLFWwindow *window UNUSED, GLFWDragEvent *ev) {
int
notify_drag_data_ready(id_type os_window_id, const char *mime_type) {
if (dnd_is_test_mode()) return 0; // In test mode, always succeed
OSWindow *w = os_window_for_id(os_window_id);
GLFWDragSourceItem item = {.mime_type = mime_type};
if (w && w->handle) return glfwStartDrag(w->handle, &item, 1, NULL, -1, false);
@ -3363,6 +3392,7 @@ static PyMethodDef module_methods[] = {
METHODB(dnd_test_set_mouse_pos, METH_VARARGS),
METHODB(dnd_test_fake_drop_event, METH_VARARGS),
METHODB(dnd_test_fake_drop_data, METH_VARARGS),
METHODB(dnd_test_force_drag_dropped, METH_VARARGS),
{NULL, NULL, 0, NULL} /* Sentinel */
};