diff --git a/lcmd.c b/lcmd.c new file mode 100644 index 00000000..70af5150 --- /dev/null +++ b/lcmd.c @@ -0,0 +1,340 @@ +/* +** $Id: lcmd.c $ +** Command execution for backtick syntax +** See Copyright Notice in lua.h +*/ + +#define lcmd_c +#define LUA_LIB + +#include "lprefix.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "lua.h" +#include "lauxlib.h" +#include "lcmd.h" + + +/* ===== argv parser ===== */ + +typedef struct { + char **argv; + char *buf; + int argc; +} ParsedArgs; + + +static void free_argv (ParsedArgs *pa) { + free(pa->buf); + free(pa->argv); +} + + +/* +** Tokenize command string into argv array. +** Handles: whitespace splitting, single quotes (literal), +** double quotes (with \", \\, \n, \t, \$ escapes), backslash escaping. +** Returns 0 on success, -1 on error (unterminated quote). +*/ +static int parse_argv (const char *cmd, ParsedArgs *result) { + size_t len = strlen(cmd); + char *buf = (char *)malloc(len + 1); + int capacity = 8; + char **argv = (char **)malloc((size_t)capacity * sizeof(char *)); + int argc = 0; + size_t bp = 0; /* position in buf */ + const char *p = cmd; + + if (buf == NULL || argv == NULL) { + free(buf); + free(argv); + return -1; + } + + while (*p != '\0') { + /* skip whitespace */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') + p++; + if (*p == '\0') + break; + + /* start of a token */ + if (argc >= capacity - 1) { + capacity *= 2; + argv = (char **)realloc(argv, (size_t)capacity * sizeof(char *)); + if (argv == NULL) { free(buf); return -1; } + } + argv[argc++] = buf + bp; + + while (*p != '\0') { + if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') + break; + + if (*p == '\'') { + /* single quote: literal until closing quote */ + p++; + while (*p != '\0' && *p != '\'') + buf[bp++] = *p++; + if (*p == '\'') p++; + else { free(buf); free(argv); return -1; } + } + else if (*p == '"') { + /* double quote: with escape sequences */ + p++; + while (*p != '\0' && *p != '"') { + if (*p == '\\' && p[1] != '\0') { + p++; + switch (*p) { + case '"': buf[bp++] = '"'; break; + case '\\': buf[bp++] = '\\'; break; + case 'n': buf[bp++] = '\n'; break; + case 't': buf[bp++] = '\t'; break; + case '$': buf[bp++] = '$'; break; + default: buf[bp++] = '\\'; buf[bp++] = *p; break; + } + p++; + } + else { + buf[bp++] = *p++; + } + } + if (*p == '"') p++; + else { free(buf); free(argv); return -1; } + } + else if (*p == '\\' && p[1] != '\0') { + /* backslash escape outside quotes */ + p++; + buf[bp++] = *p++; + } + else { + buf[bp++] = *p++; + } + } + + buf[bp++] = '\0'; + } + + argv[argc] = NULL; + result->argv = argv; + result->buf = buf; + result->argc = argc; + return 0; +} + + +/* ===== pipe reader ===== */ + +#define READ_CHUNK 4096 + +typedef struct { + char *data; + size_t len; + size_t cap; +} DynBuf; + + +static void dynbuf_init (DynBuf *b) { + b->data = NULL; + b->len = 0; + b->cap = 0; +} + + +static void dynbuf_free (DynBuf *b) { + free(b->data); + b->data = NULL; + b->len = b->cap = 0; +} + + +static int dynbuf_grow (DynBuf *b, size_t need) { + if (b->len + need > b->cap) { + size_t newcap = (b->cap == 0) ? READ_CHUNK : b->cap; + char *p; + while (newcap < b->len + need) + newcap *= 2; + p = (char *)realloc(b->data, newcap); + if (p == NULL) return -1; + b->data = p; + b->cap = newcap; + } + return 0; +} + + +/* +** Read from two fds simultaneously into DynBufs using select(). +** Avoids deadlock when child fills one pipe buffer. +*/ +static void read_pipes (int fd_out, int fd_err, + DynBuf *buf_out, DynBuf *buf_err) { + int nfds = (fd_out > fd_err ? fd_out : fd_err) + 1; + int open_out = 1, open_err = 1; + + while (open_out || open_err) { + fd_set rfds; + int ret; + + FD_ZERO(&rfds); + if (open_out) FD_SET(fd_out, &rfds); + if (open_err) FD_SET(fd_err, &rfds); + + ret = select(nfds, &rfds, NULL, NULL, NULL); + if (ret < 0) { + if (errno == EINTR) continue; + break; /* unexpected error */ + } + + if (open_out && FD_ISSET(fd_out, &rfds)) { + ssize_t n; + if (dynbuf_grow(buf_out, READ_CHUNK) != 0) break; + n = read(fd_out, buf_out->data + buf_out->len, READ_CHUNK); + if (n > 0) + buf_out->len += (size_t)n; + else { + close(fd_out); + open_out = 0; + } + } + + if (open_err && FD_ISSET(fd_err, &rfds)) { + ssize_t n; + if (dynbuf_grow(buf_err, READ_CHUNK) != 0) break; + n = read(fd_err, buf_err->data + buf_err->len, READ_CHUNK); + if (n > 0) + buf_err->len += (size_t)n; + else { + close(fd_err); + open_err = 0; + } + } + } +} + + +/* ===== luaB_command ===== */ + +int luaB_command (lua_State *L) { + const char *cmd = luaL_checkstring(L, 1); + ParsedArgs pa; + int out_pipe[2], err_pipe[2]; + pid_t pid; + int status; + DynBuf buf_out, buf_err; + struct sigaction sa_old, sa_new; + + /* parse command into argv */ + if (parse_argv(cmd, &pa) != 0) + return luaL_error(L, "unterminated quote in command"); + + /* empty command: return {code=0, stdout="", stderr=""} */ + if (pa.argc == 0) { + free_argv(&pa); + lua_createtable(L, 0, 3); + lua_pushinteger(L, 0); + lua_setfield(L, -2, "code"); + lua_pushliteral(L, ""); + lua_setfield(L, -2, "stdout"); + lua_pushliteral(L, ""); + lua_setfield(L, -2, "stderr"); + return 1; + } + + /* create pipes */ + if (pipe(out_pipe) != 0) { + free_argv(&pa); + return luaL_error(L, "pipe() failed: %s", strerror(errno)); + } + if (pipe(err_pipe) != 0) { + close(out_pipe[0]); close(out_pipe[1]); + free_argv(&pa); + return luaL_error(L, "pipe() failed: %s", strerror(errno)); + } + + /* ignore SIGPIPE so parent doesn't crash if child exits early */ + memset(&sa_new, 0, sizeof(sa_new)); + sa_new.sa_handler = SIG_IGN; + sigemptyset(&sa_new.sa_mask); + sigaction(SIGPIPE, &sa_new, &sa_old); + + pid = fork(); + if (pid < 0) { + close(out_pipe[0]); close(out_pipe[1]); + close(err_pipe[0]); close(err_pipe[1]); + free_argv(&pa); + sigaction(SIGPIPE, &sa_old, NULL); + return luaL_error(L, "fork() failed: %s", strerror(errno)); + } + + if (pid == 0) { + /* child */ + close(out_pipe[0]); + close(err_pipe[0]); + dup2(out_pipe[1], STDOUT_FILENO); + dup2(err_pipe[1], STDERR_FILENO); + close(out_pipe[1]); + close(err_pipe[1]); + + /* restore default SIGPIPE for child */ + sa_new.sa_handler = SIG_DFL; + sigaction(SIGPIPE, &sa_new, NULL); + + execvp(pa.argv[0], pa.argv); + + /* exec failed — write error to stderr and exit */ + { + const char *err = strerror(errno); + size_t namelen = strlen(pa.argv[0]); + size_t errlen = strlen(err); + /* write "cmd: error\n" */ + (void)write(STDERR_FILENO, pa.argv[0], namelen); + (void)write(STDERR_FILENO, ": ", 2); + (void)write(STDERR_FILENO, err, errlen); + (void)write(STDERR_FILENO, "\n", 1); + } + _exit(127); + } + + /* parent */ + close(out_pipe[1]); + close(err_pipe[1]); + free_argv(&pa); + + dynbuf_init(&buf_out); + dynbuf_init(&buf_err); + read_pipes(out_pipe[0], err_pipe[0], &buf_out, &buf_err); + + waitpid(pid, &status, 0); + + /* restore old SIGPIPE handler */ + sigaction(SIGPIPE, &sa_old, NULL); + + /* build result table */ + lua_createtable(L, 0, 3); + + if (WIFEXITED(status)) + lua_pushinteger(L, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + lua_pushinteger(L, 128 + WTERMSIG(status)); + else + lua_pushinteger(L, -1); + lua_setfield(L, -2, "code"); + + lua_pushlstring(L, buf_out.data ? buf_out.data : "", buf_out.len); + lua_setfield(L, -2, "stdout"); + + lua_pushlstring(L, buf_err.data ? buf_err.data : "", buf_err.len); + lua_setfield(L, -2, "stderr"); + + dynbuf_free(&buf_out); + dynbuf_free(&buf_err); + + return 1; +} diff --git a/lcmd.h b/lcmd.h new file mode 100644 index 00000000..aa7e5b3a --- /dev/null +++ b/lcmd.h @@ -0,0 +1,14 @@ +/* +** $Id: lcmd.h $ +** Command execution for backtick syntax +** See Copyright Notice in lua.h +*/ + +#ifndef lcmd_h +#define lcmd_h + +#include "lua.h" + +int luaB_command (lua_State *L); + +#endif