From b9cf9428ca7ac459e16cef3d5a316a7f7ae99d2a Mon Sep 17 00:00:00 2001 From: grialion <48643945+grialion@users.noreply.github.com> Date: Tue, 26 Aug 2025 00:03:27 +0200 Subject: [PATCH] feat: exec parser for desktop entries Feature-rich Exec= parser implementation for .desktop files * backslash stripping (e.g '\ ' -> ' ') * percentage stripping (e.g '%u' -> '', '%%' -> '%') * quote string handling (e.g 'arg1 "arg 2"' -> "arg1", "arg 2") The current implementation strips all "percentage codes", instead of handling them. Argument count is limited at 100. --- src/auth.c | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 193 insertions(+), 4 deletions(-) diff --git a/src/auth.c b/src/auth.c index 8750c2c..83ee96b 100644 --- a/src/auth.c +++ b/src/auth.c @@ -15,6 +15,185 @@ #include "unistd.h" #include "util.h" +// constants for exec string parsing +// ARG_LENGTH is the initial length of a parsed argument +#define MAX_ARGS 100 +#define ARG_LENGTH 64 + +// parse Exec=/bin/prog arg1 arg2\ with\ spaces +void free_parsed_args(char** args, int arg_count) { + if (!args) return; + for (int i = 0; i < arg_count; i++) { + free(args[i]); + } + free((void*)args); +} + +/* small closure-like struct to pass state to helper functions */ +struct ctx { + char** pcur; + size_t* pcur_len; + size_t* pcur_cap; + char*** pargv; + int* pargc; +}; +/* append_char(state, ch) -> 0 on error, 1 on success */ +int append_char(struct ctx* st, char ch) { + char** pcur = st->pcur; + size_t* plen = st->pcur_len; + size_t* pcap = st->pcur_cap; + if (*plen + 1 >= *pcap) { + size_t newcap = *pcap ? (*pcap) * 2 : ARG_LENGTH; + char* cur = (char*)realloc(*pcur, newcap); + if (!cur) return 0; + *pcur = cur; + *pcap = newcap; + } + (*pcur)[(*plen)++] = ch; + return 1; +} + +/* push_arg(state) -> 0 on error, 1 on success */ +int push_arg(struct ctx* st) { + char** pcur = st->pcur; + size_t* plen = st->pcur_len; + size_t* pcap = st->pcur_cap; + char*** pargv = st->pargv; + int* pargc = st->pargc; + + if (*pargc > MAX_ARGS) { + return 1; + } + if (!*pcur) { + char* empty = strdup(""); + if (!empty) return 0; + char** na = (char**)realloc((void*)*pargv, sizeof(char*) * ((*pargc) + 1)); + if (!na) { + free(empty); + return 0; + } + *pargv = na; + (*pargv)[(*pargc)++] = empty; + return 1; + } + if (!append_char(st, '\0')) return 0; + char* final = (char*)realloc(*pcur, *plen); + if (!final) final = *pcur; + *pcur = NULL; + *plen = 0; + *pcap = 0; + char** na = (char**)realloc((void*)*pargv, sizeof(char*) * ((*pargc) + 1)); + if (!na) { + free(final); + return 0; + } + *pargv = na; + (*pargv)[(*pargc)++] = final; + return 1; +} + +/* Return codes: + 0 = success + 1 = bad args + 2 = memory + 3 = syntax + + Important: call free_parsed_args afterwards to free the passed ***args +*/ +// NOLINTBEGIN(readability-function-cognitive-complexity) +int parse_exec_string(const char* exec_s, char*** args, int* arg_count) { + if (!exec_s || !args || !arg_count) return 1; + *args = NULL; + *arg_count = 0; + + size_t len = strlen(exec_s); + size_t idx = 0; + char* cur = NULL; + size_t cur_len = 0; + size_t cur_cap = 0; + char** argv = NULL; + int argc = 0; + int in_quote = 0; + + struct ctx ctx; + ctx.pcur = &cur; + ctx.pcur_len = &cur_len; + ctx.pcur_cap = &cur_cap; + ctx.pargv = &argv; + ctx.pargc = &argc; + + while (idx < len) { + char cur_c = exec_s[idx]; + if (!in_quote && (cur_c == ' ' || cur_c == '\t' || cur_c == '\n')) { + if (cur_cap) { + if (!push_arg(&ctx)) goto nomem; + } + idx++; + continue; + } + if (!in_quote && cur_c == '"') { + in_quote = 1; + idx++; + continue; + } + if (in_quote && cur_c == '"') { + in_quote = 0; + idx++; + continue; + } + + if (cur_c == '\\') { + if (idx + 1 >= len) goto syntax_err; + if (!append_char(&ctx, exec_s[idx + 1])) goto nomem; + idx += 2; + continue; + } + + if (cur_c == '%') { + if (idx + 1 >= len) goto syntax_err; + if (exec_s[idx + 1] == '%') { + if (!append_char(&ctx, '%')) goto nomem; + idx += 2; + continue; + } + /* drop any %X */ + idx += 2; + continue; + } + + if (!append_char(&ctx, cur_c)) goto nomem; + idx++; + } + + if (in_quote) goto syntax_err; + if (cur_cap) { + if (!push_arg(&ctx)) goto nomem; + } + char** na = (char**)realloc((void*)argv, sizeof(char*) * (argc + 1)); + if (!na) goto nomem; + argv = na; + argv[argc] = NULL; + + *args = argv; + *arg_count = argc; + return 0; + +nomem: + if (cur) free(cur); + free_parsed_args(argv, argc); + *args = NULL; + *arg_count = 0; + return 2; + +syntax_err: + if (cur) free(cur); + free_parsed_args(argv, argc); + *args = NULL; + *arg_count = 0; + return 3; +} +// NOLINTEND(readability-function-cognitive-complexity) + int pam_conversation(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) { struct pam_response* reply = @@ -237,19 +416,29 @@ bool launch(char* user, char* passwd, struct session session, void (*cb)(void), *reach_session = true; - // TODO: these will be different due to TryExec - // and, Exec/TryExec might contain spaces as args + // TODO: test existence of executable with TryExec + char** args = NULL; + int arg_count = 0; + int parse_status = parse_exec_string(session.exec, &args, &arg_count); + if (parse_status != 0 || arg_count == 0 || !args[0]) { + (void)fprintf(stderr, "failure parsing exec string '%s': %d\n", + session.exec ? session.exec : "NULL", parse_status); + free_parsed_args(args, arg_count); + return false; + } + printf("\x1b[0m"); // NOLINTNEXTLINE(bugprone-branch-clone) if (session.type == SHELL) { clear_screen(); (void)fflush(stdout); - execlp(session.exec, session.exec, NULL); + execvp(args[0], args); } else if (session.type == XORG || session.type == WAYLAND) { clear_screen(); (void)fflush(stdout); - execlp(session.exec, session.exec, NULL); + execvp(args[0], args); } + free_parsed_args(args, arg_count); perror("execl error"); (void)fputs("failure calling session\n", stderr); } else {