tinyyarn

scenario testing of Unix command line tools
git clone git://git.vx21.xyz/tinyyarn
Log | Files | Refs | README | LICENSE

tyarn.c (18025B)


      1 /* tyarn: minimal yarn implementation
      2  *
      3  * Copyright © 2019 - 2021 Richard Ipsum
      4  *
      5  * This program is free software: you can redistribute it and/or modify
      6  * it under the terms of the GNU General Public License as published by
      7  * the Free Software Foundation, version 3 of the License.
      8  *
      9  * This program is distributed in the hope that it will be useful,
     10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12  * GNU General Public License for more details.
     13  *
     14  * You should have received a copy of the GNU General Public License
     15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16  *
     17  */
     18 
     19 /* This program is dedicated to Terry A Davis,
     20  * the smartest programmer that ever lived,
     21  * may he rest in peace.
     22  */
     23 
     24 #ifdef __linux__
     25 #define _XOPEN_SOURCE 500
     26 #endif
     27 
     28 #define _POSIX_C_SOURCE 200809L
     29 
     30 #include <lua.h>
     31 #include <lauxlib.h>
     32 
     33 #include <stdio.h>
     34 #include <stdlib.h>
     35 #include <stdbool.h>
     36 #include <string.h>
     37 #include <sys/wait.h>
     38 #include <unistd.h>
     39 #include <errno.h>
     40 #include <string.h>
     41 #include <ftw.h>
     42 #include <sys/types.h>
     43 #include <sys/select.h>
     44 #include <regex.h>
     45 
     46 size_t strlcpy(char *, const char *, size_t);
     47 
     48 struct pipe_state {
     49     int fd;
     50     char *p;
     51     bool eof;
     52     char buf[1 * 1024 * 1024];
     53 };
     54 
     55 static void *xmalloc(size_t size)
     56 {
     57     void *p = malloc(size);
     58     if (p == NULL) {
     59         fprintf(stderr, "out of memory\n");
     60         exit(1);
     61     }
     62     return p;
     63 }
     64 
     65 static char *xstrdup(const char *s)
     66 {
     67     char *t = strdup(s);
     68     if (t == NULL) {
     69         fprintf(stderr, "out of memory\n");
     70         exit(1);
     71     }
     72     return t;
     73 }
     74 
     75 /* concepts stolen from
     76  * https://stackoverflow.com/questions/6137684/iterate-through-lua-table 
     77  */
     78 static const char **arr_from_lua_str_table(lua_State *L, int index, int *len_out)
     79 {
     80     int n = 0;
     81     const char **arr = xmalloc(1 * sizeof (char *));
     82     const char **p;
     83 
     84     if (arr == NULL) {
     85         return NULL;
     86     }
     87 
     88     lua_pushvalue(L, index);
     89     lua_pushnil(L);
     90 
     91     while (lua_next(L, -2)) {
     92         lua_pushvalue(L, -2);
     93 
     94         lua_tostring(L, -1);
     95         const char *value = lua_tostring(L, -2);
     96 
     97         arr[n++] = value;
     98 
     99         p = realloc(arr, (n + 1) * sizeof (char *));
    100         if (p == NULL) {
    101             free(arr);
    102             return NULL;
    103         }
    104 
    105         arr = p;
    106         lua_pop(L, 2);
    107     }
    108 
    109     lua_pop(L, 1);
    110 
    111     arr[n] = NULL;
    112     *len_out = n;
    113 
    114     return arr;
    115 }
    116 
    117 static int tyarn_path_exists(lua_State *L)
    118 {
    119     struct stat s;
    120     const char *path = luaL_checkstring(L, 1);
    121 
    122     if (stat(path, &s) == -1) {
    123         if (errno != ENOENT) {
    124             fprintf(stderr, "stat `%s': %s\n", path, strerror(errno));
    125             exit(1);
    126         }
    127         lua_pushboolean(L, false); /* ENOENT */
    128         return 1;
    129     }
    130 
    131     lua_pushboolean(L, true);
    132     return 1;
    133 }
    134 
    135 static int tyarn_mkdtemp(lua_State *L)
    136 {
    137     const char *path;
    138     char *pathbuf;
    139     size_t pathbuf_size;
    140 
    141     path = luaL_checkstring(L, 1);
    142     pathbuf_size = strlen(path) + 1;
    143     pathbuf = xmalloc(pathbuf_size);
    144     strlcpy(pathbuf, path, pathbuf_size);
    145 
    146     lua_pushstring(L, mkdtemp(pathbuf));
    147     lua_pushinteger(L, errno);
    148     free(pathbuf);
    149     return 2;
    150 }
    151 
    152 static int tyarn_strerror(lua_State *L)
    153 {
    154     lua_pushstring(L, strerror(luaL_checkinteger(L, -1)));
    155     return 1;
    156 }
    157 
    158 static void child_fn(const char **args, const char **env, const char *dir)
    159 {
    160     /* child, do exec */
    161     if (dir != NULL && chdir(dir) == -1) {
    162         fprintf(stderr, "chdir: %s\n", strerror(errno));
    163         exit(1);
    164     }
    165 
    166     if (execve(args[0], (char * const *) args, (char * const *) env) == -1) {
    167         fprintf(stderr, "exeve: %s\n", strerror(errno));
    168         exit(1);
    169     }
    170 }
    171 
    172 static int parent_fn(struct pipe_state *stdo, struct pipe_state *stde)
    173 {
    174     fd_set rfds;
    175     int maxfds, n, status;
    176     struct timeval timeout = {.tv_sec = 1, .tv_usec = 0};
    177     struct pipe_state *readers[2] = {stdo, stde};
    178     char buf[8192];
    179     ssize_t nread;
    180     size_t remaining;
    181 
    182     maxfds = readers[0]->fd > readers[1]->fd ? readers[0]->fd : readers[1]->fd;
    183 
    184     for (;;) {
    185         FD_ZERO(&rfds);
    186         FD_SET(readers[0]->fd, &rfds);
    187         FD_SET(readers[1]->fd, &rfds);
    188 
    189         if (readers[0]->eof && readers[1]->eof) {
    190             *(readers[0]->p) = '\0';
    191             *(readers[1]->p) = '\0';
    192             if (wait(&status) == -1) {
    193                 perror("wait");
    194                 exit(1);
    195             }
    196             return status;
    197         }
    198 
    199         n = select(maxfds + 1, &rfds, NULL, NULL, &timeout);
    200 
    201         switch (n) {
    202             case -1:
    203                 perror("select");
    204                 exit(1);
    205             case 0:
    206                 continue;
    207             default:
    208                 for (size_t i = 0; i < 2; i++) {
    209                     if (readers[i]->eof || !FD_ISSET(readers[i]->fd, &rfds)) {
    210                         continue;
    211                     }
    212 
    213                     nread = read(readers[i]->fd, buf, sizeof (buf));
    214                     if (nread == -1) {
    215                         perror("read");
    216                         exit(1);
    217                     }
    218 
    219                     if (nread == 0) {
    220                         readers[i]->eof = true;
    221                         continue;
    222                     }
    223 
    224                     remaining = (readers[i]->buf + sizeof (readers[i]->buf)) - readers[i]->p;
    225 
    226                     if ((size_t) nread >= remaining) {
    227                         fprintf(stderr, "out of memory\n");
    228                         exit(1);
    229                     }
    230 
    231                     memcpy(readers[i]->p, buf, nread);
    232                     readers[i]->p += nread;
    233                 }
    234         }
    235      }
    236 }
    237 
    238 static int tyarn_exec(lua_State *L)
    239 {
    240     int args_len, env_len;
    241     int status;
    242     const char **args, **env;
    243     const char *dir;
    244     int stdout_pipefds[2];
    245     int stderr_pipefds[2];
    246     struct pipe_state stdo = {0, .eof = false};
    247     struct pipe_state stde = {0, .eof = false};
    248     stdo.p = stdo.buf;
    249     stde.p = stde.buf;
    250 
    251     luaL_checktype(L, 1, LUA_TTABLE);
    252     luaL_checktype(L, 2, LUA_TTABLE);
    253     dir = luaL_checkstring(L, 3);
    254 
    255     args = arr_from_lua_str_table(L, 1, &args_len);
    256     if (args == NULL) {
    257         lua_pushinteger(L, -1);
    258         lua_pushinteger(L, EINVAL);
    259         return 2;
    260     }
    261 
    262     env = arr_from_lua_str_table(L, 2, &env_len);
    263     if (env == NULL) {
    264         lua_pushinteger(L, -1);
    265         lua_pushinteger(L, EINVAL);
    266         return 2;
    267     }
    268 
    269     if (pipe(stdout_pipefds) == -1) {
    270         perror("pipe");
    271         exit(1);
    272     }
    273 
    274     if (pipe(stderr_pipefds) == -1) {
    275         perror("pipe");
    276         exit(1);
    277     }
    278 
    279     switch (fork()) {
    280         case 0:
    281             close(STDOUT_FILENO);
    282             if (dup(stdout_pipefds[1]) == -1) {
    283                 perror("dup");
    284                 exit(1);
    285             }
    286 
    287             close(STDERR_FILENO);
    288             if (dup(stderr_pipefds[1]) == -1) {
    289                 perror("dup");
    290                 exit(1);
    291             }
    292 
    293             /* close read ends */
    294             close(stdout_pipefds[0]);
    295             close(stderr_pipefds[0]);
    296 
    297             /* close left over descriptors
    298              * no longer needed now they've been dup'd to stdout and stderr
    299              */
    300             close(stdout_pipefds[1]);
    301             close(stderr_pipefds[1]);
    302 
    303             /* child, do the work */
    304             child_fn(args, env, dir);
    305             break;
    306         case -1:
    307             perror("fork");
    308             exit(1);
    309         default:
    310             close(stdout_pipefds[1]);
    311             close(stderr_pipefds[1]);
    312             stdo.fd = stdout_pipefds[0];
    313             stde.fd = stderr_pipefds[0];
    314 
    315             /* parent, wait for child to complete */
    316             status = parent_fn(&stdo, &stde);
    317 
    318             close(stdo.fd);
    319             close(stde.fd);
    320 
    321             lua_pushinteger(L, 0);
    322             lua_pushinteger(L, WEXITSTATUS(status));
    323             lua_pushstring(L, stdo.buf);
    324             lua_pushstring(L, stde.buf);
    325             return 4;
    326     }
    327 
    328     return 0;
    329 }
    330 
    331 static int remove_entry(const char *name, const struct stat *s, int flag, struct FTW *f)
    332 {
    333     (void) flag;
    334     (void) f;
    335 
    336     if (S_ISDIR(s->st_mode)) {
    337         if (rmdir(name) == -1) {
    338             fprintf(stderr, "rmdir: %s\n", strerror(errno));
    339             return 1;
    340         }
    341 
    342         return 0;
    343     }
    344 
    345     if (unlink(name) == -1) {
    346         fprintf(stderr, "unlink: %s\n", strerror(errno));
    347         return 1;
    348     }
    349 
    350     return 0;
    351 }
    352 
    353 static int tyarn_rmutil(lua_State *L)
    354 {
    355     const char *dir = luaL_checkstring(L, -1);
    356 
    357     if (nftw(dir, remove_entry, 100, FTW_DEPTH | FTW_MOUNT | FTW_PHYS) == -1) {
    358         fprintf(stderr, "Error traversing %s\n", dir);
    359         exit(1);
    360     }
    361 
    362     return 0;
    363 }
    364 
    365 static void usage(void)
    366 {
    367     fprintf(stderr, "usage: tyarn [-12CdEehlstv] scenarios implementation...\n");
    368 }
    369 
    370 static int tyarn_usage(lua_State *L)
    371 {
    372     (void) L;
    373 
    374     usage();
    375     return 0;
    376 }
    377 
    378 static void help(void)
    379 {
    380     usage();
    381 
    382     const char *s = "\nOptions:\n"
    383         "  -C       Don't cleanup temporary files and directories\n"
    384         "  -d       Enable debug\n"
    385         "  -E       Exit early (exit when first scenario fails)\n"
    386         "  -e NAME=VALUE\n"
    387         "           Add an environment variable to the test's environment\n"
    388         "           (this argument can be repeated to add multiple variables)\n"
    389         "  -h       This help message\n"
    390         "  -l SHELL_LIBRARY\n"
    391         "  -1       Show any messages printed to stdout during execution of scenario steps\n"
    392         "  -2       Show any messages printed to stderr during execution of scenario steps\n"
    393         "           Shell library to be used by scenario implementations\n"
    394         "  -s SHELL Execute scenario implementations using SHELL\n"
    395         "  -t DIR\n"
    396         "           Use DIR as temporary directory for tests\n"
    397         "           DIR is a template e.g. /tmp/foo.XXXXXXXX\n"
    398         "           There must be at least six trailing Xs\n"
    399         "  -v       Verbose\n";
    400 
    401     fprintf(stderr, "%s", s);
    402 }
    403 
    404 static int tyarn_help(lua_State *L)
    405 {
    406     (void) L;
    407 
    408     help();
    409     return 0;
    410 }
    411 
    412 static int tyarn_parse_args(lua_State *L)
    413 {
    414     const char **args;
    415     int args_len;
    416     int c = 42;
    417     char *shell_lib = NULL;
    418     char **buf;
    419     bool debug = false;
    420     bool no_cleanup = false;
    421     bool exit_early = false;
    422     bool help = false;
    423     int verbose = 0;
    424     char *tmpdir = NULL;
    425     char *shell = NULL;
    426     char *envopts[8192];
    427     size_t envoptsn = 0;
    428     bool show_stdout = false;
    429     bool show_stderr = false;
    430     int bufn = 0;
    431 
    432     luaL_checktype(L, 1, LUA_TTABLE);
    433 
    434     args = arr_from_lua_str_table(L, 1, &args_len);
    435     if (args == NULL) {
    436         lua_pushinteger(L, -1);
    437         lua_pushinteger(L, EINVAL);
    438         return 2;
    439     }
    440 
    441     buf = xmalloc(args_len * sizeof (char *));
    442     buf[bufn++] = xstrdup("tyarn");
    443 
    444     for (int i = 0; i < args_len - 2; i++) {
    445         buf[bufn++] = xstrdup(args[i]);
    446     }
    447 
    448     while ((c = getopt(bufn, buf, ":12l:s:dECht:e:v")) != -1) {
    449         switch (c) {
    450             case '1':
    451                 show_stdout = true;
    452                 break;
    453             case '2':
    454                 show_stderr = true;
    455                 break;
    456             case 'C':
    457                 no_cleanup = true;
    458                 break;
    459             case 'E':
    460                 exit_early = true;
    461                 break;
    462             case 'e':
    463                 envopts[envoptsn++] = optarg;
    464                 break;
    465             case 'l':
    466                 shell_lib = optarg;
    467                 break;
    468             case 's':
    469                 shell = optarg;
    470                 break;
    471             case 'd':
    472                 debug = true;
    473                 break;
    474             case 'h':
    475                 help = true;
    476                 break;
    477             case 'v':
    478                 verbose++;
    479                 break;
    480             case 't':
    481                 tmpdir = optarg;
    482                 break;
    483             case ':':
    484                 fprintf(stderr, "%s: missing option\n", buf[0]);
    485                 exit(1);
    486             default:
    487                 usage();
    488                 exit(1);
    489         }
    490     }
    491 
    492     lua_createtable(L, bufn - optind, optind);
    493     lua_pushstring(L, shell_lib);
    494     lua_setfield(L, -2, "shell_lib");
    495     lua_pushboolean(L, debug);
    496     lua_setfield(L, -2, "debug");
    497     lua_pushboolean(L, no_cleanup);
    498     lua_setfield(L, -2, "no_cleanup");
    499     lua_pushboolean(L, exit_early);
    500     lua_setfield(L, -2, "exit_early");
    501     lua_pushboolean(L, help);
    502     lua_setfield(L, -2, "help");
    503     lua_pushstring(L, tmpdir);
    504     lua_setfield(L, -2, "tmpdir");
    505     lua_pushboolean(L, verbose);
    506     lua_setfield(L, -2, "verbose");
    507     lua_pushstring(L, shell);
    508     lua_setfield(L, -2, "shell");
    509     lua_pushboolean(L, show_stdout);
    510     lua_setfield(L, -2, "show_stdout");
    511     lua_pushboolean(L, show_stderr);
    512     lua_setfield(L, -2, "show_stderr");
    513 
    514     int tn = 1;
    515     for (int i = optind; i < bufn; i++) {
    516         lua_pushstring(L, buf[i]);
    517         lua_rawseti(L, -2, tn++);
    518     }
    519 
    520     lua_createtable(L, 0, envoptsn);
    521 
    522     for (size_t i = 0; i < envoptsn; i++) {
    523         char *p = strchr(envopts[i], '=');
    524         if (p == NULL) {
    525             fprintf(stderr, "%s: invalid argument to --env: %s\n",
    526                     buf[0], envopts[i]);
    527             exit(1);
    528         }
    529         char *key = envopts[i];
    530         char *value = p + 1;
    531         *p = '\0';
    532         lua_pushstring(L, value);
    533         lua_setfield(L, -2, key);
    534     }
    535 
    536     for (int i = 0; i < bufn; i++) {
    537         free(buf[i]);
    538     }
    539     free(buf);
    540     return 2;
    541 }
    542 
    543 static int tyarn_unlink(lua_State *L)
    544 {
    545     lua_pushinteger(L, unlink(luaL_checkstring(L, 1)));
    546     lua_pushinteger(L, errno);
    547     return 2;
    548 }
    549 
    550 static int tyarn_mkstemp(lua_State *L)
    551 {
    552     const char *path;
    553     char *template;
    554     int fd;
    555     size_t template_size;
    556 
    557     path = luaL_checkstring(L, 1);
    558     template_size = strlen(path) + 1;
    559     template = xmalloc(template_size);
    560     strlcpy(template, path, template_size);
    561     fd = mkstemp(template);
    562 
    563     if (fd == -1) {
    564         lua_pushnil(L);
    565         lua_pushstring(L, strerror(errno));
    566         free(template);
    567         return 2;
    568     }
    569 
    570     close(fd);
    571     /* this is super lame, a nicer way would be to make a luaStream,
    572      * http://lua-users.org/lists/lua-l/2014-11/msg00120.html, not supported in 5.1 though
    573      */
    574 
    575     lua_pushstring(L, template);
    576     free(template);
    577     return 1;
    578 }
    579 
    580 static char *str_from_offs(const char *orig, regoff_t start, regoff_t end)
    581 {
    582     char *p, *buf, *newstr;
    583     size_t bufsiz;
    584 
    585     bufsiz = strlen(orig) + 1;
    586     buf = xmalloc(bufsiz);
    587     strlcpy(buf, orig, bufsiz);
    588 
    589     p = buf + start;
    590     buf[end] = '\0';
    591 
    592     newstr = xstrdup(p);
    593     free(buf);
    594     return newstr;
    595 }
    596 
    597 static int tyarn_rematch(lua_State *L)
    598 {
    599     const char *string = luaL_checkstring(L, 1);
    600     const char *pattern = luaL_checkstring(L, 2);
    601     int ret;
    602     regex_t r = {.re_nsub = 0};
    603     regmatch_t pmatch[100];
    604     char errstr[8192];
    605 
    606     ret = regcomp(&r, pattern, REG_EXTENDED);
    607     if (ret != 0) {
    608         regerror(ret, &r, errstr, sizeof (errstr));
    609         lua_pushnil(L);
    610         lua_pushstring(L, errstr);
    611         return 2;
    612     }
    613 
    614     ret = regexec(&r, string, 100, pmatch, 0);
    615     if (ret != 0) {
    616         regerror(ret, &r, errstr, sizeof (errstr));
    617         lua_pushnil(L);
    618         lua_pushstring(L, errstr);
    619         return 2;
    620     }
    621 
    622     lua_pushinteger(L, 1);
    623 
    624     lua_createtable(L, r.re_nsub, 0);
    625     int tn = 1;
    626     for (size_t i = 1; i <= r.re_nsub; i++) {
    627         char *s = str_from_offs(string, pmatch[i].rm_so, pmatch[i].rm_eo); 
    628         lua_pushstring(L, s);
    629         lua_rawseti(L, -2, tn++);
    630         free(s);
    631     }
    632 
    633     return 2;
    634 }
    635 
    636 static int tyarn_refind(lua_State *L)
    637 {
    638     const char *string = luaL_checkstring(L, 1);
    639     const char *pattern = luaL_checkstring(L, 2);
    640     int ret;
    641     regex_t r = {.re_nsub = 0};
    642     regmatch_t pmatch[100];
    643     char errstr[8192];
    644     const char *p = string;
    645     bool match = false;
    646 
    647     ret = regcomp(&r, pattern, REG_EXTENDED);
    648     if (ret != 0) {
    649         regerror(ret, &r, errstr, sizeof (errstr));
    650         lua_pushnil(L);
    651         lua_pushstring(L, errstr);
    652         return 2;
    653     }
    654 
    655     while (*p != '\0') {
    656         ret = regexec(&r, p, 100, pmatch, 0);
    657 
    658         switch (ret) {
    659             case REG_NOMATCH:
    660                 p++;
    661                 continue;
    662             case 0:
    663                 match = true;
    664                 goto out;
    665             default:
    666                 regerror(ret, &r, errstr, sizeof (errstr));
    667                 lua_pushnil(L);
    668                 lua_pushstring(L, errstr);
    669                 return 2;
    670         }
    671     }
    672 
    673 out:
    674     if (!match) {
    675         lua_pushnil(L);
    676         lua_pushnil(L);
    677         return 2;
    678     }
    679 
    680     lua_pushinteger(L, 1);
    681     lua_createtable(L, r.re_nsub, 0);
    682     int tn = 1;
    683     for (size_t i = 1; i <= r.re_nsub; i++) {
    684         char *s = str_from_offs(p, pmatch[i].rm_so, pmatch[i].rm_eo);
    685         lua_pushstring(L, s);
    686         lua_rawseti(L, -2, tn++);
    687         free(s);
    688     }
    689 
    690     return 2;
    691 }
    692 
    693 static int tyarn_mkdir(lua_State *L)
    694 {
    695     const char *path = luaL_checkstring(L, 1);
    696 
    697     lua_pushinteger(L, mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
    698     lua_pushstring(L, strerror(errno));
    699 
    700     return 2;
    701 }
    702 
    703 static int tyarn_getcwd(lua_State *L)
    704 {
    705     char *path = getcwd(NULL, 0);
    706 
    707     if (path == NULL) {
    708         lua_pushnil(L);
    709         lua_pushstring(L, strerror(errno));
    710         return 2;
    711     }
    712 
    713     lua_pushstring(L, path);
    714     free(path);
    715     return 1;
    716 }
    717 
    718 static const struct luaL_Reg tyarn_functions[] = {
    719     {"exec", tyarn_exec},
    720     {"mkdtemp", tyarn_mkdtemp},
    721     {"strerror", tyarn_strerror},
    722     {"rmutil", tyarn_rmutil},
    723     {"parse_args", tyarn_parse_args},
    724     {"unlink", tyarn_unlink},
    725     {"mkstemp", tyarn_mkstemp},
    726     {"re_match", tyarn_rematch},
    727     {"re_find", tyarn_refind},
    728     {"usage", tyarn_usage},
    729     {"help", tyarn_help},
    730     {"path_exists", tyarn_path_exists},
    731     {"mkdir", tyarn_mkdir},
    732     {"getcwd", tyarn_getcwd},
    733     {NULL, NULL}
    734 };
    735 
    736 int luaopen_tyarn(lua_State *L)
    737 {
    738     luaL_register(L, "tyarn", tyarn_functions);
    739     return 1;
    740 }
    741