mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-08-04 14:46:03 +00:00
Merge branch 'copilot/implement-drag-drop-refactor' of https://github.com/kovidgoyal/kitty
This commit is contained in:
commit
1e714d2c31
5 changed files with 299 additions and 336 deletions
192
kitty/dnd.c
192
kitty/dnd.c
|
|
@ -114,10 +114,10 @@ drop_close_file_fd(Window *w) {
|
|||
|
||||
static void
|
||||
drop_free_request_queue(Window *w) {
|
||||
for (size_t i = 0; i < w->drop.num_data_requests; i++)
|
||||
free(w->drop.data_requests[i].payload);
|
||||
w->drop.num_data_requests = 0;
|
||||
w->drop.current_request_id = 0;
|
||||
w->drop.current_request_x = 0;
|
||||
w->drop.current_request_y = 0;
|
||||
w->drop.current_request_Y = 0;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -410,13 +410,26 @@ get_errno_name(int err) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Append the current request disambiguation keys (x, y, Y) to a header buffer.
|
||||
* Returns the number of bytes written. */
|
||||
static int
|
||||
drop_append_request_keys(Window *w, char *buf, size_t bufsize) {
|
||||
int sz = 0;
|
||||
if (w->drop.current_request_x && sz < (int)bufsize - 1)
|
||||
sz += snprintf(buf + sz, bufsize - sz, ":x=%d", (int)w->drop.current_request_x);
|
||||
if (w->drop.current_request_y && sz < (int)bufsize - 1)
|
||||
sz += snprintf(buf + sz, bufsize - sz, ":y=%d", (int)w->drop.current_request_y);
|
||||
if (w->drop.current_request_Y && sz < (int)bufsize - 1)
|
||||
sz += snprintf(buf + sz, bufsize - sz, ":Y=%d", (int)w->drop.current_request_Y);
|
||||
return sz;
|
||||
}
|
||||
|
||||
static void
|
||||
drop_send_error(Window *w, int error_code) {
|
||||
char buf[128];
|
||||
const char *e = get_errno_name(error_code);
|
||||
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=R", DND_CODE);
|
||||
if (w->drop.current_request_id)
|
||||
header_size += snprintf(buf + header_size, sizeof(buf) - header_size, ":r=%u", (unsigned)w->drop.current_request_id);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -428,26 +441,26 @@ drop_send_einval(Window *w) {
|
|||
/* Returns true if the request completed synchronously (error, no-op),
|
||||
* false if an async OS data fetch was started. */
|
||||
static bool
|
||||
do_drop_request_data(Window *w, const char *mime) {
|
||||
do_drop_request_data(Window *w, int32_t idx) {
|
||||
if (w->drop.getting_data_for_mime) { free(w->drop.getting_data_for_mime); w->drop.getting_data_for_mime = NULL; }
|
||||
OSWindow *osw = os_window_for_kitty_window(w->id);
|
||||
if (!osw) return true;
|
||||
if (w->drop.offerred_mimes) {
|
||||
for (size_t i = 0; i < w->drop.num_offerred_mimes; i++) {
|
||||
if (strcmp(mime, w->drop.offerred_mimes[i]) == 0) {
|
||||
w->drop.getting_data_for_mime = strdup(mime);
|
||||
if (w->drop.getting_data_for_mime) request_drop_data(osw, w->id, mime);
|
||||
return false; /* async: completion via drop_dispatch_data */
|
||||
}
|
||||
}
|
||||
/* idx is 1-based */
|
||||
if (idx < 1 || !w->drop.offerred_mimes || (size_t)idx > w->drop.num_offerred_mimes) {
|
||||
drop_send_error(w, ENOENT);
|
||||
return true;
|
||||
}
|
||||
drop_send_error(w, ENOENT);
|
||||
return true;
|
||||
const char *mime = w->drop.offerred_mimes[idx - 1];
|
||||
w->drop.getting_data_for_mime = strdup(mime);
|
||||
if (w->drop.getting_data_for_mime) request_drop_data(osw, w->id, mime);
|
||||
return false; /* async: completion via drop_dispatch_data */
|
||||
}
|
||||
|
||||
void
|
||||
drop_request_data(Window *w, const char *mime) {
|
||||
do_drop_request_data(w, mime);
|
||||
/* This function is no longer used in the new protocol; MIME data requests
|
||||
* are now handled through the unified t=r queue via do_drop_request_data. */
|
||||
(void)w; (void)mime;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -459,8 +472,7 @@ drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) {
|
|||
} else {
|
||||
char buf[128];
|
||||
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=r", DND_CODE);
|
||||
if (w->drop.current_request_id)
|
||||
header_size += snprintf(buf + header_size, sizeof(buf) - header_size, ":r=%u", (unsigned)w->drop.current_request_id);
|
||||
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, sz ? data : NULL, sz, true);
|
||||
if (strcmp(mime, "text/uri-list") == 0) {
|
||||
w->drop.uri_list_sz += sz;
|
||||
|
|
@ -472,7 +484,7 @@ drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- Remote file / directory transfer (t=s, t=d) ----
|
||||
// ---- Remote file / directory transfer ----
|
||||
|
||||
static void
|
||||
url_decode_inplace(char *str) {
|
||||
|
|
@ -570,8 +582,7 @@ static void
|
|||
drop_send_error_str(Window *w, const char *err_name) {
|
||||
char buf[128];
|
||||
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=R", DND_CODE);
|
||||
if (w->drop.current_request_id)
|
||||
header_size += snprintf(buf + header_size, sizeof(buf) - header_size, ":r=%u", (unsigned)w->drop.current_request_id);
|
||||
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, err_name, strlen(err_name), false);
|
||||
}
|
||||
|
||||
|
|
@ -606,8 +617,7 @@ drop_send_file_chunks(Window *w) {
|
|||
}
|
||||
char hdr[128];
|
||||
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
|
||||
if (w->drop.current_request_id)
|
||||
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":r=%u", (unsigned)w->drop.current_request_id);
|
||||
hdr_sz += drop_append_request_keys(w, hdr + hdr_sz, sizeof(hdr) - hdr_sz);
|
||||
while (1) {
|
||||
char buf[FILE_CHUNK_SIZE];
|
||||
ssize_t n;
|
||||
|
|
@ -789,9 +799,13 @@ drop_send_dir_listing(Window *w, const char *path) {
|
|||
uint32_t handle_id = drop_alloc_dir_handle(w, path, ents, ents_num);
|
||||
|
||||
char hdr[128];
|
||||
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=d:x=%u", DND_CODE, (unsigned)handle_id);
|
||||
if (w->drop.current_request_id)
|
||||
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":r=%u", (unsigned)w->drop.current_request_id);
|
||||
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r", DND_CODE);
|
||||
/* For dir listings, echo the x and y keys from the request, then add Y=new_handle:X=2 */
|
||||
if (w->drop.current_request_x)
|
||||
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":x=%d", (int)w->drop.current_request_x);
|
||||
if (w->drop.current_request_y)
|
||||
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":y=%d", (int)w->drop.current_request_y);
|
||||
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":Y=%u:X=2", (unsigned)handle_id);
|
||||
/* payload_sz includes a trailing null; omit it – the null-separated format
|
||||
* does not require a trailing null after the last entry. */
|
||||
size_t send_sz = payload_sz > 0 ? payload_sz - 1 : 0;
|
||||
|
|
@ -805,7 +819,7 @@ drop_send_dir_listing(Window *w, const char *path) {
|
|||
/* Handle a t=s request: send the file/directory at URI-list index idx.
|
||||
* Returns true if completed synchronously, false if async file I/O started. */
|
||||
static bool
|
||||
do_drop_request_uri_data(Window *w, const char *payload, size_t payload_sz) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -813,29 +827,19 @@ do_drop_request_uri_data(Window *w, const char *payload, size_t payload_sz) {
|
|||
drop_send_error(w, EPERM); return true;
|
||||
}
|
||||
|
||||
/* Payload format: "text/uri-list:idx" */
|
||||
const char *colon = memchr(payload, ':', payload_sz);
|
||||
if (!colon) { drop_send_error(w, EINVAL); return true; }
|
||||
|
||||
size_t mime_len = (size_t)(colon - payload);
|
||||
if (mime_len != 13 || strncmp(payload, "text/uri-list", 13) != 0) {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
const char *idx_str = colon + 1;
|
||||
size_t idx_len = payload_sz - mime_len - 1;
|
||||
char idx_buf[32];
|
||||
if (!idx_len || idx_len >= sizeof(idx_buf)) { drop_send_error(w, EINVAL); return true; }
|
||||
memcpy(idx_buf, idx_str, idx_len);
|
||||
idx_buf[idx_len] = 0;
|
||||
|
||||
char *endp;
|
||||
long idx = strtol(idx_buf, &endp, 10);
|
||||
if (endp == idx_buf || *endp != 0 || idx < 0) { drop_send_error(w, EINVAL); 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; }
|
||||
int file_n = file_idx - 1;
|
||||
|
||||
char *path = NULL;
|
||||
const char *err = NULL;
|
||||
if (!get_nth_file_url(w->drop.uri_list, w->drop.uri_list_sz, (int)idx, &path, &err)) {
|
||||
if (!get_nth_file_url(w->drop.uri_list, w->drop.uri_list_sz, file_n, &path, &err)) {
|
||||
drop_send_error_str(w, err);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -867,12 +871,14 @@ do_drop_request_uri_data(Window *w, const char *payload, size_t payload_sz) {
|
|||
|
||||
void
|
||||
drop_request_uri_data(Window *w, const char *payload, size_t payload_sz) {
|
||||
do_drop_request_uri_data(w, payload, payload_sz);
|
||||
(void)w; (void)payload; (void)payload_sz;
|
||||
/* This function is no longer used in the new protocol; URI data requests
|
||||
* are now handled through the unified t=r queue via do_drop_request_uri_data. */
|
||||
}
|
||||
|
||||
/* Handle a t=d request from the client.
|
||||
* handle_id: the directory handle (x= key).
|
||||
* entry_num: 0 means close the handle; >=1 means read that entry (1-based).
|
||||
/* Handle a directory request from the client.
|
||||
* handle_id: the directory handle (Y= key).
|
||||
* entry_num: 0 means close the handle; >=1 means read that entry (x= key, 1-based).
|
||||
* 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) {
|
||||
|
|
@ -922,9 +928,9 @@ do_drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) {
|
|||
}
|
||||
target[tlen] = '\0';
|
||||
char hdr[128];
|
||||
int hdr_sz = snprintf(hdr, sizeof(hdr), "\x1b]%d;t=r:X=1", DND_CODE);
|
||||
if (w->drop.current_request_id)
|
||||
hdr_sz += snprintf(hdr + hdr_sz, sizeof(hdr) - hdr_sz, ":r=%u", (unsigned)w->drop.current_request_id);
|
||||
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, (size_t)tlen, true);
|
||||
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, hdr, hdr_sz, NULL, 0, true);
|
||||
return true;
|
||||
|
|
@ -952,13 +958,14 @@ drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num) {
|
|||
static void
|
||||
drop_pop_request(Window *w) {
|
||||
if (w->drop.num_data_requests == 0) return;
|
||||
free(w->drop.data_requests[0].payload);
|
||||
w->drop.num_data_requests--;
|
||||
if (w->drop.num_data_requests > 0) {
|
||||
memmove(w->drop.data_requests, w->drop.data_requests + 1,
|
||||
w->drop.num_data_requests * sizeof(w->drop.data_requests[0]));
|
||||
}
|
||||
w->drop.current_request_id = 0;
|
||||
w->drop.current_request_x = 0;
|
||||
w->drop.current_request_y = 0;
|
||||
w->drop.current_request_Y = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -974,28 +981,27 @@ drop_finish_and_clear_queue(Window *w) {
|
|||
static void
|
||||
drop_process_queue(Window *w) {
|
||||
while (w->drop.num_data_requests > 0) {
|
||||
w->drop.current_request_id = w->drop.data_requests[0].request_id;
|
||||
char type = w->drop.data_requests[0].type;
|
||||
int32_t x = w->drop.data_requests[0].cell_x;
|
||||
int32_t y = w->drop.data_requests[0].cell_y;
|
||||
int32_t Y = w->drop.data_requests[0].pixel_y;
|
||||
w->drop.current_request_x = x;
|
||||
w->drop.current_request_y = y;
|
||||
w->drop.current_request_Y = Y;
|
||||
bool sync = true;
|
||||
switch (type) {
|
||||
case 'r':
|
||||
if (w->drop.data_requests[0].payload && w->drop.data_requests[0].payload_sz > 0)
|
||||
sync = do_drop_request_data(w, w->drop.data_requests[0].payload);
|
||||
else {
|
||||
/* finish: empty t=r */
|
||||
drop_pop_request(w);
|
||||
drop_finish_and_clear_queue(w);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
sync = do_drop_request_uri_data(w, w->drop.data_requests[0].payload, w->drop.data_requests[0].payload_sz);
|
||||
break;
|
||||
case 'd':
|
||||
sync = do_drop_handle_dir_request(w, (uint32_t)w->drop.data_requests[0].cell_x, w->drop.data_requests[0].cell_y);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (Y != 0) {
|
||||
/* Directory request: Y=handle, x=entry_num */
|
||||
sync = do_drop_handle_dir_request(w, (uint32_t)Y, x);
|
||||
} else if (y != 0) {
|
||||
/* URI file request: x=mime_idx, y=file_idx */
|
||||
sync = do_drop_request_uri_data(w, x, y);
|
||||
} else if (x != 0) {
|
||||
/* MIME data request: x=idx */
|
||||
sync = do_drop_request_data(w, x);
|
||||
} else {
|
||||
/* Finish: x=0, y=0, Y=0 */
|
||||
drop_pop_request(w);
|
||||
drop_finish_and_clear_queue(w);
|
||||
return;
|
||||
}
|
||||
if (sync) {
|
||||
drop_pop_request(w);
|
||||
|
|
@ -1008,43 +1014,33 @@ drop_process_queue(Window *w) {
|
|||
}
|
||||
|
||||
void
|
||||
drop_enqueue_request(Window *w, uint32_t request_id, char type, const char *payload, size_t payload_sz, int32_t cell_x, int32_t cell_y) {
|
||||
/* Handle finish (empty t=r): if there are no in-flight requests, finish immediately */
|
||||
if (type == 'r' && payload_sz == 0 && w->drop.num_data_requests == 0) {
|
||||
drop_enqueue_request(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_y) {
|
||||
/* Handle finish (x=0, y=0, Y=0): if there are no in-flight requests, finish immediately */
|
||||
if (cell_x == 0 && cell_y == 0 && pixel_y == 0 && w->drop.num_data_requests == 0) {
|
||||
drop_finish_and_clear_queue(w);
|
||||
return;
|
||||
}
|
||||
|
||||
if (w->drop.num_data_requests >= arraysz(w->drop.data_requests)) {
|
||||
/* Queue full: deny with EMFILE and end the drop */
|
||||
uint32_t saved = w->drop.current_request_id;
|
||||
w->drop.current_request_id = request_id;
|
||||
int32_t saved_x = w->drop.current_request_x;
|
||||
int32_t saved_y = w->drop.current_request_y;
|
||||
int32_t saved_Y = w->drop.current_request_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);
|
||||
w->drop.current_request_id = saved;
|
||||
w->drop.current_request_x = saved_x;
|
||||
w->drop.current_request_y = saved_y;
|
||||
w->drop.current_request_Y = saved_Y;
|
||||
drop_finish_and_clear_queue(w);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t idx = w->drop.num_data_requests;
|
||||
w->drop.data_requests[idx].request_id = request_id;
|
||||
w->drop.data_requests[idx].type = type;
|
||||
w->drop.data_requests[idx].payload = NULL;
|
||||
w->drop.data_requests[idx].payload_sz = 0;
|
||||
w->drop.data_requests[idx].cell_x = cell_x;
|
||||
w->drop.data_requests[idx].cell_y = cell_y;
|
||||
if (payload && payload_sz > 0) {
|
||||
w->drop.data_requests[idx].payload = malloc(payload_sz + 1);
|
||||
if (!w->drop.data_requests[idx].payload) {
|
||||
uint32_t saved = w->drop.current_request_id;
|
||||
w->drop.current_request_id = request_id;
|
||||
drop_send_error(w, ENOMEM);
|
||||
w->drop.current_request_id = saved;
|
||||
return;
|
||||
}
|
||||
memcpy(w->drop.data_requests[idx].payload, payload, payload_sz);
|
||||
w->drop.data_requests[idx].payload[payload_sz] = 0;
|
||||
w->drop.data_requests[idx].payload_sz = payload_sz;
|
||||
}
|
||||
w->drop.data_requests[idx].pixel_y = pixel_y;
|
||||
bool was_empty = (w->drop.num_data_requests == 0);
|
||||
w->drop.num_data_requests++;
|
||||
if (was_empty) drop_process_queue(w);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ void drop_request_data(Window *w, const char *mime);
|
|||
void drop_send_einval(Window *w);
|
||||
void drop_request_uri_data(Window *w, const char *payload, size_t payload_sz);
|
||||
void drop_handle_dir_request(Window *w, uint32_t handle_id, int32_t entry_num);
|
||||
void drop_enqueue_request(Window *w, uint32_t request_id, char type, const char *payload, size_t payload_sz, int32_t cell_x, int32_t cell_y);
|
||||
void drop_enqueue_request(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_y);
|
||||
void drop_set_status(Window *w, int operation, const char *payload, size_t payload_sz, bool more);
|
||||
size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count);
|
||||
void drop_dispatch_data(Window *w, const char *mime_type, const char *data, ssize_t sz);
|
||||
|
|
|
|||
|
|
@ -1532,25 +1532,7 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa
|
|||
case 'A': drop_register_window(w, NULL, 0, false, cmd->client_id, cmd->more); break;
|
||||
case 'm': drop_set_status(w, cmd->operation, (const char*)payload, cmd->payload_sz, cmd->more); break;
|
||||
case 'r': {
|
||||
drop_enqueue_request(w, cmd->request_id, 'r', (const char*)payload, cmd->payload_sz, 0, 0);
|
||||
} break;
|
||||
case 's': {
|
||||
if (cmd->payload_sz) drop_enqueue_request(w, cmd->request_id, 's', (const char*)payload, cmd->payload_sz, 0, 0);
|
||||
else {
|
||||
uint32_t saved = w->drop.current_request_id;
|
||||
w->drop.current_request_id = cmd->request_id;
|
||||
drop_send_einval(w);
|
||||
w->drop.current_request_id = saved;
|
||||
}
|
||||
} break;
|
||||
case 'd': {
|
||||
if (cmd->cell_x > 0) drop_enqueue_request(w, cmd->request_id, 'd', NULL, 0, cmd->cell_x, cmd->cell_y);
|
||||
else {
|
||||
uint32_t saved = w->drop.current_request_id;
|
||||
w->drop.current_request_id = cmd->request_id;
|
||||
drop_send_einval(w);
|
||||
w->drop.current_request_id = saved;
|
||||
}
|
||||
drop_enqueue_request(w, cmd->cell_x, cmd->cell_y, cmd->pixel_y);
|
||||
} break;
|
||||
case 'o': {
|
||||
if (cmd->payload_sz > 0) drag_add_mimes(w, (int)cmd->operation, cmd->client_id, (const char*)payload, cmd->payload_sz, cmd->more);
|
||||
|
|
|
|||
|
|
@ -289,14 +289,11 @@ typedef struct Window {
|
|||
id_type file_send_timer; /* pending file-send retry timer, 0 = none */
|
||||
|
||||
struct {
|
||||
uint32_t request_id;
|
||||
char type; /* 'r' = MIME data, 's' = URI data, 'd' = directory, 'f' = finish */
|
||||
char *payload; /* malloc'd copy of the MIME type / URI payload */
|
||||
size_t payload_sz;
|
||||
int32_t cell_x, cell_y; /* for 'd' type: handle_id, entry_num */
|
||||
int32_t cell_x, cell_y; /* x= and y= keys from request */
|
||||
int32_t pixel_y; /* Y= key from request (dir handle) */
|
||||
} data_requests[128];
|
||||
size_t num_data_requests;
|
||||
uint32_t current_request_id;
|
||||
int32_t current_request_x, current_request_y, current_request_Y;
|
||||
} drop;
|
||||
struct {
|
||||
bool can_offer;
|
||||
|
|
|
|||
|
|
@ -50,37 +50,40 @@ def client_accept(operation: int, mimes: str = '', client_id: int = 0) -> bytes:
|
|||
return _osc(f'{meta};{mimes}')
|
||||
|
||||
|
||||
def client_request_data(mime: str = '', client_id: int = 0, request_id: int = 0) -> bytes:
|
||||
"""Escape code a client sends to request data (t=r) or finish the drop (t=r with no MIME)."""
|
||||
def client_request_data(idx: int = 0, client_id: int = 0) -> bytes:
|
||||
"""Escape code a client sends to request data (t=r:x=idx) or finish the drop (t=r with no x).
|
||||
|
||||
*idx*: 1-based index into the list of MIME types. 0 or omitted means finish.
|
||||
"""
|
||||
meta = f'{DND_CODE};t=r'
|
||||
if request_id:
|
||||
meta += f':r={request_id}'
|
||||
if idx > 0:
|
||||
meta += f':x={idx}'
|
||||
if client_id:
|
||||
meta += f':i={client_id}'
|
||||
return _osc(f'{meta};{mime}')
|
||||
return _osc(meta)
|
||||
|
||||
|
||||
def client_request_uri_data(idx: int, client_id: int = 0, request_id: int = 0) -> bytes:
|
||||
"""Escape code a client sends to request a file from the URI list (t=s ; text/uri-list:idx)."""
|
||||
meta = f'{DND_CODE};t=s'
|
||||
if request_id:
|
||||
meta += f':r={request_id}'
|
||||
def client_request_uri_data(mime_idx: int, file_idx: int, client_id: int = 0) -> bytes:
|
||||
"""Escape code a client sends to request a file from the URI list (t=r:x=mime_idx:y=file_idx).
|
||||
|
||||
*mime_idx*: 1-based index of text/uri-list in the MIME list.
|
||||
*file_idx*: 1-based index into the URI list entries.
|
||||
"""
|
||||
meta = f'{DND_CODE};t=r:x={mime_idx}:y={file_idx}'
|
||||
if client_id:
|
||||
meta += f':i={client_id}'
|
||||
return _osc(f'{meta};text/uri-list:{idx}')
|
||||
return _osc(meta)
|
||||
|
||||
|
||||
def client_dir_read(handle_id: int, entry_num: int | None = None, client_id: int = 0, request_id: int = 0) -> bytes:
|
||||
"""Escape code for a directory request (t=d:x=handle_id[:y=entry_num]).
|
||||
def client_dir_read(handle_id: int, entry_num: int | None = None, client_id: int = 0) -> bytes:
|
||||
"""Escape code for a directory request (t=r:Y=handle[:x=entry_num]).
|
||||
|
||||
* entry_num=None → close the directory handle.
|
||||
* entry_num>=1 → read that entry (1-based).
|
||||
"""
|
||||
meta = f'{DND_CODE};t=d:x={handle_id}'
|
||||
meta = f'{DND_CODE};t=r:Y={handle_id}'
|
||||
if entry_num is not None:
|
||||
meta += f':y={entry_num}'
|
||||
if request_id:
|
||||
meta += f':r={request_id}'
|
||||
meta += f':x={entry_num}'
|
||||
if client_id:
|
||||
meta += f':i={client_id}'
|
||||
return _osc(meta)
|
||||
|
|
@ -416,8 +419,8 @@ class TestDnDProtocol(BaseTest):
|
|||
self.ae(events[0]['type'], 'M')
|
||||
self.assertIn(b'text/plain', events[0]['payload'])
|
||||
|
||||
# Client requests data
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
# Client requests data (idx=1 for 'text/plain', first in the MIME list)
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
|
||||
# OS delivers data
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', payload_data)
|
||||
|
|
@ -429,19 +432,19 @@ class TestDnDProtocol(BaseTest):
|
|||
self.ae(combined, payload_data)
|
||||
|
||||
# Client finishes
|
||||
parse_bytes(screen, client_request_data(''))
|
||||
parse_bytes(screen, client_request_data())
|
||||
self._assert_no_output(cap, wid)
|
||||
|
||||
def test_request_unknown_mime(self) -> None:
|
||||
"""Requesting a MIME type not in the offered set yields an error."""
|
||||
"""Requesting an out-of-range MIME index yields an error."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain'))
|
||||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
# Client requests a MIME that was not offered.
|
||||
parse_bytes(screen, client_request_data('image/png'))
|
||||
# Client requests index 99 which is out of range.
|
||||
parse_bytes(screen, client_request_data(99))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
|
|
@ -455,7 +458,7 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
|
||||
# Simulate I/O error (EIO = 5 on Linux)
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'', errno.EIO)
|
||||
|
|
@ -472,7 +475,7 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'', errno.EPERM)
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
|
|
@ -490,7 +493,7 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', big_payload)
|
||||
raw = cap.consume(wid)
|
||||
data_events = parse_escape_codes_b64(raw)
|
||||
|
|
@ -512,7 +515,7 @@ class TestDnDProtocol(BaseTest):
|
|||
self.ae(events[0]['meta'].get('i'), str(client_id))
|
||||
|
||||
def test_multiple_mimes_priority(self) -> None:
|
||||
"""The client can specify a preferred MIME ordering."""
|
||||
"""The client can request data from any offered MIME type by index."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain text/uri-list'))
|
||||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
|
|
@ -520,8 +523,8 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain', 'text/uri-list'])
|
||||
cap.consume(wid)
|
||||
|
||||
# Request text/uri-list first (different from registration order).
|
||||
parse_bytes(screen, client_request_data('text/uri-list'))
|
||||
# Request text/uri-list (idx=2, since it's the 2nd in the offered list).
|
||||
parse_bytes(screen, client_request_data(2))
|
||||
dnd_test_fake_drop_data(wid, 'text/uri-list', b'file:///tmp/test\n')
|
||||
raw = cap.consume(wid)
|
||||
data_events = parse_escape_codes_b64(raw)
|
||||
|
|
@ -584,7 +587,7 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'hello')
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes(raw)
|
||||
|
|
@ -602,7 +605,7 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'')
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes(raw)
|
||||
|
|
@ -611,7 +614,7 @@ class TestDnDProtocol(BaseTest):
|
|||
self.assertEqual(len(r_events), 1, raw)
|
||||
self.ae(r_events[0]['payload'], b'')
|
||||
|
||||
# ---- t=s / t=d (remote file/directory transfer) tests ----------------
|
||||
# ---- remote file/directory transfer tests ----------------
|
||||
|
||||
def _setup_uri_drop(self, screen, wid, cap, uri_list_data: bytes, mimes=None):
|
||||
"""Register, drop, deliver text/uri-list data, discard move/drop events."""
|
||||
|
|
@ -621,13 +624,14 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
dnd_test_fake_drop_event(wid, True, mimes)
|
||||
cap.consume(wid)
|
||||
# Client requests and receives the URI list
|
||||
parse_bytes(screen, client_request_data('text/uri-list'))
|
||||
# Client requests and receives the URI list (idx=2 for text/uri-list in the default MIME list)
|
||||
uri_idx = mimes.index('text/uri-list') + 1 # 1-based
|
||||
parse_bytes(screen, client_request_data(uri_idx))
|
||||
dnd_test_fake_drop_data(wid, 'text/uri-list', uri_list_data)
|
||||
cap.consume(wid) # discard t=r data for text/uri-list
|
||||
|
||||
def test_uri_file_transfer_basic(self) -> None:
|
||||
"""t=s request sends the content of a regular file as t=r chunks."""
|
||||
"""URI file request sends the content of a regular file as t=r chunks."""
|
||||
import os
|
||||
import tempfile
|
||||
content = b'Hello, remote DnD world!\n' * 100
|
||||
|
|
@ -638,7 +642,7 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{fpath}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
|
|
@ -663,7 +667,7 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{fpath}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
|
|
@ -674,19 +678,19 @@ class TestDnDProtocol(BaseTest):
|
|||
os.unlink(fpath)
|
||||
|
||||
def test_uri_file_transfer_enoent(self) -> None:
|
||||
"""t=s with an out-of-range index returns ENOENT."""
|
||||
"""URI file request with an out-of-range index returns ENOENT."""
|
||||
uri_list = b'file:///tmp/no_such_file_exists_dnd_test_xyz\r\n'
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
# Index 0 refers to a non-existent file
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
# File at index 1 does not exist
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
self.assertIn(events[0]['payload'].strip(), [b'ENOENT', b'EPERM'])
|
||||
|
||||
def test_uri_file_transfer_out_of_bounds(self) -> None:
|
||||
"""t=s with an index beyond the URI list returns ENOENT."""
|
||||
"""URI file request with an index beyond the URI list returns ENOENT."""
|
||||
import os
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
|
|
@ -695,7 +699,7 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{fpath}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(99)) # out of range
|
||||
parse_bytes(screen, client_request_uri_data(2, 100)) # out of range
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
|
|
@ -704,7 +708,7 @@ class TestDnDProtocol(BaseTest):
|
|||
os.unlink(fpath)
|
||||
|
||||
def test_uri_request_without_uri_list_returns_einval(self) -> None:
|
||||
"""t=s without prior text/uri-list request returns EINVAL."""
|
||||
"""URI file request without prior text/uri-list request returns EINVAL."""
|
||||
import os
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
|
|
@ -716,7 +720,7 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain', 'text/uri-list'])
|
||||
cap.consume(wid)
|
||||
# Do NOT request text/uri-list first
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
|
|
@ -725,11 +729,11 @@ class TestDnDProtocol(BaseTest):
|
|||
os.unlink(fpath)
|
||||
|
||||
def test_uri_non_regular_file_returns_einval(self) -> None:
|
||||
"""t=s for a non-regular file (e.g. /dev/null) returns EINVAL."""
|
||||
"""URI file request for a non-regular file (e.g. /dev/null) returns EINVAL."""
|
||||
uri_list = b'file:///dev/null\r\n'
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
|
|
@ -761,16 +765,16 @@ class TestDnDProtocol(BaseTest):
|
|||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
|
||||
# Request the root directory (idx=0)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_events = [e for e in events if e['type'] == 'd']
|
||||
self.assertTrue(d_events, 'expected t=d listing for root')
|
||||
d_events = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
self.assertTrue(d_events, 'expected directory listing for root')
|
||||
|
||||
root_listing_payload = b''.join(
|
||||
chunk for e in d_events for chunk in e['chunks'] if chunk
|
||||
)
|
||||
root_handle_id = int(d_events[0]['meta']['x'])
|
||||
root_handle_id = int(d_events[0]['meta']['Y'])
|
||||
self.assertGreater(root_handle_id, 0)
|
||||
|
||||
# Decode null-separated entries (no unique identifier prefix)
|
||||
|
|
@ -779,7 +783,7 @@ class TestDnDProtocol(BaseTest):
|
|||
self.assertIn('a.txt', entry_names)
|
||||
self.assertIn('b', entry_names)
|
||||
|
||||
# Find index of 'a.txt' in the entries list (1-based for t=d:y=)
|
||||
# Find index of 'a.txt' in the entries list (1-based)
|
||||
entries_list = [e.decode() for e in root_entries]
|
||||
a_idx = entries_list.index('a.txt') + 1
|
||||
b_idx = entries_list.index('b') + 1
|
||||
|
|
@ -792,17 +796,17 @@ class TestDnDProtocol(BaseTest):
|
|||
a_data = b''.join(e['payload'] for e in r_events if e['payload'])
|
||||
self.ae(a_data, a_content)
|
||||
|
||||
# Read sub-directory b → should get a new t=d listing
|
||||
# Read sub-directory b → should get a new directory listing
|
||||
parse_bytes(screen, client_dir_read(root_handle_id, b_idx))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
b_d_events = [e for e in events if e['type'] == 'd']
|
||||
self.assertTrue(b_d_events, 'expected t=d listing for b/')
|
||||
b_d_events = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
self.assertTrue(b_d_events, 'expected directory listing for b/')
|
||||
|
||||
b_listing_payload = b''.join(
|
||||
chunk for e in b_d_events for chunk in e['chunks'] if chunk
|
||||
)
|
||||
b_handle_id = int(b_d_events[0]['meta']['x'])
|
||||
b_handle_id = int(b_d_events[0]['meta']['Y'])
|
||||
self.assertNotEqual(b_handle_id, root_handle_id)
|
||||
|
||||
b_entries = [e for e in b_listing_payload.split(b'\x00') if e]
|
||||
|
|
@ -825,17 +829,17 @@ class TestDnDProtocol(BaseTest):
|
|||
self.ae(hashlib.sha256(bc_data).digest(),
|
||||
hashlib.sha256(bc_content).digest())
|
||||
|
||||
# Read sub-directory b/d → yet another t=d listing
|
||||
# Read sub-directory b/d → yet another directory listing
|
||||
parse_bytes(screen, client_dir_read(b_handle_id, bd_idx))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
bd_d_events = [e for e in events if e['type'] == 'd']
|
||||
self.assertTrue(bd_d_events, 'expected t=d listing for b/d/')
|
||||
bd_d_events = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
self.assertTrue(bd_d_events, 'expected directory listing for b/d/')
|
||||
|
||||
bd_listing_payload = b''.join(
|
||||
chunk for e in bd_d_events for chunk in e['chunks'] if chunk
|
||||
)
|
||||
bd_handle_id = int(bd_d_events[0]['meta']['x'])
|
||||
bd_handle_id = int(bd_d_events[0]['meta']['Y'])
|
||||
bd_entries = [e for e in bd_listing_payload.split(b'\x00') if e]
|
||||
bd_names = {e.decode() for e in bd_entries}
|
||||
self.assertIn('e.txt', bd_names)
|
||||
|
|
@ -867,12 +871,12 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
self.assertTrue(d_ev)
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
|
||||
# Close the handle
|
||||
parse_bytes(screen, client_dir_read(hid))
|
||||
|
|
@ -894,11 +898,11 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
|
||||
# Entry 999 does not exist
|
||||
parse_bytes(screen, client_dir_read(hid, 999))
|
||||
|
|
@ -916,10 +920,10 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
|
|
@ -939,14 +943,14 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
entries = [e.decode() for e in payload.split(b'\x00') if e]
|
||||
self.assertIn('link.txt', entries)
|
||||
self.assertIn('real.txt', entries)
|
||||
|
|
@ -975,14 +979,14 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
entries = [e.decode() for e in payload.split(b'\x00') if e]
|
||||
self.assertIn('link_to_dir', entries)
|
||||
link_idx = entries.index('link_to_dir') + 1
|
||||
|
|
@ -1009,14 +1013,14 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
entries = [e.decode() for e in payload.split(b'\x00') if e]
|
||||
link_idx = entries.index('abs_link.txt') + 1
|
||||
|
||||
|
|
@ -1039,14 +1043,14 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
entries = [e.decode() for e in payload.split(b'\x00') if e]
|
||||
reg_idx = entries.index('regular.txt') + 1
|
||||
|
||||
|
|
@ -1072,14 +1076,14 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
entries = [e.decode() for e in payload.split(b'\x00') if e]
|
||||
|
||||
# Read regular file
|
||||
|
|
@ -1115,14 +1119,14 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
root_hid = int(d_ev[0]['meta']['x'])
|
||||
root_hid = int(d_ev[0]['meta']['Y'])
|
||||
entries = [e.decode() for e in payload.split(b'\x00') if e]
|
||||
sub_idx = entries.index('sub') + 1
|
||||
|
||||
|
|
@ -1130,11 +1134,11 @@ class TestDnDProtocol(BaseTest):
|
|||
parse_bytes(screen, client_dir_read(root_hid, sub_idx))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
sub_payload = b''.join(
|
||||
chunk for e in d_ev for chunk in e['chunks'] if chunk
|
||||
)
|
||||
sub_hid = int(d_ev[0]['meta']['x'])
|
||||
sub_hid = int(d_ev[0]['meta']['Y'])
|
||||
sub_entries = [e.decode() for e in sub_payload.split(b'\x00') if e]
|
||||
self.assertIn('nested_link.txt', sub_entries)
|
||||
|
||||
|
|
@ -1157,11 +1161,11 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_ev = [e for e in events if e['type'] == 'd']
|
||||
hid = int(d_ev[0]['meta']['x'])
|
||||
d_ev = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
hid = int(d_ev[0]['meta']['Y'])
|
||||
|
||||
# Index 1 should read the first entry
|
||||
parse_bytes(screen, client_dir_read(hid, 1))
|
||||
|
|
@ -1185,7 +1189,7 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{link}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
|
|
@ -1207,10 +1211,10 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{link}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_events = [e for e in events if e['type'] == 'd']
|
||||
d_events = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
self.assertTrue(d_events, 'top-level symlink to dir should return directory listing')
|
||||
payload = b''.join(
|
||||
chunk for e in d_events for chunk in e['chunks'] if chunk
|
||||
|
|
@ -1229,7 +1233,7 @@ class TestDnDProtocol(BaseTest):
|
|||
# which calls drop_free_data → drop_free_dir_handles.
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
cap.consume(wid)
|
||||
# Intentionally leave the handle open – cleanup happens in __exit__
|
||||
|
||||
|
|
@ -1747,105 +1751,86 @@ class TestDnDProtocol(BaseTest):
|
|||
parse_bytes(screen, client_drag_start())
|
||||
self.assert_error(cap, wid)
|
||||
|
||||
# ---- Request queue and request_id tests ----------------------------------
|
||||
|
||||
def test_request_id_echoed_in_data_response(self) -> None:
|
||||
"""request_id is echoed back as r=ID in data responses."""
|
||||
payload_data = b'hello request_id'
|
||||
# ---- Request queue and disambiguation tests --------------------------------
|
||||
|
||||
def test_x_key_echoed_in_data_response(self) -> None:
|
||||
"""x= key is echoed in data responses to identify which request is being answered."""
|
||||
payload_data = b'hello disambiguation'
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain'))
|
||||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=42))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', payload_data)
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
self.assertTrue(r_events, 'no t=r events')
|
||||
for ev in r_events:
|
||||
self.ae(ev['meta'].get('r'), '42', f'expected r=42, got {ev["meta"]}')
|
||||
combined = b''.join(e['payload'] for e in r_events)
|
||||
self.ae(combined, payload_data)
|
||||
self.ae(ev['meta'].get('x'), '1')
|
||||
|
||||
def test_request_id_echoed_in_error_response(self) -> None:
|
||||
"""request_id is echoed back as r=ID in error responses."""
|
||||
def test_x_key_echoed_in_error_response(self) -> None:
|
||||
"""x= key is echoed in error responses."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain'))
|
||||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('image/png', request_id=99))
|
||||
# Request out-of-range index -> error
|
||||
parse_bytes(screen, client_request_data(99))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
self.ae(events[0]['meta'].get('r'), '99')
|
||||
self.ae(events[0]['meta'].get('x'), '99')
|
||||
self.ae(events[0]['payload'].strip(), b'ENOENT')
|
||||
|
||||
def test_request_id_zero_not_included(self) -> None:
|
||||
"""When request_id is 0 (default), r= is not included in responses."""
|
||||
payload_data = b'no request_id'
|
||||
def test_x_key_in_error_for_io_failure(self) -> None:
|
||||
"""x= key is echoed in I/O error responses."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain'))
|
||||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
# Request without request_id (defaults to 0)
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', payload_data)
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
self.assertTrue(r_events, 'no t=r events')
|
||||
for ev in r_events:
|
||||
self.assertNotIn('r', ev['meta'], f'r= should not be present when request_id=0, got {ev["meta"]}')
|
||||
|
||||
def test_request_id_in_error_for_io_failure(self) -> None:
|
||||
"""request_id is echoed in I/O error responses."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain'))
|
||||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=77))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'', errno.EIO)
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.assertEqual(len(events), 1)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
self.ae(events[0]['meta'].get('r'), '77')
|
||||
self.ae(events[0]['meta'].get('x'), '1')
|
||||
self.ae(events[0]['payload'].strip(), b'EIO')
|
||||
|
||||
def test_multiple_queued_requests_fifo(self) -> None:
|
||||
"""Multiple requests with different request_ids are served in FIFO order."""
|
||||
def test_fifo_order_with_different_indices(self) -> None:
|
||||
"""Multiple requests with different x= values are served in FIFO order."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain text/html'))
|
||||
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
|
||||
dnd_test_fake_drop_event(wid, True, ['text/plain', 'text/html'])
|
||||
cap.consume(wid)
|
||||
|
||||
# Queue two requests
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=1))
|
||||
parse_bytes(screen, client_request_data('text/html', request_id=2))
|
||||
# Queue two requests: idx=1 (text/plain) then idx=2 (text/html)
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
parse_bytes(screen, client_request_data(2))
|
||||
|
||||
# First request (text/plain) gets served first
|
||||
# First request (idx=1) gets served first
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'plain data')
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r' and e['meta'].get('r') == '1']
|
||||
self.assertTrue(r_events, 'no t=r events for first request')
|
||||
r_events = [e for e in events if e['type'] == 'r' and e['meta'].get('x') == '1']
|
||||
self.assertTrue(r_events, 'no t=r events for first request (x=1)')
|
||||
combined = b''.join(e['payload'] for e in r_events)
|
||||
self.ae(combined, b'plain data')
|
||||
|
||||
# Second request (text/html) gets served next
|
||||
# Second request (idx=2) gets served next
|
||||
dnd_test_fake_drop_data(wid, 'text/html', b'<html>data</html>')
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r' and e['meta'].get('r') == '2']
|
||||
self.assertTrue(r_events, 'no t=r events for second request')
|
||||
r_events = [e for e in events if e['type'] == 'r' and e['meta'].get('x') == '2']
|
||||
self.assertTrue(r_events, 'no t=r events for second request (x=2)')
|
||||
combined = b''.join(e['payload'] for e in r_events)
|
||||
self.ae(combined, b'<html>data</html>')
|
||||
|
||||
|
|
@ -1857,26 +1842,26 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
# Queue: request for unknown MIME (error) followed by valid request
|
||||
parse_bytes(screen, client_request_data('image/png', request_id=10))
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=11))
|
||||
# Queue: request for out-of-range index (error) followed by valid request
|
||||
parse_bytes(screen, client_request_data(99))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
|
||||
# The error for request 10 should have been sent immediately
|
||||
# The error for index 99 should have been sent immediately
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes(raw)
|
||||
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('r'), '10')
|
||||
self.ae(err_events[0]['meta'].get('x'), '99')
|
||||
self.ae(err_events[0]['payload'].strip(), b'ENOENT')
|
||||
|
||||
# Now serve request 11
|
||||
# Now serve request for index 1
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'second request data')
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
self.assertTrue(r_events, 'no t=r events for second request')
|
||||
for ev in r_events:
|
||||
self.ae(ev['meta'].get('r'), '11')
|
||||
self.ae(ev['meta'].get('x'), '1')
|
||||
|
||||
def test_queue_overflow_returns_emfile(self) -> None:
|
||||
"""Exceeding 128 queued requests returns EMFILE and ends the drop."""
|
||||
|
|
@ -1887,11 +1872,11 @@ class TestDnDProtocol(BaseTest):
|
|||
cap.consume(wid)
|
||||
|
||||
# First request starts async processing
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=1))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
|
||||
# Queue 127 more requests (fill to capacity = 128)
|
||||
for i in range(2, 129):
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=i))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
|
||||
# No error yet - queue is at capacity
|
||||
raw = cap.consume(wid)
|
||||
|
|
@ -1899,19 +1884,18 @@ class TestDnDProtocol(BaseTest):
|
|||
self.assertEqual(len(err_events), 0, f'unexpected errors: {err_events}')
|
||||
|
||||
# 129th request should trigger EMFILE
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=999))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
raw = cap.consume(wid)
|
||||
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]['meta'].get('r'), '999')
|
||||
self.ae(err_events[0]['payload'].strip(), b'EMFILE')
|
||||
|
||||
def test_request_id_in_uri_file_response(self) -> None:
|
||||
"""request_id is echoed in t=s (URI file) data responses."""
|
||||
def test_xy_keys_in_uri_file_response(self) -> None:
|
||||
"""x= and y= keys are echoed in URI file data responses."""
|
||||
import os
|
||||
import tempfile
|
||||
content = b'URI file with request_id\n'
|
||||
content = b'URI file with disambiguation\n'
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
f.write(content)
|
||||
fpath = f.name
|
||||
|
|
@ -1919,31 +1903,33 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{fpath}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0, request_id=55))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
self.assertTrue(r_events, 'no t=r events')
|
||||
for ev in r_events:
|
||||
self.ae(ev['meta'].get('r'), '55')
|
||||
self.ae(ev['meta'].get('x'), '2')
|
||||
self.ae(ev['meta'].get('y'), '1')
|
||||
combined = b''.join(e['payload'] for e in r_events)
|
||||
self.ae(combined, content)
|
||||
finally:
|
||||
os.unlink(fpath)
|
||||
|
||||
def test_request_id_in_uri_error_response(self) -> None:
|
||||
"""request_id is echoed in t=s error responses."""
|
||||
def test_xy_keys_in_uri_error_response(self) -> None:
|
||||
"""x= and y= keys are echoed in URI file error responses."""
|
||||
uri_list = b'file:///tmp/no_such_file_dnd_test_xyz\r\n'
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0, request_id=66))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
self.ae(events[0]['meta'].get('r'), '66')
|
||||
self.ae(events[0]['meta'].get('x'), '2')
|
||||
self.ae(events[0]['meta'].get('y'), '1')
|
||||
|
||||
def test_request_id_in_dir_listing_response(self) -> None:
|
||||
"""request_id is echoed in directory listing (t=d) responses."""
|
||||
def test_Y_key_in_dir_listing_response(self) -> None:
|
||||
"""Y= key (new handle) and X=2 are present in directory listing responses."""
|
||||
import os
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
|
|
@ -1951,16 +1937,18 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0, request_id=88))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_events = [e for e in events if e['type'] == 'd']
|
||||
self.assertTrue(d_events, 'expected t=d listing')
|
||||
d_events = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
self.assertTrue(d_events, 'expected directory listing')
|
||||
for ev in d_events:
|
||||
self.ae(ev['meta'].get('r'), '88')
|
||||
self.ae(ev['meta'].get('x'), '2')
|
||||
self.ae(ev['meta'].get('y'), '1')
|
||||
self.assertTrue(int(ev['meta'].get('Y', '0')) > 0, 'Y= must be non-zero handle')
|
||||
|
||||
def test_request_id_in_dir_entry_file_response(self) -> None:
|
||||
"""request_id is echoed when reading a file via directory handle (t=d)."""
|
||||
def test_Y_and_x_keys_in_dir_entry_file_response(self) -> None:
|
||||
"""Y= and x= keys are echoed when reading a file via directory handle."""
|
||||
import os
|
||||
import tempfile
|
||||
content = b'directory file content\n'
|
||||
|
|
@ -1970,30 +1958,30 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
# Get dir listing first (no request_id needed for setup)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_events = [e for e in events if e['type'] == 'd']
|
||||
d_events = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
self.assertTrue(d_events)
|
||||
handle_id = int(d_events[0]['meta']['x'])
|
||||
handle_id = int(d_events[0]['meta']['Y'])
|
||||
listing = b''.join(chunk for e in d_events for chunk in e['chunks'] if chunk)
|
||||
entries = [e.decode() for e in listing.split(b'\x00') if e]
|
||||
f_idx = entries.index('f.txt') + 1
|
||||
|
||||
# Read file with request_id
|
||||
parse_bytes(screen, client_dir_read(handle_id, f_idx, request_id=33))
|
||||
# Read file from directory
|
||||
parse_bytes(screen, client_dir_read(handle_id, f_idx))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events = [e for e in events if e['type'] == 'r']
|
||||
self.assertTrue(r_events, 'no t=r events')
|
||||
for ev in r_events:
|
||||
self.ae(ev['meta'].get('r'), '33')
|
||||
self.ae(ev['meta'].get('x'), str(f_idx))
|
||||
self.ae(ev['meta'].get('Y'), str(handle_id))
|
||||
combined = b''.join(e['payload'] for e in r_events)
|
||||
self.ae(combined, content)
|
||||
|
||||
def test_request_id_in_dir_entry_error_response(self) -> None:
|
||||
"""request_id is echoed when a directory entry read fails."""
|
||||
def test_Y_and_x_keys_in_dir_entry_error_response(self) -> None:
|
||||
"""Y= and x= keys are echoed when a directory entry read fails."""
|
||||
import os
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
|
|
@ -2001,22 +1989,23 @@ class TestDnDProtocol(BaseTest):
|
|||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
parse_bytes(screen, client_request_uri_data(0))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
d_events = [e for e in events if e['type'] == 'd']
|
||||
handle_id = int(d_events[0]['meta']['x'])
|
||||
d_events = [e for e in events if e['type'] == 'r' and e['meta'].get('X') == '2']
|
||||
handle_id = int(d_events[0]['meta']['Y'])
|
||||
|
||||
# Out-of-range entry with request_id
|
||||
parse_bytes(screen, client_dir_read(handle_id, 999, request_id=44))
|
||||
# Out-of-range entry
|
||||
parse_bytes(screen, client_dir_read(handle_id, 999))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
self.ae(events[0]['meta'].get('r'), '44')
|
||||
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')
|
||||
|
||||
def test_mixed_request_types_with_ids(self) -> None:
|
||||
"""Mixed r/s/d request types with request_ids are processed in order."""
|
||||
def test_mixed_request_types_processed_in_order(self) -> None:
|
||||
"""Mixed MIME data and URI file requests are processed in FIFO order."""
|
||||
import os
|
||||
import tempfile
|
||||
file_content = b'mixed request file\n'
|
||||
|
|
@ -2028,21 +2017,21 @@ class TestDnDProtocol(BaseTest):
|
|||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
self._setup_uri_drop(screen, wid, cap, uri_list)
|
||||
|
||||
# Queue: MIME data request, then URI file request
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=100))
|
||||
parse_bytes(screen, client_request_uri_data(0, request_id=200))
|
||||
# Queue: MIME data request (x=1), then URI file request (x=2,y=1)
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
|
||||
# Serve first request (MIME data); the URI file request
|
||||
# completes synchronously right after so all output is in one batch
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'plain text')
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
r_events_100 = [e for e in events if e['type'] == 'r' and e['meta'].get('r') == '100']
|
||||
self.assertTrue(r_events_100, 'no events with r=100')
|
||||
r_events_x1 = [e for e in events if e['type'] == 'r' and e['meta'].get('x') == '1' and 'y' not in e['meta']]
|
||||
self.assertTrue(r_events_x1, 'no events with x=1 (MIME data)')
|
||||
|
||||
r_events_200 = [e for e in events if e['type'] == 'r' and e['meta'].get('r') == '200']
|
||||
self.assertTrue(r_events_200, 'no events with r=200')
|
||||
combined = b''.join(e['payload'] for e in r_events_200)
|
||||
r_events_x2y1 = [e for e in events if e['type'] == 'r' and e['meta'].get('x') == '2' and e['meta'].get('y') == '1']
|
||||
self.assertTrue(r_events_x2y1, 'no events with x=2,y=1 (URI file)')
|
||||
combined = b''.join(e['payload'] for e in r_events_x2y1)
|
||||
self.ae(combined, file_content)
|
||||
finally:
|
||||
os.unlink(fpath)
|
||||
|
|
@ -2056,8 +2045,8 @@ class TestDnDProtocol(BaseTest):
|
|||
cap.consume(wid)
|
||||
|
||||
# Queue: data request then finish
|
||||
parse_bytes(screen, client_request_data('text/plain', request_id=5))
|
||||
parse_bytes(screen, client_request_data('')) # finish
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
parse_bytes(screen, client_request_data()) # finish
|
||||
|
||||
# Serve the data request
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', b'data before finish')
|
||||
|
|
@ -2066,7 +2055,7 @@ class TestDnDProtocol(BaseTest):
|
|||
r_events = [e for e in events if e['type'] == 'r']
|
||||
self.assertTrue(r_events, 'no t=r events')
|
||||
for ev in r_events:
|
||||
self.ae(ev['meta'].get('r'), '5')
|
||||
self.ae(ev['meta'].get('x'), '1')
|
||||
|
||||
def test_multiple_sync_errors_processed_immediately(self) -> None:
|
||||
"""Multiple queued requests that all fail synchronously are processed immediately."""
|
||||
|
|
@ -2076,33 +2065,32 @@ class TestDnDProtocol(BaseTest):
|
|||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
# Queue three requests for unknown MIMEs
|
||||
parse_bytes(screen, client_request_data('image/png', request_id=1))
|
||||
parse_bytes(screen, client_request_data('image/gif', request_id=2))
|
||||
parse_bytes(screen, client_request_data('image/jpeg', request_id=3))
|
||||
# Queue three requests for out-of-range indices
|
||||
parse_bytes(screen, client_request_data(10))
|
||||
parse_bytes(screen, client_request_data(20))
|
||||
parse_bytes(screen, client_request_data(30))
|
||||
|
||||
# All three errors should be available immediately
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes(raw)
|
||||
err_events = [e for e in events if e['type'] == 'R']
|
||||
self.assertEqual(len(err_events), 3, f'expected 3 errors, got {len(err_events)}: {err_events}')
|
||||
self.ae(err_events[0]['meta'].get('r'), '1')
|
||||
self.ae(err_events[1]['meta'].get('r'), '2')
|
||||
self.ae(err_events[2]['meta'].get('r'), '3')
|
||||
self.ae(err_events[0]['meta'].get('x'), '10')
|
||||
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')
|
||||
|
||||
def test_request_id_backward_compat_full_flow(self) -> None:
|
||||
"""Full drop flow without request_id (backward compatibility) still works."""
|
||||
payload_data = b'backward compat data'
|
||||
def test_no_r_key_in_responses(self) -> None:
|
||||
"""Responses must not contain the old r= key."""
|
||||
payload_data = b'no r= key test'
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_register('text/plain'))
|
||||
dnd_test_set_mouse_pos(wid, 2, 3, 16, 24)
|
||||
dnd_test_fake_drop_event(wid, True, ['text/plain'])
|
||||
cap.consume(wid)
|
||||
|
||||
# Request without request_id
|
||||
parse_bytes(screen, client_request_data('text/plain'))
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(wid, 'text/plain', payload_data)
|
||||
raw = cap.consume(wid)
|
||||
events = parse_escape_codes_b64(raw)
|
||||
|
|
@ -2110,10 +2098,10 @@ class TestDnDProtocol(BaseTest):
|
|||
self.assertTrue(r_events)
|
||||
combined = b''.join(e['payload'] for e in r_events)
|
||||
self.ae(combined, payload_data)
|
||||
# Verify no r= in metadata
|
||||
# Verify no r= key in metadata
|
||||
for ev in r_events:
|
||||
self.assertNotIn('r', ev['meta'])
|
||||
self.assertNotIn('r', ev['meta'], f'r= should not be present, got {ev["meta"]}')
|
||||
|
||||
# Finish
|
||||
parse_bytes(screen, client_request_data(''))
|
||||
parse_bytes(screen, client_request_data())
|
||||
self._assert_no_output(cap, wid)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue