/* 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;
}