diff --git a/CHANGELOG.md b/CHANGELOG.md index a0cb347f..fdb6d97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ `foot.ini`. These options allow custom bold/italic fonts. They are unset by default, meaning the bold/italic version of the regular font is used (https://codeberg.org/dnkl/foot/issues/169). +* Drag & drop support; text, files and URLs can now be dropped in a + foot terminal window (https://codeberg.org/dnkl/foot/issues/175). ### Changed diff --git a/meson.build b/meson.build index 16a42150..0bf3a4ac 100644 --- a/meson.build +++ b/meson.build @@ -130,6 +130,7 @@ executable( 'spawn.c', 'spawn.h', 'terminal.c', 'terminal.h', 'tokenize.c', 'tokenize.h', + 'uri.c', 'uri.h', 'user-notification.h', 'vt.c', 'vt.h', 'wayland.c', 'wayland.h', diff --git a/osc.c b/osc.c index d30d761c..bf0fff7e 100644 --- a/osc.c +++ b/osc.c @@ -3,8 +3,6 @@ #include #include #include -#include -#include #define LOG_MODULE "osc" #define LOG_ENABLE_DBG 0 @@ -14,6 +12,7 @@ #include "render.h" #include "selection.h" #include "terminal.h" +#include "uri.h" #include "vt.h" #include "xmalloc.h" @@ -99,7 +98,7 @@ struct clip_context { }; static void -from_clipboard_cb(const char *text, size_t size, void *user) +from_clipboard_cb(char *text, size_t size, void *user) { struct clip_context *ctx = user; struct terminal *term = ctx->term; @@ -352,87 +351,26 @@ parse_rgb(const char *string, uint32_t *color) return true; } -static uint8_t -nibble2hex(char c) -{ - switch (c) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - return c - '0'; - - case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': - return c - 'a' + 10; - - case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': - return c - 'A' + 10; - } - - assert(false); - return 0; -} - static void osc_set_pwd(struct terminal *term, char *string) { LOG_DBG("PWD: URI: %s", string); - if (memcmp(string, "file://", 7) != 0) - return; - string += 7; - - const char *hostname = string; - char *hostname_end = strchr(string, '/'); - if (hostname_end == NULL) - return; - - char this_host[HOST_NAME_MAX]; - if (gethostname(this_host, sizeof(this_host)) < 0) - this_host[0] = '\0'; - - /* Ignore this CWD if the hostname isn't 'localhost' or our gethostname() */ - size_t hostname_len = hostname_end - hostname; - if (strncmp(hostname, "", hostname_len) != 0 && - strncmp(hostname, "localhost", hostname_len) != 0 && - strncmp(hostname, this_host, hostname_len) != 0) - { - LOG_DBG("ignoring OSC 7: hostname mismatch: %.*s != %s", - (int)hostname_len, hostname, this_host); + char *scheme, *host, *path; + if (!uri_parse(string, strlen(string), &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("OSC7: invalid URI: %s", string); return; } - /* Decode %xx encoded characters */ - const char *path = hostname_end; - char *pwd = xmalloc(strlen(path) + 1); - char *p = pwd; + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + LOG_DBG("OSC7: pwd: %s", path); + free(term->cwd); + term->cwd = path; + } else + free(path); - while (true) { - /* Find next '%' */ - const char *next = strchr(path, '%'); - - if (next == NULL) { - strcpy(p, path); - break; - } - - /* Copy everything leading up to the '%' */ - size_t prefix_len = next - path; - memcpy(p, path, prefix_len); - p += prefix_len; - - if (isxdigit(next[1]) && isxdigit(next[2])) { - *p++ = nibble2hex(next[1]) << 4 | nibble2hex(next[2]); - *p = '\0'; - path = next + 3; - } else { - *p++ = *next; - *p = '\0'; - path = next + 1; - } - } - - LOG_DBG("PWD: decoded: %s", pwd); - free(term->cwd); - term->cwd = pwd; + free(scheme); + free(host); } #if 0 diff --git a/selection.c b/selection.c index c76a1608..6e0d8d5a 100644 --- a/selection.c +++ b/selection.c @@ -21,10 +21,18 @@ #include "grid.h" #include "misc.h" #include "render.h" +#include "uri.h" #include "util.h" #include "vt.h" #include "xmalloc.h" +static const char *const mime_type_map[] = { + [DATA_OFFER_MIME_UNSET] = NULL, + [DATA_OFFER_MIME_TEXT_PLAIN] = "text/plain", + [DATA_OFFER_MIME_TEXT_UTF8] = "text/plain;charset=utf-8", + [DATA_OFFER_MIME_URI_LIST] = "text/uri-list", +}; + bool selection_enabled(const struct terminal *term, struct seat *seat) { @@ -898,7 +906,7 @@ selection_stop_scroll_timer(struct terminal *term) static void target(void *data, struct wl_data_source *wl_data_source, const char *mime_type) { - LOG_WARN("TARGET: mime-type=%s", mime_type); + LOG_DBG("TARGET: mime-type=%s", mime_type); } struct clipboard_send { @@ -1008,19 +1016,23 @@ cancelled(void *data, struct wl_data_source *wl_data_source) clipboard->text = NULL; } +/* We don’t support dragging *from* */ static void dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) { + //LOG_DBG("DnD drop performed"); } static void dnd_finished(void *data, struct wl_data_source *wl_data_source) { + //LOG_DBG("DnD finished"); } static void action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action) { + //LOG_DBG("DnD action: %u", dnd_action); } static const struct wl_data_source_listener data_source_listener = { @@ -1092,7 +1104,7 @@ text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t clipboard->text = text; /* Configure source */ - wl_data_source_offer(clipboard->data_source, "text/plain;charset=utf-8"); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); wl_data_source_add_listener(clipboard->data_source, &data_source_listener, seat); wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial); @@ -1120,7 +1132,7 @@ struct clipboard_receive { struct itimerspec timeout; /* Callback data */ - void (*cb)(const char *data, size_t size, void *user); + void (*cb)(char *data, size_t size, void *user); void (*done)(void *user); void *user; }; @@ -1191,7 +1203,7 @@ fdm_receive(struct fdm *fdm, int fd, int events, void *data) break; /* Call cb while at same time replacing \r\n with \n */ - const char *p = text; + char *p = text; size_t left = count; again: for (size_t i = 0; i < left - 1; i++) { @@ -1216,7 +1228,7 @@ done: static void begin_receive_clipboard(struct terminal *term, int read_fd, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { int timeout_fd = -1; @@ -1269,7 +1281,7 @@ err: void text_from_clipboard(struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { struct wl_clipboard *clipboard = &seat->clipboard; @@ -1291,7 +1303,7 @@ text_from_clipboard(struct seat *seat, struct terminal *term, /* Give write-end of pipe to other client */ wl_data_offer_receive( - clipboard->data_offer, "text/plain;charset=utf-8", write_fd); + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); @@ -1299,18 +1311,94 @@ text_from_clipboard(struct seat *seat, struct terminal *term, begin_receive_clipboard(term, read_fd, cb, done, user); } +struct receive_offer_context { + struct terminal *term; + + /* URI state */ + bool add_space; + struct { + char *data; + size_t sz; + size_t idx; + } buf; + + union { + struct wl_data_offer *data_offer; + struct zwp_primary_selection_offer_v1 *primary_offer; + }; +}; + static void -from_clipboard_cb(const char *data, size_t size, void *user) +receive_offer_text(char *data, size_t size, void *user) { - struct terminal *term = user; - assert(term->is_sending_paste_data); - term_paste_data_to_slave(term, data, size); + struct receive_offer_context *ctx = user; + assert(ctx->term->is_sending_paste_data); + term_paste_data_to_slave(ctx->term, data, size); } static void -from_clipboard_done(void *user) +receive_offer_uri(char *data, size_t size, void *user) { - struct terminal *term = user; + struct receive_offer_context *ctx = user; + + while (ctx->buf.idx + size > ctx->buf.sz) { + size_t new_sz = ctx->buf.sz == 0 ? size : 2 * ctx->buf.sz; + ctx->buf.data = xrealloc(ctx->buf.data, new_sz); + ctx->buf.sz = new_sz; + } + + memcpy(&ctx->buf.data[ctx->buf.idx], data, size); + ctx->buf.idx += size; + + char *start = ctx->buf.data; + char *end = NULL; + + while ((end = memchr(start, '\n', ctx->buf.idx - (start - ctx->buf.data))) != NULL) { + const size_t len = end - start; + + LOG_DBG("URI: \"%.*s\"", (int)len, start); + + char *scheme, *host, *path; + if (!uri_parse(start, len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("drag-and-drop: invalid URI: %.*s", (int)len, start); + start = end + 1; + continue; + } + + if (ctx->add_space) + receive_offer_text(" ", 1, ctx); + ctx->add_space = true; + + receive_offer_text("'", 1, ctx); + + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) + receive_offer_text(path, strlen(path), ctx); + else + receive_offer_text(start, len, ctx); + + receive_offer_text("'", 1, ctx); + start = end + 1; + + free(scheme); + free(host); + free(path); + } + + const size_t ofs = start - ctx->buf.data; + const size_t left = ctx->buf.idx - ofs; + + memmove(&ctx->buf.data[0], &ctx->buf.data[ofs], left); + ctx->buf.idx = left; +} + +static void +receive_offer_done(void *user) +{ + struct receive_offer_context *ctx = user; + struct terminal *term = ctx->term; + + free(ctx->buf.data); + free(ctx); if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[201~", 6); @@ -1339,8 +1427,18 @@ selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t seri if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); + struct receive_offer_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct receive_offer_context) { + .term = term, + .data_offer = clipboard->data_offer, + }; + text_from_clipboard( - seat, term, &from_clipboard_cb, &from_clipboard_done, term); + seat, term, + (clipboard->mime_type == DATA_OFFER_MIME_URI_LIST + ? &receive_offer_uri + : &receive_offer_text), + &receive_offer_done, ctx); } bool @@ -1379,7 +1477,7 @@ text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t s primary->text = text; /* Configure source */ - zwp_primary_selection_source_v1_offer(primary->data_source, "text/plain;charset=utf-8"); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); zwp_primary_selection_source_v1_add_listener(primary->data_source, &primary_selection_source_listener, seat); zwp_primary_selection_device_v1_set_selection(seat->primary_selection_device, primary->data_source, serial); @@ -1403,7 +1501,7 @@ selection_to_primary(struct seat *seat, struct terminal *term, uint32_t serial) void text_from_primary( struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { if (term->wl->primary_selection_device_manager == NULL) { @@ -1430,7 +1528,7 @@ text_from_primary( /* Give write-end of pipe to other client */ zwp_primary_selection_offer_v1_receive( - primary->data_offer, "text/plain;charset=utf-8", write_fd); + primary->data_offer, mime_type_map[primary->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); @@ -1457,24 +1555,157 @@ selection_from_primary(struct seat *seat, struct terminal *term) if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); - text_from_primary(seat, term, &from_clipboard_cb, &from_clipboard_done, term); + struct receive_offer_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct receive_offer_context){ + .term = term, + .primary_offer = primary->data_offer, + }; + + text_from_primary( + seat, term, + (primary->mime_type == DATA_OFFER_MIME_URI_LIST + ? &receive_offer_uri + : &receive_offer_text), + &receive_offer_done, ctx); +} + +static void +select_mime_type_for_offer(const char *_mime_type, + enum data_offer_mime_type *type) +{ + enum data_offer_mime_type mime_type = DATA_OFFER_MIME_UNSET; + + /* Translate offered mime type to our mime type enum */ + for (size_t i = 0; i < ALEN(mime_type_map); i++) { + if (mime_type_map[i] == NULL) + continue; + + if (strcmp(_mime_type, mime_type_map[i]) == 0) { + mime_type = i; + break; + } + } + + LOG_DBG("mime-type: %s -> %s (offered type was %s)", + mime_type_map[*type], mime_type_map[mime_type], _mime_type); + + /* Mime-type transition; if the new mime-type is "better" than + * previously offered types, use the new type */ + + switch (mime_type) { + case DATA_OFFER_MIME_TEXT_PLAIN: + /* text/plain is our least preferred type. Only use if current + * type is unset */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_TEXT_UTF8: + /* text/plain;charset=utf-8 is preferred over text/plain */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + case DATA_OFFER_MIME_TEXT_PLAIN: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_URI_LIST: + /* text/uri-list is always used when offered */ + *type = mime_type; + break; + + case DATA_OFFER_MIME_UNSET: + break; + } +} + +static void +data_offer_reset(struct wl_clipboard *clipboard) +{ + if (clipboard->data_offer != NULL) { + wl_data_offer_destroy(clipboard->data_offer); + clipboard->data_offer = NULL; + } + + clipboard->window = NULL; + clipboard->mime_type = DATA_OFFER_MIME_UNSET; } -#if 0 static void offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) { + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->clipboard.mime_type); } static void source_actions(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions) { +#if defined(_DEBUG) && LOG_ENABLE_DBG + char actions_as_string[1024]; + size_t idx = 0; + + actions_as_string[0] = '\0'; + actions_as_string[sizeof(actions_as_string) - 1] = '\0'; + + for (size_t i = 0; i < 31; i++) { + if (((source_actions >> i) & 1) == 0) + continue; + + enum wl_data_device_manager_dnd_action action = 1 << i; + + const char *s = NULL; + + switch (action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = NULL; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + if (s == NULL) + continue; + + strncat(actions_as_string, s, sizeof(actions_as_string) - idx - 1); + idx += strlen(s); + strncat(actions_as_string, ", ", sizeof(actions_as_string) - idx - 1); + idx += 2; + } + + /* Strip trailing ", " */ + if (strlen(actions_as_string) > 2) + actions_as_string[strlen(actions_as_string) - 2] = '\0'; + + LOG_DBG("DnD actions: %s (0x%08x)", actions_as_string, source_actions); +#endif } static void offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) { +#if defined(_DEBUG) && LOG_ENABLE_DBG + const char *s = NULL; + + switch (dnd_action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = ""; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + LOG_DBG("DnD offer action: %s (0x%08x)", s, dnd_action); +#endif } static const struct wl_data_offer_listener data_offer_listener = { @@ -1482,24 +1713,56 @@ static const struct wl_data_offer_listener data_offer_listener = { .source_actions = &source_actions, .action = &offer_action, }; -#endif static void data_offer(void *data, struct wl_data_device *wl_data_device, - struct wl_data_offer *id) + struct wl_data_offer *offer) { + struct seat *seat = data; + data_offer_reset(&seat->clipboard); + seat->clipboard.data_offer = offer; + wl_data_offer_add_listener(offer, &data_offer_listener, seat); } static void enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, - struct wl_data_offer *id) + struct wl_data_offer *offer) { + struct seat *seat = data; + struct wayland *wayl = seat->wayl; + + assert(offer == seat->clipboard.data_offer); + + /* Remember _which_ terminal the current DnD offer is targeting */ + assert(seat->clipboard.window == NULL); + tll_foreach(wayl->terms, it) { + if (term_surface_kind(it->item, surface) == TERM_SURF_GRID && + !it->item->is_sending_paste_data) + { + wl_data_offer_accept( + offer, serial, mime_type_map[seat->clipboard.mime_type]); + wl_data_offer_set_actions( + offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); + + seat->clipboard.window = it->item->window; + return; + } + } + + /* Either terminal is already busy sending paste data, or mouse + * pointer isn’t over the grid */ + seat->clipboard.window = NULL; + wl_data_offer_set_actions(offer, 0, 0); } static void leave(void *data, struct wl_data_device *wl_data_device) { + struct seat *seat = data; + seat->clipboard.window = NULL; } static void @@ -1508,28 +1771,74 @@ motion(void *data, struct wl_data_device *wl_data_device, uint32_t time, { } +static void +receive_dnd_done(void *user) +{ + struct receive_offer_context *ctx = user; + + wl_data_offer_finish(ctx->data_offer); + receive_offer_done(user); +} + static void drop(void *data, struct wl_data_device *wl_data_device) { + struct seat *seat = data; + + assert(seat->clipboard.window != NULL); + struct terminal *term = seat->clipboard.window->term; + + struct wl_clipboard *clipboard = &seat->clipboard; + + struct receive_offer_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct receive_offer_context){ + .term = term, + .data_offer = clipboard->data_offer, + }; + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + free(ctx); + return; + } + + int read_fd = fds[0]; + int write_fd = fds[1]; + + LOG_DBG("DnD drop: mime-type=%s", mime_type_map[clipboard->mime_type]); + + /* Give write-end of pipe to other client */ + wl_data_offer_receive( + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + term->is_sending_paste_data = true; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + begin_receive_clipboard( + term, read_fd, + (clipboard->mime_type == DATA_OFFER_MIME_URI_LIST + ? &receive_offer_uri + : &receive_offer_text), + &receive_dnd_done, ctx); } static void selection(void *data, struct wl_data_device *wl_data_device, - struct wl_data_offer *id) + struct wl_data_offer *offer) { /* Selection offer from other client */ - struct seat *seat = data; - struct wl_clipboard *clipboard = &seat->clipboard; - - if (clipboard->data_offer != NULL) - wl_data_offer_destroy(clipboard->data_offer); - - clipboard->data_offer = id; -#if 0 - if (id != NULL) - wl_data_offer_add_listener(id, &data_offer_listener, term); -#endif + if (offer == NULL) + data_offer_reset(&seat->clipboard); + else + assert(offer == seat->clipboard.data_offer); } const struct wl_data_device_listener data_device_listener = { @@ -1541,46 +1850,55 @@ const struct wl_data_device_listener data_device_listener = { .selection = &selection, }; -#if 0 static void primary_offer(void *data, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer, const char *mime_type) { + LOG_DBG("primary offer: %s", mime_type); + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->primary.mime_type); } static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = { .offer = &primary_offer, }; -#endif + +static void +primary_offer_reset(struct wl_primary *primary) +{ + if (primary->data_offer != NULL) { + zwp_primary_selection_offer_v1_destroy(primary->data_offer); + primary->data_offer = NULL; + } + + primary->mime_type = DATA_OFFER_MIME_UNSET; +} static void primary_data_offer(void *data, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, struct zwp_primary_selection_offer_v1 *offer) { + struct seat *seat = data; + primary_offer_reset(&seat->primary); + seat->primary.data_offer = offer; + zwp_primary_selection_offer_v1_add_listener( + offer, &primary_selection_offer_listener, seat); } static void primary_selection(void *data, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, - struct zwp_primary_selection_offer_v1 *id) + struct zwp_primary_selection_offer_v1 *offer) { /* Selection offer from other client, for primary */ struct seat *seat = data; - struct wl_primary *primary = &seat->primary; - - if (primary->data_offer != NULL) - zwp_primary_selection_offer_v1_destroy(primary->data_offer); - - primary->data_offer = id; -#if 0 - if (id != NULL) { - zwp_primary_selection_offer_v1_add_listener( - id, &primary_selection_offer_listener, term); - } -#endif + if (offer == NULL) + primary_offer_reset(&seat->primary); + else + assert(seat->primary.data_offer == offer); } const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = { diff --git a/selection.h b/selection.h index 888b2f05..1b74efb5 100644 --- a/selection.h +++ b/selection.h @@ -67,12 +67,12 @@ bool text_to_primary( */ void text_from_clipboard( struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user); void text_from_primary( struct seat *seat, struct terminal *term, - void (*cb)(const char *data, size_t size, void *user), + void (*cb)(char *data, size_t size, void *user), void (*dont)(void *user), void *user); void selection_start_scroll_timer( diff --git a/uri.c b/uri.c new file mode 100644 index 00000000..31bbc7b6 --- /dev/null +++ b/uri.c @@ -0,0 +1,274 @@ +#include "uri.h" + +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "uri" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "xmalloc.h" + +static uint8_t +nibble2hex(char c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return c - '0'; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + return c - 'a' + 10; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + return c - 'A' + 10; + } + + assert(false); + return 0; +} + +bool +uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment) +{ + LOG_DBG("parse URI: \"%.*s\"", (int)len, uri); + + if (scheme != NULL) *scheme = NULL; + if (user != NULL) *user = NULL; + if (password != NULL) *password = NULL; + if (host != NULL) *host = NULL; + if (port != NULL) *port = 0; + if (path != NULL) *path = NULL; + if (query != NULL) *query = NULL; + if (fragment != NULL) *fragment = NULL; + + size_t left = len; + const char *start = uri; + const char *end = NULL; + + if ((end = memchr(start, ':', left)) == NULL) + goto err; + + size_t scheme_len = end - start; + if (scheme_len == 0) + goto err; + + if (scheme != NULL) + *scheme = xstrndup(start, scheme_len); + + LOG_DBG("scheme: \"%.*s\"", (int)scheme_len, start); + + start = end + 1; + left = len - (start - uri); + + /* Authinfo */ + if (left >= 2 && start[0] == '/' && start[1] == '/') { + start += 2; + left -= 2; + + /* [user[:password]@]@host[:port] */ + + /* Find beginning of path segment (required component + * following the authinfo) */ + const char *path_segment = memchr(start, '/', left); + if (path_segment == NULL) + goto err; + + size_t auth_left = path_segment - start; + + /* Do we have a user (and optionally a password)? */ + const char *user_pw_end = memchr(start, '@', auth_left); + if (user_pw_end != NULL) { + size_t user_pw_len = user_pw_end - start; + + /* Do we have a password? */ + const char *user_end = memchr(start, ':', user_pw_end - start); + if (user_end != NULL) { + size_t user_len = user_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + const char *pw = user_end + 1; + size_t pw_len = user_pw_end - pw; + if (pw_len == 0) + goto err; + + if (password != NULL) + *password = xstrndup(pw, pw_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + LOG_DBG("password: \"%.*s\"", (int)pw_len, pw); + } else { + size_t user_len = user_pw_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + } + + start = user_pw_end + 1; + left = len - (start - uri); + auth_left -= user_pw_len + 1; + } + + const char *host_end = memchr(start, ':', auth_left); + if (host_end != NULL) { + size_t host_len = host_end - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + const char *port_str = host_end + 1; + size_t port_len = path_segment - port_str; + if (port_len == 0) + goto err; + + uint16_t _port = 0; + for (size_t i = 0; i < port_len; i++) { + if (!(port_str[i] >= '0' && port_str[i] <= '9')) + goto err; + + _port *= 10; + _port += port_str[i] - '0'; + } + + if (port != NULL) + *port = _port; + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + LOG_DBG("port: \"%.*s\" (%hu)", (int)port_len, port_str, _port); + } else { + size_t host_len = path_segment - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + } + + start = path_segment; + left = len - (start - uri); + } + + /* Do we have a query? */ + const char *query_start = memchr(start, '?', left); + const char *fragment_start = memchr(start, '#', left); + + size_t path_len = + query_start != NULL ? query_start - start : + fragment_start != NULL ? fragment_start - start : + left; + + if (path_len == 0) + goto err; + + /* Path - decode %xx encoded characters */ + if (path != NULL) { + const char *encoded = start; + char *decoded = xmalloc(path_len + 1); + char *p = decoded; + + size_t encoded_len = path_len; + size_t decoded_len = 0; + + while (true) { + /* Find next '%' */ + const char *next = memchr(encoded, '%', encoded_len); + + if (next == NULL) { + strncpy(p, encoded, encoded_len); + decoded_len += encoded_len; + p += encoded_len; + break; + } + + /* Copy everything leading up to the '%' */ + size_t prefix_len = next - encoded; + memcpy(p, encoded, prefix_len); + + p += prefix_len; + encoded_len -= prefix_len; + decoded_len += prefix_len; + + if (isxdigit(next[1]) && isxdigit(next[2])) { + *p++ = nibble2hex(next[1]) << 4 | nibble2hex(next[2]); + decoded_len++; + encoded_len -= 3; + encoded = next + 3; + } else { + *p++ = *next; + decoded_len++; + encoded_len -= 1; + encoded = next + 1; + } + } + + *p = '\0'; + *path = decoded; + + LOG_DBG("path: encoded=\"%.*s\", decoded=\"%s\"", (int)path_len, start, decoded); + } else + LOG_DBG("path: encoded=\"%.*s\", decoded=", (int)path_len, start); + + start = query_start != NULL ? query_start + 1 : fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + + if (query_start != NULL) { + size_t query_len = fragment_start != NULL + ? fragment_start - start : left; + + if (query_len == 0) + goto err; + + if (query != NULL) + *query = xstrndup(start, query_len); + + LOG_DBG("query: \"%.*s\"", (int)query_len, start); + + start = fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + } + + if (fragment_start != NULL) { + if (left == 0) + goto err; + + if (fragment != NULL) + *fragment = xstrndup(start, left); + + LOG_DBG("fragment: \"%.*s\"", (int)left, start); + } + + return true; + +err: + if (scheme != NULL) free(*scheme); + if (user != NULL) free(*user); + if (password != NULL) free(*password); + if (host != NULL) free(*host); + if (path != NULL) free(*path); + if (query != NULL) free(*query); + if (fragment != NULL) free(*fragment); + return false; +} + +bool +hostname_is_localhost(const char *hostname) +{ + char this_host[HOST_NAME_MAX]; + if (gethostname(this_host, sizeof(this_host)) < 0) + this_host[0] = '\0'; + + return (strcmp(hostname, "") == 0 || + strcmp(hostname, "localhost") == 0 || + strcmp(hostname, this_host) == 0); +} diff --git a/uri.h b/uri.h new file mode 100644 index 00000000..b63290c5 --- /dev/null +++ b/uri.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +bool uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment); + +bool hostname_is_localhost(const char *hostname); diff --git a/wayland.c b/wayland.c index 2c82896a..e8a57c6a 100644 --- a/wayland.c +++ b/wayland.c @@ -849,7 +849,7 @@ handle_global(void *data, struct wl_registry *registry, } else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { - const uint32_t required = 1; + const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; diff --git a/wayland.h b/wayland.h index 70c2098a..98830d8f 100644 --- a/wayland.h +++ b/wayland.h @@ -101,9 +101,20 @@ struct key_binding_search { enum bind_action_search action; }; +/* Mime-types we support when dealing with data offers (e.g. copy-paste, or DnD) */ +enum data_offer_mime_type { + DATA_OFFER_MIME_UNSET, + DATA_OFFER_MIME_TEXT_PLAIN, + DATA_OFFER_MIME_TEXT_UTF8, + DATA_OFFER_MIME_URI_LIST, +}; + +struct wl_window; struct wl_clipboard { + struct wl_window *window; /* For DnD */ struct wl_data_source *data_source; struct wl_data_offer *data_offer; + enum data_offer_mime_type mime_type; char *text; uint32_t serial; }; @@ -111,6 +122,7 @@ struct wl_clipboard { struct wl_primary { struct zwp_primary_selection_source_v1 *data_source; struct zwp_primary_selection_offer_v1 *data_offer; + enum data_offer_mime_type mime_type; char *text; uint32_t serial; };