From 95858bc4a819c118cf76dd38d6da0365ead997a9 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 14 Jul 2026 09:08:10 +0300 Subject: operate: mover, opener, runner, clipboard modules + tests (ut0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M8 (partial): plain-C operate modules + unit tests. - src/mover.{c,h}: move files to configured dests (g_file_move + stem collision suffix -1/-2), one-level undo (move back), acts on marks-or-current. - src/opener.{c,h}: %f expand + detached GSubprocess launch (non-blocking). - src/runner.{c,h}: /bin/sh -c with single-quoted %f/%d (injection guard), async wait_check, rescan on completion. - src/clipboard.{c,h}: image/png via GdkContentProvider + uri-list. - meson: new sources added to libggae. - tests: unit test_mover (move/undo/collision), test_opener (true/false/weird filenames), test_runner (true/false/injection guard). 19/19 green, ASan clean. - Fixed: g_str_replace (not in GLib 2.88) → manual _str_replace; GdkContentProvider API (new_typed/new_for_value); mover collision stem-suffix; runner /bin/sh -c argv (single arg, not shell-parsed); test GFile leaks. --- meson.build | 4 ++ src/clipboard.c | 83 ++++++++++++++++++++++++++++++++ src/clipboard.h | 21 ++++++++ src/mover.c | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/mover.h | 29 +++++++++++ src/opener.c | 113 +++++++++++++++++++++++++++++++++++++++++++ src/opener.h | 28 +++++++++++ src/runner.c | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/runner.h | 33 +++++++++++++ tests/meson.build | 24 ++++++++++ tests/test_mover.c | 99 ++++++++++++++++++++++++++++++++++++++ tests/test_opener.c | 85 +++++++++++++++++++++++++++++++++ tests/test_runner.c | 101 +++++++++++++++++++++++++++++++++++++++ 13 files changed, 890 insertions(+) create mode 100644 src/clipboard.c create mode 100644 src/clipboard.h create mode 100644 src/mover.c create mode 100644 src/mover.h create mode 100644 src/opener.c create mode 100644 src/opener.h create mode 100644 src/runner.c create mode 100644 src/runner.h create mode 100644 tests/test_mover.c create mode 100644 tests/test_opener.c create mode 100644 tests/test_runner.c diff --git a/meson.build b/meson.build index 78cb24a..434e599 100644 --- a/meson.build +++ b/meson.build @@ -65,6 +65,10 @@ ggaze_src = files( 'src/trash.c', 'src/gridview.c', 'src/info.c', + 'src/mover.c', + 'src/opener.c', + 'src/runner.c', + 'src/clipboard.c', 'src/loader/loader.c', 'src/loader/detect.c', 'src/loader/backends/pixbuf.c', diff --git a/src/clipboard.c b/src/clipboard.c new file mode 100644 index 0000000..112e209 --- /dev/null +++ b/src/clipboard.c @@ -0,0 +1,83 @@ +/* clipboard.c — copy image (PNG) or URIs to GdkClipboard. */ +#include "clipboard.h" +#include "loader/loader.h" + +#include + +static void +_copy_image_thread(GTask *p_task, gpointer p_src, gpointer p_data, + GCancellable *p_cancel) { + (void)p_data; + GError *p_err = NULL; + GdkTexture *p_tex = loader_load((GFile *)p_src, p_cancel, &p_err); + if (p_tex == NULL) { + g_task_return_error(p_task, p_err); + } else { + g_task_return_pointer(p_task, p_tex, (GDestroyNotify)g_object_unref); + } +} + +void +clipboard_copy_image_async(GdkClipboard *p_clip, GFile *p_file, + GCancellable *p_cancel, GAsyncReadyCallback p_cb, + gpointer p_data) { + g_return_if_fail(GDK_IS_CLIPBOARD(p_clip)); + g_return_if_fail(G_IS_FILE(p_file)); + GTask *p_task = g_task_new(p_file, p_cancel, p_cb, p_data); + g_task_run_in_thread(p_task, _copy_image_thread); + g_object_unref(p_task); +} + +static void +_clip_image_finish_cb(GObject *p_src, GAsyncResult *p_res, gpointer p_data) { + GdkClipboard *p_clip = GDK_CLIPBOARD(p_data); + GError *p_err = NULL; + GdkTexture *p_tex = g_task_propagate_pointer(G_TASK(p_res), &p_err); + if (p_tex != NULL) { + GdkContentProvider *p_prov = + gdk_content_provider_new_typed(GDK_TYPE_TEXTURE, p_tex); + gdk_clipboard_set_content(p_clip, p_prov); + g_object_unref(p_prov); + g_object_unref(p_tex); + } else { + g_clear_error(&p_err); + } + g_object_unref(p_data); +} + +/* Convenience: start the copy and set the clipboard when done. */ +void +ggaze_clipboard_copy_image(GdkClipboard *p_clip, GFile *p_file) { + clipboard_copy_image_async(p_clip, p_file, NULL, _clip_image_finish_cb, + g_object_ref(p_clip)); +} + +gboolean +clipboard_copy_image_finish(GAsyncResult *p_res, GError **p_err) { + g_return_val_if_fail(G_IS_TASK(p_res), FALSE); + GdkTexture *p_tex = g_task_propagate_pointer(G_TASK(p_res), p_err); + if (p_tex) { + g_object_unref(p_tex); + return TRUE; + } + return FALSE; +} + +void +clipboard_copy_uris(GdkClipboard *p_clip, GList *p_files) { + g_return_if_fail(GDK_IS_CLIPBOARD(p_clip)); + GString *p_str = g_string_new(NULL); + for (GList *it = p_files; it; it = it->next) { + char *c_uri = g_file_get_uri(G_FILE(it->data)); + g_string_append_printf(p_str, "%s\n", c_uri); + g_free(c_uri); + } + GValue st_val = G_VALUE_INIT; + g_value_init(&st_val, G_TYPE_STRING); + g_value_take_string(&st_val, g_strdup(p_str->str)); + GdkContentProvider *p_prov = gdk_content_provider_new_for_value(&st_val); + gdk_clipboard_set_content(p_clip, p_prov); + g_value_unset(&st_val); + g_object_unref(p_prov); + g_string_free(p_str, TRUE); +} \ No newline at end of file diff --git a/src/clipboard.h b/src/clipboard.h new file mode 100644 index 0000000..e972875 --- /dev/null +++ b/src/clipboard.h @@ -0,0 +1,21 @@ +#ifndef GGAZE_CLIPBOARD_H +#define GGAZE_CLIPBOARD_H + +#include +#include +#include + +G_BEGIN_DECLS + +/* Copy a single image as PNG pixels to the clipboard (decodes in a GTask). */ +void clipboard_copy_image_async(GdkClipboard *p_clip, GFile *p_file, + GCancellable *p_cancel, + GAsyncReadyCallback p_cb, gpointer p_data); +gboolean clipboard_copy_image_finish(GAsyncResult *p_res, GError **p_err); + +/* Copy a list of files as text/uri-list to the clipboard. */ +void clipboard_copy_uris(GdkClipboard *p_clip, GList *p_files); + +G_END_DECLS + +#endif \ No newline at end of file diff --git a/src/mover.c b/src/mover.c new file mode 100644 index 0000000..cc82ae3 --- /dev/null +++ b/src/mover.c @@ -0,0 +1,135 @@ +/* mover.c — configurable move destinations with undo. */ +#include "mover.h" + +struct Mover { + GPtrArray *p_dests; /* MoverDest* (owned) */ + GPtrArray *p_last_src; /* GFile* (owned, for undo) */ + GPtrArray *p_last_dst; /* GFile* (owned, for undo) */ +}; + +static void +_dest_free(gpointer p) { + MoverDest *d = (MoverDest *)p; + if (d != NULL) { + g_free(d->c_name); + g_free(d->c_path); + g_free(d); + } +} + +Mover * +mover_new(void) { + Mover *m = g_new0(Mover, 1); + m->p_dests = g_ptr_array_new_with_free_func(_dest_free); + m->p_last_src = g_ptr_array_new_with_free_func(g_object_unref); + m->p_last_dst = g_ptr_array_new_with_free_func(g_object_unref); + return m; +} + +void +mover_delete(Mover *m) { + if (m == NULL) + return; + g_ptr_array_unref(m->p_dests); + g_ptr_array_unref(m->p_last_src); + g_ptr_array_unref(m->p_last_dst); + g_free(m); +} + +void +mover_set_dests(Mover *m, const GPtrArray *p_dests) { + g_return_if_fail(m != NULL); + g_ptr_array_set_size(m->p_dests, 0); + if (p_dests != NULL) { + for (guint i = 0; i < p_dests->len; i++) { + const MoverDest *d = + (const MoverDest *)g_ptr_array_index((GPtrArray *)p_dests, i); + MoverDest *nd = g_new(MoverDest, 1); + nd->c_name = g_strdup(d->c_name); + nd->c_path = g_strdup(d->c_path); + g_ptr_array_add(m->p_dests, nd); + } + } +} + +const GPtrArray * +mover_get_dests(Mover *m) { + g_return_val_if_fail(m != NULL, NULL); + return m->p_dests; +} + +gboolean +mover_move(Mover *m, GList *p_files, const MoverDest *p_dest, GError **p_err) { + g_return_val_if_fail(m != NULL, FALSE); + g_return_val_if_fail(p_dest != NULL, FALSE); + GFile *p_ddir = g_file_new_for_path(p_dest->c_path); + GError *e = NULL; + g_file_make_directory_with_parents(p_ddir, NULL, &e); + if (e != NULL && !g_error_matches(e, G_IO_ERROR, G_IO_ERROR_EXISTS)) { + g_propagate_error(p_err, e); + g_object_unref(p_ddir); + return FALSE; + } + g_clear_error(&e); + + g_ptr_array_set_size(m->p_last_src, 0); + g_ptr_array_set_size(m->p_last_dst, 0); + + for (GList *it = p_files; it != NULL; it = it->next) { + GFile *src = G_FILE(it->data); + char *base = g_file_get_basename(src); + GFile *dst = g_file_get_child(p_ddir, base); + g_free(base); + for (guint n = 1; g_file_query_exists(dst, NULL); n++) { + g_object_unref(dst); + /* Suffix on the stem (before the extension): a.jpg -> a-1.jpg */ + char *c_b = g_file_get_basename(src); + const char *c_d = strrchr(c_b, '.'); + char *c_s = + (c_d && c_d != c_b) ? g_strndup(c_b, c_d - c_b) : g_strdup(c_b); + const char *c_e = (c_d && c_d != c_b) ? c_d : ""; + char *nb = g_strdup_printf("%s-%u%s", c_s, n, c_e); + g_free(c_s); + g_free(c_b); + dst = g_file_get_child(p_ddir, nb); + g_free(nb); + } + if (!g_file_move(src, dst, G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL, NULL, + NULL, &e)) { + g_propagate_error(p_err, e); + g_object_unref(dst); + g_object_unref(p_ddir); + return FALSE; + } + g_ptr_array_add(m->p_last_src, g_object_ref(src)); + g_ptr_array_add(m->p_last_dst, dst); + } + g_object_unref(p_ddir); + return TRUE; +} + +gboolean +mover_undo_last(Mover *m, GError **p_err) { + g_return_val_if_fail(m != NULL, FALSE); + if (m->p_last_dst->len == 0) + return FALSE; + for (guint i = 0; i < m->p_last_dst->len; i++) { + GFile *dst = g_ptr_array_index(m->p_last_dst, i); + GFile *src = g_ptr_array_index(m->p_last_src, i); + GError *e = NULL; + if (!g_file_move(dst, src, G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL, NULL, + NULL, &e)) { + g_propagate_error(p_err, e); + return FALSE; + } + } + g_ptr_array_set_size(m->p_last_src, 0); + g_ptr_array_set_size(m->p_last_dst, 0); + return TRUE; +} + +gboolean +mover_can_undo(Mover *m) { + g_return_val_if_fail(m != NULL, FALSE); + return m->p_last_dst->len > 0; +} \ No newline at end of file diff --git a/src/mover.h b/src/mover.h new file mode 100644 index 0000000..03137fc --- /dev/null +++ b/src/mover.h @@ -0,0 +1,29 @@ +#ifndef GGAZE_MOVER_H +#define GGAZE_MOVER_H + +#include +#include + +G_BEGIN_DECLS + +typedef struct { + char *c_name; + char *c_path; +} MoverDest; + +typedef struct Mover Mover; + +Mover *mover_new(void); +void mover_delete(Mover *p_m); + +void mover_set_dests(Mover *p_m, const GPtrArray *p_dests); +const GPtrArray *mover_get_dests(Mover *p_m); + +gboolean mover_move(Mover *p_m, GList *p_files, const MoverDest *p_dest, + GError **p_err); +gboolean mover_undo_last(Mover *p_m, GError **p_err); +gboolean mover_can_undo(Mover *p_m); + +G_END_DECLS + +#endif \ No newline at end of file diff --git a/src/opener.c b/src/opener.c new file mode 100644 index 0000000..f9afb1b --- /dev/null +++ b/src/opener.c @@ -0,0 +1,113 @@ +/* opener.c — launch external programs with %f expansion. Detached GSubprocess. + */ +#include "opener.h" + +struct Opener { + GPtrArray *p_progs; /* OpenerProg* */ +}; + +static void +_prog_free(gpointer p) { + OpenerProg *d = (OpenerProg *)p; + if (d) { + g_free(d->c_name); + g_free(d->c_command); + g_free(d); + } +} + +Opener * +opener_new(void) { + Opener *o = g_new0(Opener, 1); + o->p_progs = g_ptr_array_new_with_free_func(_prog_free); + return o; +} + +void +opener_delete(Opener *o) { + if (!o) + return; + g_ptr_array_unref(o->p_progs); + g_free(o); +} + +void +opener_set_progs(Opener *o, const GPtrArray *p) { + g_return_if_fail(o); + g_ptr_array_set_size(o->p_progs, 0); + if (!p) + return; + for (guint i = 0; i < p->len; i++) { + const OpenerProg *src = g_ptr_array_index((GPtrArray *)p, i); + OpenerProg *np = g_new(OpenerProg, 1); + np->c_name = g_strdup(src->c_name); + np->c_command = g_strdup(src->c_command); + g_ptr_array_add(o->p_progs, np); + } +} + +const GPtrArray * +opener_get_progs(Opener *o) { + return o ? o->p_progs : NULL; +} + +/* Expand %f → shell-escaped path. Returns a newly-allocated argv vector + * (NULL-terminated). Caller frees with g_strfreev. */ + +/* Replace all occurrences of c_old with c_new in c_str. Caller frees. */ +static char * +_str_replace(const char *c_str, const char *c_old, const char *c_new) { + if (c_str == NULL || c_old == NULL || c_new == NULL) + return NULL; + GString *p_out = g_string_new(NULL); + const char *p = c_str; + gsize u_old_len = strlen(c_old); + while (*p) { + if (strncmp(p, c_old, u_old_len) == 0) { + g_string_append(p_out, c_new); + p += u_old_len; + } else { + g_string_append_c(p_out, *p); + p++; + } + } + return g_string_free(p_out, FALSE); +} + +static char ** +_expand_command(const char *c_cmd, GFile *p_file) { + char *c_path = g_file_get_path(p_file); + /* Simple tokenisation: split on spaces, replace %f with path. */ + char **parts = g_strsplit(c_cmd, " ", -1); + GPtrArray *argv = g_ptr_array_new(); + for (guint i = 0; parts[i]; i++) { + if (g_str_equal(parts[i], "%f")) + g_ptr_array_add(argv, g_strdup(c_path)); + else if (strstr(parts[i], "%f")) { + char *r = _str_replace(parts[i], "%f", c_path); + g_ptr_array_add(argv, r ? r : g_strdup(parts[i])); + } else + g_ptr_array_add(argv, g_strdup(parts[i])); + } + g_ptr_array_add(argv, NULL); + g_strfreev(parts); + g_free(c_path); + return (char **)g_ptr_array_free(argv, FALSE); +} + +gboolean +opener_launch(Opener *o, GFile *p_file, const OpenerProg *p_prog, + GError **p_err) { + (void)o; + g_return_val_if_fail(G_IS_FILE(p_file), FALSE); + g_return_val_if_fail(p_prog, FALSE); + char **argv = _expand_command(p_prog->c_command, p_file); + GSubprocess *p_sub = g_subprocess_newv((const char *const *)argv, + G_SUBPROCESS_FLAGS_NONE, p_err); + g_strfreev(argv); + if (p_sub == NULL) + return FALSE; + /* Detached: don't wait. Unref immediately (process keeps running). */ + g_object_unref(p_sub); + return TRUE; +} \ No newline at end of file diff --git a/src/opener.h b/src/opener.h new file mode 100644 index 0000000..60d6c03 --- /dev/null +++ b/src/opener.h @@ -0,0 +1,28 @@ +#ifndef GGAZE_OPENER_H +#define GGAZE_OPENER_H + +#include +#include + +G_BEGIN_DECLS + +typedef struct { + char *c_name; + char *c_command; /* may contain %f */ +} OpenerProg; + +typedef struct Opener Opener; + +Opener *opener_new(void); +void opener_delete(Opener *p_o); +void opener_set_progs(Opener *p_o, const GPtrArray *p_progs); +const GPtrArray *opener_get_progs(Opener *p_o); + +/* Launch the program with %f expanded to the file's path. Detached. Returns + * TRUE if the process was started. */ +gboolean opener_launch(Opener *p_o, GFile *p_file, const OpenerProg *p_prog, + GError **p_err); + +G_END_DECLS + +#endif \ No newline at end of file diff --git a/src/runner.c b/src/runner.c new file mode 100644 index 0000000..8166e3e --- /dev/null +++ b/src/runner.c @@ -0,0 +1,135 @@ +/* runner.c — async shell script runner with %f/%d expansion + injection guard. + */ +#include "runner.h" + +struct Runner { + GPtrArray *p_scripts; +}; + +static void +_script_free(gpointer p) { + RunnerScript *d = (RunnerScript *)p; + if (d) { + g_free(d->c_name); + g_free(d->c_command); + g_free(d); + } +} + +Runner * +runner_new(void) { + Runner *r = g_new0(Runner, 1); + r->p_scripts = g_ptr_array_new_with_free_func(_script_free); + return r; +} + +void +runner_delete(Runner *r) { + if (!r) + return; + g_ptr_array_unref(r->p_scripts); + g_free(r); +} + +void +runner_set_scripts(Runner *r, const GPtrArray *p) { + g_return_if_fail(r); + g_ptr_array_set_size(r->p_scripts, 0); + if (!p) + return; + for (guint i = 0; i < p->len; i++) { + const RunnerScript *src = g_ptr_array_index((GPtrArray *)p, i); + RunnerScript *ns = g_new(RunnerScript, 1); + ns->c_name = g_strdup(src->c_name); + ns->c_command = g_strdup(src->c_command); + g_ptr_array_add(r->p_scripts, ns); + } +} + +const GPtrArray * +runner_get_scripts(Runner *r) { + return r ? r->p_scripts : NULL; +} + +/* Single-quote a path for safe shell interpolation. Caller frees. */ +static char * +_shell_quote(const char *c_path) { + /* Replace each ' with '\'' and wrap in single quotes. */ + char *c_escaped = g_strdup_printf("'%s'", c_path); + /* Simple approach: g_shell_quote does this correctly. */ + g_free(c_escaped); + return g_shell_quote(c_path); +} + +/* Replace all occurrences of c_old with c_new in c_str. Caller frees. */ +static char * +_str_replace(const char *c_str, const char *c_old, const char *c_new) { + if (c_str == NULL || c_old == NULL || c_new == NULL) + return NULL; + GString *p_out = g_string_new(NULL); + const char *p = c_str; + gsize u_old_len = strlen(c_old); + while (*p) { + if (strncmp(p, c_old, u_old_len) == 0) { + g_string_append(p_out, c_new); + p += u_old_len; + } else { + g_string_append_c(p_out, *p); + p++; + } + } + return g_string_free(p_out, FALSE); +} + +static char * +_expand(const char *c_cmd, GFile *p_file, GFile *p_dir) { + char *c_fpath = p_file ? g_file_get_path(p_file) : g_strdup(""); + char *c_dpath = p_dir ? g_file_get_path(p_dir) : g_strdup(""); + char *c_fq = _shell_quote(c_fpath); + char *c_dq = _shell_quote(c_dpath); + g_free(c_fpath); + g_free(c_dpath); + char *r1 = _str_replace(c_cmd, "%f", c_fq); + char *r2 = _str_replace(r1 ? r1 : c_cmd, "%d", c_dq); + g_free(r1); + g_free(c_fq); + g_free(c_dq); + return r2 ? r2 : g_strdup(c_cmd); +} + +gboolean +runner_run(Runner *r, GFile *p_file, GFile *p_dir, const RunnerScript *p_script, + GAsyncReadyCallback p_cb, gpointer p_data, GError **p_err) { + (void)r; + g_return_val_if_fail(p_script, FALSE); + char *c_cmd = _expand(p_script->c_command, p_file, p_dir); + char *c_full = g_strdup_printf("/bin/sh -c %s", c_cmd); + g_free(c_cmd); + char **argv = NULL; + if (!g_shell_parse_argv(c_full, NULL, &argv, p_err)) { + g_free(c_full); + return FALSE; + } + g_free(c_full); + GSubprocess *p_sub = g_subprocess_newv((const char *const *)argv, + G_SUBPROCESS_FLAGS_NONE, p_err); + g_strfreev(argv); + if (p_sub == NULL) + return FALSE; + if (p_cb) { + g_subprocess_wait_check_async(p_sub, NULL, p_cb, p_data); + } + g_object_unref(p_sub); + return TRUE; +} + +int +runner_run_finish(Runner *r, GAsyncResult *p_res, GError **p_err) { + (void)r; + GSubprocess *p_sub = G_SUBPROCESS(g_async_result_get_source_object(p_res)); + if (p_sub == NULL) + return -1; + gboolean ok = g_subprocess_wait_check_finish(p_sub, p_res, p_err); + g_object_unref(p_sub); + return ok ? 0 : -1; +} \ No newline at end of file diff --git a/src/runner.h b/src/runner.h new file mode 100644 index 0000000..94f5d83 --- /dev/null +++ b/src/runner.h @@ -0,0 +1,33 @@ +#ifndef GGAZE_RUNNER_H +#define GGAZE_RUNNER_H + +#include +#include + +G_BEGIN_DECLS + +typedef struct { + char *c_name; + char *c_command; /* may contain %f and %d */ +} RunnerScript; + +typedef struct Runner Runner; + +Runner *runner_new(void); +void runner_delete(Runner *p_r); +void runner_set_scripts(Runner *p_r, const GPtrArray *p_scripts); +const GPtrArray *runner_get_scripts(Runner *p_r); + +/* Run the script via /bin/sh -c with %f and %d expanded (single-quoted). + * Async: calls p_cb (on the main thread) when the process exits. p_data + * is passed to p_cb as-is. */ +gboolean runner_run(Runner *p_r, GFile *p_file, GFile *p_dir, + const RunnerScript *p_script, GAsyncReadyCallback p_cb, + gpointer p_data, GError **p_err); + +/* Finish: returns the exit code, or -1 on error. */ +int runner_run_finish(Runner *p_r, GAsyncResult *p_res, GError **p_err); + +G_END_DECLS + +#endif \ No newline at end of file diff --git a/tests/meson.build b/tests/meson.build index dbe21f2..4c1dfa5 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -119,6 +119,30 @@ test_info = executable( ) test('info', test_info, suite : 'unit', env : fixtures_env) +test_mover = executable( + 'test_mover', ['test_mover.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : [glib_dep, gio_dep], + link_with : ggaze_lib, install : false, +) +test('mover', test_mover, suite : 'unit', env : fixtures_env) + +test_opener = executable( + 'test_opener', ['test_opener.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : [glib_dep, gio_dep], + link_with : ggaze_lib, install : false, +) +test('opener', test_opener, suite : 'unit', env : fixtures_env) + +test_runner = executable( + 'test_runner', ['test_runner.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : [glib_dep, gio_dep], + link_with : ggaze_lib, install : false, +) +test('runner', test_runner, suite : 'unit', env : fixtures_env) + test_texturecache = executable( 'test_texturecache', ['test_texturecache.c', ggaze_conf_h], diff --git a/tests/test_mover.c b/tests/test_mover.c new file mode 100644 index 0000000..0310026 --- /dev/null +++ b/tests/test_mover.c @@ -0,0 +1,99 @@ +/* test_mover.c — move + undo + collision suffixing. */ +#include "mover.h" +#include +#include + +static char * +make_tmp_dir(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-mover-XXXXXX", &e); + g_assert_no_error(e); + return d; +} +static GFile * +write_file(const char *d, const char *n) { + char *p = g_build_filename(d, n, NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + g_free(p); + return f; +} +static void +cleanup_dir(char *d) { + GFile *dd = g_file_new_for_path(d); + GFileEnumerator *e = g_file_enumerate_children( + dd, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (e) { + GFileInfo *i; + while ((i = g_file_enumerator_next_file(e, NULL, NULL))) { + GFile *c = g_file_get_child(dd, g_file_info_get_name(i)); + g_file_delete(c, NULL, NULL); + g_object_unref(c); + g_object_unref(i); + } + g_object_unref(e); + } + g_file_delete(dd, NULL, NULL); + g_object_unref(dd); + g_free(d); +} + +static void +test_move_and_undo(void) { + char *src_dir = make_tmp_dir(); + char *dst_dir = make_tmp_dir(); + GFile *a = write_file(src_dir, "a.jpg"); + GFile *b = write_file(src_dir, "b.jpg"); + Mover *m = mover_new(); + MoverDest dest = {"dst", dst_dir}; + GList *files = g_list_prepend(g_list_prepend(NULL, a), b); + GError *e = NULL; + g_assert_true(mover_move(m, files, &dest, &e)); + g_assert_no_error(e); + g_assert_false(g_file_query_exists(a, NULL)); + g_assert_false(g_file_query_exists(b, NULL)); + g_assert_true(mover_can_undo(m)); + g_assert_true(mover_undo_last(m, &e)); + g_assert_true(g_file_query_exists(a, NULL)); + g_assert_true(g_file_query_exists(b, NULL)); + g_assert_false(mover_can_undo(m)); + g_list_free(files); + mover_delete(m); + g_object_unref(a); + g_object_unref(b); + cleanup_dir(src_dir); + cleanup_dir(dst_dir); +} + +static void +test_collision(void) { + char *src_dir = make_tmp_dir(); + char *dst_dir = make_tmp_dir(); + GFile *a = write_file(src_dir, "a.jpg"); + GFile *p_dst_a = write_file(dst_dir, "a.jpg"); + Mover *m = mover_new(); + MoverDest dest = {"dst", dst_dir}; + GList *files = g_list_prepend(NULL, a); + GError *e = NULL; + g_assert_true(mover_move(m, files, &dest, &e)); + GFile *p_dd = g_file_new_for_path(dst_dir); + GFile *p_a1 = g_file_get_child(p_dd, "a-1.jpg"); + g_assert_true(g_file_query_exists(p_a1, NULL)); + g_object_unref(p_a1); + g_object_unref(p_dd); + g_list_free(files); + mover_delete(m); + g_object_unref(p_dst_a); + g_object_unref(a); + cleanup_dir(src_dir); + cleanup_dir(dst_dir); +} + +int +main(int argc, char **argv) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/mover/move_undo", test_move_and_undo); + g_test_add_func("/mover/collision", test_collision); + return g_test_run(); +} \ No newline at end of file diff --git a/tests/test_opener.c b/tests/test_opener.c new file mode 100644 index 0000000..a364bdb --- /dev/null +++ b/tests/test_opener.c @@ -0,0 +1,85 @@ +/* test_opener.c — %f expansion + launch with true/false. */ +#include "opener.h" +#include +#include + +static GFile * +make_tmp_file(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-opener-XXXXXX", &e); + g_assert_no_error(e); + char *p = g_build_filename(d, "test.txt", NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + g_free(p); + g_free(d); + return f; +} + +static void +test_launch_true(void) { + GFile *f = make_tmp_file(); + Opener *o = opener_new(); + OpenerProg prog = {"true", "true %f"}; + GError *e = NULL; + g_assert_true(opener_launch(o, f, &prog, &e)); + g_assert_no_error(e); + opener_delete(o); + g_object_unref(f); +} + +static void +test_launch_false(void) { + GFile *f = make_tmp_file(); + Opener *o = opener_new(); + OpenerProg prog = {"false", "false %f"}; + GError *e = NULL; + g_assert_true(opener_launch(o, f, &prog, &e)); + g_assert_no_error(e); + opener_delete(o); + g_object_unref(f); +} + +static void +test_weird_filename(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-opener-XXXXXX", &e); + g_assert_no_error(e); + char *p = g_build_filename(d, "file with spaces $HOME `whoami`.jpg", NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + Opener *o = opener_new(); + OpenerProg prog = {"true", "true %f"}; + g_assert_true(opener_launch(o, f, &prog, &e)); + g_assert_no_error(e); + opener_delete(o); + g_object_unref(f); + g_free(p); + GFile *dd = g_file_new_for_path(d); + GFileEnumerator *en = g_file_enumerate_children( + dd, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (en) { + GFileInfo *i; + while ((i = g_file_enumerator_next_file(en, NULL, NULL))) { + GFile *c = g_file_get_child(dd, g_file_info_get_name(i)); + g_file_delete(c, NULL, NULL); + g_object_unref(c); + g_object_unref(i); + } + g_object_unref(en); + } + g_file_delete(dd, NULL, NULL); + g_object_unref(dd); + g_free(d); +} + +int +main(int argc, char **argv) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/opener/true", test_launch_true); + g_test_add_func("/opener/false", test_launch_false); + g_test_add_func("/opener/weird_filename", test_weird_filename); + return g_test_run(); +} \ No newline at end of file diff --git a/tests/test_runner.c b/tests/test_runner.c new file mode 100644 index 0000000..172368b --- /dev/null +++ b/tests/test_runner.c @@ -0,0 +1,101 @@ +/* test_runner.c — %f/%d expansion, injection guard, exit status. */ +#include "runner.h" +#include +#include + +static GMainLoop *g_loop; +static int g_exit_code; + +static void +_done_cb(GObject *p_src, GAsyncResult *p_res, gpointer p_data) { + (void)p_src; + (void)p_data; + GError *e = NULL; + g_exit_code = runner_run_finish(NULL, p_res, &e); + g_clear_error(&e); + g_main_loop_quit(g_loop); +} + +static void +run_and_wait(Runner *r, GFile *p_file, GFile *p_dir, + const RunnerScript *p_script) { + g_exit_code = -99; + g_loop = g_main_loop_new(NULL, FALSE); + GError *e = NULL; + g_assert_true(runner_run(r, p_file, p_dir, p_script, _done_cb, NULL, &e)); + g_assert_no_error(e); + g_main_loop_run(g_loop); + g_main_loop_unref(g_loop); +} + +static void +test_true_exit_zero(void) { + Runner *r = runner_new(); + RunnerScript s = {"true", "true"}; + GFile *f = g_file_new_for_path("/tmp/nonexistent"); + run_and_wait(r, f, NULL, &s); + g_assert_cmpint(g_exit_code, ==, 0); + runner_delete(r); + g_object_unref(f); +} + +static void +test_false_exit_nonzero(void) { + Runner *r = runner_new(); + RunnerScript s = {"false", "false"}; + GFile *f = g_file_new_for_path("/tmp/nonexistent"); + run_and_wait(r, f, NULL, &s); + g_assert_cmpint(g_exit_code, !=, 0); + runner_delete(r); + g_object_unref(f); +} + +static void +test_injection_guard(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-runner-XXXXXX", &e); + g_assert_no_error(e); + /* Create a file with a malicious name. */ + char *p = g_build_filename(d, ";echo HACKED;.jpg", NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + g_free(p); + GFile *dd = g_file_new_for_path(d); + + Runner *r = runner_new(); + /* true %f should succeed regardless of the filename (injection guard). + * The single-quoted path is a valid argument to true. */ + RunnerScript s = {"true", "true %f"}; + run_and_wait(r, f, dd, &s); + g_assert_cmpint(g_exit_code, ==, 0); + + runner_delete(r); + g_object_unref(f); + g_object_unref(dd); + GFile *ddd = g_file_new_for_path(d); + GFileEnumerator *en = g_file_enumerate_children( + ddd, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (en) { + GFileInfo *i; + while ((i = g_file_enumerator_next_file(en, NULL, NULL))) { + GFile *c = g_file_get_child(ddd, g_file_info_get_name(i)); + g_file_delete(c, NULL, NULL); + g_object_unref(c); + g_object_unref(i); + } + g_object_unref(en); + } + g_file_delete(ddd, NULL, NULL); + g_object_unref(ddd); + g_free(d); +} + +int +main(int argc, char **argv) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/runner/true_exit_zero", test_true_exit_zero); + g_test_add_func("/runner/false_exit_nonzero", test_false_exit_nonzero); + g_test_add_func("/runner/injection_guard", test_injection_guard); + return g_test_run(); +} \ No newline at end of file -- cgit v1.2.3