Welcome to little lamb

Code » limb » release » tree

[release] / src / liblimb / loadopt.h / loadopt.c

/* This file is part of limb                           https://lila.oss/limb
 * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
/* SPDX-License-Identifier: GPL-2.0-only */
#include <ctype.h>
#include <errno.h>
#include <limb/bytestr.h>
#include <limb/djbunix.h>
#include <limb/esc.h>
#include <limb/loadopt.h>
#include <limb/output.h>
#include <limb/readopt.h>
#include "parseopt.h"
#include "loadopt.h"

void
add_optflags(u8 *optflags, int idx, u8 val)
{
    if (idx % 2)
        val <<= 4;
    else
        val &= 0xf;
    optflags[idx / 2] |= val;
}

u8
get_optflags(const u8 *optflags, int idx)
{
    u8 b = optflags[idx / 2];
    if (idx % 2) b >>= 4;
    return b & 0xf;
}

enum {
    ERR_PA_BADESC       = -1,
    ERR_PA_INVAL_PATH   = -2,
    ERR_PA_INVAL_FILE   = -3,
};

static int
process_arg(stralloc *sa, const struct option *option, struct loadopt *ctx)
{
    const char * const arg = sa->s + ctx->optoff;
    size_t len = sa->len - ctx->optoff - 1;

    /* need to unescape the argument? */
    if (len >= 2 && (option->flags & OPT_ESC_) && arg[0] == '"' && arg[len - 1] == '"') {
        len -= 2;
        ssize_t rr = esc_scan(sa->s + ctx->optoff, len, arg + 1, len);
        if (rr < 0) return ERR_PA_BADESC;
        sa->len = ctx->optoff + rr;
        sa->s[sa->len++] = 0;
        len = rr;
    }

    /* check for NUL */
    if ((option->flags & OPT_PATH_) && byte_chr(arg, len, 0) < len)
        return ERR_PA_INVAL_PATH;

    /* check for '/' */
    if ((option->flags & OPT_FILE_) && byte_chr(arg, len, '/') < len)
        return ERR_PA_INVAL_FILE;

    return 0;
}

static void
pa_warn(int r, int is_arg, const struct option *option)
{
    const char *m;
    switch (r) {
        case ERR_PA_BADESC:     m = "bad escaping"; break;
        case ERR_PA_INVAL_PATH: m = "invalid path: contains a NUL byte"; break;
        case ERR_PA_INVAL_FILE: m = "invalid file: contains a '/'"; break;
        default: return;

    }
    char buf[2] = { option->shortopt, 0 };
    warn("invalid argument ", (is_arg) ? "<" : "for option --", option->longopt,
         (*buf) ? "/-" : "", (is_arg) ? ">" : buf, ": ", m);
}

