diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 175053845..ce52dcfbc 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -393,7 +393,7 @@ files/directories. Every directory must be transmitted with ``X=handle``. The payload is a null separated list of regular files, directories and symlinks in the directory. ``handle`` is an integer other than ``0`` or ``1`` -that serves as an identifier for the directory. Directories must be traversed +that serves as an identifier for the directory. Directories should be traversed in breadth first order. The children of a directory are reported by adding ``Y=parent-handle:y=num`` to the escape codes above. Here ``parent-handle`` is the handle of the directory being traversed and ``num`` diff --git a/kitty/dnd.c b/kitty/dnd.c index 63d8620e4..df56aad42 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -28,6 +28,17 @@ static PyObject *g_dnd_test_write_func = NULL; 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) { @@ -64,6 +75,87 @@ machine_id(void) { 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, "s", prefix)); + 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; + } + } + } + } + } + } + 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; +} + + void dnd_set_test_write_func(PyObject *func) { (void)machine_id; @@ -1077,6 +1169,18 @@ drop_left_child(Window *w) { // 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); + } + zero_at_ptr(x); +} + void drag_free_offer(Window *w) { free(ds.mimes_buf); ds.mimes_buf = NULL; ds.bufsz = 0; @@ -1085,14 +1189,25 @@ drag_free_offer(Window *w) { 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((char*)ds.items[i].uri_list[k]); + 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); @@ -1488,7 +1603,7 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo } } -static const char** +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 @@ -1526,7 +1641,7 @@ parse_uri_list(Window *w, int fd, size_t *num_uris_out) { while (*p == '\r' || *p == '\n') p++; } - const char **result = calloc((count + 1), sizeof(const char*)); + char **result = calloc((count + 1), sizeof(const char*)); if (!result) { cancel_drag(w, ENOMEM); return NULL; } // Second pass: fill in decoded URI strings @@ -1582,12 +1697,129 @@ finish_remote_data(Window *w, size_t item_idx) { abrt(ret); } +#define mi ds.items[mime_item_idx] +static size_t REMOTE_DRAG_LIMIT = 1024 * 1024 * 1024; + +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 *p = ptr; + while ((p = memchr(ptr, 0, ri->data_sz - (ptr - (char*)ri->data))) != NULL) { + char *name = strdup(ptr); + if (!name) abrt(ENOMEM); + ri->children[ri->children_sz++].dir_entry_name = name; + ptr = p + 1; + if ((uint8_t*)ptr >= ri->data + ri->data_sz) break; + } +} + +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, 0); + 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 > 10 * 1024) abrt(EMFILE); + ri->data = realloc(ri->data, cap); + if (!ri->data) abrt(ENOMEM); + 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: + ri->data[ri->data_sz] = 0; + if (symlinkat((char*)ri->data, dirfd, ri->dir_entry_name) != 0) abrt(errno); + break; + default: + 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 ) { - (void)w; (void)uri_item_idx; (void)item_type; (void)has_more; (void)payload; (void)payload_sz; (void)mime_item_idx; + if (!mi.remote_items) { + mi.remote_items = calloc(ds.num_mimes, sizeof(mi.remote_items[0])); + if (!mi.remote_items) abrt(ENOMEM); + mi.num_remote_items = ds.num_mimes; + } + 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, 0700) != 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; + 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 @@ -1595,9 +1827,43 @@ 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 ) { - (void)w; (void)uri_item_idx; (void)item_type; (void)has_more; (void)payload; (void)payload_sz; (void)handle; (void)entry_num; (void)mime_item_idx; + 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_remote_file_data( diff --git a/kitty/safe-wrappers.h b/kitty/safe-wrappers.h index 24f686c92..cdac53c47 100644 --- a/kitty/safe-wrappers.h +++ b/kitty/safe-wrappers.h @@ -71,6 +71,16 @@ safe_open(const char *path, int flags, mode_t mode) { } } +static inline int +safe_openat(int dirfd, const char *path, int flags, mode_t mode) { + while (true) { + int fd = openat(dirfd, path, flags, mode); + if (fd == -1 && errno == EINTR) continue; + return fd; + } +} + + static inline FILE* safe_fopen(const char *path, const char *mode) { while (true) { diff --git a/kitty/state.h b/kitty/state.h index eb506cb4f..baf26c905 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -236,6 +236,19 @@ typedef struct DirHandle { typedef enum { DRAG_SOURCE_NONE, DRAG_SOURCE_BEING_BUILT, DRAG_SOURCE_STARTED, DRAG_SOURCE_DROPPED } DragSourceState; +typedef struct DragRemoteItem { + int type; // 0 regular file, 1 symlink, otherwise directory + int fd_plus_one; // for regular files + int top_level_parent_dir_fd_plus_one; + uint8_t *data; // for symlink targets and directory listing + size_t data_sz, data_capacity; + struct DragRemoteItem *children; // for directories + size_t children_sz; + char *dir_entry_name; + base64_state base64_state; + bool started; +} DragRemoteItem; + typedef struct Window { id_type id; bool visible; @@ -300,10 +313,15 @@ typedef struct Window { struct { index_type x, y; bool active; } potential_url_drag; struct { double x, y; monotonic_t at; } initial_left_press; char *mimes_buf; size_t num_mimes, bufsz; + size_t total_remote_data_size; struct { const char *mime_type; uint8_t *optional_data; size_t data_size, data_capacity; base64_state base64_state; - bool data_decode_initialized, is_uri_list, requested_remote_files; int fd_plus_one; - const char** uri_list; size_t num_uris; + bool data_decode_initialized, is_uri_list, requested_remote_files; + int fd_plus_one; + char** uri_list; size_t num_uris; + DragRemoteItem *remote_items; size_t num_remote_items; + DragRemoteItem *currently_open_subdir; + char *base_dir_for_remote_items; int base_dir_fd_plus_one; } *items; struct { int width, height, fmt; uint8_t *data; size_t sz, capacity; bool started; base64_state base64_state; diff --git a/kitty/utils.py b/kitty/utils.py index afd64ed25..b604dd972 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -23,6 +23,7 @@ from typing import ( ) from .constants import ( + cache_dir, clear_handled_signals, config_dir, is_macos, @@ -1150,3 +1151,38 @@ def unlock_file(f: BinaryIO) -> None: if not f.writable(): raise ValueError('Cannot unlock files not opened in writable mode') fcntl.lockf(f, fcntl.LOCK_UN) + + +def rmtree_best_effort(relpath: str, dir_fd: int) -> None: + import shutil + shutil.rmtree(relpath, ignore_errors=True, dir_fd=dir_fd) + + +def mktempdir_in_cache(prefix: str) -> tuple[str, int]: + import tempfile + ans = tempfile.mkdtemp(prefix, dir=cache_dir()) + try: + return os.path.abspath(ans), os.open(ans, os.O_DIRECTORY | os.O_RDWR) + except OSError as e: + import errno + import shutil + shutil.rmtree(ans, ignore_errors=True) + return "", -e.errno if e.errno is not None else -errno.EIO + + +def sanitized_filename_from_url(url: str) -> str: + import posixpath + from urllib.parse import unquote, urlparse + try: + purl = urlparse(url) + fname = posixpath.basename(unquote(purl.path)) + return fname.replace(os.sep, '_') + except Exception: + return '' + + +def as_file_url(wd: str, *components: str) -> str: + path = os.path.abspath(os.path.join(wd, *components)) + path = path.replace(os.sep, '/') + from urllib.parse import quote + return 'file://' + quote(path)