1047 lines
38 KiB
C
1047 lines
38 KiB
C
/*
|
||
* 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>
|
||
|
||
static const size_t MIME_LIST_SIZE_CAP = 1024 * 1024;
|
||
static const size_t PRESENT_DATA_CAP = 64 * 1024 * 1024;
|
||
|
||
// 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 PyObject *g_dnd_test_write_func = NULL;
|
||
|
||
void
|
||
dnd_set_test_write_func(PyObject *func) {
|
||
Py_XDECREF(g_dnd_test_write_func);
|
||
g_dnd_test_write_func = Py_XNewRef(func);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
static void
|
||
reset_drop(Window *w) {
|
||
bool wanted = w->drop.wanted; uint32_t cid = w->drop.client_id;
|
||
drop_free_data(w);
|
||
zero_at_ptr(&w->drop);
|
||
if (wanted) {
|
||
w->drop.wanted = wanted;
|
||
w->drop.client_id = cid;
|
||
}
|
||
}
|
||
|
||
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;
|
||
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;
|
||
w->drop.registered_mimes = realloc(w->drop.registered_mimes, sz + payload_sz + 1);
|
||
if (w->drop.registered_mimes) {
|
||
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;
|
||
}
|
||
|
||
|
||
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); 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();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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 ENOMEM: return "ENOMEM";
|
||
case 0: return "OK";
|
||
default: return "EUNKNOWN";
|
||
}
|
||
}
|
||
|
||
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);
|
||
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);
|
||
}
|
||
|
||
void
|
||
drop_request_data(Window *w, const char *mime) {
|
||
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;
|
||
if (w->drop.offerred_mimes) {
|
||
for (size_t i = 0; i < w->drop.num_offerred_mimes; i++) {
|
||
if (strcmp(mime, w->drop.offerred_mimes[i]) == 0) {
|
||
w->drop.getting_data_for_mime = strdup(mime);
|
||
if (w->drop.getting_data_for_mime) request_drop_data(osw, w->id, mime);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
drop_send_error(w, ENOENT);
|
||
}
|
||
|
||
void
|
||
drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) {
|
||
if (sz < 0) drop_send_error(w, -sz);
|
||
else {
|
||
char buf[128];
|
||
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=r", DND_CODE);
|
||
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz, true);
|
||
if (strcmp(mime, "text/uri-list") == 0) {
|
||
w->drop.uri_list_sz += sz;
|
||
w->drop.uri_list = realloc(w->drop.uri_list, w->drop.uri_list_sz);
|
||
if (w->drop.uri_list) memcpy(w->drop.uri_list + w->drop.uri_list_sz - sz, data, sz);
|
||
else w->drop.uri_list_sz = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Remote file / directory transfer (t=s, t=d) ----
|
||
|
||
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;
|
||
}
|
||
|
||
/* 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; }
|
||
|
||
char resolved[PATH_MAX];
|
||
if (!realpath(path, resolved)) {
|
||
switch (errno) {
|
||
case ENOENT: case ENOTDIR: *error_out = "ENOENT"; break;
|
||
case EACCES: case EPERM: *error_out = "EPERM"; break;
|
||
case ELOOP: *error_out = "ENOENT"; break;
|
||
default: *error_out = "EINVAL"; break;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
*path_out = strdup(resolved);
|
||
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);
|
||
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);
|
||
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);
|
||
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);
|
||
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);
|
||
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);
|
||
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. */
|
||
static void
|
||
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;
|
||
}
|
||
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;
|
||
}
|
||
if (!S_ISREG(st.st_mode)) { drop_send_error(w, EINVAL); safe_close(fd, __FILE__, __LINE__); return; }
|
||
w->drop.file_fd_plus_one = fd + 1;
|
||
w->drop.last_file_send_at = monotonic();
|
||
drop_send_file_chunks(w);
|
||
}
|
||
|
||
/* 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++;
|
||
if (w->drop.next_dir_handle_id == 0) w->drop.next_dir_handle_id = 1;
|
||
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) {
|
||
struct stat st;
|
||
if (stat(path, &st) < 0) { drop_send_error(w, EIO); return; }
|
||
|
||
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: unique_id\0entry1\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; }
|
||
|
||
/* First entry: unique identifier (device:inode) */
|
||
char uid[64];
|
||
int uid_len = snprintf(uid, sizeof(uid), "%llu:%llu",
|
||
(unsigned long long)st.st_dev,
|
||
(unsigned long long)st.st_ino);
|
||
|
||
#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)
|
||
|
||
APPEND(uid, uid_len);
|
||
|
||
/* 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=d:x=%u", DND_CODE, (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);
|
||
}
|
||
|
||
/* Handle a t=s request: send the file/directory at URI-list index idx. */
|
||
void
|
||
drop_request_uri_data(Window *w, const char *payload, size_t payload_sz) {
|
||
if (!w->drop.uri_list || !w->drop.uri_list_sz) {
|
||
drop_send_error(w, EINVAL); return;
|
||
}
|
||
|
||
/* Payload format: "text/uri-list:idx" */
|
||
const char *colon = memchr(payload, ':', payload_sz);
|
||
if (!colon) { drop_send_error(w, EINVAL); return; }
|
||
|
||
size_t mime_len = (size_t)(colon - payload);
|
||
if (mime_len != 13 || strncmp(payload, "text/uri-list", 13) != 0) {
|
||
drop_send_error(w, EINVAL); return;
|
||
}
|
||
|
||
const char *idx_str = colon + 1;
|
||
size_t idx_len = payload_sz - mime_len - 1;
|
||
char idx_buf[32];
|
||
if (!idx_len || idx_len >= sizeof(idx_buf)) { drop_send_error(w, EINVAL); return; }
|
||
memcpy(idx_buf, idx_str, idx_len);
|
||
idx_buf[idx_len] = 0;
|
||
|
||
char *endp;
|
||
long idx = strtol(idx_buf, &endp, 10);
|
||
if (endp == idx_buf || *endp != 0 || idx < 0) { drop_send_error(w, EINVAL); return; }
|
||
|
||
char *path = NULL;
|
||
const char *err = NULL;
|
||
if (!get_nth_file_url(w->drop.uri_list, w->drop.uri_list_sz, (int)idx, &path, &err)) {
|
||
drop_send_error_str(w, err);
|
||
return;
|
||
}
|
||
|
||
struct stat st;
|
||
if (stat(path, &st) < 0) {
|
||
free(path);
|
||
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;
|
||
}
|
||
|
||
if (S_ISDIR(st.st_mode)) {
|
||
drop_send_dir_listing(w, path);
|
||
} else if (S_ISREG(st.st_mode)) {
|
||
drop_send_file_data(w, path);
|
||
} else {
|
||
drop_send_error(w, EINVAL);
|
||
}
|
||
free(path);
|
||
}
|
||
|
||
/* Handle a t=d request from the client.
|
||
* handle_id: the directory handle (x= key).
|
||
* entry_num: 0 means close the handle; >=1 means read that entry (1-based). */
|
||
void
|
||
drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) {
|
||
if (!handle_id) { drop_send_error(w, EINVAL); return; }
|
||
|
||
DirHandle *h = drop_find_dir_handle(w, handle_id);
|
||
if (!h) { drop_send_error(w, EINVAL); return; }
|
||
|
||
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;
|
||
}
|
||
|
||
/* 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; }
|
||
|
||
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;
|
||
}
|
||
|
||
struct stat st;
|
||
if (stat(full, &st) < 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;
|
||
}
|
||
|
||
if (S_ISDIR(st.st_mode)) {
|
||
drop_send_dir_listing(w, full);
|
||
} else if (S_ISREG(st.st_mode)) {
|
||
drop_send_file_data(w, full);
|
||
} else {
|
||
drop_send_error(w, EINVAL);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
#define ds w->drag_source
|
||
|
||
static void
|
||
drag_free_built_data(Window *w) {
|
||
if (ds.items) {
|
||
for (size_t i=0; i < ds.num_mimes; i++) free(ds.items[i].optional_data);
|
||
free(ds.items);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
|
||
void
|
||
drag_free_offer(Window *w) {
|
||
free(ds.mimes_buf); ds.mimes_buf = NULL;
|
||
drag_free_built_data(w);
|
||
ds.allowed_operations = 0;
|
||
ds.state = DRAG_SOURCE_NONE;
|
||
ds.num_mimes = 0;
|
||
ds.pre_sent_total_sz = 0;
|
||
ds.images_sent_total_sz = 0;
|
||
}
|
||
|
||
static void
|
||
cancel_drag(Window *w, int error_code) {
|
||
if (error_code) drop_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);
|
||
}
|
||
|
||
void
|
||
drag_add_mimes(Window *w, int allowed_operations, const char *data, size_t sz, bool has_more) {
|
||
#define abrt(code) { cancel_drag(w, code); return; }
|
||
if (allowed_operations && ds.state != DRAG_SOURCE_NONE) cancel_drag(w, 0);
|
||
if (allowed_operations && !ds.allowed_operations) ds.allowed_operations = allowed_operations;
|
||
if (!ds.allowed_operations) { abrt(EINVAL); }
|
||
ds.state = DRAG_SOURCE_BEING_BUILT;
|
||
size_t new_sz = ds.bufsz + sz;
|
||
if (new_sz > MIME_LIST_SIZE_CAP) abrt(EFBIG);
|
||
ds.mimes_buf = realloc(ds.mimes_buf, ds.bufsz + sz + 1);
|
||
if (!ds.mimes_buf) abrt(ENOMEM);
|
||
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++].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);
|
||
item.optional_data = realloc(item.optional_data, newcap);
|
||
if (!item.optional_data) abrt(ENOMEM);
|
||
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);
|
||
img.data = realloc(img.data, newcap);
|
||
if (!img.data) abrt(ENOMEM);
|
||
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 = img.width * img.height * 4;
|
||
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 {
|
||
drag_free_built_data(w);
|
||
ds.state = DRAG_SOURCE_STARTED;
|
||
drop_send_error(w, 0); // send OK
|
||
}
|
||
}
|
||
|
||
#undef img
|
||
#undef abrt
|
||
#undef ds
|