diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b41753..957dd1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: build: @@ -51,6 +51,8 @@ jobs: needs: build if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Download Artifacts diff --git a/Makefile b/Makefile index 375506a..78ce2e4 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,17 @@ # SPDX-License-Identifier: GPL-3.0-or-later TOOLS = host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info -TESTS = tests/test_host_edit_command.out -COMMON_HEADERS = src/host_common.h src/host_path.h src/host_capture.h src/host_edit_command.h src/uae_pragmas.h +TESTS = tests/test_host_common.out tests/test_host_command_builders.out tests/test_host_edit_command.out +COMMON_HEADERS = src/host_common.h src/host_path.h src/host_capture.h src/host_clip_command.h src/host_edit_command.h src/host_notify_command.h src/host_reveal_command.h src/uae_pragmas.h PACKAGE = Host-Tools-$(VERSION).lha +.PHONY: all test debug package clean + all: $(TOOLS) test: $(TESTS) - ./$(TESTS) + @for test in $(TESTS); do \ + ./$$test || exit $$?; \ + done VERSION = 2.3 DATE = 2026-04-30 @@ -21,7 +25,7 @@ CFLAGS = -mcpu=68020 -noixemul -Os -fomit-frame-pointer -std=c99 -Wall -Wextra VERFLAGS = -DVERSION_STR="\"$(VERSION)\"" -DDATE_STR="\"$(DATE)\"" HOST_CC ?= $(shell command -v x86_64-linux-gnu-gcc 2>/dev/null || command -v cc 2>/dev/null || printf cc) HOST_NATIVE_FLAGS = $(if $(findstring x86_64-linux-gnu-gcc,$(notdir $(HOST_CC))),-B/usr/bin/x86_64-linux-gnu- -fuse-ld=bfd,) -HOST_CFLAGS = -std=c99 -Wall -Wextra -Isrc +HOST_CFLAGS = -std=c99 -Wall -Wextra -Wstrict-prototypes -Isrc host-run: src/host-run.c $(COMMON_HEADERS) $(CC) $(CFLAGS) $(VERFLAGS) $(INCLUDES) src/host-run.c -o $@ @@ -50,6 +54,12 @@ host-clip: src/host-clip.c $(COMMON_HEADERS) host-info: src/host-info.c $(COMMON_HEADERS) $(CC) $(CFLAGS) $(VERFLAGS) $(INCLUDES) src/host-info.c -o $@ +tests/test_host_common.out: tests/test_host_common.c src/host_common.h + $(HOST_CC) $(HOST_NATIVE_FLAGS) $(HOST_CFLAGS) tests/test_host_common.c -o $@ + +tests/test_host_command_builders.out: tests/test_host_command_builders.c src/host_clip_command.h src/host_common.h src/host_notify_command.h src/host_reveal_command.h + $(HOST_CC) $(HOST_NATIVE_FLAGS) $(HOST_CFLAGS) tests/test_host_command_builders.c -o $@ + tests/test_host_edit_command.out: tests/test_host_edit_command.c src/host_edit_command.h src/host_common.h $(HOST_CC) $(HOST_NATIVE_FLAGS) $(HOST_CFLAGS) tests/test_host_edit_command.c -o $@ diff --git a/src/host-clip.c b/src/host-clip.c index ef27f40..7543aa7 100644 --- a/src/host-clip.c +++ b/src/host-clip.c @@ -6,6 +6,7 @@ #include #include #include "host_capture.h" +#include "host_clip_command.h" static const char version[] = "$VER: Host-Clip v" VERSION_STR " (" DATE_STR ")"; @@ -17,27 +18,12 @@ int print_usage() return 0; } -static int append_copy_command(char *command, size_t command_size, const char *text) -{ - return host_append_literal(command, command_size, "printf %s ") && - host_append_shell_arg(command, command_size, text, 0) && - host_append_literal(command, command_size, - " | if command -v pbcopy >/dev/null 2>&1; then pbcopy; elif command -v wl-copy >/dev/null 2>&1; then wl-copy; elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard; elif command -v xsel >/dev/null 2>&1; then xsel --clipboard --input; else printf 'No host clipboard backend found\\n' >&2; exit 127; fi"); -} - int main(int argc, char *argv[]) { static char command[HOST_MAX_COMMAND_LEN]; static char text[2048]; int start = 1; - static const char paste_command[] = - "if command -v pbpaste >/dev/null 2>&1; then pbpaste; " - "elif command -v wl-paste >/dev/null 2>&1; then wl-paste -n; " - "elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard -o; " - "elif command -v xsel >/dev/null 2>&1; then xsel --clipboard --output; " - "else printf 'No host clipboard backend found\\n' >&2; exit 127; fi"; - command[0] = '\0'; text[0] = '\0'; @@ -63,7 +49,7 @@ int main(int argc, char *argv[]) printf("Unexpected argument after paste\n"); return print_usage(); } - return host_print_command_output(paste_command); + return host_print_command_output(HOST_CLIP_PASTE_COMMAND); } if (strcmp(argv[1], "copy") == 0 || strcmp(argv[1], "-c") == 0) { @@ -80,7 +66,7 @@ int main(int argc, char *argv[]) return HOST_RETURN_ERROR; } - if (!append_copy_command(command, sizeof(command), text)) { + if (!host_append_clip_copy_command(command, sizeof(command), text)) { printf("Command is too long\n"); return HOST_RETURN_ERROR; } diff --git a/src/host-notify.c b/src/host-notify.c index bc69a86..8127eda 100644 --- a/src/host-notify.c +++ b/src/host-notify.c @@ -6,6 +6,7 @@ #include #include #include "host_capture.h" +#include "host_notify_command.h" static const char version[] = "$VER: Host-Notify v" VERSION_STR " (" DATE_STR ")"; @@ -17,21 +18,6 @@ int print_usage() return 0; } -static int append_notify_command(char *command, size_t command_size, - const char *title, const char *message) -{ - return host_append_literal(command, command_size, - "if command -v notify-send >/dev/null 2>&1; then notify-send ") && - host_append_shell_arg(command, command_size, title, 0) && - host_append_shell_arg(command, command_size, message, 1) && - host_append_literal(command, command_size, - "; elif command -v osascript >/dev/null 2>&1; then osascript -e 'on run argv' -e 'display notification (item 2 of argv) with title (item 1 of argv)' -e 'end run' ") && - host_append_shell_arg(command, command_size, title, 0) && - host_append_shell_arg(command, command_size, message, 1) && - host_append_literal(command, command_size, - "; else exit 127; fi"); -} - int main(int argc, char *argv[]) { static char command[HOST_MAX_COMMAND_LEN]; @@ -39,7 +25,8 @@ int main(int argc, char *argv[]) static char message[2048]; command[0] = '\0'; - strcpy(title, "Amiga"); + title[0] = '\0'; + host_append_literal(title, sizeof(title), "Amiga"); message[0] = '\0'; if (!InitUAEResource()) @@ -77,7 +64,7 @@ int main(int argc, char *argv[]) } } - if (!append_notify_command(command, sizeof(command), title, message)) { + if (!host_append_notify_command(command, sizeof(command), title, message)) { printf("Command is too long\n"); return HOST_RETURN_ERROR; } diff --git a/src/host-reveal.c b/src/host-reveal.c index 3ade285..ecf27ff 100644 --- a/src/host-reveal.c +++ b/src/host-reveal.c @@ -7,6 +7,7 @@ #include #include "host_capture.h" #include "host_path.h" +#include "host_reveal_command.h" static const char version[] = "$VER: Host-Reveal v" VERSION_STR " (" DATE_STR ")"; @@ -18,18 +19,6 @@ int print_usage() return 0; } -static int append_reveal_command(char *command, size_t command_size, const char *path) -{ - return host_append_literal(command, command_size, - "if [ \"$(uname -s)\" = Darwin ] && command -v open >/dev/null 2>&1; then open -R ") && - host_append_shell_arg(command, command_size, path, 0) && - host_append_literal(command, command_size, - "; elif command -v xdg-open >/dev/null 2>&1; then xdg-open \"$(dirname -- ") && - host_append_shell_arg(command, command_size, path, 0) && - host_append_literal(command, command_size, - ")\"; else exit 127; fi"); -} - int main(int argc, char *argv[]) { static char filename[HOST_MAX_PATH_LEN]; @@ -78,7 +67,7 @@ int main(int argc, char *argv[]) continue; } - if (!append_reveal_command(command, sizeof(command), target)) { + if (!host_append_reveal_command(command, sizeof(command), target)) { printf("Command is too long\n"); status = HOST_RETURN_ERROR; continue; diff --git a/src/host_clip_command.h b/src/host_clip_command.h new file mode 100644 index 0000000..64738ec --- /dev/null +++ b/src/host_clip_command.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef HOST_CLIP_COMMAND_H +#define HOST_CLIP_COMMAND_H + +#include +#include "host_common.h" + +#define HOST_CLIP_PASTE_COMMAND \ + "if command -v pbpaste >/dev/null 2>&1; then pbpaste; " \ + "elif command -v wl-paste >/dev/null 2>&1; then wl-paste -n; " \ + "elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard -o; " \ + "elif command -v xsel >/dev/null 2>&1; then xsel --clipboard --output; " \ + "else printf 'No host clipboard backend found\\n' >&2; exit 127; fi" + +static inline int host_append_clip_copy_command(char *command, size_t command_size, + const char *text) +{ + return host_append_literal(command, command_size, "printf %s ") && + host_append_shell_arg(command, command_size, text, 0) && + host_append_literal(command, command_size, + " | if command -v pbcopy >/dev/null 2>&1; then pbcopy; elif command -v wl-copy >/dev/null 2>&1; then wl-copy; elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard; elif command -v xsel >/dev/null 2>&1; then xsel --clipboard --input; else printf 'No host clipboard backend found\\n' >&2; exit 127; fi"); +} + +#endif diff --git a/src/host_notify_command.h b/src/host_notify_command.h new file mode 100644 index 0000000..b68c9da --- /dev/null +++ b/src/host_notify_command.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef HOST_NOTIFY_COMMAND_H +#define HOST_NOTIFY_COMMAND_H + +#include +#include "host_common.h" + +static inline int host_append_notify_command(char *command, size_t command_size, + const char *title, const char *message) +{ + return host_append_literal(command, command_size, + "if command -v notify-send >/dev/null 2>&1; then notify-send ") && + host_append_shell_arg(command, command_size, title, 0) && + host_append_shell_arg(command, command_size, message, 1) && + host_append_literal(command, command_size, + "; elif command -v osascript >/dev/null 2>&1; then osascript -e 'on run argv' -e 'display notification (item 2 of argv) with title (item 1 of argv)' -e 'end run' ") && + host_append_shell_arg(command, command_size, title, 0) && + host_append_shell_arg(command, command_size, message, 1) && + host_append_literal(command, command_size, + "; else exit 127; fi"); +} + +#endif diff --git a/src/host_reveal_command.h b/src/host_reveal_command.h new file mode 100644 index 0000000..542e81f --- /dev/null +++ b/src/host_reveal_command.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef HOST_REVEAL_COMMAND_H +#define HOST_REVEAL_COMMAND_H + +#include +#include "host_common.h" + +static inline int host_append_reveal_command(char *command, size_t command_size, + const char *path) +{ + return host_append_literal(command, command_size, + "if [ \"$(uname -s)\" = Darwin ] && command -v open >/dev/null 2>&1; then open -R ") && + host_append_shell_arg(command, command_size, path, 0) && + host_append_literal(command, command_size, + "; elif command -v xdg-open >/dev/null 2>&1; then xdg-open \"$(dirname -- ") && + host_append_shell_arg(command, command_size, path, 0) && + host_append_literal(command, command_size, + ")\"; else exit 127; fi"); +} + +#endif diff --git a/tests/test_host_command_builders.c b/tests/test_host_command_builders.c new file mode 100644 index 0000000..36edc65 --- /dev/null +++ b/tests/test_host_command_builders.c @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include +#include +#include "host_clip_command.h" +#include "host_notify_command.h" +#include "host_reveal_command.h" + +static void require(int condition, const char *message) +{ + if (!condition) { + fprintf(stderr, "%s\n", message); + exit(1); + } +} + +static void require_contains(const char *value, const char *needle) +{ + if (strstr(value, needle) == NULL) { + fprintf(stderr, "missing substring: %s\n", needle); + fprintf(stderr, "value: %s\n", value); + exit(1); + } +} + +static void require_shell_syntax(const char *command) +{ + char syntax_command[HOST_MAX_COMMAND_LEN * 2]; + + syntax_command[0] = '\0'; + require(host_append_literal(syntax_command, sizeof(syntax_command), "sh -n -c "), + "syntax command prefix should fit"); + require(host_append_shell_arg(syntax_command, sizeof(syntax_command), command, 0), + "syntax command should fit"); + require(system(syntax_command) == 0, "generated shell command should parse"); +} + +static void test_reveal_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_reveal_command(command, sizeof(command), "/tmp/a b's.txt"), + "reveal command should build"); + + require_contains(command, "open -R '/tmp/a b'\\''s.txt'"); + require_contains(command, "xdg-open \"$(dirname -- '/tmp/a b'\\''s.txt')\""); + require_contains(command, "else exit 127; fi"); + require_shell_syntax(command); +} + +static void test_notify_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_notify_command(command, sizeof(command), + "Build's Done", "Hello $USER & goodbye"), + "notify command should build"); + + require_contains(command, "notify-send 'Build'\\''s Done' 'Hello $USER & goodbye'"); + require_contains(command, "osascript -e 'on run argv'"); + require_contains(command, "'Build'\\''s Done' 'Hello $USER & goodbye'; else exit 127; fi"); + require_shell_syntax(command); +} + +static void test_clip_copy_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_clip_copy_command(command, sizeof(command), + "copy $HOME and 'quotes'"), + "clipboard copy command should build"); + + require_contains(command, "printf %s 'copy $HOME and '\\''quotes'\\''' |"); + require_contains(command, "pbcopy"); + require_contains(command, "wl-copy"); + require_contains(command, "xclip -selection clipboard"); + require_contains(command, "xsel --clipboard --input"); + require_shell_syntax(command); +} + +static void test_clip_paste_command(void) +{ + require_contains(HOST_CLIP_PASTE_COMMAND, "pbpaste"); + require_contains(HOST_CLIP_PASTE_COMMAND, "wl-paste -n"); + require_contains(HOST_CLIP_PASTE_COMMAND, "xclip -selection clipboard -o"); + require_contains(HOST_CLIP_PASTE_COMMAND, "xsel --clipboard --output"); + require_shell_syntax(HOST_CLIP_PASTE_COMMAND); +} + +static void test_small_buffer_failures(void) +{ + char command[32]; + + command[0] = '\0'; + require(!host_append_reveal_command(command, sizeof(command), "/tmp/a.txt"), + "small reveal command buffer should fail"); + + command[0] = '\0'; + require(!host_append_notify_command(command, sizeof(command), "Title", "Message"), + "small notify command buffer should fail"); + + command[0] = '\0'; + require(!host_append_clip_copy_command(command, sizeof(command), "Text"), + "small clipboard copy command buffer should fail"); +} + +int main(void) +{ + test_reveal_command(); + test_notify_command(); + test_clip_copy_command(); + test_clip_paste_command(); + test_small_buffer_failures(); + return 0; +} diff --git a/tests/test_host_common.c b/tests/test_host_common.c new file mode 100644 index 0000000..e568ec3 --- /dev/null +++ b/tests/test_host_common.c @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include +#include +#include "host_common.h" + +static void require(int condition, const char *message) +{ + if (!condition) { + fprintf(stderr, "%s\n", message); + exit(1); + } +} + +static void require_string(const char *actual, const char *expected, const char *message) +{ + if (strcmp(actual, expected) != 0) { + fprintf(stderr, "%s\nexpected: %s\nactual: %s\n", message, expected, actual); + exit(1); + } +} + +static void test_append_literal(void) +{ + char value[8]; + + value[0] = '\0'; + require(host_append_literal(value, sizeof(value), "abc"), "literal append should fit"); + require(host_append_literal(value, sizeof(value), "def"), "second literal append should fit"); + require_string(value, "abcdef", "literal appends should concatenate"); + + require(!host_append_literal(value, sizeof(value), "gh"), "oversized append should fail"); + require_string(value, "abcdef", "failed append should leave buffer unchanged"); +} + +static void test_join_args(void) +{ + char value[32]; + char *argv[] = {"host-notify", "Title", "hello", "world"}; + + value[0] = '\0'; + require(host_join_args(value, sizeof(value), 4, argv, 2), "joined args should fit"); + require_string(value, "hello world", "joined args should use single spaces"); +} + +static void test_shell_arg_quoting(void) +{ + char command[128]; + int needs_quote; + + command[0] = '\0'; + require(host_append_shell_arg(command, sizeof(command), "safe-Path_1:/a+b=c,d@x", 0), + "safe shell arg should append"); + require(host_append_shell_arg(command, sizeof(command), "two words", 1), + "spaced shell arg should append"); + require(host_append_shell_arg(command, sizeof(command), "it's ok", 1), + "single quote shell arg should append"); + require(host_append_shell_arg(command, sizeof(command), "", 1), + "empty shell arg should append"); + require_string(command, + "safe-Path_1:/a+b=c,d@x 'two words' 'it'\\''s ok' ''", + "shell args should be safely quoted"); + + require(host_shell_arg_len("plain", &needs_quote) == 5, "plain arg length should match"); + require(!needs_quote, "plain arg should not need quoting"); + require(host_shell_arg_len("two words", &needs_quote) == 11, "quoted arg length should include quotes"); + require(needs_quote, "spaced arg should need quoting"); +} + +static void test_shell_arg_buffer_failure(void) +{ + char command[8] = "prefix"; + + require(!host_append_shell_arg(command, sizeof(command), "toolong", 1), + "oversized shell arg should fail"); + require_string(command, "prefix", "failed shell arg append should leave buffer unchanged"); +} + +static void test_uri_detection(void) +{ + require(host_is_uri("https://example.com"), "https URL should be treated as URI"); + require(host_is_uri("MAILTO:user@example.com"), "URI scheme check should be case-insensitive"); + require(host_is_uri("foo://bar"), "generic scheme with authority should be treated as URI"); + require(!host_is_uri("Work:Docs/file.txt"), "Amiga volume path should not be treated as URI"); + require(!host_is_uri("C:Tools/host-run"), "Amiga command path should not be treated as URI"); +} + +int main(void) +{ + test_append_literal(); + test_join_args(); + test_shell_arg_quoting(); + test_shell_arg_buffer_failure(); + test_uri_detection(); + return 0; +}