diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-13 17:13:17 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-13 17:13:17 +0300 |
| commit | 043781575d90d84eb336ed32749e0ab8588a684c (patch) | |
| tree | 5dc47f9e4b94343bbcf5d3dec3d7fb43893bc253 | |
| parent | b7944e4bbfa64a6bb1a1d9e1f172827dde53aed2 (diff) | |
Decode local paths from drop payloads
Drops arrive as a CRLF-separated text/uri-list of file URLs, so every
consumer percent-decodes them; wsap_term does it in its own drop code.
yasl_drop_next_path() walks the list and returns one decoded local path
at a time. It accepts file:///, file:/, and file://localhost or this
machine's name, ends the path at '?' or '#', and skips comments, other
schemes, remote hosts, malformed escapes, and escaped NUL bytes. A buffer
that is too small leaves the cursor in place, like the other functions
that fill a buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | docs/guide.md | 18 | ||||
| -rw-r--r-- | include/yasl/yasl.h | 17 | ||||
| -rw-r--r-- | src/common/meson.build | 2 | ||||
| -rw-r--r-- | src/common/uri.c | 79 | ||||
| -rw-r--r-- | tests/common/test_contracts.c | 50 |
5 files changed, 163 insertions, 3 deletions
diff --git a/docs/guide.md b/docs/guide.md index 4949f88..76279c4 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -279,8 +279,22 @@ this is not a desktop-wide macOS select-to-copy convention. External file drag hover uses the surface `drop` callback; actual file URLs arrive as `YASL_EVENT_DROP`. `text.data` is a `text/uri-list` string separated by -CRLF, not a list of native paths. Parse URL encoding, skip blank/comment lines, -and decide which schemes/hosts your application accepts before opening files. +CRLF, not a list of native paths. `yasl_drop_next_path()` walks it and decodes +each local `file:` URL into a path. It skips comments, other schemes, remote +hosts, and malformed lines: + +```c +const char *cursor = event.text.data; +char path[4096]; +size_t len; +while ((len = yasl_drop_next_path(&cursor, path, sizeof(path))) != 0) { + if (len >= sizeof(path)) break; /* Too long; grow the buffer instead. */ + /* Open path. */ +} +``` + +Decoded paths are raw bytes and may contain any character the file system +allows, including newlines. Quote them before pasting into a shell. Use `yasl_surface_ime_preedit()` to draw the uncommitted composition. The borrowed pointer expires on the next dispatch or surface destruction; copy to keep it. diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index 5946816..f798304 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -777,6 +777,23 @@ YASL_API bool yasl_surface_present(struct yasl_surface *s, const struct yasl_fra */ YASL_API bool yasl_surface_poll_event(struct yasl_surface *s, struct yasl_event *ev); /** + * Decode the next local file path from a YASL_EVENT_DROP payload. + * @param cursor Required in/out position in the NUL-terminated list. Start it + * at event.text.data; a NULL *cursor returns zero. + * @param out Output for the NUL-terminated path, or NULL when cap is zero. + * @param cap Size of out in bytes. + * @return Path length in bytes, excluding the NUL, or zero when no paths remain. + * When the result is less than cap, out holds the path and *cursor moves past + * it. Otherwise out and *cursor are unchanged; call again with a buffer of at + * least the result plus one. + * Accepts file:///path, file:/path, and file://host/path when host is + * localhost or this machine's name. Percent escapes decode to raw bytes, and + * a '?' or '#' ends the path. Skips blank lines, comments, other schemes, + * other hosts, and lines with a malformed escape or an escaped NUL. May be + * called from any thread. + */ +YASL_API size_t yasl_drop_next_path(const char **cursor, char *out, size_t cap); +/** * Copy the last frame presented from the surface's CPU target. * @param s Surface. * @param out Output for size->x * size->y premultiplied ARGB8888 pixels with diff --git a/src/common/meson.build b/src/common/meson.build index 26565ee..9a30f52 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -1 +1 @@ -sources += files('app.c', 'backend.c', 'desktop.c', 'input.c', 'native.c', 'timing.c', 'utf8.c') +sources += files('app.c', 'backend.c', 'desktop.c', 'input.c', 'native.c', 'timing.c', 'uri.c', 'utf8.c') diff --git a/src/common/uri.c b/src/common/uri.c new file mode 100644 index 0000000..d836943 --- /dev/null +++ b/src/common/uri.c @@ -0,0 +1,79 @@ +#define _POSIX_C_SOURCE 200809L +#include "yasl/yasl.h" + +#include <string.h> +#include <strings.h> +#include <unistd.h> + +/* decode_path() result for a line that is not a local file URI. */ +#define NOT_LOCAL SIZE_MAX + +static int hex_digit(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static bool local_host(const char *host, size_t len) +{ + if (len == 9 && strncasecmp(host, "localhost", 9) == 0) return true; + char name[256]; + if (gethostname(name, sizeof(name)) != 0) return false; + name[sizeof(name) - 1] = '\0'; + return strlen(name) == len && strncasecmp(host, name, len) == 0; +} + +/* Decode the path of one uri-list line. Writes to out only when out is not + * NULL; the caller checks the length first. */ +static size_t decode_path(const char *line, size_t len, char *out) +{ + const char *p = line, *end = line + len; + if (len < 5 || strncasecmp(p, "file:", 5) != 0) return NOT_LOCAL; + p += 5; + if (end - p >= 2 && p[0] == '/' && p[1] == '/') { + const char *host = p + 2; + p = host; + while (p < end && *p != '/') p++; + if (p > host && !local_host(host, (size_t)(p - host))) return NOT_LOCAL; + } + if (p == end || *p != '/') return NOT_LOCAL; + size_t n = 0; + while (p < end && *p != '?' && *p != '#') { + char c = *p++; + if (c == '%') { + if (end - p < 2) return NOT_LOCAL; + int hi = hex_digit(p[0]), lo = hex_digit(p[1]); + if (hi < 0 || lo < 0 || (hi == 0 && lo == 0)) return NOT_LOCAL; + c = (char)(hi << 4 | lo); + p += 2; + } + if (out) out[n] = c; + n++; + } + return n; +} + +size_t yasl_drop_next_path(const char **cursor, char *out, size_t cap) +{ + if (!cursor || !*cursor || (!out && cap)) return 0; + const char *p = *cursor; + while (*p) { + const char *line = p; + while (*p && *p != '\r' && *p != '\n') p++; + size_t len = (size_t)(p - line); + while (*p == '\r' || *p == '\n') p++; + while (len && (line[len - 1] == ' ' || line[len - 1] == '\t')) len--; + if (!len || line[0] == '#') continue; + size_t n = decode_path(line, len, NULL); + if (n == NOT_LOCAL) continue; + if (n >= cap) return n; + decode_path(line, len, out); + out[n] = '\0'; + *cursor = p; + return n; + } + *cursor = p; + return 0; +} diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c index 654eb1e..d1964ff 100644 --- a/tests/common/test_contracts.c +++ b/tests/common/test_contracts.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 200809L #include "common/backend.h" #include "common/input.h" #include "common/timing.h" @@ -5,8 +6,10 @@ #include <assert.h> #include <errno.h> #include <math.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> +#include <unistd.h> static void test_timing_clock(void) { @@ -182,6 +185,52 @@ static void test_held_buttons(void) input_queue_clear(&q); } +static void test_drop_paths(void) +{ + char host[256] = ""; + assert(gethostname(host, sizeof(host)) == 0); + char list[1024]; + snprintf(list, sizeof(list), + "# comment\r\n" + "\r\n" + "file:///home/user/a%%20b.txt\r\n" + "https://example.com/x\r\n" + "file://elsewhere.invalid/remote\r\n" + "file://localhost/tmp/%%C3%%A9t%%C3%%A9 \r\n" + "file://%s/srv/x\r\n" + "FILE:/single/slash\n" + "file:///bad%%2\r\n" + "file:///nul%%00byte\r\n" + "file:///q?query#frag\r\n" + "file:relative\r\n" + "file:///last", + host); + static const char *const want[] = { + "/home/user/a b.txt", "/tmp/\xc3\xa9t\xc3\xa9", "/srv/x", "/single/slash", + "/q", "/last", + }; + const char *cursor = list; + char path[64]; + for (size_t i = 0; i < sizeof(want) / sizeof(want[0]); i++) { + size_t len = strlen(want[i]); + /* A short buffer reports the length and leaves both outputs alone. */ + const char *before = cursor; + memcpy(path, "keep", 5); + assert(yasl_drop_next_path(&cursor, path, len) == len); + assert(cursor == before && strcmp(path, "keep") == 0); + assert(yasl_drop_next_path(&cursor, NULL, 0) == len && cursor == before); + assert(yasl_drop_next_path(&cursor, path, sizeof(path)) == len); + assert(strcmp(path, want[i]) == 0); + } + assert(yasl_drop_next_path(&cursor, path, sizeof(path)) == 0 && *cursor == '\0'); + assert(yasl_drop_next_path(&cursor, path, sizeof(path)) == 0); + assert(yasl_drop_next_path(NULL, path, sizeof(path)) == 0); + const char *none = NULL; + assert(yasl_drop_next_path(&none, path, sizeof(path)) == 0); + cursor = list; + assert(yasl_drop_next_path(&cursor, NULL, 8) == 0 && cursor == list); +} + static void test_native(void) { const struct yasl_backend_ops ops = {.display_caps = native_caps, @@ -372,6 +421,7 @@ int main(void) test_surface_state(); test_queue(); test_held_buttons(); + test_drop_paths(); test_validation(); test_native(); test_scale(); |
