From b3c8d3cbeaed5c38df77c42a3c3cf288c764c372 Mon Sep 17 00:00:00 2001 From: grialion <48643945+grialion@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:19:25 +0200 Subject: [PATCH] feat: exec parser for desktop entries (#78) 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. --------- Co-authored-by: javalsai --- Makefile | 4 +- include/desktop_exec.h | 7 ++ src/auth.c | 28 ++++++- src/desktop_exec.c | 183 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 include/desktop_exec.h create mode 100644 src/desktop_exec.c diff --git a/Makefile b/Makefile index ad366d9..185e987 100644 --- a/Makefile +++ b/Makefile @@ -16,10 +16,10 @@ ALLFLAGS=$(CFLAGS) $(CPPFLAGS) -I$(IDIR) LIBS=-lpam -_DEPS = version.h log.h util.h ui.h ui_state.h config.h desktop.h auth.h ofield.h efield.h keys.h users.h sessions.h chvt.h macros.h launch_state.h +_DEPS = version.h log.h util.h ui.h ui_state.h config.h desktop.h desktop_exec.h auth.h ofield.h efield.h keys.h users.h sessions.h chvt.h macros.h launch_state.h DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS)) -_OBJ = main.o log.o util.o ui.o ui_state.o config.o desktop.o auth.o ofield.o efield.o users.o sessions.o chvt.o launch_state.o +_OBJ = main.o log.o util.o ui.o ui_state.o config.o desktop.o desktop_exec.o auth.o ofield.o efield.o users.o sessions.o chvt.o launch_state.o OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ)) INFO_GIT_REV?=$$(git describe --long --tags --always || echo '?') diff --git a/include/desktop_exec.h b/include/desktop_exec.h new file mode 100644 index 0000000..2bc03e6 --- /dev/null +++ b/include/desktop_exec.h @@ -0,0 +1,7 @@ +#ifndef DESKTOP_EXEC_H_ +#define DESKTOP_EXEC_H_ + +int parse_exec_string(const char* exec_s, int* arg_count, char*** args); +void free_parsed_args(int arg_count, char** args); + +#endif diff --git a/src/auth.c b/src/auth.c index 8750c2c..5d4baa8 100644 --- a/src/auth.c +++ b/src/auth.c @@ -10,6 +10,8 @@ #include "auth.h" #include "config.h" +#include "desktop_exec.h" +#include "log.h" #include "sessions.h" #include "ui.h" #include "unistd.h" @@ -166,6 +168,22 @@ void moarEnv(char* user, struct session session, struct passwd* pw, // NOLINTBEGIN(readability-function-cognitive-complexity) bool launch(char* user, char* passwd, struct session session, void (*cb)(void), struct config* config) { + char** desktop_exec; + int desktop_count; + + if (session.type != SHELL) { + desktop_exec = NULL; + int parse_status = + parse_exec_string(session.exec, &desktop_count, &desktop_exec); + if (parse_status != 0 || desktop_count == 0 || !desktop_exec[0]) { + print_err("failure parsing exec string"); + log_printf("failure parsing exec string '%s': %d\n", + session.exec ? session.exec : "NULL", parse_status); + free_parsed_args(desktop_count, desktop_exec); + } + return false; + } + struct passwd* pw = getpwnam(user); if (pw == NULL) { print_err("could not get user info"); @@ -237,8 +255,7 @@ 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 printf("\x1b[0m"); // NOLINTNEXTLINE(bugprone-branch-clone) if (session.type == SHELL) { @@ -248,9 +265,12 @@ bool launch(char* user, char* passwd, struct session session, void (*cb)(void), } else if (session.type == XORG || session.type == WAYLAND) { clear_screen(); (void)fflush(stdout); - execlp(session.exec, session.exec, NULL); + // NOLINTNEXTLINE + execvp(desktop_exec[0], desktop_exec); + // NOLINTNEXTLINE + free_parsed_args(desktop_count, desktop_exec); } - perror("execl error"); + perror("exec error"); (void)fputs("failure calling session\n", stderr); } else { pid_t child_pid = (pid_t)pid; diff --git a/src/desktop_exec.c b/src/desktop_exec.c new file mode 100644 index 0000000..2774add --- /dev/null +++ b/src/desktop_exec.c @@ -0,0 +1,183 @@ +#include +#include +#include + +#include "desktop_exec.h" + +// constants for exec string parsing +#define MAX_ARGS 100 +// ARG_LENGTH is the initial length of a parsed argument +#define ARG_LENGTH 64 + +// parse Exec=/bin/prog arg1 arg2\ with\ spaces +void free_parsed_args(int arg_count, char** args) { + 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, int* arg_count, char*** args) { + 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(argc, argv); + *args = NULL; + *arg_count = 0; + return 2; + +syntax_err: + if (cur) free(cur); + free_parsed_args(argc, argv); + *args = NULL; + *arg_count = 0; + return 3; +} +// NOLINTEND(readability-function-cognitive-complexity)