diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-20 16:25:39 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-20 16:25:39 +0300 |
| commit | dc430ce8ec427529c75e3527a0ff01fcf3858dd2 (patch) | |
| tree | 0c5a79320b182e145f24420b67da9f01930a1101 /tests | |
| parent | f6517a56a6e4d92468dfdda1656e80d57f302816 (diff) | |
clipboard: offer text/uri-list (CRLF) + text/plain via union provider (iu0)
clipboard_copy_uris wrapped its URI text as a generic G_TYPE_STRING
provider, so targets could not request text/uri-list and file-manager
paste failed. Extracted clipboard_build_uri_provider builds a union of
two byte providers: text/uri-list (RFC 2483 CRLF-terminated URI lines,
trailing CRLF) and text/plain (newline-joined local paths, URI fallback
for non-local files). clipboard_copy_uris sets the union on the
GdkClipboard with correct refcounting (new_union steals the sub-provider
refs; clipboard takes its own ref).
Empty list returns NULL and leaves the clipboard untouched.
New integration test tests/test_clipboard.c: serializes the provider's
content per MIME type via gdk_content_provider_write_mime_type_async
into a GMemoryOutputStream and memcmps the bytes. Covers formats
(both MIME advertised, bogus type absent), single + multiple files
(exact CRLF/newline-joined bytes), empty list (no crash), and a
negative (unknown MIME rejected with GError).
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/meson.build | 9 | ||||
| -rw-r--r-- | tests/test_clipboard.c | 278 |
2 files changed, 287 insertions, 0 deletions
diff --git a/tests/meson.build b/tests/meson.build index b60475c..6a93277 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -254,4 +254,13 @@ test_shortcut = executable( test('shortcut', test_shortcut, suite : 'integration', env : fixtures_env) +test_clipboard = executable( + 'test_clipboard', ['test_clipboard.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : ggaze_deps, + link_with : ggaze_lib, install : false, +) +test('clipboard', test_clipboard, + suite : 'integration', env : fixtures_env) + subdir('integration') diff --git a/tests/test_clipboard.c b/tests/test_clipboard.c new file mode 100644 index 0000000..48e5834 --- /dev/null +++ b/tests/test_clipboard.c @@ -0,0 +1,278 @@ +/*:* + * ggaze — clipboard URI-list integration test + * + * Verifies that clipboard_build_uri_provider (the helper clipboard_copy_uris + * sets on the GdkClipboard) advertises BOTH text/uri-list and text/plain and + * serializes standards-compliant bytes for each: + * - text/uri-list : RFC 2483 CRLF-terminated URI lines (trailing CRLF). + * - text/plain : newline-joined local PATH list (trailing newline), with + * a URI fallback for non-local files. + * + * It also exercises a negative target (application/x-bogus) to prove the + * provider does not accidentally offer arbitrary MIME types, and an empty + * file list (must not crash and must yield a valid, empty-bytes provider). + * + * The fake-target technique: rather than driving a real clipboard daemon, we + * serialize the provider's content for a chosen MIME type directly through + * gdk_content_provider_write_mime_type_async into a GMemoryOutputStream, then + * compare the captured bytes against the expected payload. Needs a display (the + * GdkContentProvider/GdkContentFormats APIs require Gdk to be initialized; + * integration suite; CI uses xvfb-run). + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "clipboard.h" + +#include <gdk/gdk.h> +#include <gio/gio.h> +#include <glib.h> +#include <gtk/gtk.h> + +/* --- helpers ------------------------------------------------------------ */ + +static void +drain_main(guint u_ms) { + for (guint u = 0; u < u_ms; u++) { + g_main_context_iteration(g_main_context_default(), FALSE); + g_usleep(1000); + } +} + +/* Async-write completion context. */ +typedef struct { + GMainLoop *p_loop; + GError *p_err; + gboolean b_ok; +} WriteCtx; + +static void +_write_done_cb(GObject *p_src, GAsyncResult *p_res, gpointer p_data) { + WriteCtx *p_ctx = (WriteCtx *)p_data; + p_ctx->b_ok = gdk_content_provider_write_mime_type_finish( + GDK_CONTENT_PROVIDER(p_src), p_res, &p_ctx->p_err); + g_main_loop_quit(p_ctx->p_loop); +} + +/* Serialize p_prov's content for c_mime into freshly-allocated bytes. + * Returns NULL (and sets *p_err) if the provider cannot supply c_mime. + * Caller frees with g_free. The provider is ref'd here so the caller need + * not keep it alive across the call. */ +static guchar * +serialize_mime(GdkContentProvider *p_prov, const char *c_mime, gsize *p_len, + GError **p_err) { + GMemoryOutputStream *p_mem = + G_MEMORY_OUTPUT_STREAM(g_memory_output_stream_new_resizable()); + GMainLoop *p_loop = g_main_loop_new(NULL, FALSE); + WriteCtx st_ctx = {.p_loop = p_loop, .p_err = NULL, .b_ok = FALSE}; + + gdk_content_provider_write_mime_type_async( + p_prov, c_mime, G_OUTPUT_STREAM(p_mem), G_PRIORITY_DEFAULT, NULL, + _write_done_cb, &st_ctx); + g_main_loop_run(p_loop); + + /* The content write fills the buffer but does not close the stream; + * g_memory_output_stream_steal_data requires a closed stream. */ + g_output_stream_close(G_OUTPUT_STREAM(p_mem), NULL, NULL); + + guchar *c_out = NULL; + if (st_ctx.b_ok) { + gsize u_size = 0; + c_out = g_memory_output_stream_steal_data(p_mem); + /* steal_data returns the buffer but not its length; query it. */ + u_size = g_memory_output_stream_get_data_size(p_mem); + if (p_len != NULL) { + *p_len = u_size; + } + } else { + if (p_err != NULL) { + *p_err = st_ctx.p_err; + } else { + g_clear_error(&st_ctx.p_err); + } + } + g_object_unref(p_mem); + g_main_loop_unref(p_loop); + return (c_out); +} + +/* Build a GList of GFile* from NULL-terminated path arguments. */ +static GList * +build_file_list(const char *c_first, ...) { + GList *p_list = NULL; + if (c_first != NULL) { + p_list = g_list_append(p_list, g_file_new_for_path(c_first)); + va_list ap; + va_start(ap, c_first); + const char *c_path; + while ((c_path = va_arg(ap, const char *)) != NULL) { + p_list = g_list_append(p_list, g_file_new_for_path(c_path)); + } + va_end(ap); + } + return (p_list); +} + +static void +free_file_list(GList *p_list) { + g_list_free_full(p_list, (GDestroyNotify)g_object_unref); +} + +/* --- subtests ----------------------------------------------------------- */ + +/* Both MIME types must be in the provider's offered formats. */ +static void +test_clipboard_formats(void) { + GList *p_files = build_file_list("/tmp/a.jpg", "/tmp/b.jpg", NULL); + GdkContentProvider *p_prov = clipboard_build_uri_provider(p_files); + g_assert_nonnull(p_prov); + + GdkContentFormats *p_fmts = gdk_content_provider_ref_formats(p_prov); + g_assert_nonnull(p_fmts); + g_assert_true( + gdk_content_formats_contain_mime_type(p_fmts, "text/uri-list")); + g_assert_true(gdk_content_formats_contain_mime_type(p_fmts, "text/plain")); + /* Negative: a type we never offered must NOT be present. */ + g_assert_false( + gdk_content_formats_contain_mime_type(p_fmts, "application/x-bogus")); + gdk_content_formats_unref(p_fmts); + + g_object_unref(p_prov); + free_file_list(p_files); + drain_main(50); +} + +/* Single local file: uri-list has one CRLF-terminated line; plain has the + * path followed by a newline. */ +static void +test_clipboard_single_file(void) { + GList *p_files = build_file_list("/home/u/pic.jpg", NULL); + GdkContentProvider *p_prov = clipboard_build_uri_provider(p_files); + g_assert_nonnull(p_prov); + + /* text/uri-list: file:// URI + trailing CRLF. */ + { + gsize u_len = 0; + GError *p_err = NULL; + guchar *c_buf = serialize_mime(p_prov, "text/uri-list", &u_len, &p_err); + g_assert_no_error(p_err); + g_assert_nonnull(c_buf); + char *c_expect = g_strdup_printf("file:///home/u/pic.jpg\r\n"); + g_assert_cmpuint(u_len, ==, strlen(c_expect)); + g_assert_cmpint(memcmp(c_buf, c_expect, u_len), ==, 0); + g_free(c_expect); + g_free(c_buf); + } + /* text/plain: local path + trailing newline. */ + { + gsize u_len = 0; + GError *p_err = NULL; + guchar *c_buf = serialize_mime(p_prov, "text/plain", &u_len, &p_err); + g_assert_no_error(p_err); + g_assert_nonnull(c_buf); + char *c_expect = g_strdup_printf("/home/u/pic.jpg\n"); + g_assert_cmpuint(u_len, ==, strlen(c_expect)); + g_assert_cmpint(memcmp(c_buf, c_expect, u_len), ==, 0); + g_free(c_expect); + g_free(c_buf); + } + + g_object_unref(p_prov); + free_file_list(p_files); + drain_main(50); +} + +/* Multiple local files: each line terminated correctly, order preserved. */ +static void +test_clipboard_multiple_files(void) { + GList *p_files = build_file_list("/a/1.jpg", "/a/2.png", "/a/3.webp", NULL); + GdkContentProvider *p_prov = clipboard_build_uri_provider(p_files); + g_assert_nonnull(p_prov); + + { + gsize u_len = 0; + GError *p_err = NULL; + guchar *c_buf = serialize_mime(p_prov, "text/uri-list", &u_len, &p_err); + g_assert_no_error(p_err); + g_assert_nonnull(c_buf); + const char *c_expect = + "file:///a/1.jpg\r\nfile:///a/2.png\r\nfile:///a/3.webp\r\n"; + g_assert_cmpuint(u_len, ==, strlen(c_expect)); + g_assert_cmpint(memcmp(c_buf, c_expect, u_len), ==, 0); + g_free(c_buf); + } + { + gsize u_len = 0; + GError *p_err = NULL; + guchar *c_buf = serialize_mime(p_prov, "text/plain", &u_len, &p_err); + g_assert_no_error(p_err); + g_assert_nonnull(c_buf); + const char *c_expect = "/a/1.jpg\n/a/2.png\n/a/3.webp\n"; + g_assert_cmpuint(u_len, ==, strlen(c_expect)); + g_assert_cmpint(memcmp(c_buf, c_expect, u_len), ==, 0); + g_free(c_buf); + } + + g_object_unref(p_prov); + free_file_list(p_files); + drain_main(50); +} + +/* Empty list: must not crash. clipboard_build_uri_provider returns NULL + * (leaving the clipboard untouched rather than replacing it with an empty + * payload), and clipboard_copy_uris must handle that without crashing. */ +static void +test_clipboard_empty_list(void) { + GdkContentProvider *p_prov = clipboard_build_uri_provider(NULL); + g_assert_null(p_prov); + + /* Driving clipboard_copy_uris with an empty list must not crash; it + * should be a no-op against the default display's clipboard. */ + GdkDisplay *p_disp = gdk_display_get_default(); + if (p_disp != NULL) { + GdkClipboard *p_clip = gdk_display_get_clipboard(p_disp); + clipboard_copy_uris(p_clip, NULL); + } + + drain_main(50); +} + +/* Negative: requesting a MIME type the provider does NOT offer must fail the + * write (write_finish returns FALSE with a GError), proving the union does + * not accidentally advertise arbitrary content. */ +static void +test_clipboard_rejects_unknown_mime(void) { + GList *p_files = build_file_list("/tmp/x.jpg", NULL); + GdkContentProvider *p_prov = clipboard_build_uri_provider(p_files); + g_assert_nonnull(p_prov); + + gsize u_len = 0; + GError *p_err = NULL; + guchar *c_buf = + serialize_mime(p_prov, "application/x-bogus", &u_len, &p_err); + g_assert_null(c_buf); + g_assert_nonnull(p_err); + g_clear_error(&p_err); + + g_object_unref(p_prov); + free_file_list(p_files); + drain_main(50); +} + +int +main(int i_argc, char **c_argv) { + g_test_init(&i_argc, &c_argv, NULL); + g_log_set_always_fatal(G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); + if (!gtk_init_check()) { + g_test_skip("no display available (run under xvfb)"); + return (g_test_run()); + } + g_test_add_func("/clipboard/formats", test_clipboard_formats); + g_test_add_func("/clipboard/single_file", test_clipboard_single_file); + g_test_add_func("/clipboard/multiple_files", test_clipboard_multiple_files); + g_test_add_func("/clipboard/empty_list", test_clipboard_empty_list); + g_test_add_func("/clipboard/rejects_unknown_mime", + test_clipboard_rejects_unknown_mime); + return (g_test_run()); +}
\ No newline at end of file |
