From ee84e68ca4d21eeb70a73a457c0f1d9c1f5bb8fe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Apr 2026 09:38:17 +0530 Subject: [PATCH] Have top level symlink behavior match for local and remote drops --- docs/dnd-protocol.rst | 9 +++------ kitty/dnd.c | 29 ++++++++++++----------------- kitty_tests/dnd_kitten.py | 4 ++-- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 1e066a21b..fc9e8da2a 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -171,15 +171,12 @@ MIME type is not present in the drop, the terminal must reply with 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. 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. +Terminals must ONLY send data for regular files, symbolic links and directories. +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 +file or symlink or directory 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/kitty/dnd.c b/kitty/dnd.c index d184a0f99..6f5afd9f5 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -947,12 +947,14 @@ drop_send_dir_listing(Window *w, const char *path) { } static void -drop_send_symlink(Window *w, const char *target, size_t sz) { +drop_send_symlink(Window *w, const char *path) { + char target[PATH_MAX]; ssize_t tgtsz; + if ((tgtsz = readlink(path, target, sizeof(target)-1)) < 0) { drop_send_error(w, EIO); 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); 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, target, tgtsz, true); queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true); } @@ -985,31 +987,24 @@ do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { } 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; + 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; } - // 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; + bool sync = true; 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 if (S_ISLNK(st.st_mode)) { + drop_send_symlink(w, path); } else { drop_send_error(w, EINVAL); - sync = true; } return sync; } diff --git a/kitty_tests/dnd_kitten.py b/kitty_tests/dnd_kitten.py index 7adfe447a..de863ef97 100644 --- a/kitty_tests/dnd_kitten.py +++ b/kitty_tests/dnd_kitten.py @@ -37,7 +37,7 @@ class Capture(WriteCapture): self.pty.write_to_child(data) -def create_fs(base, include_toplevel_working_symlink=False): +def create_fs(base, include_toplevel_working_symlink=True): join = partial(os.path.join, base) def w(sz, *path): if sz == 0: @@ -232,7 +232,7 @@ class TestDnDKitten(BaseTest): self.assertEqual('text/uri-list\x00image/png', self.probe_state('drop_mimes').rstrip('\x00')) self.wait_for_state('drop_data_requests', ((1,0,0), (4,0,0))) self.assertEqual('text/uri-list', self.probe_state('drop_getting_data_for_mime')) - create_fs(self.src_data_dir, include_toplevel_working_symlink=not remote_client) + create_fs(self.src_data_dir) uri_list, path_list = [], [] for x in sorted(os.listdir(self.src_data_dir)): uri_list.append(as_file_url(self.src_data_dir, x))