diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-20 15:56:16 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-20 15:56:16 +0300 |
| commit | 74f60c99446ad592b13119da8c79ccd19256b715 (patch) | |
| tree | 8ae35e8c732053641087a8016981f45674788f90 /src | |
| parent | 0b9dc4eec752d6d48aab696d06b33f4782e9db4f (diff) | |
opener: shell-argv command parsing with %f as one safe arg (hu0)
_expand_command now parses the command template with g_shell_parse_argv
FIRST, then substitutes %f into the already-parsed argv elements, so %f is
always exactly one argv value regardless of spaces or shell metacharacters
in the path. Previously it split on literal spaces, breaking quoted commands
like sh -c '...' and mis-splitting paths with spaces.
opener_launch propagates parse errors (returns FALSE, sets GError) instead
of launching malformed argv. A non-local GFile (no path) is now rejected
with G_IO_ERROR_NOT_SUPPORTED rather than producing an argv with an
embedded NULL.
Tests: quoted, quoted-with-%f, escaped spaces, option flags, weird
filename (quoted + unquoted), malformed (unclosed quote), empty command,
direct argv-shape regression (one argv value), and no-local-path rejection.
Diffstat (limited to 'src')
| -rw-r--r-- | src/opener.c | 65 |
1 files changed, 45 insertions, 20 deletions
diff --git a/src/opener.c b/src/opener.c index f9afb1b..822e332 100644 --- a/src/opener.c +++ b/src/opener.c @@ -51,9 +51,6 @@ 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) { @@ -74,25 +71,51 @@ _str_replace(const char *c_str, const char *c_old, const char *c_new) { return g_string_free(p_out, FALSE); } -static char ** -_expand_command(const char *c_cmd, GFile *p_file) { +/* Expand %f → the file path inside an already-parsed argv. + * + * The command template is parsed with g_shell_parse_argv FIRST, so quotes, + * escapes, and option flags are resolved correctly; %f is then substituted + * into the parsed argv elements. This guarantees %f is always exactly one + * argv value no matter what spaces or shell metacharacters the path contains + * (substitution happens after shell parsing, never before). + * + * Returns a newly-allocated, NULL-terminated argv vector; caller frees with + * g_strfreev. On a parse error returns NULL and sets p_err — never launches + * malformed argv. Non-static (with the `_` prefix retained to mark it as + * non-public) so tests/test_opener.c can verify the argv shape directly via + * an extern prototype, without polluting the public opener.h. */ +char ** +_expand_command(const char *c_cmd, GFile *p_file, GError **p_err) { + g_return_val_if_fail(c_cmd != NULL, NULL); + g_return_val_if_fail(G_IS_FILE(p_file), NULL); 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])); + if (c_path == NULL) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "opener: file has no local path"); + return NULL; + } + /* Parse the raw template first: resolves quotes/escapes/options. */ + char **argv = NULL; + if (!g_shell_parse_argv(c_cmd, NULL, &argv, p_err)) { + g_free(c_path); + return NULL; + } + /* Substitute %f into each parsed element so the path is always one argv + * value, regardless of spaces or shell metacharacters it contains. */ + for (guint i = 0; argv[i]; i++) { + if (g_str_equal(argv[i], "%f")) { + g_free(argv[i]); + argv[i] = g_strdup(c_path); + } else if (strstr(argv[i], "%f")) { + char *r = _str_replace(argv[i], "%f", c_path); + if (r) { + g_free(argv[i]); + argv[i] = r; + } + } } - g_ptr_array_add(argv, NULL); - g_strfreev(parts); g_free(c_path); - return (char **)g_ptr_array_free(argv, FALSE); + return argv; } gboolean @@ -101,7 +124,9 @@ opener_launch(Opener *o, GFile *p_file, const OpenerProg *p_prog, (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); + char **argv = _expand_command(p_prog->c_command, p_file, p_err); + if (argv == NULL) + return FALSE; GSubprocess *p_sub = g_subprocess_newv((const char *const *)argv, G_SUBPROCESS_FLAGS_NONE, p_err); g_strfreev(argv); |
