Files
kitty/kitty/dnd.c

2247 lines
87 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* dnd.c
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "dnd.h"
#include "base64.h"
#include "control-codes.h"
#include "safe-wrappers.h"
#include "iqsort.h"
#include "png-reader.h"
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#define DEFAULT_MIME_LIST_SIZE_CAP 1024u * 1024u
static size_t MIME_LIST_SIZE_CAP = DEFAULT_MIME_LIST_SIZE_CAP;
#define DEFAULT_PRESENT_DATA_CAP 64 * 1024 * 1024
static size_t PRESENT_DATA_CAP = DEFAULT_PRESENT_DATA_CAP;
#define DEFAULT_REMOTE_DRAG_LIMIT 1024 * 1024 * 1024
static size_t REMOTE_DRAG_LIMIT = DEFAULT_REMOTE_DRAG_LIMIT;
static PyObject *g_dnd_test_write_func = NULL;
static const unsigned file_permissions = 0644;
static const unsigned dir_permissions = 0755;
// Utils {{{
// In test mode, this callable is invoked instead of schedule_write_to_child_if_possible.
// It receives (window_id: int, data: bytes) and its return value is ignored.
static void drop_process_queue(Window *w);
static void drop_pop_request(Window *w);
static size_t
count_occurrences(const char *str, size_t len, char target) {
size_t count = 0;
const char *ptr = str;
while ((ptr = memchr(ptr, target, len - (ptr - str))) != NULL) {
count++; ptr++; // Move past the found character
if (ptr >= str + len) break;
}
return count;
}
static const char*
get_errno_name(int err) {
switch (err) {
case EPERM: return "EPERM";
case ENOENT: return "ENOENT";
case EIO: return "EIO";
case EINVAL: return "EINVAL";
case EMFILE: return "EMFILE";
case ENOMEM: return "ENOMEM";
case EFBIG: return "EFBIG";
case EISDIR: return "EISDIR";
case ENOSPC: return "ENOSPC";
case 0: return "OK";
default: return "EUNKNOWN";
}
}
static const char*
machine_id(void) {
static bool done = false;
static char ans[512] = {0};
if (!done) {
done = true;
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.machine_id"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "machine_id"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "s", "tty-dnd-protocol-machine-id"));
if (ret) snprintf(ans, sizeof(ans), "%s", PyUnicode_AsUTF8(ret));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
}
return ans;
}
static void
rmtree_best_effort(const char *relpath, int dirfd) {
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "rmtree_best_effort"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "si", relpath, dirfd));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
safe_close(dirfd, __FILE__, __LINE__);
}
static char*
mktempdir_in_cache(const char *prefix, int *fd) {
char *ans = NULL;
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "mktempdir_in_cache"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "sO", prefix, dnd_is_test_mode() ? Py_False : Py_True));
if (ret) {
if (PyArg_ParseTuple(ret, "si", &ans, fd)) {
if (*fd < 0) {
errno = -*fd;
return NULL;
}
ans = strdup(ans);
if (!ans) {
errno = ENOMEM; return NULL;
}
return ans;
}
}
}
}
}
if (PyErr_Occurred()) PyErr_Print();
errno = EIO;
return NULL;
}
static char*
as_file_url(const char *wd, const char *middle, const char *filename) {
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "as_file_url"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "sss", wd, middle, filename));
if (ret) return strdup(PyUnicode_AsUTF8(ret));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
return NULL;
}
static char*
sanitized_filename_from_url(const char *url) {
RAII_PyObject(mname, PyUnicode_DecodeFSDefault("kitty.utils"));
if (mname) {
RAII_PyObject(module, PyImport_Import(mname));
if (module) {
RAII_PyObject(func, PyObject_GetAttrString(module, "sanitized_filename_from_url"));
if (func) {
RAII_PyObject(ret, PyObject_CallFunction(func, "s", url));
if (ret) return strdup(PyUnicode_AsUTF8(ret));
}
}
}
if (PyErr_Occurred()) PyErr_Print();
return NULL;
}
static void
dnd_set_test_write_func(PyObject *func, size_t mime_list_size_cap, size_t present_data_cap, size_t remote_drag_limit) {
(void)machine_id;
Py_CLEAR(g_dnd_test_write_func);
g_dnd_test_write_func = Py_XNewRef(func);
MIME_LIST_SIZE_CAP = mime_list_size_cap ? mime_list_size_cap : DEFAULT_MIME_LIST_SIZE_CAP;
PRESENT_DATA_CAP = present_data_cap ? present_data_cap : DEFAULT_PRESENT_DATA_CAP;
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;
for (size_t i = 0; i < an; i++) {
int ret = strcmp(a[i], b[i]);
if (ret != 0) return ret;
}
return 0;
}
static bool
test_write_chunk(id_type id, const char *buf, size_t sz) {
// In test mode, deliver the chunk to the registered Python callable.
// Returns true when the test interceptor consumed the data (no real write needed).
if (!g_dnd_test_write_func) return false;
RAII_PyObject(ret, PyObject_CallFunction(g_dnd_test_write_func, "Ky#", (unsigned long long)id, buf, (Py_ssize_t)sz));
if (!ret) PyErr_Print();
return true;
}
static size_t
send_payload_to_child(id_type id, uint32_t client_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);
if (client_id) header_sz += snprintf(buf + header_sz, sizeof(buf) - header_sz, ":i=%u", (unsigned)client_id);
if (!data_sz) {
buf[header_sz++] = 0x1b; buf[header_sz++] = '\\';
if (!test_write_chunk(id, buf, header_sz)) {
bool found, too_much_data;
schedule_write_to_child_if_possible(id, buf, header_sz, &found, &too_much_data);
if (too_much_data) return 0;
}
return 1;
}
buf[header_sz++] = ':'; buf[header_sz++] = 'm'; buf[header_sz++] = '=';
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;
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++] = '\\';
if (!test_write_chunk(id, buf, p)) {
bool found, too_much_data;
schedule_write_to_child_if_possible(id, buf, p, &found, &too_much_data);
if (too_much_data) break;
if (!found) return data_sz;
}
offset += chunk;
}
return offset;
}
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->client_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;
memmove(e->buf + e->header_sz, e->buf + e->header_sz + written, e->data_sz);
}
break;
} else {
if (!e->data_sz && !written) break;
free(e->buf); zero_at_ptr(e);
remove_i_from_array(pending->items, 0, pending->count);
}
}
return pending->count == 0;
}
#define check_for_pending_writes() \
add_main_loop_timer(ms_to_monotonic_t(20), false, flush_pending_payloads, (void*)(uintptr_t)id, NULL)
static void
flush_pending_payloads(id_type timer_id UNUSED, void *x) {
id_type id = (uintptr_t)x;
Window *w = window_for_window_id(id);
if (w && w->drop.wanted) {
if (!flush_pending(w->id, &w->drop.pending)) check_for_pending_writes();
}
}
static void
queue_payload_to_child(id_type id, uint32_t client_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, client_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);
if (data_sz - offset) 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->as_base64 = as_base64; e->client_id = client_id;
}
if (pending->count) check_for_pending_writes();
}
static bool
is_same_machine(const char *client_machine_id, size_t sz) {
if (!sz || !client_machine_id) return true;
if (sz < 20) return false;
if (client_machine_id[0] != '1' || client_machine_id[1] != ':') return false;
client_machine_id = client_machine_id + 2; sz -= 2;
const char *host_machine_id = machine_id();
if (!host_machine_id) return true;
const size_t hsz = strlen(host_machine_id);
return sz == hsz && memcmp(client_machine_id, host_machine_id, sz) == 0;
}
static void
url_decode_inplace(char *str) {
char *src = str, *dst = str;
while (*src) {
if (*src == '%' && src[1] && src[2]) {
unsigned int hi = 0, lo = 0;
char c1 = src[1], c2 = src[2];
if (c1 >= '0' && c1 <= '9') hi = c1 - '0';
else if (c1 >= 'a' && c1 <= 'f') hi = c1 - 'a' + 10;
else if (c1 >= 'A' && c1 <= 'F') hi = c1 - 'A' + 10;
else { *dst++ = *src++; continue; }
if (c2 >= '0' && c2 <= '9') lo = c2 - '0';
else if (c2 >= 'a' && c2 <= 'f') lo = c2 - 'a' + 10;
else if (c2 >= 'A' && c2 <= 'F') lo = c2 - 'A' + 10;
else { *dst++ = *src++; continue; }
*dst++ = (char)((hi << 4) | lo);
src += 3;
} else {
*dst++ = *src++;
}
}
*dst = 0;
}
// }}}
// Dropping {{{
static void
drop_free_offered_mimes(Window *w) {
if (w->drop.offerred_mimes) {
for (size_t i = 0; i < w->drop.num_offerred_mimes; i++) free((void*)w->drop.offerred_mimes[i]);
free(w->drop.offerred_mimes); w->drop.offerred_mimes = NULL;
}
w->drop.num_offerred_mimes = 0;
w->drop.offered_mimes_total_size = 0;
}
static void
drop_free_accepted_mimes(Window *w) {
free(w->drop.accepted_mimes); w->drop.accepted_mimes = NULL;
w->drop.accepted_mimes_sz = 0;
}
static void
free_pending(PendingData *pending) {
if (pending->items) {
for (size_t i = 0; i < pending->count; i++) free(pending->items[i].buf);
free(pending->items);
}
zero_at_ptr(pending);
}
static void
drop_free_dir_handle(DirHandle *h) {
free(h->path);
for (size_t i = 0; i < h->num_entries; i++) free(h->entries[i]);
free(h->entries);
zero_at_ptr(h);
}
static void
drop_free_dir_handles(Window *w) {
for (size_t i = 0; i < w->drop.num_dir_handles; i++)
drop_free_dir_handle(&w->drop.dir_handles[i]);
free(w->drop.dir_handles);
w->drop.dir_handles = NULL;
w->drop.num_dir_handles = 0;
w->drop.dir_handles_capacity = 0;
w->drop.next_dir_handle_id = 0;
}
static void
drop_close_file_fd(Window *w) {
if (w->drop.file_fd_plus_one) {
safe_close(w->drop.file_fd_plus_one - 1, __FILE__, __LINE__);
w->drop.file_fd_plus_one = 0;
}
if (w->drop.file_send_timer) {
remove_main_loop_timer(w->drop.file_send_timer);
w->drop.file_send_timer = 0;
}
}
static void
drop_free_request_queue(Window *w) {
w->drop.num_data_requests = 0;
w->drop.current_request_x = 0;
w->drop.current_request_y = 0;
w->drop.current_request_Y = 0;
}
void
drop_free_data(Window *w) {
drop_close_file_fd(w);
drop_free_offered_mimes(w);
drop_free_accepted_mimes(w);
free_pending(&w->drop.pending);
free(w->drop.registered_mimes); w->drop.registered_mimes = NULL;
free(w->drop.uri_list); w->drop.uri_list = NULL;
free(w->drop.getting_data_for_mime); w->drop.getting_data_for_mime = NULL;
drop_free_dir_handles(w);
drop_free_request_queue(w);
}
static void
reset_drop(Window *w) {
bool wanted = w->drop.wanted; uint32_t cid = w->drop.client_id;
bool is_remote_client = w->drop.is_remote_client;
drop_free_data(w);
zero_at_ptr(&w->drop);
if (wanted) {
w->drop.wanted = wanted;
w->drop.client_id = cid;
w->drop.is_remote_client = is_remote_client;
}
}
void
drop_register_window(Window *w, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id, bool more) {
w->drop.wanted = on;
w->drop.client_id = client_id;
w->drop.is_remote_client = false;
if (!on) { drop_free_data(w); zero_at_ptr(&w->drop); return; }
if (!payload || !payload_sz) return;
size_t sz = w->drop.registered_mimes ? strlen(w->drop.registered_mimes) : 0;
if (sz + payload_sz > MIME_LIST_SIZE_CAP) return;
char *tmp = realloc(w->drop.registered_mimes, sz + payload_sz + 1);
if (tmp) {
w->drop.registered_mimes = tmp;
memcpy(w->drop.registered_mimes + sz, payload, payload_sz);
sz += payload_sz;
w->drop.registered_mimes[sz] = 0;
}
if (more) return;
if (w->drop.registered_mimes) {
OSWindow *osw = os_window_for_kitty_window(w->id);
if (osw) {
size_t num = 0;
RAII_ALLOC(const char*, mimes, malloc(sizeof(char*) * strlen(w->drop.registered_mimes)));
if (mimes) {
char* token = strtok(w->drop.registered_mimes, " ");
while (token != NULL) {
mimes[num++] = token;
token = strtok(NULL, " ");
}
register_mimes_for_drop(osw, mimes, num);
}
}
}
free(w->drop.registered_mimes); w->drop.registered_mimes = NULL;
}
void
drop_register_machine_id(Window *w, const uint8_t *machine_id, size_t sz) {
w->drop.is_remote_client = !is_same_machine((const char*)machine_id, sz);
}
void
drop_move_on_child(Window *w, const char** mimes, size_t num_mimes, bool is_drop) {
if (!w->drop.hovered) {
reset_drop(w);
w->drop.hovered = true;
}
if (is_drop) { w->drop.dropped = true; w->drop.hovered = false; }
if (mimes && (w->drop.offerred_mimes == NULL || string_arrays_cmp(mimes, num_mimes, w->drop.offerred_mimes, w->drop.num_offerred_mimes) != 0)) {
drop_free_offered_mimes(w);
w->drop.offerred_mimes = malloc(num_mimes * sizeof(char*));
if (w->drop.offerred_mimes) {
w->drop.offered_mimes_total_size = 0;
for (size_t i = 0; i < num_mimes; i++) {
size_t l = strlen(mimes[i]);
w->drop.offered_mimes_total_size += 1 + l;
char *p = malloc(l + 1);
if (!p) fatal("Out of memory");
memcpy(p, mimes[i], l); p[l] = 0;
w->drop.offerred_mimes[i] = p;
}
}
w->drop.num_offerred_mimes = num_mimes;
}
// we simply drop this event if there is too much data being written to the child
if (w->drop.pending.count && !is_drop) return;
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=%c:x=%u:y=%u:X=%d:Y=%d", DND_CODE,
is_drop ? 'M' : 'm', w->mouse_pos.cell_x, w->mouse_pos.cell_y,
(int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y);
if (w->drop.offered_mimes_total_size) {
const size_t mimes_total_size = 1 + w->drop.offered_mimes_total_size;
RAII_ALLOC(char, mbuf, malloc(mimes_total_size));
if (mbuf) {
size_t pos = 0;
for (size_t i = 0; i < w->drop.num_offerred_mimes && pos < mimes_total_size; i++) {
int n = snprintf(mbuf + pos, mimes_total_size - pos, "%s ", w->drop.offerred_mimes[i]);
if (n < 0) break;
pos += n;
}
queue_payload_to_child(w->id, w->drop.client_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.client_id, &w->drop.pending, buf, header_size, NULL, 0, false);
}
}
void
drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more) {
if (!w->drop.accept_in_progress) {
drop_free_accepted_mimes(w); w->drop.accept_in_progress = true;
switch(operation) {
case 1: w->drop.accepted_operation = GLFW_DRAG_OPERATION_COPY; break;
case 2: w->drop.accepted_operation = GLFW_DRAG_OPERATION_MOVE; break;
default: w->drop.accepted_operation = GLFW_DRAG_OPERATION_NONE; break;
}
}
if (payload_sz) {
if (w->drop.accepted_mimes_sz + payload_sz > MIME_LIST_SIZE_CAP) return;
char *new_buf = realloc(w->drop.accepted_mimes, w->drop.accepted_mimes_sz + payload_sz + 2);
if (!new_buf) return;
w->drop.accepted_mimes = new_buf;
memcpy(w->drop.accepted_mimes + w->drop.accepted_mimes_sz, payload, payload_sz);
w->drop.accepted_mimes_sz += payload_sz;
}
if (!more) {
w->drop.accept_in_progress = false;
if (w->drop.accepted_mimes) {
for (size_t i = 0; i < w->drop.accepted_mimes_sz; i++)
if (w->drop.accepted_mimes[i] == ' ') w->drop.accepted_mimes[i] = 0;
w->drop.accepted_mimes[w->drop.accepted_mimes_sz++] = 0;
}
OSWindow *osw = os_window_for_kitty_window(w->id);
if (osw) request_drop_status_update(osw);
}
}
void
drop_finish(Window *w) {
OSWindow *osw = os_window_for_kitty_window(w->id);
if (osw && osw->handle) {
glfwEndDrop(osw->handle, w->drop.accepted_operation);
}
}
size_t
drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count) {
if (w->drop.accept_in_progress) return allowed_mimes_count;
if (w->drop.accepted_operation == GLFW_DRAG_OPERATION_NONE) return 0;
typedef struct mime_sorter { const char *m; ssize_t key; } mime_sorter;
if (!w->drop.accepted_mimes) return allowed_mimes_count;
RAII_ALLOC(mime_sorter, ms, malloc(sizeof(mime_sorter) * allowed_mimes_count));
if (!ms) return allowed_mimes_count;
const ssize_t sentinel = allowed_mimes_count;
for (size_t i = 0; i < allowed_mimes_count; i++) {
ms[i].m = allowed_mimes[i];
const char *p = strstr(w->drop.accepted_mimes, ms[i].m);
ms[i].key = p ? p - w->drop.accepted_mimes : sentinel;
}
#define mimes_lt(a, b) ((a)->key < (b)->key)
QSORT(mime_sorter, ms, allowed_mimes_count, mimes_lt);
#undef mimes_lt
while(allowed_mimes_count && ms[allowed_mimes_count-1].key == sentinel) allowed_mimes_count--;
for (size_t i = 0; i < allowed_mimes_count; i++) allowed_mimes[i] = ms[i].m;
return allowed_mimes_count;
}
/* Append the current request disambiguation keys (x, y, Y) to a header buffer.
* Returns the number of bytes written. */
static int
drop_append_request_keys(Window *w, char *buf, size_t bufsize) {
int sz = 0;
if (w->drop.current_request_x && sz < (int)bufsize - 1)
sz += snprintf(buf + sz, bufsize - sz, ":x=%d", (int)w->drop.current_request_x);
if (w->drop.current_request_y && sz < (int)bufsize - 1)
sz += snprintf(buf + sz, bufsize - sz, ":y=%d", (int)w->drop.current_request_y);
if (w->drop.current_request_Y && sz < (int)bufsize - 1)
sz += snprintf(buf + sz, bufsize - sz, ":Y=%d", (int)w->drop.current_request_Y);
return sz;
}
static void
drop_send_error(Window *w, int error_code) {
char buf[128];
const char *e = get_errno_name(error_code);
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=R", DND_CODE);
header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, e, strlen(e), false);
}
void
drop_send_einval(Window *w) {
drop_send_error(w, EINVAL);
}
/* Returns true if the request completed synchronously (error, no-op),
* false if an async OS data fetch was started. */
static bool
do_drop_request_data(Window *w, int32_t idx) {
if (w->drop.getting_data_for_mime) { free(w->drop.getting_data_for_mime); w->drop.getting_data_for_mime = NULL; }
OSWindow *osw = os_window_for_kitty_window(w->id);
if (!osw) return true;
/* idx is 1-based */
if (idx < 1 || !w->drop.offerred_mimes || (size_t)idx > w->drop.num_offerred_mimes) {
drop_send_error(w, ENOENT);
return true;
}
const char *mime = w->drop.offerred_mimes[idx - 1];
w->drop.getting_data_for_mime = strdup(mime);
if (w->drop.getting_data_for_mime) request_drop_data(osw, w->id, mime);
return false; /* async: completion via drop_dispatch_data */
}
void
drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) {
if (sz < 0) {
drop_send_error(w, -sz);
drop_pop_request(w);
drop_process_queue(w);
} else {
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=r", DND_CODE);
const bool is_uri_list = strcmp(mime, "text/uri-list") == 0;
if (is_uri_list) header_size += snprintf(
buf + header_size, sizeof(buf) - header_size, ":X=%d", w->drop.is_remote_client);
header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz, true);
if (is_uri_list) {
w->drop.uri_list_sz += sz;
char *tmp = realloc(w->drop.uri_list, w->drop.uri_list_sz);
if (tmp) { w->drop.uri_list = tmp; memcpy(w->drop.uri_list + w->drop.uri_list_sz - sz, data, sz); }
else { free(w->drop.uri_list); w->drop.uri_list = NULL; w->drop.uri_list_sz = 0; }
}
if (sz == 0) { drop_pop_request(w); drop_process_queue(w); }
}
}
// ---- Remote file / directory transfer ----
/* Return the nth (0-based) file path from a text/uri-list.
* On success returns true and *path_out is a malloc'd absolute resolved path.
* On failure returns false and *error_out points to a static error string. */
static bool
get_nth_file_url(const char *uri_list, size_t uri_list_sz, int n, char **path_out, const char **error_out) {
*path_out = NULL;
RAII_ALLOC(char, buf, malloc(uri_list_sz + 1));
if (!buf) { *error_out = "ENOMEM"; return false; }
memcpy(buf, uri_list, uri_list_sz);
buf[uri_list_sz] = 0;
const char *found_line = NULL;
char *p = buf;
while (*p) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = 0;
/* trim trailing whitespace */
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) { end--; *end = 0; }
if (*p && *p != '#') {
if (n <= 0) { found_line = p; break; }
n--;
}
if (saved == 0) break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
if (!found_line) { *error_out = "ENOENT"; return false; }
/* Must be a file:// URL */
if (strncmp(found_line, "file://", 7) != 0) { *error_out = "EUNKNOWN"; return false; }
const char *rest = found_line + 7;
const char *slash = strchr(rest, '/');
if (!slash) { *error_out = "EINVAL"; return false; }
/* Host part must be empty or "localhost" */
size_t host_len = (size_t)(slash - rest);
if (host_len > 0 && !(host_len == 9 && strncasecmp(rest, "localhost", 9) == 0)) {
*error_out = "EUNKNOWN"; return false;
}
RAII_ALLOC(char, path, strdup(slash));
if (!path) { *error_out = "ENOMEM"; return false; }
/* Strip any query (?...) or fragment (#...) from the path */
char *query_or_fragment_start = path + strcspn(path, "?#");
*query_or_fragment_start = 0;
url_decode_inplace(path);
if (path[0] != '/') { *error_out = "EINVAL"; return false; }
*path_out = strdup(path);
if (!*path_out) { *error_out = "ENOMEM"; return false; }
return true;
}
/* Send error using a literal string (for cases where we have a string, not int). */
static void
drop_send_error_str(Window *w, const char *err_name) {
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=R", DND_CODE);
header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, err_name, strlen(err_name), false);
}
/* Size of each file read chunk; fits in one base64 sub-chunk (≤4096 chars). */
#define FILE_CHUNK_SIZE 3072
/* Abort file transfer if no data has been sent to the child for this long. */
#define FILE_SEND_TIMEOUT_SECONDS 90
static void drop_send_file_chunks(Window *w);
static void
file_send_timer_callback(id_type timer_id UNUSED, void *x) {
id_type id = (uintptr_t)x;
Window *w = window_for_window_id(id);
if (!w || !w->drop.file_fd_plus_one) return;
w->drop.file_send_timer = 0;
if (monotonic() - w->drop.last_file_send_at > s_to_monotonic_t(FILE_SEND_TIMEOUT_SECONDS)) {
drop_close_file_fd(w);
drop_send_error(w, EIO);
drop_pop_request(w);
drop_process_queue(w);
return;
}
drop_send_file_chunks(w);
}
static void
drop_send_file_chunks(Window *w) {
if (!flush_pending(w->id, &w->drop.pending)) {
w->drop.file_send_timer = add_main_loop_timer(ms_to_monotonic_t(20), false, file_send_timer_callback, (void*)(uintptr_t)w->id, NULL);
return;
}
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
while (1) {
char buf[FILE_CHUNK_SIZE];
ssize_t n;
do { n = read(w->drop.file_fd_plus_one - 1, buf, sizeof(buf)); } while (n < 0 && errno == EINTR);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* No data available right now; retry via timer */
w->drop.file_send_timer = add_main_loop_timer(ms_to_monotonic_t(20), false, file_send_timer_callback, (void*)(uintptr_t)w->id, NULL);
return;
}
drop_close_file_fd(w);
drop_send_error(w, EIO);
drop_pop_request(w);
drop_process_queue(w);
return;
}
if (n == 0) {
/* EOF: close fd and send the empty end-of-data signal */
drop_close_file_fd(w);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
drop_pop_request(w);
drop_process_queue(w);
return;
}
size_t sent = send_payload_to_child(w->id, w->drop.client_id, hdr, hdr_sz, buf, (size_t)n, true);
if (sent > 0) w->drop.last_file_send_at = monotonic();
if (sent < (size_t)n) {
/* Partial send: rewind file pointer and retry via timer */
if (lseek(w->drop.file_fd_plus_one - 1, -(off_t)(((size_t)n) - sent), SEEK_CUR) < 0) {
drop_close_file_fd(w);
drop_send_error(w, EIO);
drop_pop_request(w);
drop_process_queue(w);
return;
}
w->drop.file_send_timer = add_main_loop_timer(ms_to_monotonic_t(20), false, file_send_timer_callback, (void*)(uintptr_t)w->id, NULL);
return;
}
/* Full chunk sent: loop and read next chunk */
}
}
/* Open a regular file and begin sending its contents as t=r chunks followed
* by an empty end-of-data t=r, using chunked I/O to avoid large allocations.
* Returns true if completed synchronously (error), false if async I/O started. */
static bool
drop_send_file_data(Window *w, const char *path) {
drop_close_file_fd(w);
int fd = safe_open(path, O_RDONLY | O_CLOEXEC | O_NONBLOCK, 0);
if (fd < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT); break;
case EACCES: case EPERM: drop_send_error(w, EPERM); break;
default: drop_send_error(w, EIO); break;
}
return true;
}
struct stat st;
if (fstat(fd, &st) < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT); break;
case EACCES: case EPERM: drop_send_error(w, EPERM); break;
default: drop_send_error(w, EIO); break;
}
safe_close(fd, __FILE__, __LINE__);
return true;
}
if (!S_ISREG(st.st_mode)) { drop_send_error(w, EINVAL); safe_close(fd, __FILE__, __LINE__); return true; }
w->drop.file_fd_plus_one = fd + 1;
w->drop.last_file_send_at = monotonic();
drop_send_file_chunks(w);
return false; /* async: completion via drop_send_file_chunks */
}
/* Allocate a new DirHandle for the given path and entries (takes ownership of
* entries array and its strings). Returns the new handle id. */
static uint32_t
drop_alloc_dir_handle(Window *w, const char *path, char **entries, size_t num_entries) {
ensure_space_for(&w->drop, dir_handles, DirHandle, w->drop.num_dir_handles + 1, dir_handles_capacity, 4, true);
w->drop.next_dir_handle_id++;
/* Handles 0 and 1 are reserved (0 = absent, 1 = symlink indicator), so
* valid directory handles must be >= 2. */
if (w->drop.next_dir_handle_id < 2) w->drop.next_dir_handle_id = 2;
DirHandle *h = &w->drop.dir_handles[w->drop.num_dir_handles++];
zero_at_ptr(h);
h->id = w->drop.next_dir_handle_id;
h->path = strdup(path);
if (!h->path) fatal("Out of memory");
h->entries = entries;
h->num_entries = num_entries;
return h->id;
}
static DirHandle *
drop_find_dir_handle(Window *w, uint32_t id) {
for (size_t i = 0; i < w->drop.num_dir_handles; i++)
if (w->drop.dir_handles[i].id == id) return &w->drop.dir_handles[i];
return NULL;
}
/* Open a directory, build the null-separated listing, create a handle, and
* send the listing to the client as a t=d:x=handle_id response. */
static void
drop_send_dir_listing(Window *w, const char *path) {
DIR *dir = opendir(path);
if (!dir) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT); break;
case EACCES: case EPERM: drop_send_error(w, EPERM); break;
default: drop_send_error(w, EIO); break;
}
return;
}
/* Build null-separated payload: entry1\0entry2\0... */
size_t payload_cap = 4096, payload_sz = 0;
char *payload = malloc(payload_cap);
if (!payload) { closedir(dir); drop_send_error(w, EIO); return; }
#define APPEND(s, n) do { \
size_t _n = (size_t)(n); \
size_t _need = payload_sz + _n + 1; \
if (_need > payload_cap) { \
while (payload_cap < _need) payload_cap *= 2; \
char *_np = realloc(payload, payload_cap); \
if (!_np) { free(payload); closedir(dir); drop_send_error(w, EIO); return; } \
payload = _np; \
} \
memcpy(payload + payload_sz, (s), _n); \
payload_sz += _n; \
payload[payload_sz++] = 0; \
} while(0)
/* Collect directory entries */
size_t ents_cap = 16, ents_num = 0;
char **ents = malloc(sizeof(char *) * ents_cap);
if (!ents) { free(payload); closedir(dir); drop_send_error(w, EIO); return; }
struct dirent *de;
while ((de = readdir(dir)) != NULL) {
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
unsigned char dtype = de->d_type;
if (dtype == DT_UNKNOWN) {
/* Fall back to lstat when d_type is unavailable */
char full[PATH_MAX];
if (snprintf(full, sizeof(full), "%s/%s", path, de->d_name) >= (int)sizeof(full)) continue;
struct stat est;
if (lstat(full, &est) < 0) continue;
if (S_ISREG(est.st_mode)) dtype = DT_REG;
else if (S_ISDIR(est.st_mode)) dtype = DT_DIR;
else if (S_ISLNK(est.st_mode)) dtype = DT_LNK;
else continue;
}
if (dtype != DT_REG && dtype != DT_DIR && dtype != DT_LNK) continue;
if (ents_num >= ents_cap) {
ents_cap *= 2;
char **ne = realloc(ents, sizeof(char *) * ents_cap);
if (!ne) {
for (size_t i = 0; i < ents_num; i++) free(ents[i]);
free(ents); free(payload); closedir(dir);
drop_send_error(w, EIO); return;
}
ents = ne;
}
ents[ents_num] = strdup(de->d_name);
if (!ents[ents_num]) {
for (size_t i = 0; i < ents_num; i++) free(ents[i]);
free(ents); free(payload); closedir(dir);
drop_send_error(w, EIO); return;
}
ents_num++;
APPEND(de->d_name, strlen(de->d_name));
}
closedir(dir);
#undef APPEND
uint32_t handle_id = drop_alloc_dir_handle(w, path, ents, ents_num);
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
/* Echo all request keys (x, y, Y) so the client can unambiguously identify
* which filesystem object this listing corresponds to. For top-level URI
* file requests Y is absent; for sub-dir reads Y holds the parent handle
* and x holds the 1-based entry index. The new handle is X itself (a value
* > 1 distinguishes directories from regular files (X absent / X=0) and
* symlinks (X=1)). */
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":X=%u", (unsigned)handle_id);
/* payload_sz includes a trailing null; omit it the null-separated format
* does not require a trailing null after the last entry. */
size_t send_sz = payload_sz > 0 ? payload_sz - 1 : 0;
if (send_sz)
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, payload, send_sz, true);
free(payload);
/* end-of-listing signal (empty payload) */
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
}
static void
drop_send_symlink(Window *w, const char *target, size_t sz) {
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":X=1");
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, target, sz, true);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
}
/* Send the file/directory at URI-list index idx.
* Returns true if completed synchronously, false if async file I/O started. */
static bool
do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) {
if (!w->drop.uri_list || !w->drop.uri_list_sz) {
drop_send_error(w, EINVAL); return true;
}
if (global_state.drag_source.from_window == w->id && w->drag_source.state != DRAG_SOURCE_NONE) {
drop_send_error(w, EPERM); return true;
}
/* Verify mime_idx (1-based) points to text/uri-list */
if (mime_idx < 1 || !w->drop.offerred_mimes || (size_t)mime_idx > w->drop.num_offerred_mimes ||
strcmp(w->drop.offerred_mimes[mime_idx - 1], "text/uri-list") != 0) {
drop_send_error(w, EINVAL); return true;
}
/* file_idx is 1-based, convert to 0-based for get_nth_file_url */
if (file_idx < 1) { drop_send_error(w, EINVAL); return true; }
int file_n = file_idx - 1;
RAII_ALLOC(char, path, NULL);
const char *err = NULL;
if (!get_nth_file_url(w->drop.uri_list, w->drop.uri_list_sz, file_n, &path, &err)) {
drop_send_error_str(w, err);
return true;
}
struct stat st;
if (stat(path, &st) < 0) {
if (lstat(path, &st) < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT); break;
case EACCES: case EPERM: drop_send_error(w, EPERM); break;
default: drop_send_error(w, EIO); break;
}
return true;
}
// We have a broken symlink
char target[PATH_MAX]; ssize_t tgtsz;
if ((tgtsz = readlink(path, target, sizeof(target)-1)) < 0) drop_send_error(w, ENOENT);
drop_send_symlink(w, target, tgtsz);
return true;
}
bool sync;
if (S_ISDIR(st.st_mode)) {
drop_send_dir_listing(w, path);
sync = true;
} else if (S_ISREG(st.st_mode)) {
sync = drop_send_file_data(w, path);
} else {
drop_send_error(w, EINVAL);
sync = true;
}
return sync;
}
/* Handle a directory request from the client.
* handle_id: the directory handle (Y= key).
* entry_num: 0 means close the handle; >=1 means read that entry (x= key, 1-based).
* Returns true if completed synchronously, false if async file I/O started. */
static bool
do_drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) {
if (!handle_id) { drop_send_error(w, EINVAL); return true; }
DirHandle *h = drop_find_dir_handle(w, handle_id);
if (!h) { drop_send_error(w, EINVAL); return true; }
if (entry_num == 0) {
/* Close the handle */
size_t hidx = (size_t)(h - w->drop.dir_handles);
drop_free_dir_handle(h);
remove_i_from_array(w->drop.dir_handles, hidx, w->drop.num_dir_handles);
return true;
}
/* Read the entry at 1-based index */
size_t eidx = (size_t)(entry_num - 1);
if (eidx >= h->num_entries) { drop_send_error(w, ENOENT); return true; }
char full[PATH_MAX];
if (snprintf(full, sizeof(full), "%s/%s", h->path, h->entries[eidx]) >= (int)sizeof(full)) {
drop_send_error(w, EIO); return true;
}
struct stat lst;
if (lstat(full, &lst) < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: case ELOOP: drop_send_error(w, ENOENT); break;
case EACCES: case EPERM: drop_send_error(w, EPERM); break;
default: drop_send_error(w, EIO); break;
}
return true;
}
if (S_ISLNK(lst.st_mode)) {
/* Symlink: send the symlink target as t=r:X=1 */
char target[PATH_MAX];
ssize_t tlen = readlink(full, target, sizeof(target) - 1);
if (tlen < 0) {
switch (errno) {
case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT); break;
case EACCES: case EPERM: drop_send_error(w, EPERM); break;
default: drop_send_error(w, EIO); break;
}
return true;
}
target[tlen] = '\0';
char hdr[128];
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":X=1");
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, target, (size_t)tlen, true);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
return true;
}
if (S_ISDIR(lst.st_mode)) {
drop_send_dir_listing(w, full);
return true;
} else if (S_ISREG(lst.st_mode)) {
return drop_send_file_data(w, full);
} else {
drop_send_error(w, EINVAL);
return true;
}
}
void
drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) {
do_drop_handle_dir_request(w, handle_id, entry_num);
}
/* --- Request queue management --- */
/* Pop the head of the queue (the request that just completed). */
static void
drop_pop_request(Window *w) {
if (w->drop.num_data_requests == 0) return;
w->drop.num_data_requests--;
if (w->drop.num_data_requests > 0) {
memmove(w->drop.data_requests, w->drop.data_requests + 1,
w->drop.num_data_requests * sizeof(w->drop.data_requests[0]));
}
w->drop.current_request_x = 0;
w->drop.current_request_y = 0;
w->drop.current_request_Y = 0;
}
static void
drop_finish_and_clear_queue(Window *w) {
drop_close_file_fd(w);
drop_free_request_queue(w);
drop_finish(w);
}
/* Process queued requests in FIFO order.
* Must be called after popping a completed request, or after enqueuing
* the first request into an empty queue. */
static void
drop_process_queue(Window *w) {
while (w->drop.num_data_requests > 0) {
int32_t x = w->drop.data_requests[0].cell_x;
int32_t y = w->drop.data_requests[0].cell_y;
int32_t Y = w->drop.data_requests[0].pixel_y;
w->drop.current_request_x = x;
w->drop.current_request_y = y;
w->drop.current_request_Y = Y;
bool sync = true;
if (Y != 0) {
/* Directory request: Y=handle, x=entry_num */
sync = do_drop_handle_dir_request(w, (uint32_t)Y, x);
} else if (y != 0) {
/* URI file request: x=mime_idx, y=file_idx */
sync = do_drop_request_uri_data(w, x, y);
} else if (x != 0) {
/* MIME data request: x=idx */
sync = do_drop_request_data(w, x);
} else {
/* Finish: x=0, y=0, Y=0 */
drop_pop_request(w);
drop_finish_and_clear_queue(w);
return;
}
if (sync) {
drop_pop_request(w);
/* Loop continues to process next request */
} else {
/* Async operation in progress; completion will call drop_process_queue */
return;
}
}
}
void
drop_enqueue_request(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_y) {
/* Handle finish (x=0, y=0, Y=0): if there are no in-flight requests, finish immediately */
if (cell_x == 0 && cell_y == 0 && pixel_y == 0 && w->drop.num_data_requests == 0) {
drop_finish_and_clear_queue(w);
return;
}
if (w->drop.num_data_requests >= arraysz(w->drop.data_requests)) {
/* Queue full: deny with EMFILE and end the drop */
int32_t saved_x = w->drop.current_request_x;
int32_t saved_y = w->drop.current_request_y;
int32_t saved_Y = w->drop.current_request_Y;
w->drop.current_request_x = cell_x;
w->drop.current_request_y = cell_y;
w->drop.current_request_Y = pixel_y;
drop_send_error(w, EMFILE);
w->drop.current_request_x = saved_x;
w->drop.current_request_y = saved_y;
w->drop.current_request_Y = saved_Y;
drop_finish_and_clear_queue(w);
return;
}
size_t idx = w->drop.num_data_requests;
w->drop.data_requests[idx].cell_x = cell_x;
w->drop.data_requests[idx].cell_y = cell_y;
w->drop.data_requests[idx].pixel_y = pixel_y;
bool was_empty = (w->drop.num_data_requests == 0);
w->drop.num_data_requests++;
if (was_empty) drop_process_queue(w);
}
void
drop_left_child(Window *w) {
w->drop.hovered = false;
w->drop.dropped = false;
drop_free_offered_mimes(w);
if (w->drop.wanted) {
char buf[128];
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=m:x=-1:y=-1", DND_CODE);
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, NULL, 0, false);
}
}
// }}}
// Dragging {{{
#define ds w->drag_source
static void
drag_free_remote_item(DragRemoteItem *x) {
free(x->dir_entry_name);
free(x->data);
if (x->fd_plus_one) safe_close(x->fd_plus_one-1, __FILE__, __LINE__);
if (x->top_level_parent_dir_fd_plus_one) safe_close(x->top_level_parent_dir_fd_plus_one-1, __FILE__, __LINE__);
if (x->children) {
for (size_t i = 0; i < x->children_sz; i++) drag_free_remote_item(x->children + i);
free(x->children);
}
zero_at_ptr(x);
}
void
drag_free_offer(Window *w) {
free(ds.mimes_buf); ds.mimes_buf = NULL; ds.bufsz = 0;
if (ds.items) {
for (size_t i=0; i < ds.num_mimes; i++) {
free(ds.items[i].optional_data);
if (ds.items[i].fd_plus_one > 0) safe_close(ds.items[i].fd_plus_one - 1, __FILE__, __LINE__);
if (ds.items[i].uri_list) {
for (size_t k = 0; k < ds.items[i].num_uris; k++) free(ds.items[i].uri_list[k]);
free(ds.items[i].uri_list);
}
if (ds.items[i].remote_items) {
for (size_t k = 0; k < ds.items[i].num_remote_items; k++) drag_free_remote_item(&ds.items[i].remote_items[k]);
free(ds.items[i].remote_items); ds.items[i].remote_items = NULL;
ds.items[i].num_remote_items = 0;
}
if (ds.items[i].base_dir_fd_plus_one) {
rmtree_best_effort(".", ds.items[i].base_dir_fd_plus_one - 1);
ds.items[i].base_dir_fd_plus_one = 0;
}
free(ds.items[i].base_dir_for_remote_items); ds.items[i].base_dir_for_remote_items = NULL;
}
free(ds.items);
ds.items = NULL;
}
ds.num_mimes = 0;
ds.total_remote_data_size = 0;
for (size_t i = 0; i < arraysz(ds.images); i++) {
if (ds.images[i].data) free(ds.images[i].data);
zero_at_ptr(ds.images + i);
}
free_pending(&ds.pending);
ds.allowed_operations = 0;
ds.state = DRAG_SOURCE_NONE;
ds.pre_sent_total_sz = 0;
ds.images_sent_total_sz = 0;
zero_at_ptr(&ds.in_flight_remote_file_data);
}
static void
drag_send_error(Window *w, int error_code) {
char buf[128];
const char *e = get_errno_name(error_code);
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=E", DND_CODE);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_size, e, strlen(e), false);
}
static void
cancel_drag(Window *w, int error_code) {
if (error_code) drag_send_error(w, error_code);
if (global_state.drag_source.is_active && global_state.drag_source.from_window == w->id) cancel_current_drag_source();
drag_free_offer(w);
}
#define abrt(code) { cancel_drag(w, code); return; }
void
drag_start_offerring(Window *w, const char *client_machine_id, size_t sz) {
ds.can_offer = true;
ds.is_remote_client = !is_same_machine(client_machine_id, sz);
}
void
drag_stop_offerring(Window *w) {
drag_free_offer(w);
ds.can_offer = false; ds.is_remote_client = false;
}
void
drag_add_mimes(Window *w, int allowed_operations, uint32_t client_id, const char *data, size_t sz, bool has_more) {
if (!ds.can_offer) abrt(EINVAL);
if (allowed_operations && !ds.allowed_operations) ds.allowed_operations = allowed_operations;
if (!ds.allowed_operations || ds.state > DRAG_SOURCE_BEING_BUILT) abrt(EINVAL);
ds.state = DRAG_SOURCE_BEING_BUILT;
ds.client_id = client_id;
size_t new_sz = ds.bufsz + sz;
if (new_sz > MIME_LIST_SIZE_CAP) abrt(EFBIG);
char *tmp = realloc(ds.mimes_buf, ds.bufsz + sz + 1);
if (!tmp) abrt(ENOMEM);
ds.mimes_buf = tmp;
memcpy(ds.mimes_buf + ds.bufsz, data, sz);
ds.bufsz = new_sz;
ds.mimes_buf[ds.bufsz] = 0;
if (!has_more) {
char *ptr = ds.mimes_buf;
size_t rough_count = 0;
while ((ptr = strchr(ptr, ' ')) != NULL) {
*ptr = 0; ptr++;
rough_count++;
}
ds.items = calloc(rough_count + 2, sizeof(ds.items[0]));
if (!ds.items) abrt(ENOMEM);
char *p = ds.mimes_buf, *end = ds.mimes_buf + ds.bufsz;
ds.num_mimes = 0;
while (p < end) {
if (*p) {
if (ds.num_mimes >= rough_count + 1) break;
ds.items[ds.num_mimes].is_uri_list = strcmp(p, "text/uri-list") == 0;
ds.items[ds.num_mimes++].mime_type = p;
p += strlen(p) + 1;
} else p++;
}
ds.pre_sent_total_sz = 0;
}
}
void
drag_add_pre_sent_data(Window *w, unsigned idx, const uint8_t *payload, size_t sz) {
if (ds.state != DRAG_SOURCE_BEING_BUILT || idx >= ds.num_mimes) abrt(EINVAL);
if (sz + ds.pre_sent_total_sz > PRESENT_DATA_CAP) abrt(EFBIG);
ds.pre_sent_total_sz += sz;
#define item ds.items[idx]
if (!item.data_decode_initialized) {
item.data_decode_initialized = true;
base64_init_stream_decoder(&item.base64_state);
}
if (item.data_capacity < sz + item.data_size) {
size_t newcap = MAX(item.data_capacity * 2, sz + item.data_size);
uint8_t *tmp = realloc(item.optional_data, newcap);
if (!tmp) abrt(ENOMEM);
item.optional_data = tmp;
item.data_capacity = newcap;
}
size_t outlen = item.data_capacity - item.data_size;
if (!base64_decode_stream(&item.base64_state, payload, sz, item.optional_data + item.data_size, &outlen)) abrt(EINVAL);
item.data_size += outlen;
#undef item
}
#define img ds.images[idx]
void
drag_add_image(Window *w, unsigned idx, int fmt, int width, int height, const uint8_t *payload, size_t sz) {
if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL);
if (idx + 1 >= arraysz(ds.images)) abrt(EFBIG);
if (ds.images_sent_total_sz + sz > PRESENT_DATA_CAP) abrt(EFBIG);
ds.images_sent_total_sz += sz;
if (!img.started) {
if (fmt != 24 && fmt != 32 && fmt != 100) abrt(EINVAL);
if (width < 1 || height < 1) abrt(EINVAL);
img.started = true;
img.width = width; img.height = height;
img.fmt = fmt;
base64_init_stream_decoder(&img.base64_state);
}
if (img.capacity < sz + img.sz) {
size_t newcap = MAX(img.capacity * 2, sz + img.sz);
uint8_t *tmp = realloc(img.data, newcap);
if (!tmp) abrt(ENOMEM);
img.data = tmp;
img.capacity = newcap;
}
size_t outlen = img.capacity - img.sz;
if (!base64_decode_stream(&img.base64_state, payload, sz, img.data + img.sz, &outlen)) abrt(EINVAL);
img.sz += outlen;
}
void
drag_change_image(Window *w, unsigned idx) {
ds.img_idx = idx;
if (ds.state == DRAG_SOURCE_STARTED) change_drag_image(idx);
}
static bool
expand_rgb_data(Window *w, size_t idx) {
#define fail(code) { cancel_drag(w, code); return false; }
if (img.sz != (size_t)img.width * (size_t)img.height * 3) fail(EINVAL);
const size_t sz = (size_t)img.width * (size_t)img.height * 4u;
RAII_ALLOC(uint8_t, expanded, malloc(sz));
if (!expanded) fail(ENOMEM);
memset(expanded, 0xff, sz);
for (int r = 0; r < img.height; r++) {
uint8_t *src_row = img.data + r * img.width * 3, *dest_row = expanded + r * img.width * 4;
for (int c = 0; c < img.width; c++) memcpy(dest_row + c * 4, src_row + c * 3, 3);
}
SWAP(img.data, expanded); img.sz = sz; img.fmt = 32;
return true;
}
static bool
expand_png_data(Window *w, size_t idx) {
png_read_data d = {0};
inflate_png_inner(&d, img.data, img.sz, 2000);
if (d.ok) {
free(img.data);
img.data = d.decompressed;
img.sz = d.sz;
img.width = d.width; img.height = d.height;
} else free(d.decompressed);
free(d.row_pointers);
return d.ok;
}
#undef fail
void
drag_start(Window *w) {
if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL);
size_t total_size = 0;
for (size_t idx = 0; idx < arraysz(ds.images); idx++) {
if (img.sz) {
switch (img.fmt) {
case 24:
if (!expand_rgb_data(w, idx)) return;
break;
case 100:
if (!expand_png_data(w, idx)) return;
break;
}
total_size += img.sz;
if (total_size > 2 * PRESENT_DATA_CAP) abrt(EFBIG);
if (img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL);
}
}
int err = start_window_drag(w);
if (err != 0) {
abrt(err);
} else {
// Free images and optional_data but keep the items array for later
// data requests from the drop target
for (size_t i = 0; i < ds.num_mimes; i++) {
free(ds.items[i].optional_data);
ds.items[i].optional_data = NULL;
ds.items[i].data_size = 0;
ds.items[i].data_capacity = 0;
ds.items[i].data_decode_initialized = false;
}
for (size_t i = 0; i < arraysz(ds.images); i++) {
if (ds.images[i].data) free(ds.images[i].data);
zero_at_ptr(ds.images + i);
}
ds.state = DRAG_SOURCE_STARTED;
drag_send_error(w, 0); // send OK
}
}
void
drag_notify(Window *w, DragNotifyType type) {
if (ds.state < DRAG_SOURCE_STARTED) return;
char buf[128];
size_t sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d", DND_CODE, type + 1);
switch(type) {
case DRAG_NOTIFY_ACCEPTED:
for (size_t i = 0; i < ds.num_mimes; i++) {
if (strcmp(ds.items[i].mime_type, global_state.drag_source.accepted_mime_type) == 0) {
sz += snprintf(buf + sz, sizeof(buf) - sz, ":y=%zu", i); break;
}
} break;
case DRAG_NOTIFY_ACTION_CHANGED:
switch (global_state.drag_source.action) {
case GLFW_DRAG_OPERATION_MOVE:
sz += snprintf(buf + sz, sizeof(buf) - sz, ":o=2"); break;
default:
sz += snprintf(buf + sz, sizeof(buf) - sz, ":o=1"); break;
} 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;
}
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, sz, NULL, 0, false);
if (type == DRAG_NOTIFY_FINISHED) drag_free_offer(w);
}
int
drag_free_data(Window *w, const char *mime_type, const char* data, size_t sz) {
(void)w; (void)mime_type; (void)sz;
free((void*)data);
return 0;
}
const char*
drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) {
*err_code = ENOENT; *sz = 0;
if (!ds.items || ds.state < DRAG_SOURCE_DROPPED) return NULL;
for (size_t i = 0; i < ds.num_mimes; i++) {
if (strcmp(ds.items[i].mime_type, mime_type) == 0) {
if (ds.items[i].fd_plus_one < 0) {
// Error was stored by drag_process_item_data
*err_code = -ds.items[i].fd_plus_one;
ds.items[i].fd_plus_one = 0;
return NULL;
}
if (ds.items[i].fd_plus_one > 0) {
// data_size = read position, data_capacity = bytes written to file
if (ds.items[i].data_capacity > ds.items[i].data_size) {
// Unread data available, use pread to read from read_pos
size_t available = ds.items[i].data_capacity - ds.items[i].data_size;
char *data = malloc(available);
if (!data) { *err_code = ENOMEM; return NULL; }
size_t total = 0;
while (total < available) {
ssize_t n = pread(ds.items[i].fd_plus_one - 1, data + total,
available - total,
(off_t)(ds.items[i].data_size + total));
if (n < 0) {
if (errno == EINTR) continue;
free(data);
*err_code = EIO;
return NULL;
}
if (n == 0) break;
total += (size_t)n;
}
ds.items[i].data_size += total;
*sz = total;
*err_code = 0;
return data;
}
// No unread data
if (!ds.items[i].data_decode_initialized) {
// Transfer complete and all data read
*err_code = 0;
return NULL;
}
// Still receiving data from client, wait
*err_code = EAGAIN;
return NULL;
}
// No fd yet, request data from the client
char buf[128];
ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list;
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu:Y=%d",
DND_CODE, DRAG_NOTIFY_FINISHED + 2, i, ds.items[i].requested_remote_files);
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
*err_code = EAGAIN;
return NULL;
}
}
return NULL;
}
static int
parse_errno_name(const uint8_t *data, size_t sz) {
if (sz >= 6 && memcmp(data, "ENOENT", 6) == 0) return ENOENT;
if (sz >= 5 && memcmp(data, "EPERM", 5) == 0) return EPERM;
if (sz >= 6 && memcmp(data, "EINVAL", 6) == 0) return EINVAL;
if (sz >= 6 && memcmp(data, "ENOMEM", 6) == 0) return ENOMEM;
if (sz >= 5 && memcmp(data, "EFBIG", 5) == 0) return EFBIG;
if (sz >= 3 && memcmp(data, "EIO", 3) == 0) return EIO;
if (sz >= 6 && memcmp(data, "EMFILE", 6) == 0) return EMFILE;
return EIO;
}
static int
open_item_tmpfile(void) {
int fd = -1;
#ifdef O_TMPFILE
fd = safe_open("/tmp", O_TMPFILE | O_CLOEXEC | O_EXCL | O_RDWR, S_IRUSR | S_IWUSR);
#endif
if (fd < 0) {
char name[] = "/tmp/kitty-dnd-XXXXXXXXXXXX";
fd = safe_mkstemp(name);
if (fd >= 0) unlink(name);
}
return fd;
}
void
drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *payload, size_t payload_sz) {
if ((ds.state < DRAG_SOURCE_DROPPED) || idx >= ds.num_mimes || !ds.items) {
abrt(EINVAL);
return;
}
if (has_more < 0) {
// Error from the client program
if (ds.items[idx].fd_plus_one > 0) {
safe_close(ds.items[idx].fd_plus_one - 1, __FILE__, __LINE__);
}
int err = parse_errno_name(payload, payload_sz);
ds.items[idx].fd_plus_one = -err;
ds.items[idx].data_decode_initialized = false;
int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type);
if (ret) cancel_drag(w, ret);
return;
}
// End of data: has_more == 0 and empty payload
if (has_more == 0 && payload_sz == 0) {
ds.items[idx].data_decode_initialized = false;
if (ds.items[idx].fd_plus_one > 0) {
if (!ds.items[idx].requested_remote_files) {
int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type);
if (ret) cancel_drag(w, ret);
}
}
return;
}
// Open temp file if not yet open
if (!ds.items[idx].fd_plus_one) {
int fd = open_item_tmpfile();
if (fd < 0) { cancel_drag(w, EIO); return; }
ds.items[idx].fd_plus_one = fd + 1;
ds.items[idx].data_decode_initialized = true;
ds.items[idx].data_size = 0; // read position for pread
ds.items[idx].data_capacity = 0; // bytes written to file
base64_init_stream_decoder(&ds.items[idx].base64_state);
}
// Decode and write payload data
if (payload_sz > 0) {
RAII_ALLOC(uint8_t, decoded, malloc(payload_sz));
if (!decoded) { cancel_drag(w, ENOMEM); return; }
size_t outlen = payload_sz;
if (!base64_decode_stream(&ds.items[idx].base64_state, payload, payload_sz, decoded, &outlen)) {
cancel_drag(w, EINVAL);
return;
}
size_t written = 0;
while (written < outlen) {
ssize_t n = safe_write(ds.items[idx].fd_plus_one - 1, decoded + written, outlen - written);
if (n < 0) {
cancel_drag(w, EIO);
return;
}
written += (size_t)n;
}
ds.items[idx].data_capacity += outlen;
// Notify as soon as any data is available
if (!ds.items[idx].requested_remote_files) {
int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[idx].mime_type);
if (ret) cancel_drag(w, ret);
}
}
}
static char**
parse_uri_list(Window *w, int fd, size_t *num_uris_out) {
*num_uris_out = 0;
// Determine file size and read all data
off_t file_size = lseek(fd, 0, SEEK_END);
if (file_size < 0) { cancel_drag(w, EIO); return NULL; }
if (lseek(fd, 0, SEEK_SET) < 0) { cancel_drag(w, EIO); return NULL; }
RAII_ALLOC(char, buf, malloc((size_t)file_size + 1));
if (!buf) { cancel_drag(w, ENOMEM); return NULL; }
size_t total = 0;
while (total < (size_t)file_size) {
ssize_t n = read(fd, buf + total, (size_t)file_size - total);
if (n < 0) {
if (errno == EINTR) continue;
cancel_drag(w, EIO); return NULL;
}
if (n == 0) break;
total += (size_t)n;
}
buf[total] = '\0';
// First pass: count non-comment, non-empty lines
size_t count = 0;
char *p = buf;
while (*p) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
char saved_end = *end; *end = '\0';
if (*p && *p != '#') count++;
*end = saved_end;
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
char **result = calloc((count + 1), sizeof(const char*));
if (!result) { cancel_drag(w, ENOMEM); return NULL; }
// Second pass: fill in decoded URI strings
size_t idx = 0;
p = buf;
while (*p && idx < count) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
*end = '\0';
if (*p && *p != '#') {
char *decoded = strdup(p);
if (!decoded) {
for (size_t k = 0; k < idx; k++) free((char*)result[k]);
free(result); cancel_drag(w, ENOMEM); return NULL;
}
result[idx++] = decoded;
}
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
*num_uris_out = idx;
return result;
}
static int
write_all(int fd, const void *buf, size_t sz) {
size_t pos = 0; const char *p = buf;
while (pos < sz) {
ssize_t ret = safe_write(fd, p + pos, sz - pos);
if (ret < 0) return ret;
pos += ret;
}
return 0;
}
static void
finish_remote_data(Window *w, size_t item_idx) {
const int fd = ds.items[item_idx].fd_plus_one - 1;
if (safe_ftruncate(fd, 0) != 0) abrt(errno);
if (lseek(fd, 0, SEEK_SET) == -1) abrt(errno);
for (size_t i = 0; i < ds.items[item_idx].num_uris; i++) {
int ret = write_all(fd, ds.items[item_idx].uri_list[i], strlen(ds.items[item_idx].uri_list[i]));
free((char*)ds.items[item_idx].uri_list[i]); ds.items[item_idx].uri_list[i] = NULL;
if (ret) abrt(ret);
if ((ret = write_all(fd, "\r\n", 2))) abrt(ret);
}
free(ds.items[item_idx].uri_list); ds.items[item_idx].uri_list = NULL; ds.items[item_idx].num_uris = 0;
int ret = notify_drag_data_ready(global_state.drag_source.from_os_window, ds.items[item_idx].mime_type);
abrt(ret);
}
#define mi ds.items[mime_item_idx]
static void
populate_dir_entries(Window *w, DragRemoteItem *ri) {
size_t num = count_occurrences((char*)ri->data, ri->data_sz, 0) + 1;
ri->children = calloc(num + 1, sizeof(ri->children[0]));
if (!ri->children) abrt(ENOMEM);
ri->children_sz = 0;
const char *ptr = (char*)ri->data;
const char *end = (char*)ri->data + ri->data_sz;
while (ptr < end) {
const char *p = memchr(ptr, 0, (size_t)(end - ptr));
size_t len = p ? (size_t)(p - ptr) : (size_t)(end - ptr);
if (len > 0) {
char *name = strndup(ptr, len);
if (!name) abrt(ENOMEM);
ri->children[ri->children_sz++].dir_entry_name = name;
}
ptr = p ? p + 1 : end;
}
}
static void
add_payload(Window *w, DragRemoteItem *ri, bool has_more, const uint8_t *payload, size_t payload_sz, int dirfd) {
if (payload_sz && payload) {
if (payload_sz > 4096) abrt(EINVAL);
switch (ri->type) {
case 0: {
if (!ri->fd_plus_one) {
int fd = safe_openat(dirfd, ri->dir_entry_name, O_CREAT | O_WRONLY, file_permissions);
if (fd < 0) abrt(errno);
ri->fd_plus_one = fd + 1;
}
uint8_t buf[4096];
size_t outlen = sizeof(buf);
if (!base64_decode_stream(&ri->base64_state, payload, payload_sz, buf, &outlen)) abrt(EINVAL);
ds.total_remote_data_size += outlen;
if (outlen && write_all(ri->fd_plus_one-1, buf, outlen) < 0) abrt(errno);
} break;
default: {
if (ri->data_sz + payload_sz > ri->data_capacity) {
size_t cap = MAX(ri->data_capacity * 2, ri->data_sz + payload_sz + 4096);
if (cap > PRESENT_DATA_CAP) abrt(EMFILE);
uint8_t *tmp = realloc(ri->data, cap);
if (!tmp) abrt(ENOMEM);
ri->data = tmp;
ri->data_capacity = cap;
}
size_t outlen = ri->data_capacity - ri->data_sz;
if (!base64_decode_stream(&ri->base64_state, payload, payload_sz, ri->data + ri->data_sz, &outlen)) abrt(EINVAL);
ds.total_remote_data_size += outlen;
ri->data_sz += outlen;
} break;
}
}
if (ds.total_remote_data_size > REMOTE_DRAG_LIMIT) abrt(EMFILE);
if (!has_more && !payload_sz) { // all data received
switch (ri->type) {
case 0:
safe_close(ri->fd_plus_one-1, __FILE__, __LINE__);
ri->fd_plus_one = 0;
break;
case 1:
// Ensure room for the null terminator needed by symlinkat
if (ri->data_sz >= ri->data_capacity) {
uint8_t *tmp = realloc(ri->data, ri->data_sz + 1);
if (!tmp) abrt(ENOMEM);
ri->data = tmp;
ri->data_capacity = ri->data_sz + 1;
}
ri->data[ri->data_sz] = 0;
if (symlinkat((char*)ri->data, dirfd, ri->dir_entry_name) != 0) abrt(errno);
break;
default:
if (mkdirat(dirfd, ri->dir_entry_name, dir_permissions) != 0 && errno != EEXIST) abrt(errno);
populate_dir_entries(w, ri);
break;
}
free(ri->data); ri->data = 0; ri->data_capacity = 0; ri->data_sz = 0;
}
}
static void
toplevel_data_for_drag(
Window *w, unsigned mime_item_idx, unsigned uri_item_idx, unsigned item_type,
bool has_more, const uint8_t *payload, size_t payload_sz
) {
if (!mi.remote_items) {
mi.remote_items = calloc(mi.num_uris, sizeof(mi.remote_items[0]));
if (!mi.remote_items) abrt(ENOMEM);
mi.num_remote_items = mi.num_uris;
}
if (!mi.base_dir_for_remote_items) {
int fd;
mi.base_dir_for_remote_items = mktempdir_in_cache("dnd-drag-", &fd);
if (!mi.base_dir_for_remote_items) abrt(errno);
mi.base_dir_fd_plus_one = fd + 1;
}
if (uri_item_idx >= mi.num_remote_items) abrt(EINVAL);
DragRemoteItem *ri = mi.remote_items + uri_item_idx;
if (!ri->started) {
ri->started = true;
ri->type = item_type;
base64_init_stream_decoder(&ri->base64_state);
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);
ri->dir_entry_name = fname;
char path[32];
snprintf(path, sizeof(path), "%u", uri_item_idx);
if (mkdirat(mi.base_dir_fd_plus_one - 1, path, dir_permissions) != 0 && errno != EEXIST) abrt(errno);
int fd = safe_openat(mi.base_dir_fd_plus_one - 1, path, O_RDONLY | O_DIRECTORY, 0);
if (fd < 0) abrt(errno);
ri->top_level_parent_dir_fd_plus_one = fd + 1;
free(mi.uri_list[uri_item_idx]);
mi.uri_list[uri_item_idx] = as_file_url(mi.base_dir_for_remote_items, path, ri->dir_entry_name);
}
add_payload(w, ri, has_more, payload, payload_sz, ri->top_level_parent_dir_fd_plus_one - 1);
}
static DragRemoteItem*
find_by_handle(DragRemoteItem *parent, int handle, char *path_to_parent, size_t *path_len) {
if (parent->type == handle) return parent;
DragRemoteItem *x;
for (size_t i = 0; i < parent->children_sz; i++) {
DragRemoteItem *child = parent->children + i;
size_t before = *path_len;
size_t n = snprintf(path_to_parent + before, PATH_MAX - before, "/%s", child->dir_entry_name);
if (n + before + 1 >= PATH_MAX) return NULL;
*path_len += n;
if ((x = find_by_handle(parent->children + i, handle, path_to_parent, path_len))) return x;
*path_len = before;
}
return NULL;
}
static void
subdir_data_for_drag(
Window *w, unsigned mime_item_idx, unsigned uri_item_idx, int handle, unsigned entry_num, unsigned item_type,
bool has_more, const uint8_t *payload, size_t payload_sz
) {
if (!mi.remote_items || uri_item_idx >= mi.num_remote_items) abrt(EINVAL);
DragRemoteItem *parent = NULL;
if (mi.currently_open_subdir) {
if (mi.currently_open_subdir->type == handle) parent = mi.currently_open_subdir;
else {
if (mi.currently_open_subdir->fd_plus_one) {
safe_close(mi.currently_open_subdir->fd_plus_one - 1, __FILE__, __LINE__);
mi.currently_open_subdir->fd_plus_one = 0;
}
mi.currently_open_subdir = NULL;
}
}
if (parent == NULL || !parent->fd_plus_one) {
char path[PATH_MAX+1]; path[PATH_MAX] = 0;
DragRemoteItem *root = mi.remote_items + uri_item_idx;
if (!root->dir_entry_name) abrt(EINVAL);
size_t pos = snprintf(path, PATH_MAX, "%s/%u/%s",
mi.base_dir_for_remote_items, uri_item_idx, root->dir_entry_name);
parent = find_by_handle(root, handle, path, &pos);
if (!parent) abrt(EINVAL);
mi.currently_open_subdir = parent;
if (!parent->fd_plus_one) {
int fd = safe_open(path, O_DIRECTORY | O_RDONLY, 0);
if (fd < 0) abrt(errno);
parent->fd_plus_one = fd + 1;
}
}
if (entry_num >= parent->children_sz) abrt(EINVAL);
DragRemoteItem *ri = parent->children + entry_num;
if (!ri->started) {
ri->started = true;
ri->type = item_type;
base64_init_stream_decoder(&ri->base64_state);
}
add_payload(w, ri, has_more, payload, payload_sz, parent->fd_plus_one - 1);
}
#undef mi
void
drag_offer_start_to_child(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_x, int32_t pixel_y) {
char buf[256];
int header_size = snprintf(
buf, sizeof(buf), "%d;t=o:x=%d:y=%d:X=%d:Y=%d", DND_CODE, cell_x, cell_y, pixel_x, pixel_y);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_size, NULL, 0, false);
}
void
drag_remote_file_data(
Window *w, int32_t x, int32_t y, int32_t X, int32_t Y, bool has_more, const uint8_t *payload, size_t payload_sz
) {
if (ds.in_flight_remote_file_data.active) {
x = ds.in_flight_remote_file_data.x; y = ds.in_flight_remote_file_data.y;
ds.in_flight_remote_file_data.Y = Y; ds.in_flight_remote_file_data.X = X;
}
if (!has_more) zero_at_ptr(&ds.in_flight_remote_file_data);
else if (!ds.in_flight_remote_file_data.active) {
ds.in_flight_remote_file_data.active = true;
ds.in_flight_remote_file_data.x = x; ds.in_flight_remote_file_data.y = y;
ds.in_flight_remote_file_data.Y = Y; ds.in_flight_remote_file_data.X = X;
}
size_t item_idx = ds.num_mimes;
for (size_t i = 0; i < ds.num_mimes; i++) {
if (ds.items[i].requested_remote_files) {
item_idx = i; break;
}
}
if (item_idx == ds.num_mimes || ds.items[item_idx].fd_plus_one == 0) abrt(EINVAL);
if (ds.items[item_idx].uri_list == NULL) {
ds.items[item_idx].uri_list = parse_uri_list(w, ds.items[item_idx].fd_plus_one-1, &ds.items[item_idx].num_uris);
if (!ds.items[item_idx].uri_list) return;
}
if (X < 0) abrt(EINVAL);
if (!x && !y && !Y) { finish_remote_data(w, item_idx); return; }
if (!Y) toplevel_data_for_drag(w, item_idx, x - 1, X, has_more, payload, payload_sz);
else subdir_data_for_drag(w, item_idx, x - 1, Y, y - 1, X, has_more, payload, payload_sz);
}
#undef img
#undef abrt
#undef ds
// }}}
// DnD testing infrastructure {{{
static PyObject *
py_dnd_set_test_write_func(PyObject *self UNUSED, PyObject *args) {
PyObject *func = Py_None; unsigned mime_list_size_cap = 0, present_data_cap = 0, remote_drag_limit = 0;
if (!PyArg_ParseTuple(args, "|OIII", &func, &mime_list_size_cap, &present_data_cap, &remote_drag_limit)) return NULL;
// Pass None to clear the interceptor and restore normal operation.
dnd_set_test_write_func(func == Py_None ? NULL : func, mime_list_size_cap, present_data_cap, remote_drag_limit);
Py_RETURN_NONE;
}
static void
destroy_fake_window_contents(Window *w) {
// Free window resources without touching GPU objects (none allocated for fake windows).
drop_free_data(w);
drag_free_offer(w);
free(w->pending_clicks.clicks); zero_at_ptr(&w->pending_clicks);
free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys);
Py_CLEAR(w->render_data.screen);
Py_CLEAR(w->title);
Py_CLEAR(w->title_bar_data.last_drawn_title_object_id);
free(w->title_bar_data.buf); w->title_bar_data.buf = NULL;
Py_CLEAR(w->url_target_bar_data.last_drawn_title_object_id);
free(w->url_target_bar_data.buf); w->url_target_bar_data.buf = NULL;
// render_data.vao_idx is -1 so release_gpu_resources_for_window is safe, but we skip it
// since we never allocated those resources.
}
static PyObject *
dnd_test_create_fake_window(PyObject *self UNUSED, PyObject *args UNUSED) {
// Create a minimal OS window + tab + window without any OpenGL/GPU resources.
// Returns (os_window_id, window_id).
ensure_space_for(&global_state, os_windows, OSWindow, global_state.num_os_windows + 1, capacity, 1, true);
OSWindow *osw = global_state.os_windows + global_state.num_os_windows++;
zero_at_ptr(osw);
osw->id = ++global_state.os_window_id_counter;
osw->tab_bar_render_data.vao_idx = -1;
osw->background_opacity.alpha = OPT(background_opacity);
osw->created_at = monotonic();
// osw->handle intentionally left NULL - no real GLFW window
ensure_space_for(osw, tabs, Tab, 1, capacity, 1, true);
Tab *tab = &osw->tabs[0];
zero_at_ptr(tab);
tab->id = ++global_state.tab_id_counter;
tab->border_rects.vao_idx = -1;
osw->num_tabs = 1;
osw->active_tab = 0;
ensure_space_for(tab, windows, Window, 1, capacity, 1, true);
Window *w = &tab->windows[0];
zero_at_ptr(w);
w->id = ++global_state.window_id_counter;
w->visible = true;
w->render_data.vao_idx = -1;
w->window_title_render_data.vao_idx = -1;
w->drop.wanted = true;
tab->num_windows = 1;
tab->active_window = 0;
global_state.mouse_hover_in_window = w->id;
return Py_BuildValue("KK", (unsigned long long)osw->id, (unsigned long long)w->id);
}
static PyObject *
dnd_test_cleanup_fake_window(PyObject *self UNUSED, PyObject *args) {
unsigned long long os_window_id;
if (!PyArg_ParseTuple(args, "K", &os_window_id)) return NULL;
for (size_t i = 0; i < global_state.num_os_windows; i++) {
if (global_state.os_windows[i].id == (id_type)os_window_id) {
OSWindow *osw = global_state.os_windows + i;
for (size_t t = 0; t < osw->num_tabs; t++) {
Tab *tab = osw->tabs + t;
for (size_t j = 0; j < tab->num_windows; j++) {
Window *win = tab->windows + j;
if (global_state.mouse_hover_in_window == win->id)
global_state.mouse_hover_in_window = 0;
destroy_fake_window_contents(win);
}
free(tab->border_rects.rect_buf); tab->border_rects.rect_buf = NULL;
free(tab->windows); tab->windows = NULL;
}
Py_CLEAR(osw->window_title);
Py_CLEAR(osw->tab_bar_render_data.screen);
free(osw->tabs); osw->tabs = NULL;
remove_i_from_array(global_state.os_windows, i, global_state.num_os_windows);
break;
}
}
Py_RETURN_NONE;
}
static PyObject *
dnd_test_set_mouse_pos(PyObject *self UNUSED, PyObject *args) {
unsigned long long window_id;
int cell_x, cell_y, pixel_x, pixel_y;
if (!PyArg_ParseTuple(args, "Kiiii", &window_id, &cell_x, &cell_y, &pixel_x, &pixel_y)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
w->mouse_pos.cell_x = (unsigned int)cell_x;
w->mouse_pos.cell_y = (unsigned int)cell_y;
w->mouse_pos.global_x = pixel_x;
w->mouse_pos.global_y = pixel_y;
Py_RETURN_NONE;
}
static PyObject *
dnd_test_fake_drop_event(PyObject *self UNUSED, PyObject *args) {
// Simulate a drop enter/move/drop event. mimes_seq must be a sequence of str, or
// None to simulate a leave event.
unsigned long long window_id;
int is_drop;
PyObject *mimes_seq = Py_None;
int x = -2, y = -2;
if (!PyArg_ParseTuple(args, "Kp|Oii", &window_id, &is_drop, &mimes_seq, &x, &y)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (mimes_seq == Py_None) {
drop_left_child(w);
Py_RETURN_NONE;
}
RAII_PyObject(fast_seq, PySequence_Fast(mimes_seq, "mimes must be a sequence"));
if (!fast_seq) return NULL;
Py_ssize_t num_mimes = PySequence_Fast_GET_SIZE(fast_seq);
RAII_ALLOC(const char*, mimes, malloc(sizeof(const char*) * (num_mimes ? num_mimes : 1)));
if (!mimes) return PyErr_NoMemory();
for (Py_ssize_t i = 0; i < num_mimes; i++) {
mimes[i] = PyUnicode_AsUTF8(PySequence_Fast_GET_ITEM(fast_seq, i));
if (!mimes[i]) return NULL;
}
if (x > -1) w->mouse_pos.cell_x = x;
if (y > -1) w->mouse_pos.cell_y = y;
drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false);
Py_RETURN_NONE;
}
static PyObject *
dnd_test_fake_drop_data(PyObject *self UNUSED, PyObject *args) {
// Simulate OS delivering drop data for the given MIME type.
// If error_code > 0, simulate an error (e.g. ENOENT=2, EIO=5, EPERM=1).
// Otherwise deliver data and the mandatory end-of-data signal.
unsigned long long window_id;
const char *mime;
RAII_PY_BUFFER(data);
int error_code = 0, no_eod = 0;
if (!PyArg_ParseTuple(args, "Ksy*|ip", &window_id, &mime, &data, &error_code, &no_eod)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (error_code > 0) {
drop_dispatch_data(w, mime, NULL, -(ssize_t)error_code);
} else if (data.len > 0) {
drop_dispatch_data(w, mime, (const char*)data.buf, (ssize_t)data.len);
if (!no_eod) drop_dispatch_data(w, mime, NULL, 0); // mandatory end-of-data signal
} else {
// Empty data: just the end-of-data signal (sz=0 is the sentinel for "no more data").
drop_dispatch_data(w, mime, NULL, 0);
}
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 PyObject *
dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) {
// Simulate what drag_get_data does initially: find the MIME item at the
// given index, set requested_remote_files if appropriate, and return the
// escape code that would be sent to the client.
unsigned long long window_id;
unsigned idx;
if (!PyArg_ParseTuple(args, "KI", &window_id, &idx)) 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_DROPPED || idx >= w->drag_source.num_mimes || !w->drag_source.items) {
PyErr_SetString(PyExc_ValueError, "Invalid state or index"); return NULL;
}
w->drag_source.items[idx].requested_remote_files = w->drag_source.is_remote_client && w->drag_source.items[idx].is_uri_list;
Py_RETURN_NONE;
}
static PyObject *
dnd_test_drag_notify(PyObject *self UNUSED, PyObject *args) {
// Call drag_notify with a specific type for testing the protocol output.
// type: 0=ACCEPTED, 1=ACTION_CHANGED, 2=DROPPED, 3=FINISHED
// accepted_mime: the MIME type to set in global_state.drag_source.accepted_mime_type (for ACCEPTED)
// action: the action to set in global_state.drag_source.action (for ACTION_CHANGED)
// was_canceled: whether the drag was canceled (for FINISHED)
unsigned long long window_id;
int type;
const char *accepted_mime = NULL;
int action = 0, was_canceled = 0;
if (!PyArg_ParseTuple(args, "Ki|sii", &window_id, &type, &accepted_mime, &action, &was_canceled)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (type < 0 || type > 3) { PyErr_SetString(PyExc_ValueError, "Invalid type"); return NULL; }
if (accepted_mime && *accepted_mime) {
free(global_state.drag_source.accepted_mime_type);
global_state.drag_source.accepted_mime_type = strdup(accepted_mime);
if (!global_state.drag_source.accepted_mime_type) { PyErr_NoMemory(); return NULL; }
}
global_state.drag_source.action = action;
global_state.drag_source.was_canceled = was_canceled;
drag_notify(w, (DragNotifyType)type);
Py_RETURN_NONE;
}
static PyObject*
dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) {
const char *q; unsigned long long window_id;
if (!PyArg_ParseTuple(args, "Ks", &window_id, &q)) return NULL;
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
if (strcmp(q, "drop_wanted") == 0) {
return Py_NewRef(w->drop.wanted ? Py_True : Py_False);
}
if (strcmp(q, "drag_can_offer") == 0) {
return Py_NewRef(w->drag_source.can_offer ? Py_True : Py_False);
}
if (strcmp(q, "drop_is_remote_client") == 0) {
return Py_NewRef(w->drop.is_remote_client ? Py_True : Py_False);
}
if (strcmp(q, "drag_is_remote_client") == 0) {
return Py_NewRef(w->drag_source.is_remote_client ? Py_True : Py_False);
}
if (strcmp(q, "drop_action") == 0) {
return PyLong_FromLong((long)w->drop.accepted_operation);
}
if (strcmp(q, "drop_mimes") == 0) {
if (w->drop.accepted_mimes == NULL) return PyUnicode_FromString("");
return PyUnicode_FromStringAndSize(w->drop.accepted_mimes, w->drop.accepted_mimes_sz);
}
if (strcmp(q, "drop_data_requests") == 0) {
PyObject *ans = PyTuple_New(w->drop.num_data_requests);
for (size_t i = 0; i < w->drop.num_data_requests; i++) {
#define item w->drop.data_requests[i]
PyObject *x = Py_BuildValue("iii", item.cell_x, item.cell_y, item.pixel_y);
PyTuple_SET_ITEM(ans, i, x);
#undef item
}
return ans;
}
if (strcmp(q, "drop_getting_data_for_mime") == 0) {
return PyUnicode_FromString(w->drop.getting_data_for_mime ? w->drop.getting_data_for_mime : "");
}
Py_RETURN_NONE;
}
static PyMethodDef dnd_methods[] = {
{"dnd_set_test_write_func", (PyCFunction)py_dnd_set_test_write_func, METH_VARARGS, ""},
METHODB(dnd_test_create_fake_window, METH_NOARGS),
METHODB(dnd_test_cleanup_fake_window, METH_VARARGS),
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),
METHODB(dnd_test_request_drag_data, METH_VARARGS),
METHODB(dnd_test_drag_notify, METH_VARARGS),
METHODB(dnd_test_probe_state, METH_VARARGS),
{NULL, NULL, 0, NULL}
};
bool
init_dnd(PyObject *m) {
if (PyModule_AddFunctions(m, dnd_methods) != 0) return false;
return true;
}
// }}}