From 634c078168415018ba1bd70206b6e76be9b2e3f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 15:31:15 +0000 Subject: [PATCH] Fix empty file not created when dragging from remote Linux to Finder When dragging an empty file (or a directory containing an empty file) from a remote Linux machine to macOS Finder, the empty file would not be copied. Root cause: in add_payload() in dnd.c, the file is only created with O_CREAT when payload data arrives. For an empty file, no payload is ever sent, so the file was never created on disk. When Finder then tried to hard-link the (non-existent) temp file to the destination, it failed silently. Fix: in the "all data received" handler for type 0 (regular file), check if the fd was ever opened. If not (empty file), create the empty file explicitly before finishing. Also add: - A new test probe drag_remote_item_path:N to retrieve the filesystem path of a specific remote item by URI index. - Two regression tests: test_remote_drag_empty_file (verifying the empty file is created on disk) and test_remote_drag_dir_with_empty_file (verifying an empty child file inside a directory is created on disk). Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/da8b4577-3de8-4784-afc0-c1967f605dec Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com> --- kitty/dnd.c | 32 ++++++++++++++++++++++++++++++-- kitty_tests/dnd.py | 27 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/kitty/dnd.c b/kitty/dnd.c index 859501ef9..0ed4a8f34 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -2088,8 +2088,16 @@ add_payload(Window *w, DragRemoteItem *ri, bool has_more, const uint8_t *payload 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; + if (ri->fd_plus_one) { + safe_close(ri->fd_plus_one - 1, __FILE__, __LINE__); + ri->fd_plus_one = 0; + } else { + // Empty file: no data payload was ever received so the file was never opened; + // create it now so that it exists at the expected path for the caller. + int fd = safe_openat(dirfd, ri->dir_entry_name, O_CREAT | O_WRONLY, file_permissions); + if (fd < 0) abrt(errno, "could not create empty drag source item file"); + safe_close(fd, __FILE__, __LINE__); + } break; case 1: // Ensure room for the null terminator needed by symlinkat @@ -2636,6 +2644,26 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) { } Py_RETURN_NONE; } + // "drag_remote_item_path:N" returns the full filesystem path for URI item N (0-based) + if (strncmp(q, "drag_remote_item_path:", 22) == 0) { + size_t uri_idx = (size_t)atoi(q + 22); + if (w->drag_source.base_dir_for_remote_items) { + for (size_t idx = 0; idx < w->drag_source.num_mimes; idx++) { +#define mi w->drag_source.items[idx] + if (mi.is_uri_list && mi.remote_items && uri_idx < mi.num_remote_items) { + const char *name = mi.remote_items[uri_idx].dir_entry_name; + if (name) { + char path[4096]; + snprintf(path, sizeof(path), "%s/%zu/%s", + w->drag_source.base_dir_for_remote_items, uri_idx, name); + return PyUnicode_FromString(path); + } + } +#undef mi + } + } + Py_RETURN_NONE; + } Py_RETURN_NONE; } diff --git a/kitty_tests/dnd.py b/kitty_tests/dnd.py index 9381c4c1c..f78e870cd 100644 --- a/kitty_tests/dnd.py +++ b/kitty_tests/dnd.py @@ -2933,6 +2933,12 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file(1, '', item_type=0)) self._assert_no_output(cap) self.assert_drag_data_complete(cap) + # Verify the empty file was actually created on disk + import os + path = dnd_test_probe_state(cap.window_id, 'drag_remote_item_path:0') + self.assertIsNotNone(path, 'empty file path should be known') + self.assertTrue(os.path.isfile(path), f'empty file must exist on disk: {path}') + self.assertEqual(os.path.getsize(path), 0, f'file must be empty: {path}') def test_remote_drag_empty_directory(self) -> None: """Transfer a directory with no entries.""" @@ -2945,6 +2951,27 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file(1, '', item_type=2)) self.assert_drag_data_complete(cap) + def test_remote_drag_dir_with_empty_file(self) -> None: + """Directory containing an empty file: the empty child file must be created on disk.""" + uri_list = b'file:///home/user/mydir\r\n' + dir_entries = b'empty_child.txt' + with dnd_test_window() as (screen, cap): + self._setup_remote_drag(screen, cap, uri_list) + b64 = standard_b64encode(dir_entries).decode() + parse_bytes(screen, client_remote_file(1, b64, item_type=2)) + parse_bytes(screen, client_remote_file(1, '', item_type=2)) + # Send end-of-data for the child with no payload (empty file) + parse_bytes(screen, client_remote_file(1, '', item_type=0, parent_handle=2, entry_num=1)) + self._assert_no_output(cap) + self.assert_drag_data_complete(cap) + # Verify the empty child file was actually created on disk + import os + dir_path = dnd_test_probe_state(cap.window_id, 'drag_remote_item_path:0') + self.assertIsNotNone(dir_path, 'dir path should be known') + child_path = os.path.join(dir_path, 'empty_child.txt') + self.assertTrue(os.path.isfile(child_path), f'empty child file must exist on disk: {child_path}') + self.assertEqual(os.path.getsize(child_path), 0, f'child file must be empty: {child_path}') + def test_remote_drag_uri_list_with_comments(self) -> None: """URI list with comment lines (starting with #) should filter them out.""" uri_list = b'# this is a comment\r\nfile:///home/user/f.txt\r\n# another comment\r\n'