The `OPTION_INTEGER` option type accepts a signed integer. The type of
the underlying integer is a simple `int`, which restricts the range of
values accepted by such options. But there is a catch: because the
caller provides a pointer to the value via the `.value` field, which is
a simple void pointer. This has two consequences:
- There is no check whether the passed value is sufficiently long to
store the entire range of `int`. This can lead to integer wraparound
in the best case and out-of-bounds writes in the worst case.
- Even when a caller knows that they want to store a value larger than
`INT_MAX` they don't have a way to do so.
In practice this doesn't tend to be a huge issue because users typically
don't end up passing huge values to most commands. But the parsing logic
is demonstrably broken, and it is too easy to get the calling convention
wrong.
Improve the situation by introducing a new `precision` field into the
structure. This field gets assigned automatically by `OPT_INTEGER_F()`
and tracks the size of the passed value. Like this it becomes possible
for the caller to pass arbitrarily-sized integers and the underlying
logic knows to handle it correctly by doing range checks. Furthermore,
convert the code to use `strtoimax()` intstead of `strtol()` so that we
can also parse values larger than `LONG_MAX`.
Note that we do not yet assert signedness of the passed variable, which
is another source of bugs. This will be handled in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
368 lines
10 KiB
C
368 lines
10 KiB
C
#include "test-tool.h"
|
|
#include "parse-options.h"
|
|
#include "strbuf.h"
|
|
#include "string-list.h"
|
|
#include "trace2.h"
|
|
|
|
static int boolean = 0;
|
|
static int integer = 0;
|
|
static unsigned long unsigned_integer = 0;
|
|
static timestamp_t timestamp;
|
|
static int abbrev = 7;
|
|
static int verbose = -1; /* unspecified */
|
|
static int dry_run = 0, quiet = 0;
|
|
static char *string = NULL;
|
|
static char *file = NULL;
|
|
static int ambiguous;
|
|
|
|
static struct {
|
|
int called;
|
|
const char *arg;
|
|
int unset;
|
|
} length_cb;
|
|
|
|
static int mode34_callback(const struct option *opt, const char *arg, int unset)
|
|
{
|
|
if (unset)
|
|
*(int *)opt->value = 0;
|
|
else if (!strcmp(arg, "3"))
|
|
*(int *)opt->value = 3;
|
|
else if (!strcmp(arg, "4"))
|
|
*(int *)opt->value = 4;
|
|
else
|
|
return error("invalid value for '%s': '%s'", "--mode34", arg);
|
|
return 0;
|
|
}
|
|
|
|
static int length_callback(const struct option *opt, const char *arg, int unset)
|
|
{
|
|
length_cb.called = 1;
|
|
length_cb.arg = arg;
|
|
length_cb.unset = unset;
|
|
|
|
if (unset)
|
|
return 1; /* do not support unset */
|
|
|
|
*(int *)opt->value = strlen(arg);
|
|
return 0;
|
|
}
|
|
|
|
static int number_callback(const struct option *opt, const char *arg, int unset)
|
|
{
|
|
BUG_ON_OPT_NEG(unset);
|
|
*(int *)opt->value = strtol(arg, NULL, 10);
|
|
return 0;
|
|
}
|
|
|
|
static int collect_expect(const struct option *opt, const char *arg, int unset)
|
|
{
|
|
struct string_list *expect;
|
|
struct string_list_item *item;
|
|
struct strbuf label = STRBUF_INIT;
|
|
const char *colon;
|
|
|
|
if (!arg || unset)
|
|
die("malformed --expect option");
|
|
|
|
expect = (struct string_list *)opt->value;
|
|
colon = strchr(arg, ':');
|
|
if (!colon)
|
|
die("malformed --expect option, lacking a colon");
|
|
strbuf_add(&label, arg, colon - arg);
|
|
item = string_list_insert(expect, strbuf_detach(&label, NULL));
|
|
if (item->util)
|
|
die("malformed --expect option, duplicate %s", label.buf);
|
|
item->util = (void *)arg;
|
|
return 0;
|
|
}
|
|
|
|
__attribute__((format (printf,3,4)))
|
|
static void show(struct string_list *expect, int *status, const char *fmt, ...)
|
|
{
|
|
struct string_list_item *item;
|
|
struct strbuf buf = STRBUF_INIT;
|
|
va_list args;
|
|
|
|
va_start(args, fmt);
|
|
strbuf_vaddf(&buf, fmt, args);
|
|
va_end(args);
|
|
|
|
if (!expect->nr)
|
|
printf("%s\n", buf.buf);
|
|
else {
|
|
char *colon = strchr(buf.buf, ':');
|
|
if (!colon)
|
|
die("malformed output format, output lacking colon: %s", fmt);
|
|
*colon = '\0';
|
|
item = string_list_lookup(expect, buf.buf);
|
|
*colon = ':';
|
|
if (!item)
|
|
; /* not among entries being checked */
|
|
else {
|
|
if (strcmp((const char *)item->util, buf.buf)) {
|
|
printf("-%s\n", (char *)item->util);
|
|
printf("+%s\n", buf.buf);
|
|
*status = 1;
|
|
}
|
|
}
|
|
}
|
|
strbuf_release(&buf);
|
|
}
|
|
|
|
int cmd__parse_options(int argc, const char **argv)
|
|
{
|
|
const char *prefix = "prefix/";
|
|
const char *usage[] = {
|
|
"test-tool parse-options <options>",
|
|
"",
|
|
"A helper function for the parse-options API.",
|
|
NULL
|
|
};
|
|
struct string_list expect = STRING_LIST_INIT_NODUP;
|
|
struct string_list list = STRING_LIST_INIT_NODUP;
|
|
int16_t i16 = 0;
|
|
|
|
struct option options[] = {
|
|
OPT_BOOL(0, "yes", &boolean, "get a boolean"),
|
|
OPT_BOOL('D', "no-doubt", &boolean, "begins with 'no-'"),
|
|
{
|
|
.type = OPTION_SET_INT,
|
|
.short_name = 'B',
|
|
.long_name = "no-fear",
|
|
.value = &boolean,
|
|
.help = "be brave",
|
|
.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,
|
|
.defval = 1,
|
|
},
|
|
OPT_COUNTUP('b', "boolean", &boolean, "increment by one"),
|
|
OPT_BIT('4', "or4", &boolean,
|
|
"bitwise-or boolean with ...0100", 4),
|
|
OPT_NEGBIT(0, "neg-or4", &boolean, "same as --no-or4", 4),
|
|
OPT_GROUP(""),
|
|
OPT_INTEGER('i', "integer", &integer, "get a integer"),
|
|
OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"),
|
|
OPT_INTEGER('j', NULL, &integer, "get a integer, too"),
|
|
OPT_UNSIGNED('u', "unsigned", &unsigned_integer, "get an unsigned integer"),
|
|
OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23),
|
|
OPT_CMDMODE(0, "mode1", &integer, "set integer to 1 (cmdmode option)", 1),
|
|
OPT_CMDMODE(0, "mode2", &integer, "set integer to 2 (cmdmode option)", 2),
|
|
OPT_CALLBACK_F(0, "mode34", &integer, "(3|4)",
|
|
"set integer to 3 or 4 (cmdmode option)",
|
|
PARSE_OPT_CMDMODE, mode34_callback),
|
|
OPT_CALLBACK('L', "length", &integer, "str",
|
|
"get length of <str>", length_callback),
|
|
OPT_FILENAME('F', "file", &file, "set file to <file>"),
|
|
OPT_GROUP("String options"),
|
|
OPT_STRING('s', "string", &string, "string", "get a string"),
|
|
OPT_STRING(0, "string2", &string, "str", "get another string"),
|
|
OPT_STRING(0, "st", &string, "st", "get another string (pervert ordering)"),
|
|
OPT_STRING('o', NULL, &string, "str", "get another string"),
|
|
OPT_NOOP_NOARG(0, "obsolete"),
|
|
OPT_SET_INT_F(0, "longhelp", &integer, "help text of this entry\n"
|
|
"spans multiple lines", 0, PARSE_OPT_NONEG),
|
|
OPT_STRING_LIST(0, "list", &list, "str", "add str to list"),
|
|
OPT_GROUP("Magic arguments"),
|
|
OPT_NUMBER_CALLBACK(&integer, "set integer to NUM",
|
|
number_callback),
|
|
{
|
|
.type = OPTION_COUNTUP,
|
|
.short_name = '+',
|
|
.value = &boolean,
|
|
.help = "same as -b",
|
|
.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH,
|
|
},
|
|
{
|
|
.type = OPTION_COUNTUP,
|
|
.long_name = "ambiguous",
|
|
.value = &ambiguous,
|
|
.help = "positive ambiguity",
|
|
.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,
|
|
},
|
|
{
|
|
.type = OPTION_COUNTUP,
|
|
.long_name = "no-ambiguous",
|
|
.value = &ambiguous,
|
|
.help = "negative ambiguity",
|
|
.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,
|
|
},
|
|
OPT_GROUP("Standard options"),
|
|
OPT__ABBREV(&abbrev),
|
|
OPT__VERBOSE(&verbose, "be verbose"),
|
|
OPT__DRY_RUN(&dry_run, "dry run"),
|
|
OPT__QUIET(&quiet, "be quiet"),
|
|
OPT_CALLBACK(0, "expect", &expect, "string",
|
|
"expected output in the variable dump",
|
|
collect_expect),
|
|
OPT_GROUP("Alias"),
|
|
OPT_STRING('A', "alias-source", &string, "string", "get a string"),
|
|
OPT_ALIAS('Z', "alias-target", "alias-source"),
|
|
OPT_END(),
|
|
};
|
|
int ret = 0;
|
|
|
|
trace2_cmd_name("_parse_");
|
|
|
|
argc = parse_options(argc, (const char **)argv, prefix, options, usage, 0);
|
|
|
|
if (length_cb.called) {
|
|
const char *arg = length_cb.arg;
|
|
int unset = length_cb.unset;
|
|
show(&expect, &ret, "Callback: \"%s\", %d",
|
|
(arg ? arg : "not set"), unset);
|
|
}
|
|
show(&expect, &ret, "boolean: %d", boolean);
|
|
show(&expect, &ret, "integer: %d", integer);
|
|
show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16);
|
|
show(&expect, &ret, "unsigned: %lu", unsigned_integer);
|
|
show(&expect, &ret, "timestamp: %"PRItime, timestamp);
|
|
show(&expect, &ret, "string: %s", string ? string : "(not set)");
|
|
show(&expect, &ret, "abbrev: %d", abbrev);
|
|
show(&expect, &ret, "verbose: %d", verbose);
|
|
show(&expect, &ret, "quiet: %d", quiet);
|
|
show(&expect, &ret, "dry run: %s", dry_run ? "yes" : "no");
|
|
show(&expect, &ret, "file: %s", file ? file : "(not set)");
|
|
|
|
for (size_t i = 0; i < list.nr; i++)
|
|
show(&expect, &ret, "list: %s", list.items[i].string);
|
|
|
|
for (int i = 0; i < argc; i++)
|
|
show(&expect, &ret, "arg %02d: %s", i, argv[i]);
|
|
|
|
expect.strdup_strings = 1;
|
|
string_list_clear(&expect, 0);
|
|
string_list_clear(&list, 0);
|
|
free(file);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void print_args(int argc, const char **argv)
|
|
{
|
|
int i;
|
|
for (i = 0; i < argc; i++)
|
|
printf("arg %02d: %s\n", i, argv[i]);
|
|
}
|
|
|
|
static int parse_options_flags__cmd(int argc, const char **argv,
|
|
enum parse_opt_flags test_flags)
|
|
{
|
|
const char *usage[] = {
|
|
"<...> cmd [options]",
|
|
NULL
|
|
};
|
|
int opt = 0;
|
|
const struct option options[] = {
|
|
OPT_INTEGER('o', "opt", &opt, "an integer option"),
|
|
OPT_END()
|
|
};
|
|
|
|
argc = parse_options(argc, argv, NULL, options, usage, test_flags);
|
|
|
|
printf("opt: %d\n", opt);
|
|
print_args(argc, argv);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static enum parse_opt_flags test_flags = 0;
|
|
static const struct option test_flag_options[] = {
|
|
OPT_GROUP("flag-options:"),
|
|
OPT_BIT(0, "keep-dashdash", &test_flags,
|
|
"pass PARSE_OPT_KEEP_DASHDASH to parse_options()",
|
|
PARSE_OPT_KEEP_DASHDASH),
|
|
OPT_BIT(0, "stop-at-non-option", &test_flags,
|
|
"pass PARSE_OPT_STOP_AT_NON_OPTION to parse_options()",
|
|
PARSE_OPT_STOP_AT_NON_OPTION),
|
|
OPT_BIT(0, "keep-argv0", &test_flags,
|
|
"pass PARSE_OPT_KEEP_ARGV0 to parse_options()",
|
|
PARSE_OPT_KEEP_ARGV0),
|
|
OPT_BIT(0, "keep-unknown-opt", &test_flags,
|
|
"pass PARSE_OPT_KEEP_UNKNOWN_OPT to parse_options()",
|
|
PARSE_OPT_KEEP_UNKNOWN_OPT),
|
|
OPT_BIT(0, "no-internal-help", &test_flags,
|
|
"pass PARSE_OPT_NO_INTERNAL_HELP to parse_options()",
|
|
PARSE_OPT_NO_INTERNAL_HELP),
|
|
OPT_BIT(0, "subcommand-optional", &test_flags,
|
|
"pass PARSE_OPT_SUBCOMMAND_OPTIONAL to parse_options()",
|
|
PARSE_OPT_SUBCOMMAND_OPTIONAL),
|
|
OPT_END()
|
|
};
|
|
|
|
int cmd__parse_options_flags(int argc, const char **argv)
|
|
{
|
|
const char *usage[] = {
|
|
"test-tool parse-options-flags [flag-options] cmd [options]",
|
|
NULL
|
|
};
|
|
|
|
argc = parse_options(argc, argv, NULL, test_flag_options, usage,
|
|
PARSE_OPT_STOP_AT_NON_OPTION);
|
|
|
|
if (!argc || strcmp(argv[0], "cmd")) {
|
|
error("'cmd' is mandatory");
|
|
usage_with_options(usage, test_flag_options);
|
|
}
|
|
|
|
return parse_options_flags__cmd(argc, argv, test_flags);
|
|
}
|
|
|
|
static int subcmd_one(int argc, const char **argv, const char *prefix UNUSED,
|
|
struct repository *repo UNUSED)
|
|
{
|
|
printf("fn: subcmd_one\n");
|
|
print_args(argc, argv);
|
|
return 0;
|
|
}
|
|
|
|
static int subcmd_two(int argc, const char **argv, const char *prefix UNUSED,
|
|
struct repository *repo UNUSED)
|
|
{
|
|
printf("fn: subcmd_two\n");
|
|
print_args(argc, argv);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_subcommand__cmd(int argc, const char **argv,
|
|
enum parse_opt_flags test_flags)
|
|
{
|
|
const char *usage[] = {
|
|
"<...> cmd subcmd-one",
|
|
"<...> cmd subcmd-two",
|
|
NULL
|
|
};
|
|
parse_opt_subcommand_fn *fn = NULL;
|
|
int opt = 0;
|
|
struct option options[] = {
|
|
OPT_SUBCOMMAND("subcmd-one", &fn, subcmd_one),
|
|
OPT_SUBCOMMAND("subcmd-two", &fn, subcmd_two),
|
|
OPT_INTEGER('o', "opt", &opt, "an integer option"),
|
|
OPT_END()
|
|
};
|
|
|
|
if (test_flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
|
|
fn = subcmd_one;
|
|
argc = parse_options(argc, argv, NULL, options, usage, test_flags);
|
|
|
|
printf("opt: %d\n", opt);
|
|
|
|
return fn(argc, argv, NULL, NULL);
|
|
}
|
|
|
|
int cmd__parse_subcommand(int argc, const char **argv)
|
|
{
|
|
const char *usage[] = {
|
|
"test-tool parse-subcommand [flag-options] cmd <subcommand>",
|
|
NULL
|
|
};
|
|
|
|
argc = parse_options(argc, argv, NULL, test_flag_options, usage,
|
|
PARSE_OPT_STOP_AT_NON_OPTION);
|
|
|
|
if (!argc || strcmp(argv[0], "cmd")) {
|
|
error("'cmd' is mandatory");
|
|
usage_with_options(usage, test_flag_options);
|
|
}
|
|
|
|
return parse_subcommand__cmd(argc, argv, test_flags);
|
|
}
|