summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/guide.md18
-rw-r--r--include/yasl/yasl.h17
-rw-r--r--src/common/meson.build2
-rw-r--r--src/common/uri.c79
-rw-r--r--tests/common/test_contracts.c50
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();