diff --git a/kitty/dnd.c b/kitty/dnd.c index dc6d55e9c..d98e5be38 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -632,17 +632,19 @@ drop_append_request_keys(Window *w, char *buf, size_t bufsize) { } static void -drop_send_error(Window *w, int error_code) { - char buf[128]; - const char *e = get_errno_name(error_code); +drop_send_error(Window *w, int error_code, const char *desc) { + char buf[128], details_buf[1024]; + int n; + if (desc && desc[0]) n = snprintf(details_buf, sizeof(details_buf), "%s:%s", get_errno_name(error_code), desc); + else n = snprintf(details_buf, sizeof(details_buf), "%s:%s", get_errno_name(error_code), desc); int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=R", DND_CODE); header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size); - queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, e, strlen(e), false); + queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, details_buf, n, false); } void -drop_send_einval(Window *w) { - drop_send_error(w, EINVAL); +drop_send_einval(Window *w, const char *desc) { + drop_send_error(w, EINVAL, desc); } /* Returns true if the request completed synchronously (error, no-op), @@ -654,7 +656,7 @@ do_drop_request_data(Window *w, int32_t idx) { if (!osw) return true; /* idx is 1-based */ if (idx < 1 || !w->drop.offerred_mimes || (size_t)idx > w->drop.num_offerred_mimes) { - drop_send_error(w, ENOENT); + drop_send_error(w, ENOENT, "drop data request index out of bounds"); return true; } const char *mime = w->drop.offerred_mimes[idx - 1]; @@ -666,7 +668,7 @@ do_drop_request_data(Window *w, int32_t idx) { void drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) { if (sz < 0) { - drop_send_error(w, -sz); + drop_send_error(w, -sz, "drop data request failed to read data"); drop_pop_request(w); drop_process_queue(w); } else { @@ -767,7 +769,7 @@ file_send_timer_callback(id_type timer_id UNUSED, void *x) { 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); + drop_send_error(w, EIO, "timed out waiting for child to read file data"); drop_pop_request(w); drop_process_queue(w); return; @@ -795,7 +797,7 @@ drop_send_file_chunks(Window *w) { return; } drop_close_file_fd(w); - drop_send_error(w, EIO); + drop_send_error(w, EIO, "failed ot read from drop data file"); drop_pop_request(w); drop_process_queue(w); return; @@ -814,7 +816,7 @@ drop_send_file_chunks(Window *w) { /* 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); + drop_send_error(w, EIO, "failed to seek() on drop data file"); drop_pop_request(w); drop_process_queue(w); return; @@ -835,23 +837,23 @@ drop_send_file_data(Window *w, const char *path) { 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; + case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "drop data file does not exist"); break; + case EACCES: case EPERM: drop_send_error(w, EPERM, "opening drop data file permission denied"); break; + default: drop_send_error(w, EIO, "failed to open drop data file"); break; } return true; } 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; + case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "failed to stat drop data file"); break; + case EACCES: case EPERM: drop_send_error(w, EPERM, "failed to stat drop data file"); break; + default: drop_send_error(w, EIO, "failed to stat drop data file"); break; } safe_close(fd, __FILE__, __LINE__); return true; } - if (!S_ISREG(st.st_mode)) { drop_send_error(w, EINVAL); safe_close(fd, __FILE__, __LINE__); return true; } + if (!S_ISREG(st.st_mode)) { drop_send_error(w, EINVAL, "drop data file is not a regular file"); safe_close(fd, __FILE__, __LINE__); return true; } w->drop.file_fd_plus_one = fd + 1; w->drop.last_file_send_at = monotonic(); drop_send_file_chunks(w); @@ -891,9 +893,9 @@ drop_send_dir_listing(Window *w, const char *path) { 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; + case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "drop data dir does not exist"); break; + case EACCES: case EPERM: drop_send_error(w, EPERM, "drop data dir permission denied"); break; + default: drop_send_error(w, EIO, "drop data dir opening failed"); break; } return; } @@ -901,7 +903,7 @@ drop_send_dir_listing(Window *w, const char *path) { /* Build null-separated payload: entry1\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; } + if (!payload) { closedir(dir); drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return; } #define APPEND(s, n) do { \ size_t _n = (size_t)(n); \ @@ -909,7 +911,7 @@ drop_send_dir_listing(Window *w, const char *path) { 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; } \ + if (!_np) { free(payload); closedir(dir); drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return; } \ payload = _np; \ } \ memcpy(payload + payload_sz, (s), _n); \ @@ -920,7 +922,7 @@ drop_send_dir_listing(Window *w, const char *path) { /* 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; } + if (!ents) { free(payload); closedir(dir); drop_send_error(w, ENOMEM, "out of memory reading directory contents"); return; } struct dirent *de; while ((de = readdir(dir)) != NULL) { @@ -946,7 +948,7 @@ drop_send_dir_listing(Window *w, const char *path) { 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; + drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return; } ents = ne; } @@ -954,7 +956,7 @@ drop_send_dir_listing(Window *w, const char *path) { 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; + drop_send_error(w, ENOMEM, "out of memory reading drop data dir"); return; } ents_num++; @@ -989,7 +991,7 @@ drop_send_dir_listing(Window *w, const char *path) { static void 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; } + if ((tgtsz = readlink(path, target, sizeof(target)-1)) < 0) { drop_send_error(w, EIO, "failed to read symlink for drop data"); 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); @@ -1003,20 +1005,20 @@ drop_send_symlink(Window *w, const char *path) { static bool do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { if (!w->drop.uri_list || !w->drop.uri_list_sz) { - drop_send_error(w, EINVAL); return true; + drop_send_error(w, EINVAL, "drop data uri list empty"); return true; } if (global_state.drag_source.from_window == w->id && w->drag_source.state != DRAG_SOURCE_NONE) { - drop_send_error(w, EPERM); return true; + drop_send_error(w, EPERM, "cannot drop into self window"); return true; } /* Verify mime_idx (1-based) points to text/uri-list */ if (mime_idx < 1 || !w->drop.offerred_mimes || (size_t)mime_idx > w->drop.num_offerred_mimes || strcmp(w->drop.offerred_mimes[mime_idx - 1], "text/uri-list") != 0) { - drop_send_error(w, EINVAL); return true; + drop_send_error(w, EINVAL, "drop data mime index out of bounds"); return true; } /* file_idx is 1-based, convert to 0-based for get_nth_file_url */ - if (file_idx < 1) { drop_send_error(w, EINVAL); return true; } + if (file_idx < 1) { drop_send_error(w, EINVAL, "drop data file url index out of bounds"); return true; } int file_n = file_idx - 1; RAII_ALLOC(char, path, NULL); @@ -1029,9 +1031,9 @@ do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { struct stat st; 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; + case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "drop data file does not exist"); break; + case EACCES: case EPERM: drop_send_error(w, EPERM, "permission denied for stat() on drop data file"); break; + default: drop_send_error(w, EIO, "stat() on drop data file failed"); break; } return true; } @@ -1044,7 +1046,7 @@ do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { } else if (S_ISLNK(st.st_mode)) { drop_send_symlink(w, path); } else { - drop_send_error(w, EINVAL); + drop_send_error(w, EINVAL, "drop data file is neother a regular file, directory or symlink"); } return sync; } @@ -1055,10 +1057,10 @@ do_drop_request_uri_data(Window *w, int32_t mime_idx, int32_t file_idx) { * Returns true if completed synchronously, false if async file I/O started. */ static bool do_drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) { - if (!handle_id) { drop_send_error(w, EINVAL); return true; } + if (!handle_id) { drop_send_error(w, EINVAL, "no parent directory handle specified"); return true; } DirHandle *h = drop_find_dir_handle(w, handle_id); - if (!h) { drop_send_error(w, EINVAL); return true; } + if (!h) { drop_send_error(w, EINVAL, "parent directory handle not found"); return true; } if (entry_num == 0) { /* Close the handle */ @@ -1070,19 +1072,19 @@ do_drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) { /* 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 true; } + if (eidx >= h->num_entries) { drop_send_error(w, EINVAL, "entry index out of bounds for parent directory"); return true; } 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 true; + drop_send_error(w, EIO, "drop data file path too long"); return true; } struct stat lst; if (lstat(full, &lst) < 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; + case ENOENT: case ENOTDIR: case ELOOP: drop_send_error(w, ENOENT, "drop data entry does not exist"); break; + case EACCES: case EPERM: drop_send_error(w, EPERM, "parmission denied while trying to stat() drop data entry"); break; + default: drop_send_error(w, EIO, "stt() failed on drop data entry"); break; } return true; } @@ -1093,9 +1095,9 @@ do_drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) { ssize_t tlen = readlink(full, target, sizeof(target) - 1); if (tlen < 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; + case ENOENT: case ENOTDIR: drop_send_error(w, ENOENT, "readlink() failed on drop data entry"); break; + case EACCES: case EPERM: drop_send_error(w, EPERM, "readlink() failed on drop data entry"); break; + default: drop_send_error(w, EIO, "readlink() failed on drop data entry"); break; } return true; } @@ -1115,7 +1117,7 @@ do_drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) { } else if (S_ISREG(lst.st_mode)) { return drop_send_file_data(w, full); } else { - drop_send_error(w, EINVAL); + drop_send_error(w, EINVAL, "drop data entry is neither a regular file nor a directory"); return true; } } @@ -1204,7 +1206,7 @@ drop_enqueue_request(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_y, w->drop.current_request_x = cell_x; w->drop.current_request_y = cell_y; w->drop.current_request_Y = pixel_y; - drop_send_error(w, EMFILE); + drop_send_error(w, EMFILE, "too many drop data requests"); w->drop.current_request_x = saved_x; w->drop.current_request_y = saved_y; w->drop.current_request_Y = saved_Y; diff --git a/kitty/dnd.h b/kitty/dnd.h index b45da1255..d2a172c21 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -15,7 +15,7 @@ void drop_register_machine_id(Window *w, const uint8_t *machine_id, size_t sz); void drop_move_on_child(Window *w, const char **mimes, size_t num_mimes, bool is_drop); void drop_left_child(Window *w); void drop_free_data(Window *w); -void drop_send_einval(Window *w); +void drop_send_einval(Window *w, const char *desc); void drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num); void drop_enqueue_request(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_y, uint32_t operation); void drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more); diff --git a/kitty_tests/dnd.py b/kitty_tests/dnd.py index 2d24062c6..6eb449434 100644 --- a/kitty_tests/dnd.py +++ b/kitty_tests/dnd.py @@ -524,7 +524,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1, events) self.ae(events[0]['type'], 'R') - self.ae(events[0]['payload'].strip(), b'ENOENT') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'ENOENT') def test_data_error_propagation(self) -> None: """When data retrieval fails the client receives a t=R error code.""" @@ -541,7 +541,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1, events) self.ae(events[0]['type'], 'R') - self.ae(events[0]['payload'].strip(), b'EIO') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EIO') def test_data_eperm_error(self) -> None: """EPERM error is correctly forwarded to the client.""" @@ -556,7 +556,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1, events) self.ae(events[0]['type'], 'R') - self.ae(events[0]['payload'].strip(), b'EPERM') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EPERM') def test_large_data_chunking(self) -> None: """Data larger than the chunk limit is sent in multiple base64 chunks.""" @@ -764,7 +764,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1, events) self.ae(events[0]['type'], 'R') - self.assertIn(events[0]['payload'].strip(), [b'ENOENT', b'EPERM']) + self.assertIn(events[0]['payload'].strip().partition(b':')[0], [b'ENOENT', b'EPERM']) def test_uri_file_transfer_out_of_bounds(self) -> None: """URI file request with an index beyond the URI list returns ENOENT.""" @@ -801,7 +801,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1, events) self.ae(events[0]['type'], 'R') - self.ae(events[0]['payload'].strip(), b'EINVAL') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EINVAL') finally: os.unlink(fpath) @@ -814,7 +814,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1, events) self.ae(events[0]['type'], 'R') - self.ae(events[0]['payload'].strip(), b'EINVAL') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EINVAL') def test_uri_broken_symlink_returns_symlink_target(self) -> None: """A broken symlink in the URI list is transmitted as a symlink (X=1) with the target.""" @@ -1083,7 +1083,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1) self.ae(events[0]['type'], 'R') - self.ae(events[0]['payload'].strip(), b'EINVAL') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EINVAL') def test_dir_entry_out_of_bounds_returns_enoent(self) -> None: """Reading a directory entry with an out-of-range index returns ENOENT.""" @@ -1105,7 +1105,7 @@ class TestDnDProtocol(BaseTest): events = self._get_events(cap) self.assertEqual(len(events), 1) self.ae(events[0]['type'], 'R') - self.ae(events[0]['payload'].strip(), b'ENOENT') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EINVAL') def test_dir_no_unique_identifier(self) -> None: """Directory listings should not contain a unique identifier prefix.""" @@ -2014,7 +2014,7 @@ class TestDnDProtocol(BaseTest): self.assertEqual(len(events), 1, events) self.ae(events[0]['type'], 'R') self.ae(events[0]['meta'].get('x'), '99') - self.ae(events[0]['payload'].strip(), b'ENOENT') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'ENOENT') def test_x_key_in_error_for_io_failure(self) -> None: """x= key is echoed in I/O error responses.""" @@ -2030,7 +2030,7 @@ class TestDnDProtocol(BaseTest): self.assertEqual(len(events), 1) self.ae(events[0]['type'], 'R') self.ae(events[0]['meta'].get('x'), '1') - self.ae(events[0]['payload'].strip(), b'EIO') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EIO') def test_fifo_order_with_different_indices(self) -> None: """Multiple requests with different x= values are served in FIFO order.""" @@ -2080,7 +2080,7 @@ class TestDnDProtocol(BaseTest): err_events = [e for e in events if e['type'] == 'R'] self.assertEqual(len(err_events), 1, events) self.ae(err_events[0]['meta'].get('x'), '99') - self.ae(err_events[0]['payload'].strip(), b'ENOENT') + self.ae(err_events[0]['payload'].strip().partition(b':')[0], b'ENOENT') # Now serve request for index 1 dnd_test_fake_drop_data(cap.window_id, 'text/plain', b'second request data') @@ -2117,7 +2117,7 @@ class TestDnDProtocol(BaseTest): events = parse_escape_codes(raw) err_events = [e for e in events if e['type'] == 'R'] self.assertTrue(err_events, 'expected EMFILE error') - self.ae(err_events[0]['payload'].strip(), b'EMFILE') + self.ae(err_events[0]['payload'].strip().partition(b':')[0], b'EMFILE') def test_xy_keys_in_uri_file_response(self) -> None: """x= and y= keys are echoed in URI file data responses.""" @@ -2241,7 +2241,7 @@ class TestDnDProtocol(BaseTest): self.ae(events[0]['type'], 'R') self.ae(events[0]['meta'].get('x'), '999') self.ae(events[0]['meta'].get('Y'), str(handle_id)) - self.ae(events[0]['payload'].strip(), b'ENOENT') + self.ae(events[0]['payload'].strip().partition(b':')[0], b'EINVAL') def test_mixed_request_types_processed_in_order(self) -> None: """Mixed MIME data and URI file requests are processed in FIFO order.""" @@ -2297,7 +2297,7 @@ class TestDnDProtocol(BaseTest): self.ae(err_events[1]['meta'].get('x'), '20') self.ae(err_events[2]['meta'].get('x'), '30') for ev in err_events: - self.ae(ev['payload'].strip(), b'ENOENT') + self.ae(ev['payload'].strip().partition(b':')[0], b'ENOENT') def test_no_r_key_in_responses(self) -> None: """Responses must not contain the old r= key."""