int
loadopt(stralloc *sa, int argc, const char **argv, const struct option *options,
        int bfd, const char *confdir, unsigned int poflags, struct loadopt *ctx)
{
    /* init */
    if (!ctx->state) {
        for (int i = 0; options[i].longopt; ++i)
            if (options[i].flags & OPT_SKIP)
                add_optflags(ctx->optflags, i, OPT_SKIP);
        ctx->state = STATE_INIT;
    }

    /* init is done, parse options from command line */
    if (ctx->state == STATE_INIT) {
        int c;
        c = parseopt(argc, argv, options, poflags, &ctx->po);

        if (c > 0) {
            add_optflags(ctx->optflags, ctx->po.idx, OPT_SET);
            /* put argument into sa? */
            if (ctx->po.arg && (options[ctx->po.idx].flags & OPT_SA)) {
                ctx->optoff = sa->len;
                if (!stralloc_cats0(sa, ctx->po.arg))
                    return -1;
                ctx->po.arg = NULL;

                /* extra processing: unescaping, path/file checking */
                int r = process_arg(sa, &options[ctx->po.idx], ctx);
                if (r < 0) {
                    if (!(poflags & PARSEOPT_SILENT))
                        pa_warn(r, 0, &options[ctx->po.idx]);
                    return -1;
                }
            }
        }

        if (c)
            return c;
        ctx->state = STATE_CMDLINE;
    }

    /* command line is done, check if there's still a need to load from confdir */
    if (ctx->state == STATE_CMDLINE) {
        if (confdir) {
            /* find next option to be read from file */
            int i;
            for (ctx->left = 0, i = 0; options[i].longopt; ++i)
                if (!(get_optflags(ctx->optflags, i) & (OPT_SET | OPT_SKIP)))
                    ++ctx->left;
            if (!ctx->left)
                /* all options are set or to be skipped from file */
                ctx->state = STATE_CONFIG;
        } else {
            ctx->state = STATE_CONFIG;
        }
    }

    /* command line is done, now onto the confdir */
    if (ctx->state == STATE_CMDLINE) {
        ctx->confdir = opendirat(bfd, confdir);
        if (ctx->confdir) {
            ctx->state = STATE_CONFDIR;
        } else {
            ctx->state = STATE_CONFIG;
            if (errno != ENOENT) {
                warnusys("open ", ESC, confdir, ESC);
                return -1;
            }
        }
    }

    /* confdir is opened, parse options */
    if (ctx->state == STATE_CONFDIR) {
        direntry *de;

        errno = 0;
        while ((de = readdir(ctx->confdir))) {
            if (ISDOTDOT(de)) continue;

            int i;
            for (i = 0; options[i].longopt; ++i)
                if (!strcmp(options[i].longopt, de->d_name))
                    break;
            if (!options[i].longopt
                    /* ignore options to skip or already set */
                    || (get_optflags(ctx->optflags, i) & (OPT_SET | OPT_SKIP)))
                continue;

            ssize_t r;
            for (int i = 1; ; ++i) {
                if (!stralloc_readyplus(sa, i * 256))
                    r = -1;
                else
                    r = readopt(sa->s + sa->len, sa->a - sa->len - 1,
                                dirfd(ctx->confdir), de->d_name);

                if (r >= 0) break;
                if (r < 0 && errno != ENOBUFS) {
                    warnusys("read ", ESC, confdir, "/", de->d_name, ESC);
                    return -1;
                }
            }

            ctx->po.idx = i;
            /* mark option as set */
            add_optflags(ctx->optflags, i, OPT_SET);
            if (!--ctx->left) {
                /* no more option could be set from confdir */
                dir_close(ctx->confdir);
                ctx->state = STATE_CONFIG;
            }
            /* if required, ensure there's an argument */
            if (options[i].arg == ARG_REQ && !r) {
                errno = ENOMSG;
                if (!(poflags & PARSEOPT_SILENT))
                    parseopt_warn(argv, options, &ctx->po);
                return -1;
            }
            /* set our return values */
            if (options[i].arg == ARG_NONE || !r) {
                ctx->po.arg = NULL;
            } else {
                sa->s[sa->len + r] = 0;
                ctx->po.arg = sa->s + sa->len;
            }
            ctx->from_file = 1;

            /* put argument into sa */
            if (ctx->po.arg && (options[i].flags & OPT_SA)) {
                ctx->optoff = sa->len;
                sa->len += r + 1;
                ctx->po.arg = NULL;

                /* extra processing: unescaping, path/file checking */
                int r = process_arg(sa, &options[i], ctx);
                if (r < 0) {
                    if (!(poflags & PARSEOPT_SILENT))
                        pa_warn(r, 0, &options[i]);
                    return -1;
                }
            }

            return (options[i].id) ? options[i].id : options[i].shortopt;
        }
        dir_close(ctx->confdir);
        ctx->state = STATE_CONFIG;
        if (errno) {
            warnusys("read ", ESC, confdir, ESC);
            return -1;
        }
    }

    /* confdir done, check all required options were set */
    if (ctx->state == STATE_CONFIG) {
        for (ctx->po.idx = 0; options[ctx->po.idx].longopt; ++ctx->po.idx) {
            if ((get_optflags(ctx->optflags, ctx->po.idx) & (OPT_REQ | OPT_SET)) == OPT_REQ) {
                char buf[2] = { options[ctx->po.idx].shortopt, 0 };
                warn("option --", options[ctx->po.idx].longopt, (*buf) ? "/-" : "", buf, " missing");
                return (errno = ENOKEY, -1);
            }
        }
        --ctx->po.idx;
        ctx->left = 0;
        ctx->state = STATE_OPTIONS;
    }

    /* options done, arguments? */
    if (ctx->state == STATE_OPTIONS) {
nextarg:
        ++ctx->po.idx;
        const struct option *arg = &options[ctx->po.idx];
        if (arg->flags & OPT_DONE) {
            /* LOADOPT_DONE / OPTION_DONE || LOADOPT_STOP */
            if (arg->arg == ARG_NONE || arg->arg == ARG_OPT) {
                /* DONE means there shouldn't be any more args */
                if (arg->arg == ARG_NONE && ctx->po.cur < argc) {
                    warn("too many arguments");
                    return (errno = ETOOMANYREFS, -1);
                }
                ctx->state = STATE_ARGS;
            } else if (arg->arg == ARG_REQ) {
                /* LOADOPT_ARGUMENTS: on to arguments now... */
                goto nextarg;
            }
        } else {
            if (ctx->po.cur == argc) {
                if (arg->arg == ARG_REQ && !ctx->left) {
                    warn("argument <", arg->longopt, "> missing");
                    return (errno = ENODATA, -1);
                }
                ctx->state = STATE_ARGS;
            } else {
                ctx->po.arg = argv[ctx->po.cur];

                /* put argument into sa? */
                if (arg->flags & OPT_SA) {
                    ctx->optoff = sa->len;
                    if (!stralloc_cats0(sa, ctx->po.arg))
                        return -1;
                    ctx->po.arg = NULL;

                    /* extra processing: unescaping, path/file checking */
                    int r = process_arg(sa, arg, ctx);
                    if (r < 0) {
                        if (!(poflags & PARSEOPT_SILENT))
                            pa_warn(r, 1, arg);
                        return -1;
                    }
                }

                if (arg->flags & OPT_RPT) {
                    ctx->left = 1;
                    --ctx->po.idx;
                }
                ++ctx->po.cur;
                return arg->id;
            }
        }
    }

    /* arguments done, we're finally done */
    if (ctx->state == STATE_ARGS)
        ctx->state = STATE_DONE;

    /* this is the end... */
    return 0;
}