From 4c6f7ff6b5ca60994446ed9e63d709c15261dc36 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 23 Apr 2026 12:36:22 +0530 Subject: [PATCH] Modify drop protocol to allow transmission of broken symlinks in the URI list --- docs/dnd-protocol.rst | 12 +++++++---- kittens/dnd/drop.go | 5 ++++- kitty/dnd.c | 42 +++++++++++++++++++++------------------ kitty_tests/dnd_kitten.py | 2 +- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 2abf192a9..5008fea81 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -169,10 +169,14 @@ If the client requests an entry that is not a supported URI type the terminal must reply with ``EUNKNOWN``. Terminals must ONLY send data for regular files or directories. Symbolic links must be -resolved and the corresponding file or directory read. If the terminal does not have -permission to read the file it must reply with ``EPERM``. Terminals -must respond with ``EINVAL`` if the file is not a regular file after -resolving symlinks and ``ENOENT`` if the file does not exist. If an +resolved and the corresponding file or directory read. Only if the symbolic +link cannot be resolved must it be transmitted as a symbolic link (in which +case ``X=1`` and the payload is the base64 encoded target of the symlink. See +below for more details about sending symlinks. + +If the terminal does not have permission to read the file it must reply with +``EPERM``. Terminals must respond with ``EINVAL`` if the file is not a regular +file after resolving symlinks and ``ENOENT`` if the file does not exist. If an I/O error occurs the terminal must send ``EIO``. For security reasons, terminals must reply with ``EPERM`` if the drag diff --git a/kittens/dnd/drop.go b/kittens/dnd/drop.go index eb003e122..2a1446d6e 100644 --- a/kittens/dnd/drop.go +++ b/kittens/dnd/drop.go @@ -396,7 +396,10 @@ func (dnd *dnd) on_remote_drop_data(cmd DC) (err error) { } if e.dest == nil { // this entry is finished drop_status.open_remote_dir.num_children_finished++ - if e.item_type != 0 && e.item_type != 1 { + if len(e.children) > 0 { + if e.item_type != 0 && e.item_type != 1 { + dnd.lp.QueueDnDData(DC{Type: 'r', Yp: e.item_type}) // close directory in terminal + } // TODO: request the children } } diff --git a/kitty/dnd.c b/kitty/dnd.c index 05711fad9..c5bee51cd 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -690,18 +690,7 @@ get_nth_file_url(const char *uri_list, size_t uri_list_sz, int n, char **path_ou *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: case ELOOP: *error_out = "ENOENT"; break; - case EACCES: case EPERM: *error_out = "EPERM"; break; - default: *error_out = "EINVAL"; break; - } - return false; - } - - *path_out = strdup(resolved); + *path_out = strdup(path); if (!*path_out) { *error_out = "ENOMEM"; return false; } return true; } @@ -949,6 +938,16 @@ drop_send_dir_listing(Window *w, const char *path) { 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 @@ -970,7 +969,7 @@ do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { if (file_idx < 1) { drop_send_error(w, EINVAL); return true; } int file_n = file_idx - 1; - char *path = NULL; + 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); @@ -979,12 +978,18 @@ do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { 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; + 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; } @@ -998,7 +1003,6 @@ do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { drop_send_error(w, EINVAL); sync = true; } - free(path); return sync; } diff --git a/kitty_tests/dnd_kitten.py b/kitty_tests/dnd_kitten.py index 9b2d8f0c7..82e5b9f60 100644 --- a/kitty_tests/dnd_kitten.py +++ b/kitty_tests/dnd_kitten.py @@ -42,7 +42,7 @@ def create_fs(base): f.write(b'x' * sz) os.makedirs(join('d1', 'sd', 'ssd')) os.mkdir(join('d2')) - # os.symlink('/does-not-exist', join('s1')) + os.symlink('/does-not-exist', join('s1')) os.symlink('d1', join('sd')) os.symlink('/', join('sr')) os.symlink('../d1', join('d1', 'sr'))