* [PATCH 0/5] parse-options: harden handling of integer values
@ 2025-04-01 15:01 Patrick Steinhardt
2025-04-01 15:01 ` [PATCH 1/5] global: use designated initializers for options Patrick Steinhardt
` (7 more replies)
0 siblings, 8 replies; 46+ messages in thread
From: Patrick Steinhardt @ 2025-04-01 15:01 UTC (permalink / raw)
To: git
Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe,
SZEDER Gábor, Derrick Stolee, Jeff King
Hi,
this patch series addresses the issues raised in [1] and [2]. As
discussed in [1], the series also introduces a couple of safeguards to
make it harder to misuse `OPT_INTEGER()` and `OPT_MAGNITUDE()`:
- We now track the precision of the underlying integer types. This
makes it possible to pass arbitrarily-sized integers to those
options, not only `int` and `unsigned long`, respectively.
- We introduce a build assert to verify that the passed variable has
correct signedness.
Furthermore, the series introduces `OPT_UNSIGNED()` to adapt all
callsites that previously used variables with the wrong signedness.
Thanks!
Patrick
[1]: <89257ab82cd60d135cce02d51eacee7ec35c1c37.camel@physik.fu-berlin.de>
[2]: <Z8HW6petWuMRWSXf@teonanacatl.net>
---
Patrick Steinhardt (5):
global: use designated initializers for options
parse-options: introduce precision handling for `OPTION_INTEGER`
parse-options: introduce precision handling for `OPTION_MAGNITUDE`
parse-options: introduce `OPTION_UNSIGNED`
parse-options: detect mismatches in integer signedness
apply.c | 4 +-
archive.c | 35 +++++++---
builtin/am.c | 28 +++++---
builtin/backfill.c | 4 +-
builtin/clone.c | 13 +++-
builtin/column.c | 2 +-
builtin/commit-tree.c | 12 +++-
builtin/commit.c | 62 ++++++++++++++----
builtin/config.c | 13 +++-
builtin/describe.c | 24 +++++--
builtin/fetch.c | 10 ++-
builtin/fmt-merge-msg.c | 27 ++++++--
builtin/gc.c | 12 +++-
builtin/grep.c | 18 ++++--
builtin/init-db.c | 13 ++--
builtin/ls-remote.c | 11 +++-
builtin/merge.c | 38 ++++++++---
builtin/read-tree.c | 11 +++-
builtin/rebase.c | 25 ++++++--
builtin/revert.c | 12 +++-
builtin/show-branch.c | 13 +++-
builtin/tag.c | 24 +++++--
builtin/update-index.c | 131 ++++++++++++++++++++++++++------------
builtin/write-tree.c | 12 ++--
diff.c | 13 ++--
git-compat-util.h | 7 ++
parse-options.c | 145 ++++++++++++++++++++++++++++++++++++------
parse-options.h | 23 ++++++-
ref-filter.h | 15 +++--
t/helper/test-parse-options.c | 46 +++++++++++---
t/t0040-parse-options.sh | 57 ++++++++++++++++-
31 files changed, 671 insertions(+), 189 deletions(-)
---
base-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff
change-id: 20250401-b4-pks-parse-options-integers-9b4bbcf21011
^ permalink raw reply [flat|nested] 46+ messages in thread* [PATCH 1/5] global: use designated initializers for options 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt @ 2025-04-01 15:01 ` Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt ` (6 subsequent siblings) 7 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-01 15:01 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King While we expose macros for most of our different option types understood by the "parse-options" subsystem, not every combination of fields that has one as that would otherwise quickly lead to an explosion of macros. Instead, we just initialize structures manually for those variants of fields that don't have a macro. Callsites that open-code these structure initialization don't use designated initializers though and instead just provide values for each of the fields that they want to initialize. This has three significant downsides: - Callsites need to specify all values up to the last field that they care about. This often includes fields that should simply be left at their default zero-initialized state, which adds distraction. - Any reader not deeply familiar with the layout of the structure has a hard time figuring out what the respective initializers mean. - Reordering or introducing new fields in the middle of the structure is impossible without adapting all callsites. Convert all sites to instead use designated initializers, which we have started using in our codebase quite a while ago. This allows us to skip any default-initialized fields, gives the reader context by specifying the field names and allows us to reorder or introduce new fields where we want to. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- archive.c | 35 ++++++++--- builtin/am.c | 28 ++++++--- builtin/clone.c | 13 ++++- builtin/commit-tree.c | 12 +++- builtin/commit.c | 62 +++++++++++++++----- builtin/config.c | 13 ++++- builtin/describe.c | 24 ++++++-- builtin/fetch.c | 10 +++- builtin/fmt-merge-msg.c | 25 +++++--- builtin/gc.c | 12 +++- builtin/grep.c | 14 +++-- builtin/init-db.c | 13 +++-- builtin/ls-remote.c | 11 +++- builtin/merge.c | 37 +++++++++--- builtin/read-tree.c | 11 +++- builtin/rebase.c | 25 ++++++-- builtin/revert.c | 12 +++- builtin/show-branch.c | 12 +++- builtin/tag.c | 23 ++++++-- builtin/update-index.c | 131 +++++++++++++++++++++++++++++------------- builtin/write-tree.c | 12 ++-- diff.c | 13 +++-- ref-filter.h | 15 +++-- t/helper/test-parse-options.c | 38 +++++++++--- 24 files changed, 443 insertions(+), 158 deletions(-) diff --git a/archive.c b/archive.c index 8be4e7ac8db..67bba3cd301 100644 --- a/archive.c +++ b/archive.c @@ -650,20 +650,37 @@ static int parse_archive_args(int argc, const char **argv, OPT_STRING(0, "format", &format, N_("fmt"), N_("archive format")), OPT_STRING(0, "prefix", &base, N_("prefix"), N_("prepend prefix to each pathname in the archive")), - { OPTION_CALLBACK, 0, "add-file", args, N_("file"), - N_("add untracked file to archive"), 0, add_file_cb, - (intptr_t)&base }, - { OPTION_CALLBACK, 0, "add-virtual-file", args, - N_("path:content"), N_("add untracked file to archive"), 0, - add_file_cb, (intptr_t)&base }, + { + .type = OPTION_CALLBACK, + .long_name = "add-file", + .value = args, + .argh = N_("file"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, + { + .type = OPTION_CALLBACK, + .long_name = "add-virtual-file", + .value = args, + .argh = N_("path:content"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, OPT_STRING('o', "output", &output, N_("file"), N_("write the archive to this file")), OPT_BOOL(0, "worktree-attributes", &worktree_attributes, N_("read .gitattributes in working directory")), OPT__VERBOSE(&verbose, N_("report archived files on stderr")), - { OPTION_STRING, 0, "mtime", &mtime_option, N_("time"), - N_("set modification time of archive entries"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "mtime", + .value = &mtime_option, + .argh = N_("time"), + .help = N_("set modification time of archive entries"), + .flags = PARSE_OPT_NONEG, + }, OPT_NUMBER_CALLBACK(&compression_level, N_("set compression level"), number_callback), OPT_GROUP(""), diff --git a/builtin/am.c b/builtin/am.c index 3b61bd4c333..4afb519830f 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -2400,11 +2400,16 @@ int cmd_am(int argc, OPT_CMDMODE(0, "quit", &resume_mode, N_("abort the patching operation but keep HEAD where it is"), RESUME_QUIT), - { OPTION_CALLBACK, 0, "show-current-patch", &resume_mode, - "(diff|raw)", - N_("show the patch being applied"), - PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW }, + { + .type = OPTION_CALLBACK, + .long_name = "show-current-patch", + .value = &resume_mode, + .argh = "(diff|raw)", + .help = N_("show the patch being applied"), + .flags = PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .callback = parse_opt_show_current_patch, + .defval = RESUME_SHOW_PATCH_RAW, + }, OPT_CMDMODE(0, "retry", &resume_mode, N_("try to apply current patch again"), RESUME_APPLY), @@ -2417,9 +2422,16 @@ int cmd_am(int argc, OPT_BOOL(0, "ignore-date", &state.ignore_date, N_("use current timestamp for author date")), OPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate), - { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &state.sign_commit, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_CALLBACK_F(0, "empty", &state.empty_type, "(stop|drop|keep)", N_("how to handle empty patches"), PARSE_OPT_NONEG, am_option_parse_empty), diff --git a/builtin/clone.c b/builtin/clone.c index 88276e5b7ab..9c3547f41e3 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -930,9 +930,16 @@ int cmd_clone(int argc, N_("don't use local hardlinks, always copy")), OPT_BOOL('s', "shared", &option_shared, N_("setup as shared repository")), - { OPTION_CALLBACK, 0, "recurse-submodules", &option_recurse_submodules, - N_("pathspec"), N_("initialize submodules in the clone"), - PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)"." }, + { + .type = OPTION_CALLBACK, + .long_name = "recurse-submodules", + .value = &option_recurse_submodules, + .argh = N_("pathspec"), + .help = N_("initialize submodules in the clone"), + .flags = PARSE_OPT_OPTARG, + .callback = recurse_submodules_cb, + .defval = (intptr_t)".", + }, OPT_ALIAS(0, "recursive", "recurse-submodules"), OPT_INTEGER('j', "jobs", &max_jobs, N_("number of submodules cloned in parallel")), diff --git a/builtin/commit-tree.c b/builtin/commit-tree.c index 38457600a4e..c787133d004 100644 --- a/builtin/commit-tree.c +++ b/builtin/commit-tree.c @@ -111,8 +111,16 @@ int cmd_commit_tree(int argc, OPT_CALLBACK_F('F', NULL, &buffer, N_("file"), N_("read commit log message from file"), PARSE_OPT_NONEG, parse_file_arg_callback), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; int ret; diff --git a/builtin/commit.c b/builtin/commit.c index 2f459682221..66bd91fd523 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1542,17 +1542,34 @@ struct repository *repo UNUSED) STATUS_FORMAT_LONG), OPT_BOOL('z', "null", &s.null_termination, N_("terminate entries with NUL")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, - N_("mode"), - N_("show untracked files, optional modes: all, normal, no. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, - { OPTION_STRING, 0, "ignored", &ignored_arg, - N_("mode"), - N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"traditional" }, - { OPTION_STRING, 0, "ignore-submodules", &ignore_submodule_arg, N_("when"), - N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, + { + .type = OPTION_STRING, + .long_name = "ignored", + .value = &ignored_arg, + .argh = N_("mode"), + .help = N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"traditional", + }, + { + .type = OPTION_STRING, + .long_name = "ignore-submodules", + .value = &ignore_submodule_arg, + .argh = N_("when"), + .help = N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")), OPT_BOOL(0, "no-renames", &no_renames, N_("do not detect renames")), OPT_CALLBACK_F('M', "find-renames", &rename_score_arg, @@ -1688,8 +1705,16 @@ int cmd_commit(int argc, OPT_BOOL('e', "edit", &edit_flag, N_("force edit of commit")), OPT_CLEANUP(&cleanup_arg), OPT_BOOL(0, "status", &include_status, N_("include status in commit message template")), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, /* end commit message options */ OPT_GROUP(N_("Commit contents options")), @@ -1714,7 +1739,16 @@ int cmd_commit(int argc, N_("terminate entries with NUL")), OPT_BOOL(0, "amend", &amend, N_("amend previous commit")), OPT_BOOL(0, "no-post-rewrite", &no_post_rewrite, N_("bypass post-rewrite hook")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, N_("mode"), N_("show untracked files, optional modes: all, normal, no. (Default: all)"), PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), /* end commit contents options */ diff --git a/builtin/config.c b/builtin/config.c index 53a90094e31..f70d6354772 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -131,9 +131,16 @@ struct config_display_options { #define TYPE_COLOR 6 #define TYPE_BOOL_OR_STR 7 -#define OPT_CALLBACK_VALUE(s, l, v, h, i) \ - { OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \ - PARSE_OPT_NONEG, option_parse_type, (i) } +#define OPT_CALLBACK_VALUE(s, l, v, h, i) { \ + .type = OPTION_CALLBACK, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v), \ + .help = (h), \ + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, \ + .callback = option_parse_type, \ + .defval = (i), \ +} static int option_parse_type(const struct option *opt, const char *arg, int unset) diff --git a/builtin/describe.c b/builtin/describe.c index e2e73f3d757..2da9f4fed01 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -601,12 +601,24 @@ int cmd_describe(int argc, N_("do not consider tags matching <pattern>")), OPT_BOOL(0, "always", &always, N_("show abbreviated commit object as fallback")), - {OPTION_STRING, 0, "dirty", &dirty, N_("mark"), - N_("append <mark> on dirty working tree (default: \"-dirty\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-dirty"}, - {OPTION_STRING, 0, "broken", &broken, N_("mark"), - N_("append <mark> on broken working tree (default: \"-broken\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-broken"}, + { + .type = OPTION_STRING, + .long_name = "dirty", + .value = &dirty, + .argh = N_("mark"), + .help = N_("append <mark> on dirty working tree (default: \"-dirty\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-dirty", + }, + { + .type = OPTION_STRING, + .long_name = "broken", + .value = &broken, + .argh = N_("mark"), + .help = N_("append <mark> on broken working tree (default: \"-broken\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-broken", + }, OPT_END(), }; diff --git a/builtin/fetch.c b/builtin/fetch.c index 02af5054690..3a5159d9e69 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2359,8 +2359,14 @@ int cmd_fetch(int argc, OPT_SET_INT_F(0, "refetch", &refetch, N_("re-fetch without negotiating common commits"), 1, PARSE_OPT_NONEG), - { OPTION_STRING, 0, "submodule-prefix", &submodule_prefix, N_("dir"), - N_("prepend this to submodule path output"), PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "submodule-prefix", + .value = &submodule_prefix, + .argh = N_("dir"), + .help = N_("prepend this to submodule path output"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_CALLBACK_F(0, "recurse-submodules-default", &recurse_submodules_default, N_("on-demand"), N_("default for recursive fetching of submodules " diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 189cd1096a0..240cdb474bc 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -20,13 +20,24 @@ int cmd_fmt_merge_msg(int argc, char *into_name = NULL; int shortlog_len = -1; struct option options[] = { - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("populate log with at most <n> entries from shortlog"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, - { OPTION_INTEGER, 0, "summary", &shortlog_len, N_("n"), - N_("alias for --log (deprecated)"), - PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL, - DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("populate log with at most <n> entries from shortlog"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, + { + .type = OPTION_INTEGER, + .long_name = "summary", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("alias for --log (deprecated)"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_STRING('m', "message", &message, N_("text"), N_("use <text> as start of message")), OPT_STRING(0, "into-name", &into_name, N_("name"), diff --git a/builtin/gc.c b/builtin/gc.c index 99431fd4674..6707a26bc6e 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -699,9 +699,15 @@ struct repository *repo UNUSED) int ret; struct option builtin_gc_options[] = { OPT__QUIET(&quiet, N_("suppress progress reporting")), - { OPTION_STRING, 0, "prune", &prune_expire_arg, N_("date"), - N_("prune unreferenced objects"), - PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg }, + { + .type = OPTION_STRING, + .long_name = "prune", + .value = &prune_expire_arg, + .argh = N_("date"), + .help = N_("prune unreferenced objects"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)prune_expire_arg, + }, OPT_BOOL(0, "cruft", &cfg.cruft_packs, N_("pack unreferenced objects separately")), OPT_MAGNITUDE(0, "max-cruft-size", &cfg.max_cruft_size, N_("with --cruft, limit the size of new cruft packs")), diff --git a/builtin/grep.c b/builtin/grep.c index d1427290f77..c4869733e1b 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -1017,10 +1017,16 @@ int cmd_grep(int argc, OPT_BOOL(0, "all-match", &opt.all_match, N_("show only matches from files that match all patterns")), OPT_GROUP(""), - { OPTION_STRING, 'O', "open-files-in-pager", &show_in_pager, - N_("pager"), N_("show matching files in the pager"), - PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, - NULL, (intptr_t)default_pager }, + { + .type = OPTION_STRING, + .short_name = 'O', + .long_name = "open-files-in-pager", + .value = &show_in_pager, + .argh = N_("pager"), + .help = N_("show matching files in the pager"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, + .defval = (intptr_t)default_pager, + }, OPT_BOOL_F(0, "ext-grep", &external_grep_allowed__ignored, N_("allow calling of grep(1) (ignored by this build)"), PARSE_OPT_NOCOMPLETE), diff --git a/builtin/init-db.c b/builtin/init-db.c index 196dccdd77a..4a950e44d8d 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -93,10 +93,15 @@ int cmd_init_db(int argc, N_("directory from which templates will be used")), OPT_SET_INT(0, "bare", &is_bare_repository_cfg, N_("create a bare repository"), 1), - { OPTION_CALLBACK, 0, "shared", &init_shared_repository, - N_("permissions"), - N_("specify that the git repository is to be shared amongst several users"), - PARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0}, + { + .type = OPTION_CALLBACK, + .long_name = "shared", + .value = &init_shared_repository, + .argh = N_("permissions"), + .help = N_("specify that the git repository is to be shared amongst several users"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + .callback = shared_callback + }, OPT_BIT('q', "quiet", &flags, N_("be quiet"), INIT_DB_QUIET), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), N_("separate git dir from working tree")), diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c index 42f34e12361..01a4d4daa1f 100644 --- a/builtin/ls-remote.c +++ b/builtin/ls-remote.c @@ -67,9 +67,14 @@ int cmd_ls_remote(int argc, OPT__QUIET(&quiet, N_("do not print remote URL")), OPT_STRING(0, "upload-pack", &uploadpack, N_("exec"), N_("path of git-upload-pack on the remote host")), - { OPTION_STRING, 0, "exec", &uploadpack, N_("exec"), - N_("path of git-upload-pack on the remote host"), - PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "exec", + .value = &uploadpack, + .argh = N_("exec"), + .help = N_("path of git-upload-pack on the remote host"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_BIT('t', "tags", &flags, N_("limit to tags"), REF_TAGS), OPT_BIT('b', "branches", &flags, N_("limit to branches"), REF_BRANCHES), OPT_BIT_F('h', "heads", &flags, diff --git a/builtin/merge.c b/builtin/merge.c index ba9faf126aa..21787d45165 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -250,9 +250,15 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "stat", &show_diffstat, N_("show a diffstat at the end of the merge")), OPT_BOOL(0, "summary", &show_diffstat, N_("(synonym to --stat)")), - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("add (at most <n>) entries from shortlog to merge commit message"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("add (at most <n>) entries from shortlog to merge commit message"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_BOOL(0, "squash", &squash, N_("create a single commit instead of doing a merge")), OPT_BOOL(0, "commit", &option_commit, @@ -274,9 +280,16 @@ static struct option builtin_merge_options[] = { OPT_CALLBACK('m', "message", &merge_msg, N_("message"), N_("merge commit message (for a non-fast-forward merge)"), option_parse_message), - { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"), - N_("read message from file"), PARSE_OPT_NONEG, - NULL, 0, option_read_message }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'F', + .long_name = "file", + .value = &merge_msg, + .argh = N_("path"), + .help = N_("read message from file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = option_read_message, + }, OPT_STRING(0, "into-name", &into_name, N_("name"), N_("use <name> instead of the real target")), OPT__VERBOSITY(&verbosity), @@ -289,8 +302,16 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "allow-unrelated-histories", &allow_unrelated_histories, N_("allow merging unrelated histories")), OPT_SET_INT(0, "progress", &show_progress, N_("force progress reporting"), 1), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&autostash), OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")), OPT_BOOL(0, "signoff", &signoff, N_("add a Signed-off-by trailer")), diff --git a/builtin/read-tree.c b/builtin/read-tree.c index d2a807a828b..a8f352f7cd9 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -135,9 +135,14 @@ int cmd_read_tree(int argc, N_("3-way merge in presence of adds and removes")), OPT_BOOL(0, "reset", &opts.reset, N_("same as -m, but discard unmerged entries")), - { OPTION_STRING, 0, "prefix", &opts.prefix, N_("<subdirectory>/"), - N_("read the tree into the index under <subdirectory>/"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "prefix", + .value = &opts.prefix, + .argh = N_("<subdirectory>/"), + .help = N_("read the tree into the index under <subdirectory>/"), + .flags = PARSE_OPT_NONEG, + }, OPT_BOOL('u', NULL, &opts.update, N_("update working tree with merge result")), OPT_CALLBACK_F(0, "exclude-per-directory", &opts, diff --git a/builtin/rebase.c b/builtin/rebase.c index d4715ed35d7..d4083350090 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1122,9 +1122,15 @@ int cmd_rebase(int argc, OPT_BIT('v', "verbose", &options.flags, N_("display a diffstat of what changed upstream"), REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), - {OPTION_NEGBIT, 'n', "no-stat", &options.flags, NULL, - N_("do not show diffstat of what changed upstream"), - PARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT }, + { + .type = OPTION_NEGBIT, + .short_name = 'n', + .long_name = "no-stat", + .value = &options.flags, + .help = N_("do not show diffstat of what changed upstream"), + .flags = PARSE_OPT_NOARG, + .defval = REBASE_DIFFSTAT, + }, OPT_BOOL(0, "signoff", &options.signoff, N_("add a Signed-off-by trailer to each commit")), OPT_BOOL(0, "committer-date-is-author-date", @@ -1190,9 +1196,16 @@ int cmd_rebase(int argc, OPT_BOOL(0, "update-refs", &options.update_refs, N_("update branches that point to commits " "that are being rebased")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&options.autostash), OPT_STRING_LIST('x', "exec", &options.exec, N_("exec"), N_("add exec lines after each commit of the " diff --git a/builtin/revert.c b/builtin/revert.c index aca6c293cdf..4f5ef975494 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -132,8 +132,16 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, OPT_STRING(0, "strategy", &strategy, N_("strategy"), N_("merge strategy")), OPT_STRVEC('X', "strategy-option", &opts->xopts, N_("option"), N_("option for merge strategy")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; struct option *options = base_options; diff --git a/builtin/show-branch.c b/builtin/show-branch.c index fce6b404e92..dab37019d29 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -667,9 +667,15 @@ int cmd_show_branch(int ac, N_("show remote-tracking branches")), OPT__COLOR(&showbranch_use_color, N_("color '*!+-' corresponding to the branch")), - { OPTION_INTEGER, 0, "more", &extra, N_("n"), - N_("show <n> more commits after the common ancestor"), - PARSE_OPT_OPTARG, NULL, (intptr_t)1 }, + { + .type = OPTION_INTEGER, + .long_name = "more", + .value = &extra, + .argh = N_("n"), + .help = N_("show <n> more commits after the common ancestor"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_SET_INT(0, "list", &extra, N_("synonym to more=-1"), -1), OPT_BOOL(0, "no-name", &no_name, N_("suppress naming strings")), OPT_BOOL(0, "current", &with_current_branch, diff --git a/builtin/tag.c b/builtin/tag.c index d3e0943b734..b266f12bb48 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -479,9 +479,15 @@ int cmd_tag(int argc, int edit_flag = 0; struct option options[] = { OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'), - { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"), - N_("print <n> lines of each tag message"), - PARSE_OPT_OPTARG, NULL, 1 }, + { + .type = OPTION_INTEGER, + .short_name = 'n', + .value = &filter.lines, + .argh = N_("n"), + .help = N_("print <n> lines of each tag message"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_CMDMODE('d', "delete", &cmdmode, N_("delete tags"), 'd'), OPT_CMDMODE('v', "verify", &cmdmode, N_("verify tags"), 'v'), @@ -513,9 +519,14 @@ int cmd_tag(int argc, N_("do not output a newline after empty formatted refs")), OPT_REF_SORT(&sorting_options), { - OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), - N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT, - parse_opt_object_name, (intptr_t) "HEAD" + .type = OPTION_CALLBACK, + .long_name = "points-at", + .value = &filter.points_at, + .argh = N_("object"), + .help = N_("print only tags of the object"), + .flags = PARSE_OPT_LASTARG_DEFAULT, + .callback = parse_opt_object_name, + .defval = (intptr_t) "HEAD", }, OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), diff --git a/builtin/update-index.c b/builtin/update-index.c index b2f6b1a3fbb..ee64b022679 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -964,29 +964,51 @@ int cmd_update_index(int argc, N_("like --refresh, but ignore assume-unchanged setting"), PARSE_OPT_NOARG | PARSE_OPT_NONEG, really_refresh_callback), - {OPTION_LOWLEVEL_CALLBACK, 0, "cacheinfo", NULL, - N_("<mode>,<object>,<path>"), - N_("add the specified entry to the index"), - PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ - PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - NULL, 0, - cacheinfo_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "cacheinfo", + .argh = N_("<mode>,<object>,<path>"), + .help = N_("add the specified entry to the index"), + .flags = PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ + PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .ll_callback = cacheinfo_callback, + }, OPT_CALLBACK_F(0, "chmod", &set_executable_bit, "(+|-)x", N_("override the executable bit of the listed files"), PARSE_OPT_NONEG, chmod_callback), - {OPTION_SET_INT, 0, "assume-unchanged", &mark_valid_only, NULL, - N_("mark files as \"not changing\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-assume-unchanged", &mark_valid_only, NULL, - N_("clear assumed-unchanged bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, - {OPTION_SET_INT, 0, "skip-worktree", &mark_skip_worktree_only, NULL, - N_("mark files as \"index-only\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-skip-worktree", &mark_skip_worktree_only, NULL, - N_("clear skip-worktree bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "assume-unchanged", + .value = &mark_valid_only, + .help = N_("mark files as \"not changing\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-assume-unchanged", + .value = &mark_valid_only, + .help = N_("clear assumed-unchanged bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("mark files as \"index-only\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("clear skip-worktree bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_BOOL(0, "ignore-skip-worktree-entries", &ignore_skip_worktree_entries, N_("do not touch index-only entries")), OPT_SET_INT(0, "info-only", &info_only, @@ -995,22 +1017,39 @@ int cmd_update_index(int argc, N_("remove named paths even if present in worktree"), 1), OPT_BOOL('z', NULL, &nul_term_line, N_("with --stdin: input lines are terminated by null bytes")), - {OPTION_LOWLEVEL_CALLBACK, 0, "stdin", &read_from_stdin, NULL, - N_("read list of paths to be updated from standard input"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "index-info", &nul_term_line, NULL, - N_("add entries from standard input to the index"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_cacheinfo_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "unresolve", &has_errors, NULL, - N_("repopulate stages #2 and #3 for the listed paths"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, unresolve_callback}, - {OPTION_LOWLEVEL_CALLBACK, 'g', "again", &has_errors, NULL, - N_("only update entries that differ from HEAD"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, reupdate_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "stdin", + .value = &read_from_stdin, + .help = N_("read list of paths to be updated from standard input"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "index-info", + .value = &nul_term_line, + .help = N_("add entries from standard input to the index"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_cacheinfo_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "unresolve", + .value = &has_errors, + .help = N_("repopulate stages #2 and #3 for the listed paths"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = unresolve_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'g', + .long_name = "again", + .value = &has_errors, + .help = N_("only update entries that differ from HEAD"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = reupdate_callback, + }, OPT_BIT(0, "ignore-missing", &refresh_args.flags, N_("ignore files missing from worktree"), REFRESH_IGNORE_MISSING), @@ -1036,12 +1075,22 @@ int cmd_update_index(int argc, N_("write out the index even if is not flagged as changed"), 1), OPT_BOOL(0, "fsmonitor", &fsmonitor, N_("enable or disable file system monitor")), - {OPTION_SET_INT, 0, "fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("mark files as fsmonitor valid"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("clear fsmonitor valid bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("mark files as fsmonitor valid"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("clear fsmonitor valid bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_END() }; diff --git a/builtin/write-tree.c b/builtin/write-tree.c index 43f233e69b0..5a8dc377ec0 100644 --- a/builtin/write-tree.c +++ b/builtin/write-tree.c @@ -31,10 +31,14 @@ int cmd_write_tree(int argc, WRITE_TREE_MISSING_OK), OPT_STRING(0, "prefix", &tree_prefix, N_("<prefix>/"), N_("write tree object for a subdirectory <prefix>")), - { OPTION_BIT, 0, "ignore-cache-tree", &flags, NULL, - N_("only useful for debugging"), - PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL, - WRITE_TREE_IGNORE_CACHE_TREE }, + { + .type = OPTION_BIT, + .long_name = "ignore-cache-tree", + .value = &flags, + .help = N_("only useful for debugging"), + .flags = PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, + .defval = WRITE_TREE_IGNORE_CACHE_TREE, + }, OPT_END() }; diff --git a/diff.c b/diff.c index 08f5e00a2cc..f2fcc7f3c22 100644 --- a/diff.c +++ b/diff.c @@ -5892,10 +5892,15 @@ struct option *add_diff_options(const struct option *opts, OPT_CALLBACK_F(0, "diff-filter", options, N_("[(A|C|D|M|R|T|U|X|B)...[*]]"), N_("select files by diff type"), PARSE_OPT_NONEG, diff_opt_diff_filter), - { OPTION_CALLBACK, 0, "output", options, N_("<file>"), - N_("output to a specific file"), - PARSE_OPT_NONEG, NULL, 0, diff_opt_output }, - + { + .type = OPTION_CALLBACK, + .long_name = "output", + .value = options, + .argh = N_("<file>"), + .help = N_("output to a specific file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = diff_opt_output, + }, OPT_END() }; diff --git a/ref-filter.h b/ref-filter.h index 013d4cfa64b..c98c4fbd4c1 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -114,11 +114,16 @@ struct ref_format { } /* Macros for checking --merged and --no-merged options */ -#define _OPT_MERGED_NO_MERGED(option, filter, h) \ - { OPTION_CALLBACK, 0, option, (filter), N_("commit"), (h), \ - PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ - parse_opt_merge_filter, (intptr_t) "HEAD" \ - } +#define _OPT_MERGED_NO_MERGED(option, filter, h) { \ + .type = OPTION_CALLBACK, \ + .long_name = option, \ + .value = (filter), \ + .argh = N_("commit"), \ + .help = (h), \ + .flags = PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ + .callback = parse_opt_merge_filter, \ + .defval = (intptr_t) "HEAD", \ +} #define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED("merged", f, h) #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h) diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index bfe45ec68b0..997f55fd45b 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -124,8 +124,15 @@ int cmd__parse_options(int argc, const char **argv) struct option options[] = { OPT_BOOL(0, "yes", &boolean, "get a boolean"), OPT_BOOL('D', "no-doubt", &boolean, "begins with 'no-'"), - { OPTION_SET_INT, 'B', "no-fear", &boolean, NULL, - "be brave", PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { + .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), @@ -155,12 +162,27 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP("Magic arguments"), OPT_NUMBER_CALLBACK(&integer, "set integer to NUM", number_callback), - { OPTION_COUNTUP, '+', NULL, &boolean, NULL, "same as -b", - PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH }, - { OPTION_COUNTUP, 0, "ambiguous", &ambiguous, NULL, - "positive ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, - { OPTION_COUNTUP, 0, "no-ambiguous", &ambiguous, NULL, - "negative ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, + { + .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"), -- 2.49.0.604.gff1f9ca942.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 1/5] global: use designated initializers for options Patrick Steinhardt @ 2025-04-01 15:01 ` Patrick Steinhardt 2025-04-01 18:47 ` René Scharfe 2025-04-01 15:01 ` [PATCH 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt ` (5 subsequent siblings) 7 siblings, 1 reply; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-01 15:01 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King 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. Funny enough, even if the caller gets everything correct the parsing logic is still insufficient because we use `strtol()` to parse the argument, which returns a `long`. But as that value is implicitly cast when assigning it to the `int` field we may still get invalid results. 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> --- builtin/fmt-merge-msg.c | 2 ++ builtin/merge.c | 1 + builtin/show-branch.c | 1 + builtin/tag.c | 1 + parse-options.c | 53 +++++++++++++++++++++++++++++++------------ parse-options.h | 6 +++++ t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 23 ++++++++++++++++++- 8 files changed, 75 insertions(+), 15 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 240cdb474bc..3b6aac2cf7f 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("populate log with at most <n> entries from shortlog"), .flags = PARSE_OPT_OPTARG, @@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "summary", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("alias for --log (deprecated)"), .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, diff --git a/builtin/merge.c b/builtin/merge.c index 21787d45165..9ab10c7db0a 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -254,6 +254,7 @@ static struct option builtin_merge_options[] = { .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("add (at most <n>) entries from shortlog to merge commit message"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/show-branch.c b/builtin/show-branch.c index dab37019d29..b549d8c3f5b 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -671,6 +671,7 @@ int cmd_show_branch(int ac, .type = OPTION_INTEGER, .long_name = "more", .value = &extra, + .precision = sizeof(extra), .argh = N_("n"), .help = N_("show <n> more commits after the common ancestor"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/tag.c b/builtin/tag.c index b266f12bb48..7597d93c71b 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -483,6 +483,7 @@ int cmd_tag(int argc, .type = OPTION_INTEGER, .short_name = 'n', .value = &filter.lines, + .precision = sizeof(filter.lines), .argh = N_("n"), .help = N_("print <n> lines of each tag message"), .flags = PARSE_OPT_OPTARG, diff --git a/parse-options.c b/parse-options.c index 35fbb3b0d63..dbda9b7cfe7 100644 --- a/parse-options.c +++ b/parse-options.c @@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, return (*opt->ll_callback)(p, opt, p_arg, p_unset); } case OPTION_INTEGER: + { + intmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1); + intmax_t lower_bound = -upper_bound - 1; + intmax_t value; + if (unset) { - *(int *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(int *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!*arg) + } else if (!*arg) { return error(_("%s expects a numerical value"), optname(opt, flags)); - *(int *)opt->value = strtol(arg, (char **)&s, 10); - if (*s) - return error(_("%s expects a numerical value"), - optname(opt, flags)); - return 0; + } else { + value = strtoimax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); + + } + if (value < lower_bound || value > upper_bound) + return error(_("value %"PRIdMAX" for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + value, optname(opt, flags), lower_bound, upper_bound); + + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; + return 0; + case 2: + *(int16_t *)opt->value = value; + return 0; + case 4: + *(int32_t *)opt->value = value; + return 0; + case 8: + *(int64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } case OPTION_MAGNITUDE: if (unset) { *(unsigned long *)opt->value = 0; diff --git a/parse-options.h b/parse-options.h index 997ffbee805..8d5f9c95f9c 100644 --- a/parse-options.h +++ b/parse-options.h @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, * `value`:: * stores pointers to the values to be filled. * + * `precision`:: + * precision of the integer pointed to by `value`. Should typically be its + * `sizeof()`. + * * `argh`:: * token to explain the kind of argument this option wants. Does not * begin in capital letter, and does not end with a full stop. @@ -151,6 +155,7 @@ struct option { int short_name; const char *long_name; void *value; + size_t precision; const char *argh; const char *help; @@ -214,6 +219,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = (f), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 997f55fd45b..b1275dfade4 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; 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"), @@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv) 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_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23), @@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv) } show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); + show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); show(&expect, &ret, "magnitude: %lu", magnitude); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 2fe3522305f..e3ca7a27738 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -22,6 +22,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer + --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude --[no-]set23 set integer to 23 @@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 0 magnitude: 16384 timestamp: 0 string: 123 @@ -156,6 +158,7 @@ test_expect_success 'short options' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 9000 magnitude: 16384 timestamp: 0 string: 321 @@ -167,7 +170,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' cat >expect <<-EOF && boolean: 0 integer: 0 + i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' ' cat >expect <<\EOF boolean: 1 integer: 13 +i16: 0 magnitude: 0 timestamp: 0 string: 123 @@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' ' cat >expect <<\EOF boolean: 0 integer: 2 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -343,6 +349,7 @@ cat >expect <<\EOF Callback: "four", 0 boolean: 5 integer: 4 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' ' cat >expect <<\EOF boolean: 1 integer: 23 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' ' cat >expect <<\EOF boolean: 0 integer: 0 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -783,4 +792,16 @@ test_expect_success 'magnitude with units but no numbers' ' test_must_be_empty out ' +test_expect_success 'i16 limits range' ' + test-tool parse-options --i16 32767 >out && + test_grep "i16: 32767" out && + test_must_fail test-tool parse-options --i16 32768 2>err && + test_grep "value 32768 for option .i16. not in range \[-32768,32767\]" err && + + test-tool parse-options --i16 -32768 >out && + test_grep "i16: -32768" out && + test_must_fail test-tool parse-options --i16 -32769 2>err && + test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err +' + test_done -- 2.49.0.604.gff1f9ca942.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-01 15:01 ` [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt @ 2025-04-01 18:47 ` René Scharfe 2025-04-15 10:26 ` Patrick Steinhardt 0 siblings, 1 reply; 46+ messages in thread From: René Scharfe @ 2025-04-01 18:47 UTC (permalink / raw) To: Patrick Steinhardt, git Cc: John Paul Adrian Glaubitz, Todd Zullinger, SZEDER Gábor, Derrick Stolee, Jeff King Am 01.04.25 um 17:01 schrieb Patrick Steinhardt: > diff --git a/parse-options.c b/parse-options.c > index 35fbb3b0d63..dbda9b7cfe7 100644 > --- a/parse-options.c > +++ b/parse-options.c > @@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, > return (*opt->ll_callback)(p, opt, p_arg, p_unset); > } > case OPTION_INTEGER: > + { > + intmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1); Ugh, how does this not overflow? The macro maximum_signed_value_of_type does a similar calculation better. > + intmax_t lower_bound = -upper_bound - 1; This depends on two's complement being used, which is bad for purity and portability to obsolete machines, but probably OK in practice. > + intmax_t value; > + > if (unset) { > - *(int *)opt->value = 0; > - return 0; > - } > - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { > - *(int *)opt->value = opt->defval; > - return 0; > - } > - if (get_arg(p, opt, flags, &arg)) > + value = 0; > + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { > + value = opt->defval; > + } else if (get_arg(p, opt, flags, &arg)) { > return -1; > - if (!*arg) > + } else if (!*arg) { > return error(_("%s expects a numerical value"), > optname(opt, flags)); > - *(int *)opt->value = strtol(arg, (char **)&s, 10); > - if (*s) > - return error(_("%s expects a numerical value"), > - optname(opt, flags)); > - return 0; > + } else { > + value = strtoimax(arg, (char **)&s, 10); > + if (*s) > + return error(_("%s expects a numerical value"), > + optname(opt, flags)); > + > + } > > + if (value < lower_bound || value > upper_bound) > + return error(_("value %"PRIdMAX" for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), > + value, optname(opt, flags), lower_bound, upper_bound); > + > + switch (opt->precision) { > + case 1: > + *(int8_t *)opt->value = value; > + return 0; > + case 2: > + *(int16_t *)opt->value = value; > + return 0; > + case 4: > + *(int32_t *)opt->value = value; > + return 0; > + case 8: > + *(int64_t *)opt->value = value; > + return 0; Do we even need all these sizes? Or can we whittle it down to ssize_t? And for which quantities do we need to accept negative values anyway? René ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-01 18:47 ` René Scharfe @ 2025-04-15 10:26 ` Patrick Steinhardt 0 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-15 10:26 UTC (permalink / raw) To: René Scharfe Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, SZEDER Gábor, Derrick Stolee, Jeff King On Tue, Apr 01, 2025 at 08:47:12PM +0200, René Scharfe wrote: > Am 01.04.25 um 17:01 schrieb Patrick Steinhardt: > > diff --git a/parse-options.c b/parse-options.c > > index 35fbb3b0d63..dbda9b7cfe7 100644 > > --- a/parse-options.c > > +++ b/parse-options.c > > @@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, > > return (*opt->ll_callback)(p, opt, p_arg, p_unset); > > } > > case OPTION_INTEGER: > > + { > > + intmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1); > > Ugh, how does this not overflow? The macro maximum_signed_value_of_type > does a similar calculation better. Oh, nice :) I'll definitely use this, thanks! > > + intmax_t lower_bound = -upper_bound - 1; > > This depends on two's complement being used, which is bad for purity and > portability to obsolete machines, but probably OK in practice. Agreed. We would notice quite fast in case there are any machines out there that don't handle this well as our tests exercise this logic. > > + intmax_t value; > > + > > if (unset) { > > - *(int *)opt->value = 0; > > - return 0; > > - } > > - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { > > - *(int *)opt->value = opt->defval; > > - return 0; > > - } > > - if (get_arg(p, opt, flags, &arg)) > > + value = 0; > > + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { > > + value = opt->defval; > > + } else if (get_arg(p, opt, flags, &arg)) { > > return -1; > > - if (!*arg) > > + } else if (!*arg) { > > return error(_("%s expects a numerical value"), > > optname(opt, flags)); > > - *(int *)opt->value = strtol(arg, (char **)&s, 10); > > - if (*s) > > - return error(_("%s expects a numerical value"), > > - optname(opt, flags)); > > - return 0; > > + } else { > > + value = strtoimax(arg, (char **)&s, 10); > > + if (*s) > > + return error(_("%s expects a numerical value"), > > + optname(opt, flags)); > > + > > + } > > > > + if (value < lower_bound || value > upper_bound) > > + return error(_("value %"PRIdMAX" for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), > > + value, optname(opt, flags), lower_bound, upper_bound); > > + > > + switch (opt->precision) { > > + case 1: > > + *(int8_t *)opt->value = value; > > + return 0; > > + case 2: > > + *(int16_t *)opt->value = value; > > + return 0; > > + case 4: > > + *(int32_t *)opt->value = value; > > + return 0; > > + case 8: > > + *(int64_t *)opt->value = value; > > + return 0; > > Do we even need all these sizes? Or can we whittle it down to ssize_t? Some of them. We already pass both `int` and `size_t`, which means that we definitely need at least `int32` and `int64`. Whether we also need to handle `int8` or `int16` is a different question, but it doesn't add much complexity anyway. > And for which quantities do we need to accept negative values anyway? In most cases we probably don't need that, agreed. But there are callsites where we use negative values to indicate the default value, and I bet there are some options where negative values do make sense indeed. But in the end I think we should harden our integer handling for options, which is where the next patch comes in that introduces `OPTION_UNSIGNED()`. I don't want to convert all users in this patch series, but I definitely think that we should adapt them over time so that they stop accepting signed integers. Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 1/5] global: use designated initializers for options Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt @ 2025-04-01 15:01 ` Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt ` (4 subsequent siblings) 7 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-01 15:01 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King This commit is the equivalent to the preceding commit, but instead of introducing precision handling for `OPTION_INTEGER` we introduce it for `OPTION_MAGNITUDE`. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 50 ++++++++++++++++++++++++++++++++++--------- parse-options.h | 1 + t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 18 +++++++++++++++- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/parse-options.c b/parse-options.c index dbda9b7cfe7..3954ee0e570 100644 --- a/parse-options.c +++ b/parse-options.c @@ -217,21 +217,51 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, } } case OPTION_MAGNITUDE: + { + uintmax_t upper_bound = 0; + unsigned long value; + + /* + * It's stupid, but the obvious way of calculating the upper + * bound via `2 ^ n - 1` overflows. + */ + for (size_t i = 0; i < opt->precision * 8; i++) + upper_bound |= ((uintmax_t) 1 << i); + if (unset) { - *(unsigned long *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(unsigned long *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!git_parse_ulong(arg, opt->value)) + } else if (!git_parse_ulong(arg, &value)) { return error(_("%s expects a non-negative integer value" " with an optional k/m/g suffix"), optname(opt, flags)); - return 0; + } + + if (value > upper_bound) + return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), + (uintmax_t) value, optname(opt, flags), upper_bound); + + switch (opt->precision) { + case 1: + *(uint8_t *)opt->value = value; + return 0; + case 2: + *(uint16_t *)opt->value = value; + return 0; + case 4: + *(uint32_t *)opt->value = value; + return 0; + case 8: + *(uint64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } default: BUG("opt->type %d should not happen", opt->type); diff --git a/parse-options.h b/parse-options.h index 8d5f9c95f9c..4b561679581 100644 --- a/parse-options.h +++ b/parse-options.h @@ -281,6 +281,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = PARSE_OPT_NONEG, \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index b1275dfade4..46deb4317ef 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; + uint16_t m16 = 0; int16_t i16 = 0; struct option options[] = { @@ -143,6 +144,7 @@ int cmd__parse_options(int argc, const char **argv) OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), + OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), 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), @@ -214,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); show(&expect, &ret, "magnitude: %lu", magnitude); + show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); show(&expect, &ret, "abbrev: %d", abbrev); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index e3ca7a27738..5f503b26cc8 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -25,6 +25,7 @@ usage: test-tool parse-options <options> --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude + --m16 <n> get a 16 bit magnitude --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) @@ -139,6 +140,7 @@ boolean: 2 integer: 1729 i16: 0 magnitude: 16384 +m16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -160,6 +162,7 @@ boolean: 2 integer: 1729 i16: 9000 magnitude: 16384 +m16: 32768 timestamp: 0 string: 321 abbrev: 10 @@ -171,7 +174,7 @@ EOF test_expect_success 'long options' ' test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ - --boolean --string2=321 --verbose --verbose --no-dry-run \ + --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && test_must_be_empty output.err && @@ -184,6 +187,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' integer: 0 i16: 0 magnitude: 0 + m16: 0 timestamp: 0 string: (not set) abbrev: 100 @@ -259,6 +263,7 @@ boolean: 1 integer: 13 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -283,6 +288,7 @@ boolean: 0 integer: 2 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -351,6 +357,7 @@ boolean: 5 integer: 4 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -377,6 +384,7 @@ boolean: 1 integer: 23 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -457,6 +465,7 @@ boolean: 0 integer: 0 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -804,4 +813,11 @@ test_expect_success 'i16 limits range' ' test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err ' +test_expect_success 'm16 limits range' ' + test-tool parse-options --m16 65535 >out && + test_grep "m16: 65535" out && + test_must_fail test-tool parse-options --m16 65536 2>err && + test_grep "value 65536 for option .m16. exceeds 65535" err +' + test_done -- 2.49.0.604.gff1f9ca942.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH 4/5] parse-options: introduce `OPTION_UNSIGNED` 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (2 preceding siblings ...) 2025-04-01 15:01 ` [PATCH 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt @ 2025-04-01 15:01 ` Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 5/5] parse-options: detect mismatches in integer signedness Patrick Steinhardt ` (3 subsequent siblings) 7 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-01 15:01 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King We have two generic ways to parse integers in the "parse-options" subsytem: - `OPTION_INTEGER` parses a signed integer. - `OPTION_MAGNITUDE` parses an unsigned integer, but it also interprets suffixes like "k" or "g". Notably missing is a middle ground that parses unsigned integers without interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to plug this gap. This option type will be used in subsequent commits. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 50 +++++++++++++++++++++++++++++++++++++++++++ parse-options.h | 12 +++++++++++ t/helper/test-parse-options.c | 4 +++- t/t0040-parse-options.sh | 18 +++++++++++++++- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/parse-options.c b/parse-options.c index 3954ee0e570..fcc415b43fb 100644 --- a/parse-options.c +++ b/parse-options.c @@ -216,6 +216,56 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, optname(opt, flags)); } } + case OPTION_UNSIGNED: + { + uintmax_t upper_bound = 0; + uintmax_t value; + + /* + * It's stupid, but the obvious way of calculating the upper + * bound via `2 ^ n - 1` overflows. + */ + for (size_t i = 0; i < opt->precision * 8; i++) + upper_bound |= ((uintmax_t) 1 << i); + + if (unset) { + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { + return -1; + } else if (!*arg) { + return error(_("%s expects a numerical value"), + optname(opt, flags)); + } else { + value = strtoumax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); + } + + if (value > upper_bound) + return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), + value, optname(opt, flags), upper_bound); + + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; + return 0; + case 2: + *(int16_t *)opt->value = value; + return 0; + case 4: + *(int32_t *)opt->value = value; + return 0; + case 8: + *(int64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } case OPTION_MAGNITUDE: { uintmax_t upper_bound = 0; diff --git a/parse-options.h b/parse-options.h index 4b561679581..20ea7d2ab13 100644 --- a/parse-options.h +++ b/parse-options.h @@ -25,6 +25,7 @@ enum parse_opt_type { /* options with arguments (usually) */ OPTION_STRING, OPTION_INTEGER, + OPTION_UNSIGNED, OPTION_MAGNITUDE, OPTION_CALLBACK, OPTION_LOWLEVEL_CALLBACK, @@ -224,6 +225,16 @@ struct option { .help = (h), \ .flags = (f), \ } +#define OPT_UNSIGNED_F(s, l, v, h, f) { \ + .type = OPTION_UNSIGNED, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v), \ + .precision = sizeof(*v), \ + .argh = N_("n"), \ + .help = (h), \ + .flags = (f), \ +} #define OPT_END() { \ .type = OPTION_END, \ @@ -276,6 +287,7 @@ struct option { #define OPT_CMDMODE(s, l, v, h, i) OPT_CMDMODE_F(s, l, v, h, i, 0) #define OPT_INTEGER(s, l, v, h) OPT_INTEGER_F(s, l, v, h, 0) +#define OPT_UNSIGNED(s, l, v, h) OPT_UNSIGNED_F(s, l, v, h, 0) #define OPT_MAGNITUDE(s, l, v, h) { \ .type = OPTION_MAGNITUDE, \ .short_name = (s), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 46deb4317ef..0d559288d9c 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; - uint16_t m16 = 0; + uint16_t m16 = 0, u16 = 0; int16_t i16 = 0; struct option options[] = { @@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP(""), OPT_INTEGER('i', "integer", &integer, "get a integer"), OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), + OPT_UNSIGNED(0, "u16", &u16, "get a 16 bit unsigned integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), @@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); + show(&expect, &ret, "u16: %"PRIuMAX, (uintmax_t) u16); show(&expect, &ret, "magnitude: %lu", magnitude); show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 5f503b26cc8..9946e69f586 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -23,6 +23,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer --[no-]i16 <n> get a 16 bit integer + --[no-]u16 <n> get a 16 bit unsigned integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude --m16 <n> get a 16 bit magnitude @@ -139,6 +140,7 @@ cat >expect <<\EOF boolean: 2 integer: 1729 i16: 0 +u16: 0 magnitude: 16384 m16: 0 timestamp: 0 @@ -161,6 +163,7 @@ cat >expect <<\EOF boolean: 2 integer: 1729 i16: 9000 +u16: 5432 magnitude: 16384 m16: 32768 timestamp: 0 @@ -173,7 +176,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \ --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' boolean: 0 integer: 0 i16: 0 + u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -262,6 +266,7 @@ cat >expect <<\EOF boolean: 1 integer: 13 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -287,6 +292,7 @@ cat >expect <<\EOF boolean: 0 integer: 2 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -356,6 +362,7 @@ Callback: "four", 0 boolean: 5 integer: 4 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -383,6 +390,7 @@ cat >expect <<\EOF boolean: 1 integer: 23 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -464,6 +472,7 @@ cat >expect <<\EOF boolean: 0 integer: 0 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -820,4 +829,11 @@ test_expect_success 'm16 limits range' ' test_grep "value 65536 for option .m16. exceeds 65535" err ' +test_expect_success 'u16 limits range' ' + test-tool parse-options --u16 65535 >out && + test_grep "u16: 65535" out && + test_must_fail test-tool parse-options --u16 65536 2>err && + test_grep "value 65536 for option .u16. exceeds 65535" err +' + test_done -- 2.49.0.604.gff1f9ca942.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH 5/5] parse-options: detect mismatches in integer signedness 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (3 preceding siblings ...) 2025-04-01 15:01 ` [PATCH 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt @ 2025-04-01 15:01 ` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (2 subsequent siblings) 7 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-01 15:01 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King It was reported that "t5620-backfill.sh" fails on s390x and sparc64 in a test that exercises the "--min-batch-size" command line option. The symptom was that the option didn't seem to have an effect: we didn't fetch objects with a batch size of 20, but instead fetched all objects at once. As it turns out, the root cause is that `--min-batch-size` uses `OPT_INTEGER()` to parse the command line option. While this macro expects the caller to pass a pointer to an integer, we instead pass a pointer to a `size_t`. This coincidentally works on most platforms, but it breaks apart on the mentioned platforms because they are big endian. This issue isn't specific to git-backfill(1): there are a couple of other places where we have the same type confusion going on. This indicates that the issue really is the interface that the parse-options subsystem provides -- it is simply too easy to get this wrong as there isn't any kind of compiler warning, and things just work on the most common systems. Address the systemic issue by introducing two new build asserts `BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names already hint at, those macros will cause a compiler error when passed a value that is not signed or unsigned, respectively. Adapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to use those asserts. This uncovers a small set of sites where we indeed have the same bug as in git-backfill(1). Adapt all of them to use the correct option. Reported-by: Todd Zullinger <tmz@pobox.com> Reported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de> Helped-by: SZEDER Gábor <szeder.dev@gmail.com> Helped-by: Jeff King <peff@peff.net> Signed-off-by: Patrick Steinhardt <ps@pks.im> --- apply.c | 4 ++-- builtin/backfill.c | 4 ++-- builtin/column.c | 2 +- builtin/grep.c | 4 ++-- git-compat-util.h | 7 +++++++ parse-options.h | 6 +++--- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apply.c b/apply.c index f274a379487..a850c7d75fe 100644 --- a/apply.c +++ b/apply.c @@ -5123,8 +5123,8 @@ int apply_parse_options(int argc, const char **argv, /* Think twice before adding "--nul" synonym to this */ OPT_SET_INT('z', NULL, &state->line_termination, N_("paths are separated with NUL character"), '\0'), - OPT_INTEGER('C', NULL, &state->p_context, - N_("ensure at least <n> lines of context match")), + OPT_UNSIGNED('C', NULL, &state->p_context, + N_("ensure at least <n> lines of context match")), OPT_CALLBACK(0, "whitespace", state, N_("action"), N_("detect new or modified lines that have whitespace errors"), apply_option_parse_whitespace), diff --git a/builtin/backfill.c b/builtin/backfill.c index 33e1ea2f84f..d95d7a2d4d6 100644 --- a/builtin/backfill.c +++ b/builtin/backfill.c @@ -123,8 +123,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit .sparse = 0, }; struct option options[] = { - OPT_INTEGER(0, "min-batch-size", &ctx.min_batch_size, - N_("Minimum number of objects to request at a time")), + OPT_UNSIGNED(0, "min-batch-size", &ctx.min_batch_size, + N_("Minimum number of objects to request at a time")), OPT_BOOL(0, "sparse", &ctx.sparse, N_("Restrict the missing objects to the current sparse-checkout")), OPT_END(), diff --git a/builtin/column.c b/builtin/column.c index 50314cc2559..ce6443d5fac 100644 --- a/builtin/column.c +++ b/builtin/column.c @@ -31,7 +31,7 @@ int cmd_column(int argc, struct option options[] = { OPT_STRING(0, "command", &real_command, N_("name"), N_("lookup config vars")), OPT_COLUMN(0, "mode", &colopts, N_("layout to use")), - OPT_INTEGER(0, "raw-mode", &colopts, N_("layout to use")), + OPT_UNSIGNED(0, "raw-mode", &colopts, N_("layout to use")), OPT_INTEGER(0, "width", &copts.width, N_("maximum width")), OPT_STRING(0, "indent", &copts.indent, N_("string"), N_("padding space on left border")), OPT_STRING(0, "nl", &copts.nl, N_("string"), N_("padding space on right border")), diff --git a/builtin/grep.c b/builtin/grep.c index c4869733e1b..f23a6f1dc86 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -983,9 +983,9 @@ int cmd_grep(int argc, OPT_CALLBACK('C', "context", &opt, N_("n"), N_("show <n> context lines before and after matches"), context_callback), - OPT_INTEGER('B', "before-context", &opt.pre_context, + OPT_UNSIGNED('B', "before-context", &opt.pre_context, N_("show <n> context lines before matches")), - OPT_INTEGER('A', "after-context", &opt.post_context, + OPT_UNSIGNED('A', "after-context", &opt.post_context, N_("show <n> context lines after matches")), OPT_INTEGER(0, "threads", &num_threads, N_("use <n> worker threads")), diff --git a/git-compat-util.h b/git-compat-util.h index cf733b38acd..1218fcf81a4 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -110,12 +110,19 @@ DISABLE_WARNING(-Wsign-compare) # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(__builtin_types_compatible_p(__typeof__(*(dst)), \ __typeof__(*(src)))) + +# define BARF_UNLESS_SIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) < 0) +# define BARF_UNLESS_UNSIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) > 0) #else # define BARF_UNLESS_AN_ARRAY(arr) 0 # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(0 ? ((*(dst) = *(src)), 0) : \ sizeof(*(dst)) == sizeof(*(src))) + +# define BARF_UNLESS_SIGNED(var) 0 +# define BARF_UNLESS_UNSIGNED(var) 0 #endif + /* * ARRAY_SIZE - get the number of elements in a visible array * @x: the array whose size you want. diff --git a/parse-options.h b/parse-options.h index 20ea7d2ab13..7b7c9d901cb 100644 --- a/parse-options.h +++ b/parse-options.h @@ -219,7 +219,7 @@ struct option { .type = OPTION_INTEGER, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_SIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ @@ -229,7 +229,7 @@ struct option { .type = OPTION_UNSIGNED, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ @@ -292,7 +292,7 @@ struct option { .type = OPTION_MAGNITUDE, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ -- 2.49.0.604.gff1f9ca942.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v2 0/5] parse-options: harden handling of integer values 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (4 preceding siblings ...) 2025-04-01 15:01 ` [PATCH 5/5] parse-options: detect mismatches in integer signedness Patrick Steinhardt @ 2025-04-15 12:14 ` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 1/5] global: use designated initializers for options Patrick Steinhardt ` (4 more replies) 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt 7 siblings, 5 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-15 12:14 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King Hi, this patch series addresses the issues raised in [1] and [2]. As discussed in [1], the series also introduces a couple of safeguards to make it harder to misuse `OPT_INTEGER()` and `OPT_MAGNITUDE()`: - We now track the precision of the underlying integer types. This makes it possible to pass arbitrarily-sized integers to those options, not only `int` and `unsigned long`, respectively. - We introduce a build assert to verify that the passed variable has correct signedness. Furthermore, the series introduces `OPT_UNSIGNED()` to adapt all callsites that previously used variables with the wrong signedness. Changes in v2: - Adapt computation of upper bounds to use similar logic to `maximum_signed_value_of_type()`. - Link to v1: https://lore.kernel.org/r/20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im Thanks! Patrick [1]: <89257ab82cd60d135cce02d51eacee7ec35c1c37.camel@physik.fu-berlin.de> [2]: <Z8HW6petWuMRWSXf@teonanacatl.net> --- Patrick Steinhardt (5): global: use designated initializers for options parse-options: introduce precision handling for `OPTION_INTEGER` parse-options: introduce precision handling for `OPTION_MAGNITUDE` parse-options: introduce `OPTION_UNSIGNED` parse-options: detect mismatches in integer signedness apply.c | 4 +- archive.c | 35 ++++++++--- builtin/am.c | 28 ++++++--- builtin/backfill.c | 4 +- builtin/clone.c | 13 ++++- builtin/column.c | 2 +- builtin/commit-tree.c | 12 +++- builtin/commit.c | 62 +++++++++++++++----- builtin/config.c | 13 ++++- builtin/describe.c | 24 ++++++-- builtin/fetch.c | 10 +++- builtin/fmt-merge-msg.c | 27 ++++++--- builtin/gc.c | 12 +++- builtin/grep.c | 18 ++++-- builtin/init-db.c | 13 +++-- builtin/ls-remote.c | 11 +++- builtin/merge.c | 38 +++++++++--- builtin/read-tree.c | 11 +++- builtin/rebase.c | 25 ++++++-- builtin/revert.c | 12 +++- builtin/show-branch.c | 13 ++++- builtin/tag.c | 24 ++++++-- builtin/update-index.c | 131 +++++++++++++++++++++++++++++------------- builtin/write-tree.c | 12 ++-- diff.c | 13 +++-- git-compat-util.h | 7 +++ parse-options.c | 131 +++++++++++++++++++++++++++++++++++------- parse-options.h | 23 +++++++- ref-filter.h | 15 +++-- t/helper/test-parse-options.c | 46 ++++++++++++--- t/t0040-parse-options.sh | 57 +++++++++++++++++- 31 files changed, 657 insertions(+), 189 deletions(-) Range-diff versus v1: 1: 632c627b9ba = 1: 92d1cbd1153 global: use designated initializers for options 2: 65c450a4395 ! 2: 8d2400d7470 parse-options: introduce precision handling for `OPTION_INTEGER` @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ } case OPTION_INTEGER: + { -+ intmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1); ++ intmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision); + intmax_t lower_bound = -upper_bound - 1; + intmax_t value; + 3: 4946ca0f702 ! 3: a9b0d8c1127 parse-options: introduce precision handling for `OPTION_MAGNITUDE` @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ } case OPTION_MAGNITUDE: + { -+ uintmax_t upper_bound = 0; ++ uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + unsigned long value; -+ -+ /* -+ * It's stupid, but the obvious way of calculating the upper -+ * bound via `2 ^ n - 1` overflows. -+ */ -+ for (size_t i = 0; i < opt->precision * 8; i++) -+ upper_bound |= ((uintmax_t) 1 << i); + if (unset) { - *(unsigned long *)opt->value = 0; 4: 9728d57d5df ! 4: 743afbf539e parse-options: introduce `OPTION_UNSIGNED` @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ } + case OPTION_UNSIGNED: + { -+ uintmax_t upper_bound = 0; ++ uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + uintmax_t value; + -+ /* -+ * It's stupid, but the obvious way of calculating the upper -+ * bound via `2 ^ n - 1` overflows. -+ */ -+ for (size_t i = 0; i < opt->precision * 8; i++) -+ upper_bound |= ((uintmax_t) 1 << i); -+ + if (unset) { + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ + } case OPTION_MAGNITUDE: { - uintmax_t upper_bound = 0; + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); ## parse-options.h ## @@ parse-options.h: enum parse_opt_type { 5: 59aacc72548 = 5: 1e42672a439 parse-options: detect mismatches in integer signedness --- base-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff change-id: 20250401-b4-pks-parse-options-integers-9b4bbcf21011 ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v2 1/5] global: use designated initializers for options 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt @ 2025-04-15 12:14 ` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt ` (3 subsequent siblings) 4 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-15 12:14 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King While we expose macros for most of our different option types understood by the "parse-options" subsystem, not every combination of fields that has one as that would otherwise quickly lead to an explosion of macros. Instead, we just initialize structures manually for those variants of fields that don't have a macro. Callsites that open-code these structure initialization don't use designated initializers though and instead just provide values for each of the fields that they want to initialize. This has three significant downsides: - Callsites need to specify all values up to the last field that they care about. This often includes fields that should simply be left at their default zero-initialized state, which adds distraction. - Any reader not deeply familiar with the layout of the structure has a hard time figuring out what the respective initializers mean. - Reordering or introducing new fields in the middle of the structure is impossible without adapting all callsites. Convert all sites to instead use designated initializers, which we have started using in our codebase quite a while ago. This allows us to skip any default-initialized fields, gives the reader context by specifying the field names and allows us to reorder or introduce new fields where we want to. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- archive.c | 35 ++++++++--- builtin/am.c | 28 ++++++--- builtin/clone.c | 13 ++++- builtin/commit-tree.c | 12 +++- builtin/commit.c | 62 +++++++++++++++----- builtin/config.c | 13 ++++- builtin/describe.c | 24 ++++++-- builtin/fetch.c | 10 +++- builtin/fmt-merge-msg.c | 25 +++++--- builtin/gc.c | 12 +++- builtin/grep.c | 14 +++-- builtin/init-db.c | 13 +++-- builtin/ls-remote.c | 11 +++- builtin/merge.c | 37 +++++++++--- builtin/read-tree.c | 11 +++- builtin/rebase.c | 25 ++++++-- builtin/revert.c | 12 +++- builtin/show-branch.c | 12 +++- builtin/tag.c | 23 ++++++-- builtin/update-index.c | 131 +++++++++++++++++++++++++++++------------- builtin/write-tree.c | 12 ++-- diff.c | 13 +++-- ref-filter.h | 15 +++-- t/helper/test-parse-options.c | 38 +++++++++--- 24 files changed, 443 insertions(+), 158 deletions(-) diff --git a/archive.c b/archive.c index 8be4e7ac8db..67bba3cd301 100644 --- a/archive.c +++ b/archive.c @@ -650,20 +650,37 @@ static int parse_archive_args(int argc, const char **argv, OPT_STRING(0, "format", &format, N_("fmt"), N_("archive format")), OPT_STRING(0, "prefix", &base, N_("prefix"), N_("prepend prefix to each pathname in the archive")), - { OPTION_CALLBACK, 0, "add-file", args, N_("file"), - N_("add untracked file to archive"), 0, add_file_cb, - (intptr_t)&base }, - { OPTION_CALLBACK, 0, "add-virtual-file", args, - N_("path:content"), N_("add untracked file to archive"), 0, - add_file_cb, (intptr_t)&base }, + { + .type = OPTION_CALLBACK, + .long_name = "add-file", + .value = args, + .argh = N_("file"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, + { + .type = OPTION_CALLBACK, + .long_name = "add-virtual-file", + .value = args, + .argh = N_("path:content"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, OPT_STRING('o', "output", &output, N_("file"), N_("write the archive to this file")), OPT_BOOL(0, "worktree-attributes", &worktree_attributes, N_("read .gitattributes in working directory")), OPT__VERBOSE(&verbose, N_("report archived files on stderr")), - { OPTION_STRING, 0, "mtime", &mtime_option, N_("time"), - N_("set modification time of archive entries"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "mtime", + .value = &mtime_option, + .argh = N_("time"), + .help = N_("set modification time of archive entries"), + .flags = PARSE_OPT_NONEG, + }, OPT_NUMBER_CALLBACK(&compression_level, N_("set compression level"), number_callback), OPT_GROUP(""), diff --git a/builtin/am.c b/builtin/am.c index 3b61bd4c333..4afb519830f 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -2400,11 +2400,16 @@ int cmd_am(int argc, OPT_CMDMODE(0, "quit", &resume_mode, N_("abort the patching operation but keep HEAD where it is"), RESUME_QUIT), - { OPTION_CALLBACK, 0, "show-current-patch", &resume_mode, - "(diff|raw)", - N_("show the patch being applied"), - PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW }, + { + .type = OPTION_CALLBACK, + .long_name = "show-current-patch", + .value = &resume_mode, + .argh = "(diff|raw)", + .help = N_("show the patch being applied"), + .flags = PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .callback = parse_opt_show_current_patch, + .defval = RESUME_SHOW_PATCH_RAW, + }, OPT_CMDMODE(0, "retry", &resume_mode, N_("try to apply current patch again"), RESUME_APPLY), @@ -2417,9 +2422,16 @@ int cmd_am(int argc, OPT_BOOL(0, "ignore-date", &state.ignore_date, N_("use current timestamp for author date")), OPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate), - { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &state.sign_commit, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_CALLBACK_F(0, "empty", &state.empty_type, "(stop|drop|keep)", N_("how to handle empty patches"), PARSE_OPT_NONEG, am_option_parse_empty), diff --git a/builtin/clone.c b/builtin/clone.c index 88276e5b7ab..9c3547f41e3 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -930,9 +930,16 @@ int cmd_clone(int argc, N_("don't use local hardlinks, always copy")), OPT_BOOL('s', "shared", &option_shared, N_("setup as shared repository")), - { OPTION_CALLBACK, 0, "recurse-submodules", &option_recurse_submodules, - N_("pathspec"), N_("initialize submodules in the clone"), - PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)"." }, + { + .type = OPTION_CALLBACK, + .long_name = "recurse-submodules", + .value = &option_recurse_submodules, + .argh = N_("pathspec"), + .help = N_("initialize submodules in the clone"), + .flags = PARSE_OPT_OPTARG, + .callback = recurse_submodules_cb, + .defval = (intptr_t)".", + }, OPT_ALIAS(0, "recursive", "recurse-submodules"), OPT_INTEGER('j', "jobs", &max_jobs, N_("number of submodules cloned in parallel")), diff --git a/builtin/commit-tree.c b/builtin/commit-tree.c index 38457600a4e..c787133d004 100644 --- a/builtin/commit-tree.c +++ b/builtin/commit-tree.c @@ -111,8 +111,16 @@ int cmd_commit_tree(int argc, OPT_CALLBACK_F('F', NULL, &buffer, N_("file"), N_("read commit log message from file"), PARSE_OPT_NONEG, parse_file_arg_callback), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; int ret; diff --git a/builtin/commit.c b/builtin/commit.c index 2f459682221..66bd91fd523 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1542,17 +1542,34 @@ struct repository *repo UNUSED) STATUS_FORMAT_LONG), OPT_BOOL('z', "null", &s.null_termination, N_("terminate entries with NUL")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, - N_("mode"), - N_("show untracked files, optional modes: all, normal, no. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, - { OPTION_STRING, 0, "ignored", &ignored_arg, - N_("mode"), - N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"traditional" }, - { OPTION_STRING, 0, "ignore-submodules", &ignore_submodule_arg, N_("when"), - N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, + { + .type = OPTION_STRING, + .long_name = "ignored", + .value = &ignored_arg, + .argh = N_("mode"), + .help = N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"traditional", + }, + { + .type = OPTION_STRING, + .long_name = "ignore-submodules", + .value = &ignore_submodule_arg, + .argh = N_("when"), + .help = N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")), OPT_BOOL(0, "no-renames", &no_renames, N_("do not detect renames")), OPT_CALLBACK_F('M', "find-renames", &rename_score_arg, @@ -1688,8 +1705,16 @@ int cmd_commit(int argc, OPT_BOOL('e', "edit", &edit_flag, N_("force edit of commit")), OPT_CLEANUP(&cleanup_arg), OPT_BOOL(0, "status", &include_status, N_("include status in commit message template")), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, /* end commit message options */ OPT_GROUP(N_("Commit contents options")), @@ -1714,7 +1739,16 @@ int cmd_commit(int argc, N_("terminate entries with NUL")), OPT_BOOL(0, "amend", &amend, N_("amend previous commit")), OPT_BOOL(0, "no-post-rewrite", &no_post_rewrite, N_("bypass post-rewrite hook")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, N_("mode"), N_("show untracked files, optional modes: all, normal, no. (Default: all)"), PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), /* end commit contents options */ diff --git a/builtin/config.c b/builtin/config.c index 53a90094e31..f70d6354772 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -131,9 +131,16 @@ struct config_display_options { #define TYPE_COLOR 6 #define TYPE_BOOL_OR_STR 7 -#define OPT_CALLBACK_VALUE(s, l, v, h, i) \ - { OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \ - PARSE_OPT_NONEG, option_parse_type, (i) } +#define OPT_CALLBACK_VALUE(s, l, v, h, i) { \ + .type = OPTION_CALLBACK, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v), \ + .help = (h), \ + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, \ + .callback = option_parse_type, \ + .defval = (i), \ +} static int option_parse_type(const struct option *opt, const char *arg, int unset) diff --git a/builtin/describe.c b/builtin/describe.c index e2e73f3d757..2da9f4fed01 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -601,12 +601,24 @@ int cmd_describe(int argc, N_("do not consider tags matching <pattern>")), OPT_BOOL(0, "always", &always, N_("show abbreviated commit object as fallback")), - {OPTION_STRING, 0, "dirty", &dirty, N_("mark"), - N_("append <mark> on dirty working tree (default: \"-dirty\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-dirty"}, - {OPTION_STRING, 0, "broken", &broken, N_("mark"), - N_("append <mark> on broken working tree (default: \"-broken\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-broken"}, + { + .type = OPTION_STRING, + .long_name = "dirty", + .value = &dirty, + .argh = N_("mark"), + .help = N_("append <mark> on dirty working tree (default: \"-dirty\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-dirty", + }, + { + .type = OPTION_STRING, + .long_name = "broken", + .value = &broken, + .argh = N_("mark"), + .help = N_("append <mark> on broken working tree (default: \"-broken\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-broken", + }, OPT_END(), }; diff --git a/builtin/fetch.c b/builtin/fetch.c index 02af5054690..3a5159d9e69 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2359,8 +2359,14 @@ int cmd_fetch(int argc, OPT_SET_INT_F(0, "refetch", &refetch, N_("re-fetch without negotiating common commits"), 1, PARSE_OPT_NONEG), - { OPTION_STRING, 0, "submodule-prefix", &submodule_prefix, N_("dir"), - N_("prepend this to submodule path output"), PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "submodule-prefix", + .value = &submodule_prefix, + .argh = N_("dir"), + .help = N_("prepend this to submodule path output"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_CALLBACK_F(0, "recurse-submodules-default", &recurse_submodules_default, N_("on-demand"), N_("default for recursive fetching of submodules " diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 189cd1096a0..240cdb474bc 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -20,13 +20,24 @@ int cmd_fmt_merge_msg(int argc, char *into_name = NULL; int shortlog_len = -1; struct option options[] = { - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("populate log with at most <n> entries from shortlog"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, - { OPTION_INTEGER, 0, "summary", &shortlog_len, N_("n"), - N_("alias for --log (deprecated)"), - PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL, - DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("populate log with at most <n> entries from shortlog"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, + { + .type = OPTION_INTEGER, + .long_name = "summary", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("alias for --log (deprecated)"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_STRING('m', "message", &message, N_("text"), N_("use <text> as start of message")), OPT_STRING(0, "into-name", &into_name, N_("name"), diff --git a/builtin/gc.c b/builtin/gc.c index 99431fd4674..6707a26bc6e 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -699,9 +699,15 @@ struct repository *repo UNUSED) int ret; struct option builtin_gc_options[] = { OPT__QUIET(&quiet, N_("suppress progress reporting")), - { OPTION_STRING, 0, "prune", &prune_expire_arg, N_("date"), - N_("prune unreferenced objects"), - PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg }, + { + .type = OPTION_STRING, + .long_name = "prune", + .value = &prune_expire_arg, + .argh = N_("date"), + .help = N_("prune unreferenced objects"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)prune_expire_arg, + }, OPT_BOOL(0, "cruft", &cfg.cruft_packs, N_("pack unreferenced objects separately")), OPT_MAGNITUDE(0, "max-cruft-size", &cfg.max_cruft_size, N_("with --cruft, limit the size of new cruft packs")), diff --git a/builtin/grep.c b/builtin/grep.c index d1427290f77..c4869733e1b 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -1017,10 +1017,16 @@ int cmd_grep(int argc, OPT_BOOL(0, "all-match", &opt.all_match, N_("show only matches from files that match all patterns")), OPT_GROUP(""), - { OPTION_STRING, 'O', "open-files-in-pager", &show_in_pager, - N_("pager"), N_("show matching files in the pager"), - PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, - NULL, (intptr_t)default_pager }, + { + .type = OPTION_STRING, + .short_name = 'O', + .long_name = "open-files-in-pager", + .value = &show_in_pager, + .argh = N_("pager"), + .help = N_("show matching files in the pager"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, + .defval = (intptr_t)default_pager, + }, OPT_BOOL_F(0, "ext-grep", &external_grep_allowed__ignored, N_("allow calling of grep(1) (ignored by this build)"), PARSE_OPT_NOCOMPLETE), diff --git a/builtin/init-db.c b/builtin/init-db.c index 196dccdd77a..4a950e44d8d 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -93,10 +93,15 @@ int cmd_init_db(int argc, N_("directory from which templates will be used")), OPT_SET_INT(0, "bare", &is_bare_repository_cfg, N_("create a bare repository"), 1), - { OPTION_CALLBACK, 0, "shared", &init_shared_repository, - N_("permissions"), - N_("specify that the git repository is to be shared amongst several users"), - PARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0}, + { + .type = OPTION_CALLBACK, + .long_name = "shared", + .value = &init_shared_repository, + .argh = N_("permissions"), + .help = N_("specify that the git repository is to be shared amongst several users"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + .callback = shared_callback + }, OPT_BIT('q', "quiet", &flags, N_("be quiet"), INIT_DB_QUIET), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), N_("separate git dir from working tree")), diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c index 42f34e12361..01a4d4daa1f 100644 --- a/builtin/ls-remote.c +++ b/builtin/ls-remote.c @@ -67,9 +67,14 @@ int cmd_ls_remote(int argc, OPT__QUIET(&quiet, N_("do not print remote URL")), OPT_STRING(0, "upload-pack", &uploadpack, N_("exec"), N_("path of git-upload-pack on the remote host")), - { OPTION_STRING, 0, "exec", &uploadpack, N_("exec"), - N_("path of git-upload-pack on the remote host"), - PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "exec", + .value = &uploadpack, + .argh = N_("exec"), + .help = N_("path of git-upload-pack on the remote host"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_BIT('t', "tags", &flags, N_("limit to tags"), REF_TAGS), OPT_BIT('b', "branches", &flags, N_("limit to branches"), REF_BRANCHES), OPT_BIT_F('h', "heads", &flags, diff --git a/builtin/merge.c b/builtin/merge.c index ba9faf126aa..21787d45165 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -250,9 +250,15 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "stat", &show_diffstat, N_("show a diffstat at the end of the merge")), OPT_BOOL(0, "summary", &show_diffstat, N_("(synonym to --stat)")), - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("add (at most <n>) entries from shortlog to merge commit message"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("add (at most <n>) entries from shortlog to merge commit message"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_BOOL(0, "squash", &squash, N_("create a single commit instead of doing a merge")), OPT_BOOL(0, "commit", &option_commit, @@ -274,9 +280,16 @@ static struct option builtin_merge_options[] = { OPT_CALLBACK('m', "message", &merge_msg, N_("message"), N_("merge commit message (for a non-fast-forward merge)"), option_parse_message), - { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"), - N_("read message from file"), PARSE_OPT_NONEG, - NULL, 0, option_read_message }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'F', + .long_name = "file", + .value = &merge_msg, + .argh = N_("path"), + .help = N_("read message from file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = option_read_message, + }, OPT_STRING(0, "into-name", &into_name, N_("name"), N_("use <name> instead of the real target")), OPT__VERBOSITY(&verbosity), @@ -289,8 +302,16 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "allow-unrelated-histories", &allow_unrelated_histories, N_("allow merging unrelated histories")), OPT_SET_INT(0, "progress", &show_progress, N_("force progress reporting"), 1), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&autostash), OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")), OPT_BOOL(0, "signoff", &signoff, N_("add a Signed-off-by trailer")), diff --git a/builtin/read-tree.c b/builtin/read-tree.c index d2a807a828b..a8f352f7cd9 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -135,9 +135,14 @@ int cmd_read_tree(int argc, N_("3-way merge in presence of adds and removes")), OPT_BOOL(0, "reset", &opts.reset, N_("same as -m, but discard unmerged entries")), - { OPTION_STRING, 0, "prefix", &opts.prefix, N_("<subdirectory>/"), - N_("read the tree into the index under <subdirectory>/"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "prefix", + .value = &opts.prefix, + .argh = N_("<subdirectory>/"), + .help = N_("read the tree into the index under <subdirectory>/"), + .flags = PARSE_OPT_NONEG, + }, OPT_BOOL('u', NULL, &opts.update, N_("update working tree with merge result")), OPT_CALLBACK_F(0, "exclude-per-directory", &opts, diff --git a/builtin/rebase.c b/builtin/rebase.c index d4715ed35d7..d4083350090 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1122,9 +1122,15 @@ int cmd_rebase(int argc, OPT_BIT('v', "verbose", &options.flags, N_("display a diffstat of what changed upstream"), REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), - {OPTION_NEGBIT, 'n', "no-stat", &options.flags, NULL, - N_("do not show diffstat of what changed upstream"), - PARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT }, + { + .type = OPTION_NEGBIT, + .short_name = 'n', + .long_name = "no-stat", + .value = &options.flags, + .help = N_("do not show diffstat of what changed upstream"), + .flags = PARSE_OPT_NOARG, + .defval = REBASE_DIFFSTAT, + }, OPT_BOOL(0, "signoff", &options.signoff, N_("add a Signed-off-by trailer to each commit")), OPT_BOOL(0, "committer-date-is-author-date", @@ -1190,9 +1196,16 @@ int cmd_rebase(int argc, OPT_BOOL(0, "update-refs", &options.update_refs, N_("update branches that point to commits " "that are being rebased")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&options.autostash), OPT_STRING_LIST('x', "exec", &options.exec, N_("exec"), N_("add exec lines after each commit of the " diff --git a/builtin/revert.c b/builtin/revert.c index aca6c293cdf..4f5ef975494 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -132,8 +132,16 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, OPT_STRING(0, "strategy", &strategy, N_("strategy"), N_("merge strategy")), OPT_STRVEC('X', "strategy-option", &opts->xopts, N_("option"), N_("option for merge strategy")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; struct option *options = base_options; diff --git a/builtin/show-branch.c b/builtin/show-branch.c index fce6b404e92..dab37019d29 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -667,9 +667,15 @@ int cmd_show_branch(int ac, N_("show remote-tracking branches")), OPT__COLOR(&showbranch_use_color, N_("color '*!+-' corresponding to the branch")), - { OPTION_INTEGER, 0, "more", &extra, N_("n"), - N_("show <n> more commits after the common ancestor"), - PARSE_OPT_OPTARG, NULL, (intptr_t)1 }, + { + .type = OPTION_INTEGER, + .long_name = "more", + .value = &extra, + .argh = N_("n"), + .help = N_("show <n> more commits after the common ancestor"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_SET_INT(0, "list", &extra, N_("synonym to more=-1"), -1), OPT_BOOL(0, "no-name", &no_name, N_("suppress naming strings")), OPT_BOOL(0, "current", &with_current_branch, diff --git a/builtin/tag.c b/builtin/tag.c index d3e0943b734..b266f12bb48 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -479,9 +479,15 @@ int cmd_tag(int argc, int edit_flag = 0; struct option options[] = { OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'), - { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"), - N_("print <n> lines of each tag message"), - PARSE_OPT_OPTARG, NULL, 1 }, + { + .type = OPTION_INTEGER, + .short_name = 'n', + .value = &filter.lines, + .argh = N_("n"), + .help = N_("print <n> lines of each tag message"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_CMDMODE('d', "delete", &cmdmode, N_("delete tags"), 'd'), OPT_CMDMODE('v', "verify", &cmdmode, N_("verify tags"), 'v'), @@ -513,9 +519,14 @@ int cmd_tag(int argc, N_("do not output a newline after empty formatted refs")), OPT_REF_SORT(&sorting_options), { - OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), - N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT, - parse_opt_object_name, (intptr_t) "HEAD" + .type = OPTION_CALLBACK, + .long_name = "points-at", + .value = &filter.points_at, + .argh = N_("object"), + .help = N_("print only tags of the object"), + .flags = PARSE_OPT_LASTARG_DEFAULT, + .callback = parse_opt_object_name, + .defval = (intptr_t) "HEAD", }, OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), diff --git a/builtin/update-index.c b/builtin/update-index.c index b2f6b1a3fbb..ee64b022679 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -964,29 +964,51 @@ int cmd_update_index(int argc, N_("like --refresh, but ignore assume-unchanged setting"), PARSE_OPT_NOARG | PARSE_OPT_NONEG, really_refresh_callback), - {OPTION_LOWLEVEL_CALLBACK, 0, "cacheinfo", NULL, - N_("<mode>,<object>,<path>"), - N_("add the specified entry to the index"), - PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ - PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - NULL, 0, - cacheinfo_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "cacheinfo", + .argh = N_("<mode>,<object>,<path>"), + .help = N_("add the specified entry to the index"), + .flags = PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ + PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .ll_callback = cacheinfo_callback, + }, OPT_CALLBACK_F(0, "chmod", &set_executable_bit, "(+|-)x", N_("override the executable bit of the listed files"), PARSE_OPT_NONEG, chmod_callback), - {OPTION_SET_INT, 0, "assume-unchanged", &mark_valid_only, NULL, - N_("mark files as \"not changing\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-assume-unchanged", &mark_valid_only, NULL, - N_("clear assumed-unchanged bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, - {OPTION_SET_INT, 0, "skip-worktree", &mark_skip_worktree_only, NULL, - N_("mark files as \"index-only\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-skip-worktree", &mark_skip_worktree_only, NULL, - N_("clear skip-worktree bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "assume-unchanged", + .value = &mark_valid_only, + .help = N_("mark files as \"not changing\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-assume-unchanged", + .value = &mark_valid_only, + .help = N_("clear assumed-unchanged bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("mark files as \"index-only\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("clear skip-worktree bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_BOOL(0, "ignore-skip-worktree-entries", &ignore_skip_worktree_entries, N_("do not touch index-only entries")), OPT_SET_INT(0, "info-only", &info_only, @@ -995,22 +1017,39 @@ int cmd_update_index(int argc, N_("remove named paths even if present in worktree"), 1), OPT_BOOL('z', NULL, &nul_term_line, N_("with --stdin: input lines are terminated by null bytes")), - {OPTION_LOWLEVEL_CALLBACK, 0, "stdin", &read_from_stdin, NULL, - N_("read list of paths to be updated from standard input"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "index-info", &nul_term_line, NULL, - N_("add entries from standard input to the index"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_cacheinfo_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "unresolve", &has_errors, NULL, - N_("repopulate stages #2 and #3 for the listed paths"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, unresolve_callback}, - {OPTION_LOWLEVEL_CALLBACK, 'g', "again", &has_errors, NULL, - N_("only update entries that differ from HEAD"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, reupdate_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "stdin", + .value = &read_from_stdin, + .help = N_("read list of paths to be updated from standard input"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "index-info", + .value = &nul_term_line, + .help = N_("add entries from standard input to the index"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_cacheinfo_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "unresolve", + .value = &has_errors, + .help = N_("repopulate stages #2 and #3 for the listed paths"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = unresolve_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'g', + .long_name = "again", + .value = &has_errors, + .help = N_("only update entries that differ from HEAD"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = reupdate_callback, + }, OPT_BIT(0, "ignore-missing", &refresh_args.flags, N_("ignore files missing from worktree"), REFRESH_IGNORE_MISSING), @@ -1036,12 +1075,22 @@ int cmd_update_index(int argc, N_("write out the index even if is not flagged as changed"), 1), OPT_BOOL(0, "fsmonitor", &fsmonitor, N_("enable or disable file system monitor")), - {OPTION_SET_INT, 0, "fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("mark files as fsmonitor valid"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("clear fsmonitor valid bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("mark files as fsmonitor valid"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("clear fsmonitor valid bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_END() }; diff --git a/builtin/write-tree.c b/builtin/write-tree.c index 43f233e69b0..5a8dc377ec0 100644 --- a/builtin/write-tree.c +++ b/builtin/write-tree.c @@ -31,10 +31,14 @@ int cmd_write_tree(int argc, WRITE_TREE_MISSING_OK), OPT_STRING(0, "prefix", &tree_prefix, N_("<prefix>/"), N_("write tree object for a subdirectory <prefix>")), - { OPTION_BIT, 0, "ignore-cache-tree", &flags, NULL, - N_("only useful for debugging"), - PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL, - WRITE_TREE_IGNORE_CACHE_TREE }, + { + .type = OPTION_BIT, + .long_name = "ignore-cache-tree", + .value = &flags, + .help = N_("only useful for debugging"), + .flags = PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, + .defval = WRITE_TREE_IGNORE_CACHE_TREE, + }, OPT_END() }; diff --git a/diff.c b/diff.c index 08f5e00a2cc..f2fcc7f3c22 100644 --- a/diff.c +++ b/diff.c @@ -5892,10 +5892,15 @@ struct option *add_diff_options(const struct option *opts, OPT_CALLBACK_F(0, "diff-filter", options, N_("[(A|C|D|M|R|T|U|X|B)...[*]]"), N_("select files by diff type"), PARSE_OPT_NONEG, diff_opt_diff_filter), - { OPTION_CALLBACK, 0, "output", options, N_("<file>"), - N_("output to a specific file"), - PARSE_OPT_NONEG, NULL, 0, diff_opt_output }, - + { + .type = OPTION_CALLBACK, + .long_name = "output", + .value = options, + .argh = N_("<file>"), + .help = N_("output to a specific file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = diff_opt_output, + }, OPT_END() }; diff --git a/ref-filter.h b/ref-filter.h index 013d4cfa64b..c98c4fbd4c1 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -114,11 +114,16 @@ struct ref_format { } /* Macros for checking --merged and --no-merged options */ -#define _OPT_MERGED_NO_MERGED(option, filter, h) \ - { OPTION_CALLBACK, 0, option, (filter), N_("commit"), (h), \ - PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ - parse_opt_merge_filter, (intptr_t) "HEAD" \ - } +#define _OPT_MERGED_NO_MERGED(option, filter, h) { \ + .type = OPTION_CALLBACK, \ + .long_name = option, \ + .value = (filter), \ + .argh = N_("commit"), \ + .help = (h), \ + .flags = PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ + .callback = parse_opt_merge_filter, \ + .defval = (intptr_t) "HEAD", \ +} #define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED("merged", f, h) #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h) diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index bfe45ec68b0..997f55fd45b 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -124,8 +124,15 @@ int cmd__parse_options(int argc, const char **argv) struct option options[] = { OPT_BOOL(0, "yes", &boolean, "get a boolean"), OPT_BOOL('D', "no-doubt", &boolean, "begins with 'no-'"), - { OPTION_SET_INT, 'B', "no-fear", &boolean, NULL, - "be brave", PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { + .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), @@ -155,12 +162,27 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP("Magic arguments"), OPT_NUMBER_CALLBACK(&integer, "set integer to NUM", number_callback), - { OPTION_COUNTUP, '+', NULL, &boolean, NULL, "same as -b", - PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH }, - { OPTION_COUNTUP, 0, "ambiguous", &ambiguous, NULL, - "positive ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, - { OPTION_COUNTUP, 0, "no-ambiguous", &ambiguous, NULL, - "negative ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, + { + .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"), -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 1/5] global: use designated initializers for options Patrick Steinhardt @ 2025-04-15 12:14 ` Patrick Steinhardt 2025-04-15 15:51 ` Phillip Wood 2025-04-15 16:59 ` Junio C Hamano 2025-04-15 12:14 ` [PATCH v2 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt ` (2 subsequent siblings) 4 siblings, 2 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-15 12:14 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King 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. Funny enough, even if the caller gets everything correct the parsing logic is still insufficient because we use `strtol()` to parse the argument, which returns a `long`. But as that value is implicitly cast when assigning it to the `int` field we may still get invalid results. 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> --- builtin/fmt-merge-msg.c | 2 ++ builtin/merge.c | 1 + builtin/show-branch.c | 1 + builtin/tag.c | 1 + parse-options.c | 53 +++++++++++++++++++++++++++++++------------ parse-options.h | 6 +++++ t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 23 ++++++++++++++++++- 8 files changed, 75 insertions(+), 15 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 240cdb474bc..3b6aac2cf7f 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("populate log with at most <n> entries from shortlog"), .flags = PARSE_OPT_OPTARG, @@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "summary", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("alias for --log (deprecated)"), .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, diff --git a/builtin/merge.c b/builtin/merge.c index 21787d45165..9ab10c7db0a 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -254,6 +254,7 @@ static struct option builtin_merge_options[] = { .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("add (at most <n>) entries from shortlog to merge commit message"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/show-branch.c b/builtin/show-branch.c index dab37019d29..b549d8c3f5b 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -671,6 +671,7 @@ int cmd_show_branch(int ac, .type = OPTION_INTEGER, .long_name = "more", .value = &extra, + .precision = sizeof(extra), .argh = N_("n"), .help = N_("show <n> more commits after the common ancestor"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/tag.c b/builtin/tag.c index b266f12bb48..7597d93c71b 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -483,6 +483,7 @@ int cmd_tag(int argc, .type = OPTION_INTEGER, .short_name = 'n', .value = &filter.lines, + .precision = sizeof(filter.lines), .argh = N_("n"), .help = N_("print <n> lines of each tag message"), .flags = PARSE_OPT_OPTARG, diff --git a/parse-options.c b/parse-options.c index 35fbb3b0d63..6ddebc20516 100644 --- a/parse-options.c +++ b/parse-options.c @@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, return (*opt->ll_callback)(p, opt, p_arg, p_unset); } case OPTION_INTEGER: + { + intmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision); + intmax_t lower_bound = -upper_bound - 1; + intmax_t value; + if (unset) { - *(int *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(int *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!*arg) + } else if (!*arg) { return error(_("%s expects a numerical value"), optname(opt, flags)); - *(int *)opt->value = strtol(arg, (char **)&s, 10); - if (*s) - return error(_("%s expects a numerical value"), - optname(opt, flags)); - return 0; + } else { + value = strtoimax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); + + } + if (value < lower_bound || value > upper_bound) + return error(_("value %"PRIdMAX" for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + value, optname(opt, flags), lower_bound, upper_bound); + + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; + return 0; + case 2: + *(int16_t *)opt->value = value; + return 0; + case 4: + *(int32_t *)opt->value = value; + return 0; + case 8: + *(int64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } case OPTION_MAGNITUDE: if (unset) { *(unsigned long *)opt->value = 0; diff --git a/parse-options.h b/parse-options.h index 997ffbee805..8d5f9c95f9c 100644 --- a/parse-options.h +++ b/parse-options.h @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, * `value`:: * stores pointers to the values to be filled. * + * `precision`:: + * precision of the integer pointed to by `value`. Should typically be its + * `sizeof()`. + * * `argh`:: * token to explain the kind of argument this option wants. Does not * begin in capital letter, and does not end with a full stop. @@ -151,6 +155,7 @@ struct option { int short_name; const char *long_name; void *value; + size_t precision; const char *argh; const char *help; @@ -214,6 +219,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = (f), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 997f55fd45b..b1275dfade4 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; 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"), @@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv) 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_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23), @@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv) } show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); + show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); show(&expect, &ret, "magnitude: %lu", magnitude); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 2fe3522305f..e3ca7a27738 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -22,6 +22,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer + --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude --[no-]set23 set integer to 23 @@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 0 magnitude: 16384 timestamp: 0 string: 123 @@ -156,6 +158,7 @@ test_expect_success 'short options' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 9000 magnitude: 16384 timestamp: 0 string: 321 @@ -167,7 +170,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' cat >expect <<-EOF && boolean: 0 integer: 0 + i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' ' cat >expect <<\EOF boolean: 1 integer: 13 +i16: 0 magnitude: 0 timestamp: 0 string: 123 @@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' ' cat >expect <<\EOF boolean: 0 integer: 2 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -343,6 +349,7 @@ cat >expect <<\EOF Callback: "four", 0 boolean: 5 integer: 4 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' ' cat >expect <<\EOF boolean: 1 integer: 23 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' ' cat >expect <<\EOF boolean: 0 integer: 0 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -783,4 +792,16 @@ test_expect_success 'magnitude with units but no numbers' ' test_must_be_empty out ' +test_expect_success 'i16 limits range' ' + test-tool parse-options --i16 32767 >out && + test_grep "i16: 32767" out && + test_must_fail test-tool parse-options --i16 32768 2>err && + test_grep "value 32768 for option .i16. not in range \[-32768,32767\]" err && + + test-tool parse-options --i16 -32768 >out && + test_grep "i16: -32768" out && + test_must_fail test-tool parse-options --i16 -32769 2>err && + test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-15 12:14 ` [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt @ 2025-04-15 15:51 ` Phillip Wood 2025-04-16 10:28 ` Patrick Steinhardt 2025-04-15 16:59 ` Junio C Hamano 1 sibling, 1 reply; 46+ messages in thread From: Phillip Wood @ 2025-04-15 15:51 UTC (permalink / raw) To: Patrick Steinhardt, git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King Hi Patrick On 15/04/2025 13:14, Patrick Steinhardt wrote: > > 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`. Nice, this is a really useful improvement. I've left one comment below > 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. > > + } else { > + value = strtoimax(arg, (char **)&s, 10); > + if (*s) > + return error(_("%s expects a numerical value"), > + optname(opt, flags)); To catch overflow errors for arguments of intimax_t we need to do errno = 0 value = strtoimax(arg, (Char **)&s, 10); if (errno || *s) return error(...) to catch the error when we parse the string as the checks below only work for narrower types. Best Wishes Phillip > + > + } > > + if (value < lower_bound || value > upper_bound) > + return error(_("value %"PRIdMAX" for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), > + value, optname(opt, flags), lower_bound, upper_bound); > + > + switch (opt->precision) { > + case 1: > + *(int8_t *)opt->value = value; > + return 0; > + case 2: > + *(int16_t *)opt->value = value; > + return 0; > + case 4: > + *(int32_t *)opt->value = value; > + return 0; > + case 8: > + *(int64_t *)opt->value = value; > + return 0; > + default: > + BUG("invalid precision for option %s", > + optname(opt, flags)); > + } > + } > case OPTION_MAGNITUDE: > if (unset) { > *(unsigned long *)opt->value = 0; > diff --git a/parse-options.h b/parse-options.h > index 997ffbee805..8d5f9c95f9c 100644 > --- a/parse-options.h > +++ b/parse-options.h > @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, > * `value`:: > * stores pointers to the values to be filled. > * > + * `precision`:: > + * precision of the integer pointed to by `value`. Should typically be its > + * `sizeof()`. > + * > * `argh`:: > * token to explain the kind of argument this option wants. Does not > * begin in capital letter, and does not end with a full stop. > @@ -151,6 +155,7 @@ struct option { > int short_name; > const char *long_name; > void *value; > + size_t precision; > const char *argh; > const char *help; > > @@ -214,6 +219,7 @@ struct option { > .short_name = (s), \ > .long_name = (l), \ > .value = (v), \ > + .precision = sizeof(*v), \ > .argh = N_("n"), \ > .help = (h), \ > .flags = (f), \ > diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c > index 997f55fd45b..b1275dfade4 100644 > --- a/t/helper/test-parse-options.c > +++ b/t/helper/test-parse-options.c > @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) > }; > 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"), > @@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv) > 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_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), > OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23), > @@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv) > } > show(&expect, &ret, "boolean: %d", boolean); > show(&expect, &ret, "integer: %d", integer); > + show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); > show(&expect, &ret, "magnitude: %lu", magnitude); > show(&expect, &ret, "timestamp: %"PRItime, timestamp); > show(&expect, &ret, "string: %s", string ? string : "(not set)"); > diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh > index 2fe3522305f..e3ca7a27738 100755 > --- a/t/t0040-parse-options.sh > +++ b/t/t0040-parse-options.sh > @@ -22,6 +22,7 @@ usage: test-tool parse-options <options> > > -i, --[no-]integer <n> > get a integer > + --[no-]i16 <n> get a 16 bit integer > -j <n> get a integer, too > -m, --magnitude <n> get a magnitude > --[no-]set23 set integer to 23 > @@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' ' > cat >expect <<\EOF > boolean: 2 > integer: 1729 > +i16: 0 > magnitude: 16384 > timestamp: 0 > string: 123 > @@ -156,6 +158,7 @@ test_expect_success 'short options' ' > cat >expect <<\EOF > boolean: 2 > integer: 1729 > +i16: 9000 > magnitude: 16384 > timestamp: 0 > string: 321 > @@ -167,7 +170,7 @@ file: prefix/fi.le > EOF > > test_expect_success 'long options' ' > - test-tool parse-options --boolean --integer 1729 --magnitude 16k \ > + test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ > --boolean --string2=321 --verbose --verbose --no-dry-run \ > --abbrev=10 --file fi.le --obsolete \ > >output 2>output.err && > @@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' > cat >expect <<-EOF && > boolean: 0 > integer: 0 > + i16: 0 > magnitude: 0 > timestamp: 0 > string: (not set) > @@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' ' > cat >expect <<\EOF > boolean: 1 > integer: 13 > +i16: 0 > magnitude: 0 > timestamp: 0 > string: 123 > @@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' ' > cat >expect <<\EOF > boolean: 0 > integer: 2 > +i16: 0 > magnitude: 0 > timestamp: 0 > string: (not set) > @@ -343,6 +349,7 @@ cat >expect <<\EOF > Callback: "four", 0 > boolean: 5 > integer: 4 > +i16: 0 > magnitude: 0 > timestamp: 0 > string: (not set) > @@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' ' > cat >expect <<\EOF > boolean: 1 > integer: 23 > +i16: 0 > magnitude: 0 > timestamp: 0 > string: (not set) > @@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' ' > cat >expect <<\EOF > boolean: 0 > integer: 0 > +i16: 0 > magnitude: 0 > timestamp: 0 > string: (not set) > @@ -783,4 +792,16 @@ test_expect_success 'magnitude with units but no numbers' ' > test_must_be_empty out > ' > > +test_expect_success 'i16 limits range' ' > + test-tool parse-options --i16 32767 >out && > + test_grep "i16: 32767" out && > + test_must_fail test-tool parse-options --i16 32768 2>err && > + test_grep "value 32768 for option .i16. not in range \[-32768,32767\]" err && > + > + test-tool parse-options --i16 -32768 >out && > + test_grep "i16: -32768" out && > + test_must_fail test-tool parse-options --i16 -32769 2>err && > + test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err > +' > + > test_done > ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-15 15:51 ` Phillip Wood @ 2025-04-16 10:28 ` Patrick Steinhardt 0 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:28 UTC (permalink / raw) To: phillip.wood Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King On Tue, Apr 15, 2025 at 04:51:59PM +0100, Phillip Wood wrote: > On 15/04/2025 13:14, Patrick Steinhardt wrote: > > 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. > > > > + } else { > > + value = strtoimax(arg, (char **)&s, 10); > > + if (*s) > > + return error(_("%s expects a numerical value"), > > + optname(opt, flags)); > > To catch overflow errors for arguments of intimax_t we need to do > > errno = 0 > value = strtoimax(arg, (Char **)&s, 10); > if (errno || *s) > return error(...) > > to catch the error when we parse the string as the checks below only work > for narrower types. Fair. This issue isn't new -- it already existed before my patch series. But that's not a good enough reason to not fix it while we're at it. Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-15 12:14 ` [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt 2025-04-15 15:51 ` Phillip Wood @ 2025-04-15 16:59 ` Junio C Hamano 2025-04-16 10:28 ` Patrick Steinhardt 1 sibling, 1 reply; 46+ messages in thread From: Junio C Hamano @ 2025-04-15 16:59 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King Patrick Steinhardt <ps@pks.im> writes: > diff --git a/parse-options.h b/parse-options.h > index 997ffbee805..8d5f9c95f9c 100644 > --- a/parse-options.h > +++ b/parse-options.h > @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, > * `value`:: > * stores pointers to the values to be filled. > * > + * `precision`:: > + * precision of the integer pointed to by `value`. Should typically be its > + * `sizeof()`. The fact of the integer allowing to store up to 16-bit vs 32-bit, is that "precision"? "My --size option runs up to 200,000, what value should I set it to?" is a natural question the readers of this sentence would have in their mind, if we call it "range" or something (which might not be a bad thing to have, but that is totally outside the theme of this topic). In any case, include a phrase "number of bytes" somewhere in the description to make it clear what unit we are counting. Are their common use case where this number is *not* its sizeof() already in the codebase? ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-15 16:59 ` Junio C Hamano @ 2025-04-16 10:28 ` Patrick Steinhardt 0 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:28 UTC (permalink / raw) To: Junio C Hamano Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King On Tue, Apr 15, 2025 at 09:59:12AM -0700, Junio C Hamano wrote: > Patrick Steinhardt <ps@pks.im> writes: > > > diff --git a/parse-options.h b/parse-options.h > > index 997ffbee805..8d5f9c95f9c 100644 > > --- a/parse-options.h > > +++ b/parse-options.h > > @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, > > * `value`:: > > * stores pointers to the values to be filled. > > * > > + * `precision`:: > > + * precision of the integer pointed to by `value`. Should typically be its > > + * `sizeof()`. > > The fact of the integer allowing to store up to 16-bit vs 32-bit, is > that "precision"? "My --size option runs up to 200,000, what value > should I set it to?" is a natural question the readers of this > sentence would have in their mind, if we call it "range" or > something (which might not be a bad thing to have, but that is > totally outside the theme of this topic). > > In any case, include a phrase "number of bytes" somewhere in the > description to make it clear what unit we are counting. Makes sense, will dot. > Are their common use case where this number is *not* its sizeof() > already in the codebase? Yeah, this is something that I thought about quite a lot. I think the "precision" and "range" are somewhat orthogonal to one another: - The range specifies which values are accepted. - The precision specifies the underlying width of the type. Right now I only handle the latter, but the next step here would be to also start handling the former. But even if we also started to handle the range there is merit in tracking both, as there will be usecases where the range would fit into a smaller underlying type, but due to other reasons one still wants to use the bigger underlying type. The next step would thus be to introduce such an optional range so that options can restrict it independent of the precision. "parse-options.c" would in that case learn to have a run-time assert that the specified range fits into the given precision (and signedness). And obviously, it would of course also learn to return an error if the value passed by the user exceeds the range. Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v2 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 1/5] global: use designated initializers for options Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt @ 2025-04-15 12:14 ` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 5/5] parse-options: detect mismatches in integer signedness Patrick Steinhardt 4 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-15 12:14 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King This commit is the equivalent to the preceding commit, but instead of introducing precision handling for `OPTION_INTEGER` we introduce it for `OPTION_MAGNITUDE`. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 43 +++++++++++++++++++++++++++++++++---------- parse-options.h | 1 + t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 18 +++++++++++++++++- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/parse-options.c b/parse-options.c index 6ddebc20516..ae836c384c7 100644 --- a/parse-options.c +++ b/parse-options.c @@ -217,21 +217,44 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, } } case OPTION_MAGNITUDE: + { + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + unsigned long value; + if (unset) { - *(unsigned long *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(unsigned long *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!git_parse_ulong(arg, opt->value)) + } else if (!git_parse_ulong(arg, &value)) { return error(_("%s expects a non-negative integer value" " with an optional k/m/g suffix"), optname(opt, flags)); - return 0; + } + + if (value > upper_bound) + return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), + (uintmax_t) value, optname(opt, flags), upper_bound); + + switch (opt->precision) { + case 1: + *(uint8_t *)opt->value = value; + return 0; + case 2: + *(uint16_t *)opt->value = value; + return 0; + case 4: + *(uint32_t *)opt->value = value; + return 0; + case 8: + *(uint64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } default: BUG("opt->type %d should not happen", opt->type); diff --git a/parse-options.h b/parse-options.h index 8d5f9c95f9c..4b561679581 100644 --- a/parse-options.h +++ b/parse-options.h @@ -281,6 +281,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = PARSE_OPT_NONEG, \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index b1275dfade4..46deb4317ef 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; + uint16_t m16 = 0; int16_t i16 = 0; struct option options[] = { @@ -143,6 +144,7 @@ int cmd__parse_options(int argc, const char **argv) OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), + OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), 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), @@ -214,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); show(&expect, &ret, "magnitude: %lu", magnitude); + show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); show(&expect, &ret, "abbrev: %d", abbrev); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index e3ca7a27738..5f503b26cc8 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -25,6 +25,7 @@ usage: test-tool parse-options <options> --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude + --m16 <n> get a 16 bit magnitude --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) @@ -139,6 +140,7 @@ boolean: 2 integer: 1729 i16: 0 magnitude: 16384 +m16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -160,6 +162,7 @@ boolean: 2 integer: 1729 i16: 9000 magnitude: 16384 +m16: 32768 timestamp: 0 string: 321 abbrev: 10 @@ -171,7 +174,7 @@ EOF test_expect_success 'long options' ' test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ - --boolean --string2=321 --verbose --verbose --no-dry-run \ + --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && test_must_be_empty output.err && @@ -184,6 +187,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' integer: 0 i16: 0 magnitude: 0 + m16: 0 timestamp: 0 string: (not set) abbrev: 100 @@ -259,6 +263,7 @@ boolean: 1 integer: 13 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -283,6 +288,7 @@ boolean: 0 integer: 2 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -351,6 +357,7 @@ boolean: 5 integer: 4 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -377,6 +384,7 @@ boolean: 1 integer: 23 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -457,6 +465,7 @@ boolean: 0 integer: 0 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -804,4 +813,11 @@ test_expect_success 'i16 limits range' ' test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err ' +test_expect_success 'm16 limits range' ' + test-tool parse-options --m16 65535 >out && + test_grep "m16: 65535" out && + test_must_fail test-tool parse-options --m16 65536 2>err && + test_grep "value 65536 for option .m16. exceeds 65535" err +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (2 preceding siblings ...) 2025-04-15 12:14 ` [PATCH v2 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt @ 2025-04-15 12:14 ` Patrick Steinhardt 2025-04-15 15:52 ` Phillip Wood 2025-04-15 17:38 ` René Scharfe 2025-04-15 12:14 ` [PATCH v2 5/5] parse-options: detect mismatches in integer signedness Patrick Steinhardt 4 siblings, 2 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-15 12:14 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King We have two generic ways to parse integers in the "parse-options" subsytem: - `OPTION_INTEGER` parses a signed integer. - `OPTION_MAGNITUDE` parses an unsigned integer, but it also interprets suffixes like "k" or "g". Notably missing is a middle ground that parses unsigned integers without interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to plug this gap. This option type will be used in subsequent commits. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 43 +++++++++++++++++++++++++++++++++++++++++++ parse-options.h | 12 ++++++++++++ t/helper/test-parse-options.c | 4 +++- t/t0040-parse-options.sh | 18 +++++++++++++++++- 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/parse-options.c b/parse-options.c index ae836c384c7..9670e46a679 100644 --- a/parse-options.c +++ b/parse-options.c @@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, optname(opt, flags)); } } + case OPTION_UNSIGNED: + { + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + uintmax_t value; + + if (unset) { + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { + return -1; + } else if (!*arg) { + return error(_("%s expects a numerical value"), + optname(opt, flags)); + } else { + value = strtoumax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); + } + + if (value > upper_bound) + return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), + value, optname(opt, flags), upper_bound); + + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; + return 0; + case 2: + *(int16_t *)opt->value = value; + return 0; + case 4: + *(int32_t *)opt->value = value; + return 0; + case 8: + *(int64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } case OPTION_MAGNITUDE: { uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); diff --git a/parse-options.h b/parse-options.h index 4b561679581..20ea7d2ab13 100644 --- a/parse-options.h +++ b/parse-options.h @@ -25,6 +25,7 @@ enum parse_opt_type { /* options with arguments (usually) */ OPTION_STRING, OPTION_INTEGER, + OPTION_UNSIGNED, OPTION_MAGNITUDE, OPTION_CALLBACK, OPTION_LOWLEVEL_CALLBACK, @@ -224,6 +225,16 @@ struct option { .help = (h), \ .flags = (f), \ } +#define OPT_UNSIGNED_F(s, l, v, h, f) { \ + .type = OPTION_UNSIGNED, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v), \ + .precision = sizeof(*v), \ + .argh = N_("n"), \ + .help = (h), \ + .flags = (f), \ +} #define OPT_END() { \ .type = OPTION_END, \ @@ -276,6 +287,7 @@ struct option { #define OPT_CMDMODE(s, l, v, h, i) OPT_CMDMODE_F(s, l, v, h, i, 0) #define OPT_INTEGER(s, l, v, h) OPT_INTEGER_F(s, l, v, h, 0) +#define OPT_UNSIGNED(s, l, v, h) OPT_UNSIGNED_F(s, l, v, h, 0) #define OPT_MAGNITUDE(s, l, v, h) { \ .type = OPTION_MAGNITUDE, \ .short_name = (s), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 46deb4317ef..0d559288d9c 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; - uint16_t m16 = 0; + uint16_t m16 = 0, u16 = 0; int16_t i16 = 0; struct option options[] = { @@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP(""), OPT_INTEGER('i', "integer", &integer, "get a integer"), OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), + OPT_UNSIGNED(0, "u16", &u16, "get a 16 bit unsigned integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), @@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); + show(&expect, &ret, "u16: %"PRIuMAX, (uintmax_t) u16); show(&expect, &ret, "magnitude: %lu", magnitude); show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 5f503b26cc8..9946e69f586 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -23,6 +23,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer --[no-]i16 <n> get a 16 bit integer + --[no-]u16 <n> get a 16 bit unsigned integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude --m16 <n> get a 16 bit magnitude @@ -139,6 +140,7 @@ cat >expect <<\EOF boolean: 2 integer: 1729 i16: 0 +u16: 0 magnitude: 16384 m16: 0 timestamp: 0 @@ -161,6 +163,7 @@ cat >expect <<\EOF boolean: 2 integer: 1729 i16: 9000 +u16: 5432 magnitude: 16384 m16: 32768 timestamp: 0 @@ -173,7 +176,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \ --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' boolean: 0 integer: 0 i16: 0 + u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -262,6 +266,7 @@ cat >expect <<\EOF boolean: 1 integer: 13 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -287,6 +292,7 @@ cat >expect <<\EOF boolean: 0 integer: 2 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -356,6 +362,7 @@ Callback: "four", 0 boolean: 5 integer: 4 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -383,6 +390,7 @@ cat >expect <<\EOF boolean: 1 integer: 23 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -464,6 +472,7 @@ cat >expect <<\EOF boolean: 0 integer: 0 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -820,4 +829,11 @@ test_expect_success 'm16 limits range' ' test_grep "value 65536 for option .m16. exceeds 65535" err ' +test_expect_success 'u16 limits range' ' + test-tool parse-options --u16 65535 >out && + test_grep "u16: 65535" out && + test_must_fail test-tool parse-options --u16 65536 2>err && + test_grep "value 65536 for option .u16. exceeds 65535" err +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` 2025-04-15 12:14 ` [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt @ 2025-04-15 15:52 ` Phillip Wood 2025-04-16 10:27 ` Patrick Steinhardt 2025-04-15 17:38 ` René Scharfe 1 sibling, 1 reply; 46+ messages in thread From: Phillip Wood @ 2025-04-15 15:52 UTC (permalink / raw) To: Patrick Steinhardt, git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King Hi Patrick On 15/04/2025 13:14, Patrick Steinhardt wrote: > We have two generic ways to parse integers in the "parse-options" > subsytem: > > - `OPTION_INTEGER` parses a signed integer. > > - `OPTION_MAGNITUDE` parses an unsigned integer, but it also > interprets suffixes like "k" or "g". > > Notably missing is a middle ground that parses unsigned integers without > interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to > plug this gap. This option type will be used in subsequent commits. I think this is a useful addition. I wonder about the way it handles negative values though. For types narrower than uintmax_t "-1" will be rejected but large negative values that parse as small positive numbers will be accepted. Perhaps we should explicitly reject strings starting with "-" as we do in git_parse_ulong() which is used by OPTION_MAGNITUDE. This patch also needs the fix from patch 2 to detect overflows for uintmax_t. Best Wishes Phillip [1] https://lore.kernel.org/git/NYMqsJ7uttDzFT2OOEg5LLsxCSoQhTzqBs16KrMHGEKC7LzOAiYnYTEZavRQWqGH41UgjdwScwer7MssNzI7AEDHnD8GTBWvoBIqJ2e7D6g=@proton.me/ > Signed-off-by: Patrick Steinhardt <ps@pks.im> > --- > parse-options.c | 43 +++++++++++++++++++++++++++++++++++++++++++ > parse-options.h | 12 ++++++++++++ > t/helper/test-parse-options.c | 4 +++- > t/t0040-parse-options.sh | 18 +++++++++++++++++- > 4 files changed, 75 insertions(+), 2 deletions(-) > > diff --git a/parse-options.c b/parse-options.c > index ae836c384c7..9670e46a679 100644 > --- a/parse-options.c > +++ b/parse-options.c > @@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, > optname(opt, flags)); > } > } > + case OPTION_UNSIGNED: > + { > + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); > + uintmax_t value; > + > + if (unset) { > + value = 0; > + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { > + value = opt->defval; > + } else if (get_arg(p, opt, flags, &arg)) { > + return -1; > + } else if (!*arg) { > + return error(_("%s expects a numerical value"), > + optname(opt, flags)); > + } else { > + value = strtoumax(arg, (char **)&s, 10); > + if (*s) > + return error(_("%s expects a numerical value"), > + optname(opt, flags)); > + } > + > + if (value > upper_bound) > + return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), > + value, optname(opt, flags), upper_bound); > + > + switch (opt->precision) { > + case 1: > + *(int8_t *)opt->value = value; > + return 0; > + case 2: > + *(int16_t *)opt->value = value; > + return 0; > + case 4: > + *(int32_t *)opt->value = value; > + return 0; > + case 8: > + *(int64_t *)opt->value = value; > + return 0; > + default: > + BUG("invalid precision for option %s", > + optname(opt, flags)); > + } > + } > case OPTION_MAGNITUDE: > { > uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); > diff --git a/parse-options.h b/parse-options.h > index 4b561679581..20ea7d2ab13 100644 > --- a/parse-options.h > +++ b/parse-options.h > @@ -25,6 +25,7 @@ enum parse_opt_type { > /* options with arguments (usually) */ > OPTION_STRING, > OPTION_INTEGER, > + OPTION_UNSIGNED, > OPTION_MAGNITUDE, > OPTION_CALLBACK, > OPTION_LOWLEVEL_CALLBACK, > @@ -224,6 +225,16 @@ struct option { > .help = (h), \ > .flags = (f), \ > } > +#define OPT_UNSIGNED_F(s, l, v, h, f) { \ > + .type = OPTION_UNSIGNED, \ > + .short_name = (s), \ > + .long_name = (l), \ > + .value = (v), \ > + .precision = sizeof(*v), \ > + .argh = N_("n"), \ > + .help = (h), \ > + .flags = (f), \ > +} > > #define OPT_END() { \ > .type = OPTION_END, \ > @@ -276,6 +287,7 @@ struct option { > #define OPT_CMDMODE(s, l, v, h, i) OPT_CMDMODE_F(s, l, v, h, i, 0) > > #define OPT_INTEGER(s, l, v, h) OPT_INTEGER_F(s, l, v, h, 0) > +#define OPT_UNSIGNED(s, l, v, h) OPT_UNSIGNED_F(s, l, v, h, 0) > #define OPT_MAGNITUDE(s, l, v, h) { \ > .type = OPTION_MAGNITUDE, \ > .short_name = (s), \ > diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c > index 46deb4317ef..0d559288d9c 100644 > --- a/t/helper/test-parse-options.c > +++ b/t/helper/test-parse-options.c > @@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv) > }; > struct string_list expect = STRING_LIST_INIT_NODUP; > struct string_list list = STRING_LIST_INIT_NODUP; > - uint16_t m16 = 0; > + uint16_t m16 = 0, u16 = 0; > int16_t i16 = 0; > > struct option options[] = { > @@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv) > OPT_GROUP(""), > OPT_INTEGER('i', "integer", &integer, "get a integer"), > OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), > + OPT_UNSIGNED(0, "u16", &u16, "get a 16 bit unsigned integer"), > OPT_INTEGER('j', NULL, &integer, "get a integer, too"), > OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), > OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), > @@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) > show(&expect, &ret, "boolean: %d", boolean); > show(&expect, &ret, "integer: %d", integer); > show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); > + show(&expect, &ret, "u16: %"PRIuMAX, (uintmax_t) u16); > show(&expect, &ret, "magnitude: %lu", magnitude); > show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); > show(&expect, &ret, "timestamp: %"PRItime, timestamp); > diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh > index 5f503b26cc8..9946e69f586 100755 > --- a/t/t0040-parse-options.sh > +++ b/t/t0040-parse-options.sh > @@ -23,6 +23,7 @@ usage: test-tool parse-options <options> > -i, --[no-]integer <n> > get a integer > --[no-]i16 <n> get a 16 bit integer > + --[no-]u16 <n> get a 16 bit unsigned integer > -j <n> get a integer, too > -m, --magnitude <n> get a magnitude > --m16 <n> get a 16 bit magnitude > @@ -139,6 +140,7 @@ cat >expect <<\EOF > boolean: 2 > integer: 1729 > i16: 0 > +u16: 0 > magnitude: 16384 > m16: 0 > timestamp: 0 > @@ -161,6 +163,7 @@ cat >expect <<\EOF > boolean: 2 > integer: 1729 > i16: 9000 > +u16: 5432 > magnitude: 16384 > m16: 32768 > timestamp: 0 > @@ -173,7 +176,7 @@ file: prefix/fi.le > EOF > > test_expect_success 'long options' ' > - test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ > + test-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \ > --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ > --abbrev=10 --file fi.le --obsolete \ > >output 2>output.err && > @@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' > boolean: 0 > integer: 0 > i16: 0 > + u16: 0 > magnitude: 0 > m16: 0 > timestamp: 0 > @@ -262,6 +266,7 @@ cat >expect <<\EOF > boolean: 1 > integer: 13 > i16: 0 > +u16: 0 > magnitude: 0 > m16: 0 > timestamp: 0 > @@ -287,6 +292,7 @@ cat >expect <<\EOF > boolean: 0 > integer: 2 > i16: 0 > +u16: 0 > magnitude: 0 > m16: 0 > timestamp: 0 > @@ -356,6 +362,7 @@ Callback: "four", 0 > boolean: 5 > integer: 4 > i16: 0 > +u16: 0 > magnitude: 0 > m16: 0 > timestamp: 0 > @@ -383,6 +390,7 @@ cat >expect <<\EOF > boolean: 1 > integer: 23 > i16: 0 > +u16: 0 > magnitude: 0 > m16: 0 > timestamp: 0 > @@ -464,6 +472,7 @@ cat >expect <<\EOF > boolean: 0 > integer: 0 > i16: 0 > +u16: 0 > magnitude: 0 > m16: 0 > timestamp: 0 > @@ -820,4 +829,11 @@ test_expect_success 'm16 limits range' ' > test_grep "value 65536 for option .m16. exceeds 65535" err > ' > > +test_expect_success 'u16 limits range' ' > + test-tool parse-options --u16 65535 >out && > + test_grep "u16: 65535" out && > + test_must_fail test-tool parse-options --u16 65536 2>err && > + test_grep "value 65536 for option .u16. exceeds 65535" err > +' > + > test_done > ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` 2025-04-15 15:52 ` Phillip Wood @ 2025-04-16 10:27 ` Patrick Steinhardt 2025-04-16 13:31 ` phillip.wood123 0 siblings, 1 reply; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:27 UTC (permalink / raw) To: phillip.wood Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King On Tue, Apr 15, 2025 at 04:52:02PM +0100, Phillip Wood wrote: > Hi Patrick > > On 15/04/2025 13:14, Patrick Steinhardt wrote: > > We have two generic ways to parse integers in the "parse-options" > > subsytem: > > > > - `OPTION_INTEGER` parses a signed integer. > > > > - `OPTION_MAGNITUDE` parses an unsigned integer, but it also > > interprets suffixes like "k" or "g". > > > > Notably missing is a middle ground that parses unsigned integers without > > interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to > > plug this gap. This option type will be used in subsequent commits. > > I think this is a useful addition. I wonder about the way it handles > negative values though. For types narrower than uintmax_t "-1" will be > rejected but large negative values that parse as small positive numbers will > be accepted. Perhaps we should explicitly reject strings starting with "-" > as we do in git_parse_ulong() which is used by OPTION_MAGNITUDE. Wait, does it? Why would `strtoul()` or any of its variants ever accept a string prefixed with a "-"? If the minus sign was part of the input sequence, the numeric value calculated from the sequence of digits is negated as if by unary minus in the result type, which applies unsigned integer wraparound rules. Oh dear... all these integer conversion functions are really a gift that keeps on giving. Gross. > This patch also needs the fix from patch 2 to detect overflows for > uintmax_t. Yup, will add. Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` 2025-04-16 10:27 ` Patrick Steinhardt @ 2025-04-16 13:31 ` phillip.wood123 0 siblings, 0 replies; 46+ messages in thread From: phillip.wood123 @ 2025-04-16 13:31 UTC (permalink / raw) To: Patrick Steinhardt, phillip.wood Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King On 16/04/2025 11:27, Patrick Steinhardt wrote: > On Tue, Apr 15, 2025 at 04:52:02PM +0100, Phillip Wood wrote: > > Wait, does it? Why would `strtoul()` or any of its variants ever accept > a string prefixed with a "-"? > > If the minus sign was part of the input sequence, the numeric value > calculated from the sequence of digits is negated as if by unary > minus in the result type, which applies unsigned integer wraparound > rules. > > Oh dear... all these integer conversion functions are really a gift that > keeps on giving. Gross. Yes it's horrible behavior for a function that parses an unsigned integer. We should perhaps think about adding 64 bit versions of strtoui_i() and strtoul_ui() so we have a safer option for parsing large integers but I don't think we need to do that as part of this series. I'll try and take a look at V3 tomorrow Phillip >> This patch also needs the fix from patch 2 to detect overflows for >> uintmax_t. > > Yup, will add. > > Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` 2025-04-15 12:14 ` [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt 2025-04-15 15:52 ` Phillip Wood @ 2025-04-15 17:38 ` René Scharfe 2025-04-16 10:28 ` Patrick Steinhardt 1 sibling, 1 reply; 46+ messages in thread From: René Scharfe @ 2025-04-15 17:38 UTC (permalink / raw) To: Patrick Steinhardt, git Cc: John Paul Adrian Glaubitz, Todd Zullinger, SZEDER Gábor, Derrick Stolee, Jeff King Am 15.04.25 um 14:14 schrieb Patrick Steinhardt: > We have two generic ways to parse integers in the "parse-options" > subsytem: "subsystem" > - `OPTION_INTEGER` parses a signed integer. > > - `OPTION_MAGNITUDE` parses an unsigned integer, but it also > interprets suffixes like "k" or "g". > > Notably missing is a middle ground that parses unsigned integers without > interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to > plug this gap. This option type will be used in subsequent commits. > > Signed-off-by: Patrick Steinhardt <ps@pks.im> > --- > parse-options.c | 43 +++++++++++++++++++++++++++++++++++++++++++ > parse-options.h | 12 ++++++++++++ > t/helper/test-parse-options.c | 4 +++- > t/t0040-parse-options.sh | 18 +++++++++++++++++- > 4 files changed, 75 insertions(+), 2 deletions(-) > > diff --git a/parse-options.c b/parse-options.c > index ae836c384c7..9670e46a679 100644 > --- a/parse-options.c > +++ b/parse-options.c > @@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, > optname(opt, flags)); > } > } > + case OPTION_UNSIGNED: > + { > + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); > + uintmax_t value; > + > + if (unset) { > + value = 0; > + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { > + value = opt->defval; > + } else if (get_arg(p, opt, flags, &arg)) { > + return -1; > + } else if (!*arg) { > + return error(_("%s expects a numerical value"), > + optname(opt, flags)); > + } else { > + value = strtoumax(arg, (char **)&s, 10); > + if (*s) > + return error(_("%s expects a numerical value"), > + optname(opt, flags)); > + } > + > + if (value > upper_bound) > + return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), > + value, optname(opt, flags), upper_bound); > + > + switch (opt->precision) { > + case 1: > + *(int8_t *)opt->value = value; uint8_t, surely. Similarly for the other casts below. > + return 0; > + case 2: > + *(int16_t *)opt->value = value; > + return 0; > + case 4: > + *(int32_t *)opt->value = value; > + return 0; > + case 8: > + *(int64_t *)opt->value = value; > + return 0; > + default: > + BUG("invalid precision for option %s", > + optname(opt, flags)); > + } > + } > case OPTION_MAGNITUDE: > { > uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` 2025-04-15 17:38 ` René Scharfe @ 2025-04-16 10:28 ` Patrick Steinhardt 0 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:28 UTC (permalink / raw) To: René Scharfe Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, SZEDER Gábor, Derrick Stolee, Jeff King On Tue, Apr 15, 2025 at 07:38:04PM +0200, René Scharfe wrote: > Am 15.04.25 um 14:14 schrieb Patrick Steinhardt: > > diff --git a/parse-options.c b/parse-options.c > > index ae836c384c7..9670e46a679 100644 > > --- a/parse-options.c > > +++ b/parse-options.c > > @@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, > > optname(opt, flags)); > > } > > } > > + case OPTION_UNSIGNED: > > + { > > + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); > > + uintmax_t value; > > + > > + if (unset) { > > + value = 0; > > + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { > > + value = opt->defval; > > + } else if (get_arg(p, opt, flags, &arg)) { > > + return -1; > > + } else if (!*arg) { > > + return error(_("%s expects a numerical value"), > > + optname(opt, flags)); > > + } else { > > + value = strtoumax(arg, (char **)&s, 10); > > + if (*s) > > + return error(_("%s expects a numerical value"), > > + optname(opt, flags)); > > + } > > + > > + if (value > upper_bound) > > + return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), > > + value, optname(opt, flags), upper_bound); > > + > > + switch (opt->precision) { > > + case 1: > > + *(int8_t *)opt->value = value; > > uint8_t, surely. Similarly for the other casts below. Oof, of course. Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v2 5/5] parse-options: detect mismatches in integer signedness 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (3 preceding siblings ...) 2025-04-15 12:14 ` [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt @ 2025-04-15 12:14 ` Patrick Steinhardt 2025-04-15 17:02 ` Junio C Hamano 4 siblings, 1 reply; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-15 12:14 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King It was reported that "t5620-backfill.sh" fails on s390x and sparc64 in a test that exercises the "--min-batch-size" command line option. The symptom was that the option didn't seem to have an effect: we didn't fetch objects with a batch size of 20, but instead fetched all objects at once. As it turns out, the root cause is that `--min-batch-size` uses `OPT_INTEGER()` to parse the command line option. While this macro expects the caller to pass a pointer to an integer, we instead pass a pointer to a `size_t`. This coincidentally works on most platforms, but it breaks apart on the mentioned platforms because they are big endian. This issue isn't specific to git-backfill(1): there are a couple of other places where we have the same type confusion going on. This indicates that the issue really is the interface that the parse-options subsystem provides -- it is simply too easy to get this wrong as there isn't any kind of compiler warning, and things just work on the most common systems. Address the systemic issue by introducing two new build asserts `BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names already hint at, those macros will cause a compiler error when passed a value that is not signed or unsigned, respectively. Adapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to use those asserts. This uncovers a small set of sites where we indeed have the same bug as in git-backfill(1). Adapt all of them to use the correct option. Reported-by: Todd Zullinger <tmz@pobox.com> Reported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de> Helped-by: SZEDER Gábor <szeder.dev@gmail.com> Helped-by: Jeff King <peff@peff.net> Signed-off-by: Patrick Steinhardt <ps@pks.im> --- apply.c | 4 ++-- builtin/backfill.c | 4 ++-- builtin/column.c | 2 +- builtin/grep.c | 4 ++-- git-compat-util.h | 7 +++++++ parse-options.h | 6 +++--- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apply.c b/apply.c index f274a379487..a850c7d75fe 100644 --- a/apply.c +++ b/apply.c @@ -5123,8 +5123,8 @@ int apply_parse_options(int argc, const char **argv, /* Think twice before adding "--nul" synonym to this */ OPT_SET_INT('z', NULL, &state->line_termination, N_("paths are separated with NUL character"), '\0'), - OPT_INTEGER('C', NULL, &state->p_context, - N_("ensure at least <n> lines of context match")), + OPT_UNSIGNED('C', NULL, &state->p_context, + N_("ensure at least <n> lines of context match")), OPT_CALLBACK(0, "whitespace", state, N_("action"), N_("detect new or modified lines that have whitespace errors"), apply_option_parse_whitespace), diff --git a/builtin/backfill.c b/builtin/backfill.c index 33e1ea2f84f..d95d7a2d4d6 100644 --- a/builtin/backfill.c +++ b/builtin/backfill.c @@ -123,8 +123,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit .sparse = 0, }; struct option options[] = { - OPT_INTEGER(0, "min-batch-size", &ctx.min_batch_size, - N_("Minimum number of objects to request at a time")), + OPT_UNSIGNED(0, "min-batch-size", &ctx.min_batch_size, + N_("Minimum number of objects to request at a time")), OPT_BOOL(0, "sparse", &ctx.sparse, N_("Restrict the missing objects to the current sparse-checkout")), OPT_END(), diff --git a/builtin/column.c b/builtin/column.c index 50314cc2559..ce6443d5fac 100644 --- a/builtin/column.c +++ b/builtin/column.c @@ -31,7 +31,7 @@ int cmd_column(int argc, struct option options[] = { OPT_STRING(0, "command", &real_command, N_("name"), N_("lookup config vars")), OPT_COLUMN(0, "mode", &colopts, N_("layout to use")), - OPT_INTEGER(0, "raw-mode", &colopts, N_("layout to use")), + OPT_UNSIGNED(0, "raw-mode", &colopts, N_("layout to use")), OPT_INTEGER(0, "width", &copts.width, N_("maximum width")), OPT_STRING(0, "indent", &copts.indent, N_("string"), N_("padding space on left border")), OPT_STRING(0, "nl", &copts.nl, N_("string"), N_("padding space on right border")), diff --git a/builtin/grep.c b/builtin/grep.c index c4869733e1b..f23a6f1dc86 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -983,9 +983,9 @@ int cmd_grep(int argc, OPT_CALLBACK('C', "context", &opt, N_("n"), N_("show <n> context lines before and after matches"), context_callback), - OPT_INTEGER('B', "before-context", &opt.pre_context, + OPT_UNSIGNED('B', "before-context", &opt.pre_context, N_("show <n> context lines before matches")), - OPT_INTEGER('A', "after-context", &opt.post_context, + OPT_UNSIGNED('A', "after-context", &opt.post_context, N_("show <n> context lines after matches")), OPT_INTEGER(0, "threads", &num_threads, N_("use <n> worker threads")), diff --git a/git-compat-util.h b/git-compat-util.h index cf733b38acd..1218fcf81a4 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -110,12 +110,19 @@ DISABLE_WARNING(-Wsign-compare) # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(__builtin_types_compatible_p(__typeof__(*(dst)), \ __typeof__(*(src)))) + +# define BARF_UNLESS_SIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) < 0) +# define BARF_UNLESS_UNSIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) > 0) #else # define BARF_UNLESS_AN_ARRAY(arr) 0 # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(0 ? ((*(dst) = *(src)), 0) : \ sizeof(*(dst)) == sizeof(*(src))) + +# define BARF_UNLESS_SIGNED(var) 0 +# define BARF_UNLESS_UNSIGNED(var) 0 #endif + /* * ARRAY_SIZE - get the number of elements in a visible array * @x: the array whose size you want. diff --git a/parse-options.h b/parse-options.h index 20ea7d2ab13..7b7c9d901cb 100644 --- a/parse-options.h +++ b/parse-options.h @@ -219,7 +219,7 @@ struct option { .type = OPTION_INTEGER, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_SIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ @@ -229,7 +229,7 @@ struct option { .type = OPTION_UNSIGNED, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ @@ -292,7 +292,7 @@ struct option { .type = OPTION_MAGNITUDE, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH v2 5/5] parse-options: detect mismatches in integer signedness 2025-04-15 12:14 ` [PATCH v2 5/5] parse-options: detect mismatches in integer signedness Patrick Steinhardt @ 2025-04-15 17:02 ` Junio C Hamano 0 siblings, 0 replies; 46+ messages in thread From: Junio C Hamano @ 2025-04-15 17:02 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King Patrick Steinhardt <ps@pks.im> writes: > Address the systemic issue by introducing two new build asserts > `BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names > already hint at, those macros will cause a compiler error when passed a > value that is not signed or unsigned, respectively. > > Adapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to > use those asserts. This uncovers a small set of sites where we indeed > have the same bug as in git-backfill(1). Adapt all of them to use the > correct option. Nicely described and implemented. Thanks. ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v3 0/7] parse-options: harden handling of integer values 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (5 preceding siblings ...) 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 1/7] global: use designated initializers for options Patrick Steinhardt ` (6 more replies) 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt 7 siblings, 7 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood Hi, this patch series addresses the issues raised in [1] and [2]. As discussed in [1], the series also introduces a couple of safeguards to make it harder to misuse `OPT_INTEGER()` and `OPT_MAGNITUDE()`: - We now track the precision of the underlying integer types. This makes it possible to pass arbitrarily-sized integers to those options, not only `int` and `unsigned long`, respectively. - We introduce a build assert to verify that the passed variable has correct signedness. Furthermore, the series introduces `OPT_UNSIGNED()` to adapt all callsites that previously used variables with the wrong signedness. Changes in v2: - Adapt computation of upper bounds to use similar logic to `maximum_signed_value_of_type()`. - Link to v1: https://lore.kernel.org/r/20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im Changes in v3: - Introduce `errno` checks for `strto{u,i}max()`. - Note that the precision is in bytes. - Reject leading '-' when parsing unsigned integers. - Introduce bounded integer options. This patch is mostly a proof of concept that demonstrates that precision and ranges are orthogonal to one another, so I consider it to be an optional patch. It may be useful in the future, but I haven't converted any callsites to use bounds yet. - Link to v2: https://lore.kernel.org/r/20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im Thanks! Patrick [1]: <89257ab82cd60d135cce02d51eacee7ec35c1c37.camel@physik.fu-berlin.de> [2]: <Z8HW6petWuMRWSXf@teonanacatl.net> --- Patrick Steinhardt (7): global: use designated initializers for options parse-options: check for overflow when parsing integers parse-options: introduce precision handling for `OPTION_INTEGER` parse-options: introduce precision handling for `OPTION_MAGNITUDE` parse-options: introduce `OPTION_UNSIGNED` parse-options: detect mismatches in integer signedness parse-options: introduce bounded integer options apply.c | 4 +- archive.c | 35 ++++++--- builtin/am.c | 28 +++++-- builtin/backfill.c | 4 +- builtin/clone.c | 13 +++- builtin/column.c | 2 +- builtin/commit-tree.c | 12 ++- builtin/commit.c | 62 +++++++++++---- builtin/config.c | 13 +++- builtin/describe.c | 24 ++++-- builtin/fetch.c | 10 ++- builtin/fmt-merge-msg.c | 27 +++++-- builtin/gc.c | 12 ++- builtin/grep.c | 18 +++-- builtin/init-db.c | 13 +++- builtin/ls-remote.c | 11 ++- builtin/merge.c | 38 +++++++-- builtin/read-tree.c | 11 ++- builtin/rebase.c | 25 ++++-- builtin/revert.c | 12 ++- builtin/show-branch.c | 13 +++- builtin/tag.c | 24 ++++-- builtin/update-index.c | 131 +++++++++++++++++++++---------- builtin/write-tree.c | 12 ++- diff.c | 13 +++- git-compat-util.h | 7 ++ parse-options.c | 176 +++++++++++++++++++++++++++++++++++++----- parse-options.h | 75 +++++++++++++++++- ref-filter.h | 15 ++-- t/helper/test-parse-options.c | 51 ++++++++++-- t/t0040-parse-options.sh | 102 +++++++++++++++++++++++- 31 files changed, 805 insertions(+), 188 deletions(-) Range-diff versus v2: 1: 80c8b943e9b = 1: 3dc362ae91b global: use designated initializers for options -: ----------- > 2: a5a71ad6da8 parse-options: check for overflow when parsing integers 2: 307eaeb463c ! 3: 1ad133c639e parse-options: introduce precision handling for `OPTION_INTEGER` @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ + } else if (!*arg) { return error(_("%s expects a numerical value"), optname(opt, flags)); -- *(int *)opt->value = strtol(arg, (char **)&s, 10); -- if (*s) -- return error(_("%s expects a numerical value"), -- optname(opt, flags)); -- return 0; + } else { ++ errno = 0; + value = strtoimax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); -+ ++ if (errno == ERANGE) ++ return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), ++ arg, optname(opt, flags), lower_bound, upper_bound); ++ if (errno) ++ return error_errno(_("value %s for %s cannot be parsed"), ++ arg, optname(opt, flags)); + } +- errno = 0; +- *(int *)opt->value = strtol(arg, (char **)&s, 10); +- if (*s) +- return error(_("%s expects a numerical value"), +- optname(opt, flags)); +- if (errno == ERANGE) + if (value < lower_bound || value > upper_bound) -+ return error(_("value %"PRIdMAX" for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), -+ value, optname(opt, flags), lower_bound, upper_bound); -+ + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), +- arg, optname(opt, flags), (intmax_t)LONG_MIN, (intmax_t)LONG_MAX); +- if (errno) +- return error_errno(_("value %s for %s cannot be parsed"), +- arg, optname(opt, flags)); ++ arg, optname(opt, flags), lower_bound, upper_bound); + +- return 0; + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; @@ parse-options.h: typedef int parse_opt_subcommand_fn(int argc, const char **argv * stores pointers to the values to be filled. * + * `precision`:: -+ * precision of the integer pointed to by `value`. Should typically be its -+ * `sizeof()`. ++ * precision of the integer pointed to by `value` in number of bytes. Should ++ * typically be its `sizeof()`. + * * `argh`:: * token to explain the kind of argument this option wants. Does not @@ t/t0040-parse-options.sh: test_expect_success 'OPT_NUMBER_CALLBACK() works' ' magnitude: 0 timestamp: 0 string: (not set) -@@ t/t0040-parse-options.sh: test_expect_success 'magnitude with units but no numbers' ' +@@ t/t0040-parse-options.sh: test_expect_success 'overflowing integer' ' test_must_be_empty out ' 3: 5bfb9af2262 ! 4: 01e56b43c77 parse-options: introduce precision handling for `OPTION_MAGNITUDE` @@ Commit message ## parse-options.c ## @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, + + if (value < lower_bound || value > upper_bound) + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), +- arg, optname(opt, flags), lower_bound, upper_bound); ++ arg, optname(opt, flags), (intmax_t)lower_bound, (intmax_t)upper_bound); + + switch (opt->precision) { + case 1: +@@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, } } case OPTION_MAGNITUDE: @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ + } + + if (value > upper_bound) -+ return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), -+ (uintmax_t) value, optname(opt, flags), upper_bound); ++ return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), ++ arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + + switch (opt->precision) { + case 1: @@ t/t0040-parse-options.sh: test_expect_success 'i16 limits range' ' + test-tool parse-options --m16 65535 >out && + test_grep "m16: 65535" out && + test_must_fail test-tool parse-options --m16 65536 2>err && -+ test_grep "value 65536 for option .m16. exceeds 65535" err ++ test_grep "value 65536 for option .m16. not in range \[0,65535\]" err +' + test_done 4: 75ff495b897 ! 5: e7458eadea9 parse-options: introduce `OPTION_UNSIGNED` @@ Commit message parse-options: introduce `OPTION_UNSIGNED` We have two generic ways to parse integers in the "parse-options" - subsytem: + subsystem: - `OPTION_INTEGER` parses a signed integer. @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ + } else if (!*arg) { + return error(_("%s expects a numerical value"), + optname(opt, flags)); ++ } else if (*arg == '-') { ++ return error(_("%s does not accept negative values"), ++ optname(opt, flags)); + } else { ++ errno = 0; + value = strtoumax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); ++ if (errno == ERANGE) ++ return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), ++ arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); ++ if (errno) ++ return error_errno(_("value %s for %s cannot be parsed"), ++ arg, optname(opt, flags)); ++ + } + + if (value > upper_bound) -+ return error(_("value %"PRIuMAX" for %s exceeds %"PRIuMAX), -+ value, optname(opt, flags), upper_bound); ++ return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), ++ arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + + switch (opt->precision) { + case 1: -+ *(int8_t *)opt->value = value; ++ *(uint8_t *)opt->value = value; + return 0; + case 2: -+ *(int16_t *)opt->value = value; ++ *(uint16_t *)opt->value = value; + return 0; + case 4: -+ *(int32_t *)opt->value = value; ++ *(uint32_t *)opt->value = value; + return 0; + case 8: -+ *(int64_t *)opt->value = value; ++ *(uint64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", @@ t/t0040-parse-options.sh: cat >expect <<\EOF m16: 0 timestamp: 0 @@ t/t0040-parse-options.sh: test_expect_success 'm16 limits range' ' - test_grep "value 65536 for option .m16. exceeds 65535" err + test_grep "value 65536 for option .m16. not in range \[0,65535\]" err ' +test_expect_success 'u16 limits range' ' + test-tool parse-options --u16 65535 >out && + test_grep "u16: 65535" out && + test_must_fail test-tool parse-options --u16 65536 2>err && -+ test_grep "value 65536 for option .u16. exceeds 65535" err ++ test_grep "value 65536 for option .u16. not in range \[0,65535\]" err ++' ++ ++test_expect_success 'u16 does not accept negative value' ' ++ test_must_fail test-tool parse-options --u16 -1 >out 2>err && ++ test_grep "option .u16. does not accept negative values" err && ++ test_must_be_empty out +' + test_done 5: 20ab753baef = 6: 5b05451c346 parse-options: detect mismatches in integer signedness -: ----------- > 7: 7115a7a31aa parse-options: introduce bounded integer options --- base-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff change-id: 20250401-b4-pks-parse-options-integers-9b4bbcf21011 ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v3 1/7] global: use designated initializers for options 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 2/7] parse-options: check for overflow when parsing integers Patrick Steinhardt ` (5 subsequent siblings) 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood While we expose macros for most of our different option types understood by the "parse-options" subsystem, not every combination of fields that has one as that would otherwise quickly lead to an explosion of macros. Instead, we just initialize structures manually for those variants of fields that don't have a macro. Callsites that open-code these structure initialization don't use designated initializers though and instead just provide values for each of the fields that they want to initialize. This has three significant downsides: - Callsites need to specify all values up to the last field that they care about. This often includes fields that should simply be left at their default zero-initialized state, which adds distraction. - Any reader not deeply familiar with the layout of the structure has a hard time figuring out what the respective initializers mean. - Reordering or introducing new fields in the middle of the structure is impossible without adapting all callsites. Convert all sites to instead use designated initializers, which we have started using in our codebase quite a while ago. This allows us to skip any default-initialized fields, gives the reader context by specifying the field names and allows us to reorder or introduce new fields where we want to. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- archive.c | 35 ++++++++--- builtin/am.c | 28 ++++++--- builtin/clone.c | 13 ++++- builtin/commit-tree.c | 12 +++- builtin/commit.c | 62 +++++++++++++++----- builtin/config.c | 13 ++++- builtin/describe.c | 24 ++++++-- builtin/fetch.c | 10 +++- builtin/fmt-merge-msg.c | 25 +++++--- builtin/gc.c | 12 +++- builtin/grep.c | 14 +++-- builtin/init-db.c | 13 +++-- builtin/ls-remote.c | 11 +++- builtin/merge.c | 37 +++++++++--- builtin/read-tree.c | 11 +++- builtin/rebase.c | 25 ++++++-- builtin/revert.c | 12 +++- builtin/show-branch.c | 12 +++- builtin/tag.c | 23 ++++++-- builtin/update-index.c | 131 +++++++++++++++++++++++++++++------------- builtin/write-tree.c | 12 ++-- diff.c | 13 +++-- ref-filter.h | 15 +++-- t/helper/test-parse-options.c | 38 +++++++++--- 24 files changed, 443 insertions(+), 158 deletions(-) diff --git a/archive.c b/archive.c index 8be4e7ac8db..67bba3cd301 100644 --- a/archive.c +++ b/archive.c @@ -650,20 +650,37 @@ static int parse_archive_args(int argc, const char **argv, OPT_STRING(0, "format", &format, N_("fmt"), N_("archive format")), OPT_STRING(0, "prefix", &base, N_("prefix"), N_("prepend prefix to each pathname in the archive")), - { OPTION_CALLBACK, 0, "add-file", args, N_("file"), - N_("add untracked file to archive"), 0, add_file_cb, - (intptr_t)&base }, - { OPTION_CALLBACK, 0, "add-virtual-file", args, - N_("path:content"), N_("add untracked file to archive"), 0, - add_file_cb, (intptr_t)&base }, + { + .type = OPTION_CALLBACK, + .long_name = "add-file", + .value = args, + .argh = N_("file"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, + { + .type = OPTION_CALLBACK, + .long_name = "add-virtual-file", + .value = args, + .argh = N_("path:content"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, OPT_STRING('o', "output", &output, N_("file"), N_("write the archive to this file")), OPT_BOOL(0, "worktree-attributes", &worktree_attributes, N_("read .gitattributes in working directory")), OPT__VERBOSE(&verbose, N_("report archived files on stderr")), - { OPTION_STRING, 0, "mtime", &mtime_option, N_("time"), - N_("set modification time of archive entries"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "mtime", + .value = &mtime_option, + .argh = N_("time"), + .help = N_("set modification time of archive entries"), + .flags = PARSE_OPT_NONEG, + }, OPT_NUMBER_CALLBACK(&compression_level, N_("set compression level"), number_callback), OPT_GROUP(""), diff --git a/builtin/am.c b/builtin/am.c index 3b61bd4c333..4afb519830f 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -2400,11 +2400,16 @@ int cmd_am(int argc, OPT_CMDMODE(0, "quit", &resume_mode, N_("abort the patching operation but keep HEAD where it is"), RESUME_QUIT), - { OPTION_CALLBACK, 0, "show-current-patch", &resume_mode, - "(diff|raw)", - N_("show the patch being applied"), - PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW }, + { + .type = OPTION_CALLBACK, + .long_name = "show-current-patch", + .value = &resume_mode, + .argh = "(diff|raw)", + .help = N_("show the patch being applied"), + .flags = PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .callback = parse_opt_show_current_patch, + .defval = RESUME_SHOW_PATCH_RAW, + }, OPT_CMDMODE(0, "retry", &resume_mode, N_("try to apply current patch again"), RESUME_APPLY), @@ -2417,9 +2422,16 @@ int cmd_am(int argc, OPT_BOOL(0, "ignore-date", &state.ignore_date, N_("use current timestamp for author date")), OPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate), - { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &state.sign_commit, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_CALLBACK_F(0, "empty", &state.empty_type, "(stop|drop|keep)", N_("how to handle empty patches"), PARSE_OPT_NONEG, am_option_parse_empty), diff --git a/builtin/clone.c b/builtin/clone.c index 88276e5b7ab..9c3547f41e3 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -930,9 +930,16 @@ int cmd_clone(int argc, N_("don't use local hardlinks, always copy")), OPT_BOOL('s', "shared", &option_shared, N_("setup as shared repository")), - { OPTION_CALLBACK, 0, "recurse-submodules", &option_recurse_submodules, - N_("pathspec"), N_("initialize submodules in the clone"), - PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)"." }, + { + .type = OPTION_CALLBACK, + .long_name = "recurse-submodules", + .value = &option_recurse_submodules, + .argh = N_("pathspec"), + .help = N_("initialize submodules in the clone"), + .flags = PARSE_OPT_OPTARG, + .callback = recurse_submodules_cb, + .defval = (intptr_t)".", + }, OPT_ALIAS(0, "recursive", "recurse-submodules"), OPT_INTEGER('j', "jobs", &max_jobs, N_("number of submodules cloned in parallel")), diff --git a/builtin/commit-tree.c b/builtin/commit-tree.c index 38457600a4e..c787133d004 100644 --- a/builtin/commit-tree.c +++ b/builtin/commit-tree.c @@ -111,8 +111,16 @@ int cmd_commit_tree(int argc, OPT_CALLBACK_F('F', NULL, &buffer, N_("file"), N_("read commit log message from file"), PARSE_OPT_NONEG, parse_file_arg_callback), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; int ret; diff --git a/builtin/commit.c b/builtin/commit.c index 2f459682221..66bd91fd523 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1542,17 +1542,34 @@ struct repository *repo UNUSED) STATUS_FORMAT_LONG), OPT_BOOL('z', "null", &s.null_termination, N_("terminate entries with NUL")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, - N_("mode"), - N_("show untracked files, optional modes: all, normal, no. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, - { OPTION_STRING, 0, "ignored", &ignored_arg, - N_("mode"), - N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"traditional" }, - { OPTION_STRING, 0, "ignore-submodules", &ignore_submodule_arg, N_("when"), - N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, + { + .type = OPTION_STRING, + .long_name = "ignored", + .value = &ignored_arg, + .argh = N_("mode"), + .help = N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"traditional", + }, + { + .type = OPTION_STRING, + .long_name = "ignore-submodules", + .value = &ignore_submodule_arg, + .argh = N_("when"), + .help = N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")), OPT_BOOL(0, "no-renames", &no_renames, N_("do not detect renames")), OPT_CALLBACK_F('M', "find-renames", &rename_score_arg, @@ -1688,8 +1705,16 @@ int cmd_commit(int argc, OPT_BOOL('e', "edit", &edit_flag, N_("force edit of commit")), OPT_CLEANUP(&cleanup_arg), OPT_BOOL(0, "status", &include_status, N_("include status in commit message template")), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, /* end commit message options */ OPT_GROUP(N_("Commit contents options")), @@ -1714,7 +1739,16 @@ int cmd_commit(int argc, N_("terminate entries with NUL")), OPT_BOOL(0, "amend", &amend, N_("amend previous commit")), OPT_BOOL(0, "no-post-rewrite", &no_post_rewrite, N_("bypass post-rewrite hook")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, N_("mode"), N_("show untracked files, optional modes: all, normal, no. (Default: all)"), PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), /* end commit contents options */ diff --git a/builtin/config.c b/builtin/config.c index 53a90094e31..f70d6354772 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -131,9 +131,16 @@ struct config_display_options { #define TYPE_COLOR 6 #define TYPE_BOOL_OR_STR 7 -#define OPT_CALLBACK_VALUE(s, l, v, h, i) \ - { OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \ - PARSE_OPT_NONEG, option_parse_type, (i) } +#define OPT_CALLBACK_VALUE(s, l, v, h, i) { \ + .type = OPTION_CALLBACK, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v), \ + .help = (h), \ + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, \ + .callback = option_parse_type, \ + .defval = (i), \ +} static int option_parse_type(const struct option *opt, const char *arg, int unset) diff --git a/builtin/describe.c b/builtin/describe.c index e2e73f3d757..2da9f4fed01 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -601,12 +601,24 @@ int cmd_describe(int argc, N_("do not consider tags matching <pattern>")), OPT_BOOL(0, "always", &always, N_("show abbreviated commit object as fallback")), - {OPTION_STRING, 0, "dirty", &dirty, N_("mark"), - N_("append <mark> on dirty working tree (default: \"-dirty\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-dirty"}, - {OPTION_STRING, 0, "broken", &broken, N_("mark"), - N_("append <mark> on broken working tree (default: \"-broken\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-broken"}, + { + .type = OPTION_STRING, + .long_name = "dirty", + .value = &dirty, + .argh = N_("mark"), + .help = N_("append <mark> on dirty working tree (default: \"-dirty\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-dirty", + }, + { + .type = OPTION_STRING, + .long_name = "broken", + .value = &broken, + .argh = N_("mark"), + .help = N_("append <mark> on broken working tree (default: \"-broken\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-broken", + }, OPT_END(), }; diff --git a/builtin/fetch.c b/builtin/fetch.c index 02af5054690..3a5159d9e69 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2359,8 +2359,14 @@ int cmd_fetch(int argc, OPT_SET_INT_F(0, "refetch", &refetch, N_("re-fetch without negotiating common commits"), 1, PARSE_OPT_NONEG), - { OPTION_STRING, 0, "submodule-prefix", &submodule_prefix, N_("dir"), - N_("prepend this to submodule path output"), PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "submodule-prefix", + .value = &submodule_prefix, + .argh = N_("dir"), + .help = N_("prepend this to submodule path output"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_CALLBACK_F(0, "recurse-submodules-default", &recurse_submodules_default, N_("on-demand"), N_("default for recursive fetching of submodules " diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 189cd1096a0..240cdb474bc 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -20,13 +20,24 @@ int cmd_fmt_merge_msg(int argc, char *into_name = NULL; int shortlog_len = -1; struct option options[] = { - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("populate log with at most <n> entries from shortlog"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, - { OPTION_INTEGER, 0, "summary", &shortlog_len, N_("n"), - N_("alias for --log (deprecated)"), - PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL, - DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("populate log with at most <n> entries from shortlog"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, + { + .type = OPTION_INTEGER, + .long_name = "summary", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("alias for --log (deprecated)"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_STRING('m', "message", &message, N_("text"), N_("use <text> as start of message")), OPT_STRING(0, "into-name", &into_name, N_("name"), diff --git a/builtin/gc.c b/builtin/gc.c index 99431fd4674..6707a26bc6e 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -699,9 +699,15 @@ struct repository *repo UNUSED) int ret; struct option builtin_gc_options[] = { OPT__QUIET(&quiet, N_("suppress progress reporting")), - { OPTION_STRING, 0, "prune", &prune_expire_arg, N_("date"), - N_("prune unreferenced objects"), - PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg }, + { + .type = OPTION_STRING, + .long_name = "prune", + .value = &prune_expire_arg, + .argh = N_("date"), + .help = N_("prune unreferenced objects"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)prune_expire_arg, + }, OPT_BOOL(0, "cruft", &cfg.cruft_packs, N_("pack unreferenced objects separately")), OPT_MAGNITUDE(0, "max-cruft-size", &cfg.max_cruft_size, N_("with --cruft, limit the size of new cruft packs")), diff --git a/builtin/grep.c b/builtin/grep.c index d1427290f77..c4869733e1b 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -1017,10 +1017,16 @@ int cmd_grep(int argc, OPT_BOOL(0, "all-match", &opt.all_match, N_("show only matches from files that match all patterns")), OPT_GROUP(""), - { OPTION_STRING, 'O', "open-files-in-pager", &show_in_pager, - N_("pager"), N_("show matching files in the pager"), - PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, - NULL, (intptr_t)default_pager }, + { + .type = OPTION_STRING, + .short_name = 'O', + .long_name = "open-files-in-pager", + .value = &show_in_pager, + .argh = N_("pager"), + .help = N_("show matching files in the pager"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, + .defval = (intptr_t)default_pager, + }, OPT_BOOL_F(0, "ext-grep", &external_grep_allowed__ignored, N_("allow calling of grep(1) (ignored by this build)"), PARSE_OPT_NOCOMPLETE), diff --git a/builtin/init-db.c b/builtin/init-db.c index 196dccdd77a..4a950e44d8d 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -93,10 +93,15 @@ int cmd_init_db(int argc, N_("directory from which templates will be used")), OPT_SET_INT(0, "bare", &is_bare_repository_cfg, N_("create a bare repository"), 1), - { OPTION_CALLBACK, 0, "shared", &init_shared_repository, - N_("permissions"), - N_("specify that the git repository is to be shared amongst several users"), - PARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0}, + { + .type = OPTION_CALLBACK, + .long_name = "shared", + .value = &init_shared_repository, + .argh = N_("permissions"), + .help = N_("specify that the git repository is to be shared amongst several users"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + .callback = shared_callback + }, OPT_BIT('q', "quiet", &flags, N_("be quiet"), INIT_DB_QUIET), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), N_("separate git dir from working tree")), diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c index 42f34e12361..01a4d4daa1f 100644 --- a/builtin/ls-remote.c +++ b/builtin/ls-remote.c @@ -67,9 +67,14 @@ int cmd_ls_remote(int argc, OPT__QUIET(&quiet, N_("do not print remote URL")), OPT_STRING(0, "upload-pack", &uploadpack, N_("exec"), N_("path of git-upload-pack on the remote host")), - { OPTION_STRING, 0, "exec", &uploadpack, N_("exec"), - N_("path of git-upload-pack on the remote host"), - PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "exec", + .value = &uploadpack, + .argh = N_("exec"), + .help = N_("path of git-upload-pack on the remote host"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_BIT('t', "tags", &flags, N_("limit to tags"), REF_TAGS), OPT_BIT('b', "branches", &flags, N_("limit to branches"), REF_BRANCHES), OPT_BIT_F('h', "heads", &flags, diff --git a/builtin/merge.c b/builtin/merge.c index ba9faf126aa..21787d45165 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -250,9 +250,15 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "stat", &show_diffstat, N_("show a diffstat at the end of the merge")), OPT_BOOL(0, "summary", &show_diffstat, N_("(synonym to --stat)")), - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("add (at most <n>) entries from shortlog to merge commit message"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("add (at most <n>) entries from shortlog to merge commit message"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_BOOL(0, "squash", &squash, N_("create a single commit instead of doing a merge")), OPT_BOOL(0, "commit", &option_commit, @@ -274,9 +280,16 @@ static struct option builtin_merge_options[] = { OPT_CALLBACK('m', "message", &merge_msg, N_("message"), N_("merge commit message (for a non-fast-forward merge)"), option_parse_message), - { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"), - N_("read message from file"), PARSE_OPT_NONEG, - NULL, 0, option_read_message }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'F', + .long_name = "file", + .value = &merge_msg, + .argh = N_("path"), + .help = N_("read message from file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = option_read_message, + }, OPT_STRING(0, "into-name", &into_name, N_("name"), N_("use <name> instead of the real target")), OPT__VERBOSITY(&verbosity), @@ -289,8 +302,16 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "allow-unrelated-histories", &allow_unrelated_histories, N_("allow merging unrelated histories")), OPT_SET_INT(0, "progress", &show_progress, N_("force progress reporting"), 1), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&autostash), OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")), OPT_BOOL(0, "signoff", &signoff, N_("add a Signed-off-by trailer")), diff --git a/builtin/read-tree.c b/builtin/read-tree.c index d2a807a828b..a8f352f7cd9 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -135,9 +135,14 @@ int cmd_read_tree(int argc, N_("3-way merge in presence of adds and removes")), OPT_BOOL(0, "reset", &opts.reset, N_("same as -m, but discard unmerged entries")), - { OPTION_STRING, 0, "prefix", &opts.prefix, N_("<subdirectory>/"), - N_("read the tree into the index under <subdirectory>/"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "prefix", + .value = &opts.prefix, + .argh = N_("<subdirectory>/"), + .help = N_("read the tree into the index under <subdirectory>/"), + .flags = PARSE_OPT_NONEG, + }, OPT_BOOL('u', NULL, &opts.update, N_("update working tree with merge result")), OPT_CALLBACK_F(0, "exclude-per-directory", &opts, diff --git a/builtin/rebase.c b/builtin/rebase.c index d4715ed35d7..d4083350090 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1122,9 +1122,15 @@ int cmd_rebase(int argc, OPT_BIT('v', "verbose", &options.flags, N_("display a diffstat of what changed upstream"), REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), - {OPTION_NEGBIT, 'n', "no-stat", &options.flags, NULL, - N_("do not show diffstat of what changed upstream"), - PARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT }, + { + .type = OPTION_NEGBIT, + .short_name = 'n', + .long_name = "no-stat", + .value = &options.flags, + .help = N_("do not show diffstat of what changed upstream"), + .flags = PARSE_OPT_NOARG, + .defval = REBASE_DIFFSTAT, + }, OPT_BOOL(0, "signoff", &options.signoff, N_("add a Signed-off-by trailer to each commit")), OPT_BOOL(0, "committer-date-is-author-date", @@ -1190,9 +1196,16 @@ int cmd_rebase(int argc, OPT_BOOL(0, "update-refs", &options.update_refs, N_("update branches that point to commits " "that are being rebased")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&options.autostash), OPT_STRING_LIST('x', "exec", &options.exec, N_("exec"), N_("add exec lines after each commit of the " diff --git a/builtin/revert.c b/builtin/revert.c index aca6c293cdf..4f5ef975494 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -132,8 +132,16 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, OPT_STRING(0, "strategy", &strategy, N_("strategy"), N_("merge strategy")), OPT_STRVEC('X', "strategy-option", &opts->xopts, N_("option"), N_("option for merge strategy")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; struct option *options = base_options; diff --git a/builtin/show-branch.c b/builtin/show-branch.c index fce6b404e92..dab37019d29 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -667,9 +667,15 @@ int cmd_show_branch(int ac, N_("show remote-tracking branches")), OPT__COLOR(&showbranch_use_color, N_("color '*!+-' corresponding to the branch")), - { OPTION_INTEGER, 0, "more", &extra, N_("n"), - N_("show <n> more commits after the common ancestor"), - PARSE_OPT_OPTARG, NULL, (intptr_t)1 }, + { + .type = OPTION_INTEGER, + .long_name = "more", + .value = &extra, + .argh = N_("n"), + .help = N_("show <n> more commits after the common ancestor"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_SET_INT(0, "list", &extra, N_("synonym to more=-1"), -1), OPT_BOOL(0, "no-name", &no_name, N_("suppress naming strings")), OPT_BOOL(0, "current", &with_current_branch, diff --git a/builtin/tag.c b/builtin/tag.c index d3e0943b734..b266f12bb48 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -479,9 +479,15 @@ int cmd_tag(int argc, int edit_flag = 0; struct option options[] = { OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'), - { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"), - N_("print <n> lines of each tag message"), - PARSE_OPT_OPTARG, NULL, 1 }, + { + .type = OPTION_INTEGER, + .short_name = 'n', + .value = &filter.lines, + .argh = N_("n"), + .help = N_("print <n> lines of each tag message"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_CMDMODE('d', "delete", &cmdmode, N_("delete tags"), 'd'), OPT_CMDMODE('v', "verify", &cmdmode, N_("verify tags"), 'v'), @@ -513,9 +519,14 @@ int cmd_tag(int argc, N_("do not output a newline after empty formatted refs")), OPT_REF_SORT(&sorting_options), { - OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), - N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT, - parse_opt_object_name, (intptr_t) "HEAD" + .type = OPTION_CALLBACK, + .long_name = "points-at", + .value = &filter.points_at, + .argh = N_("object"), + .help = N_("print only tags of the object"), + .flags = PARSE_OPT_LASTARG_DEFAULT, + .callback = parse_opt_object_name, + .defval = (intptr_t) "HEAD", }, OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), diff --git a/builtin/update-index.c b/builtin/update-index.c index b2f6b1a3fbb..ee64b022679 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -964,29 +964,51 @@ int cmd_update_index(int argc, N_("like --refresh, but ignore assume-unchanged setting"), PARSE_OPT_NOARG | PARSE_OPT_NONEG, really_refresh_callback), - {OPTION_LOWLEVEL_CALLBACK, 0, "cacheinfo", NULL, - N_("<mode>,<object>,<path>"), - N_("add the specified entry to the index"), - PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ - PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - NULL, 0, - cacheinfo_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "cacheinfo", + .argh = N_("<mode>,<object>,<path>"), + .help = N_("add the specified entry to the index"), + .flags = PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ + PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .ll_callback = cacheinfo_callback, + }, OPT_CALLBACK_F(0, "chmod", &set_executable_bit, "(+|-)x", N_("override the executable bit of the listed files"), PARSE_OPT_NONEG, chmod_callback), - {OPTION_SET_INT, 0, "assume-unchanged", &mark_valid_only, NULL, - N_("mark files as \"not changing\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-assume-unchanged", &mark_valid_only, NULL, - N_("clear assumed-unchanged bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, - {OPTION_SET_INT, 0, "skip-worktree", &mark_skip_worktree_only, NULL, - N_("mark files as \"index-only\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-skip-worktree", &mark_skip_worktree_only, NULL, - N_("clear skip-worktree bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "assume-unchanged", + .value = &mark_valid_only, + .help = N_("mark files as \"not changing\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-assume-unchanged", + .value = &mark_valid_only, + .help = N_("clear assumed-unchanged bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("mark files as \"index-only\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("clear skip-worktree bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_BOOL(0, "ignore-skip-worktree-entries", &ignore_skip_worktree_entries, N_("do not touch index-only entries")), OPT_SET_INT(0, "info-only", &info_only, @@ -995,22 +1017,39 @@ int cmd_update_index(int argc, N_("remove named paths even if present in worktree"), 1), OPT_BOOL('z', NULL, &nul_term_line, N_("with --stdin: input lines are terminated by null bytes")), - {OPTION_LOWLEVEL_CALLBACK, 0, "stdin", &read_from_stdin, NULL, - N_("read list of paths to be updated from standard input"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "index-info", &nul_term_line, NULL, - N_("add entries from standard input to the index"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_cacheinfo_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "unresolve", &has_errors, NULL, - N_("repopulate stages #2 and #3 for the listed paths"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, unresolve_callback}, - {OPTION_LOWLEVEL_CALLBACK, 'g', "again", &has_errors, NULL, - N_("only update entries that differ from HEAD"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, reupdate_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "stdin", + .value = &read_from_stdin, + .help = N_("read list of paths to be updated from standard input"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "index-info", + .value = &nul_term_line, + .help = N_("add entries from standard input to the index"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_cacheinfo_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "unresolve", + .value = &has_errors, + .help = N_("repopulate stages #2 and #3 for the listed paths"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = unresolve_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'g', + .long_name = "again", + .value = &has_errors, + .help = N_("only update entries that differ from HEAD"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = reupdate_callback, + }, OPT_BIT(0, "ignore-missing", &refresh_args.flags, N_("ignore files missing from worktree"), REFRESH_IGNORE_MISSING), @@ -1036,12 +1075,22 @@ int cmd_update_index(int argc, N_("write out the index even if is not flagged as changed"), 1), OPT_BOOL(0, "fsmonitor", &fsmonitor, N_("enable or disable file system monitor")), - {OPTION_SET_INT, 0, "fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("mark files as fsmonitor valid"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("clear fsmonitor valid bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("mark files as fsmonitor valid"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("clear fsmonitor valid bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_END() }; diff --git a/builtin/write-tree.c b/builtin/write-tree.c index 43f233e69b0..5a8dc377ec0 100644 --- a/builtin/write-tree.c +++ b/builtin/write-tree.c @@ -31,10 +31,14 @@ int cmd_write_tree(int argc, WRITE_TREE_MISSING_OK), OPT_STRING(0, "prefix", &tree_prefix, N_("<prefix>/"), N_("write tree object for a subdirectory <prefix>")), - { OPTION_BIT, 0, "ignore-cache-tree", &flags, NULL, - N_("only useful for debugging"), - PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL, - WRITE_TREE_IGNORE_CACHE_TREE }, + { + .type = OPTION_BIT, + .long_name = "ignore-cache-tree", + .value = &flags, + .help = N_("only useful for debugging"), + .flags = PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, + .defval = WRITE_TREE_IGNORE_CACHE_TREE, + }, OPT_END() }; diff --git a/diff.c b/diff.c index 08f5e00a2cc..f2fcc7f3c22 100644 --- a/diff.c +++ b/diff.c @@ -5892,10 +5892,15 @@ struct option *add_diff_options(const struct option *opts, OPT_CALLBACK_F(0, "diff-filter", options, N_("[(A|C|D|M|R|T|U|X|B)...[*]]"), N_("select files by diff type"), PARSE_OPT_NONEG, diff_opt_diff_filter), - { OPTION_CALLBACK, 0, "output", options, N_("<file>"), - N_("output to a specific file"), - PARSE_OPT_NONEG, NULL, 0, diff_opt_output }, - + { + .type = OPTION_CALLBACK, + .long_name = "output", + .value = options, + .argh = N_("<file>"), + .help = N_("output to a specific file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = diff_opt_output, + }, OPT_END() }; diff --git a/ref-filter.h b/ref-filter.h index 013d4cfa64b..c98c4fbd4c1 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -114,11 +114,16 @@ struct ref_format { } /* Macros for checking --merged and --no-merged options */ -#define _OPT_MERGED_NO_MERGED(option, filter, h) \ - { OPTION_CALLBACK, 0, option, (filter), N_("commit"), (h), \ - PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ - parse_opt_merge_filter, (intptr_t) "HEAD" \ - } +#define _OPT_MERGED_NO_MERGED(option, filter, h) { \ + .type = OPTION_CALLBACK, \ + .long_name = option, \ + .value = (filter), \ + .argh = N_("commit"), \ + .help = (h), \ + .flags = PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ + .callback = parse_opt_merge_filter, \ + .defval = (intptr_t) "HEAD", \ +} #define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED("merged", f, h) #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h) diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index bfe45ec68b0..997f55fd45b 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -124,8 +124,15 @@ int cmd__parse_options(int argc, const char **argv) struct option options[] = { OPT_BOOL(0, "yes", &boolean, "get a boolean"), OPT_BOOL('D', "no-doubt", &boolean, "begins with 'no-'"), - { OPTION_SET_INT, 'B', "no-fear", &boolean, NULL, - "be brave", PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { + .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), @@ -155,12 +162,27 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP("Magic arguments"), OPT_NUMBER_CALLBACK(&integer, "set integer to NUM", number_callback), - { OPTION_COUNTUP, '+', NULL, &boolean, NULL, "same as -b", - PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH }, - { OPTION_COUNTUP, 0, "ambiguous", &ambiguous, NULL, - "positive ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, - { OPTION_COUNTUP, 0, "no-ambiguous", &ambiguous, NULL, - "negative ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, + { + .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"), -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v3 2/7] parse-options: check for overflow when parsing integers 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 1/7] global: use designated initializers for options Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt ` (4 subsequent siblings) 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood We use `strtol()` to parse the argument of `OPTION_INTEGER` options. And while we do check that the argument was fully parsed, we don't check `errno` at all and thus may not notice cases where `strtol()` fails. Most importantly, this includes the case where the parsed integer does not fit into a `long` at all. The consequence is that we'll happily continue with an invalid value. Fix the bug by checking `errno`. Note that this change alone is not sufficient to detect all possible overflows: `strtol()` returns a `long`, but we end up assigning the value to an `int` and will thus truncate the value. This will be fixed in subsequent patches. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 10 +++++++++- t/t0040-parse-options.sh | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/parse-options.c b/parse-options.c index 35fbb3b0d63..e8c08e55e02 100644 --- a/parse-options.c +++ b/parse-options.c @@ -185,12 +185,20 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, if (!*arg) return error(_("%s expects a numerical value"), optname(opt, flags)); + + errno = 0; *(int *)opt->value = strtol(arg, (char **)&s, 10); if (*s) return error(_("%s expects a numerical value"), optname(opt, flags)); - return 0; + if (errno == ERANGE) + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + arg, optname(opt, flags), (intmax_t)LONG_MIN, (intmax_t)LONG_MAX); + if (errno) + return error_errno(_("value %s for %s cannot be parsed"), + arg, optname(opt, flags)); + return 0; case OPTION_MAGNITUDE: if (unset) { *(unsigned long *)opt->value = 0; diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 2fe3522305f..5eb1feb61b4 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -783,4 +783,10 @@ test_expect_success 'magnitude with units but no numbers' ' test_must_be_empty out ' +test_expect_success 'overflowing integer' ' + test_must_fail test-tool parse-options --integer 9223372036854775808 >out 2>err && + test_grep "value .* for option .* not in range" err && + test_must_be_empty out +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 1/7] global: use designated initializers for options Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 2/7] parse-options: check for overflow when parsing integers Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 17:29 ` Junio C Hamano 2025-04-16 10:02 ` [PATCH v3 4/7] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt ` (3 subsequent siblings) 6 siblings, 1 reply; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood 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. Funny enough, even if the caller gets everything correct the parsing logic is still insufficient because we use `strtol()` to parse the argument, which returns a `long`. But as that value is implicitly cast when assigning it to the `int` field we may still get invalid results. 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> --- builtin/fmt-merge-msg.c | 2 ++ builtin/merge.c | 1 + builtin/show-branch.c | 1 + builtin/tag.c | 1 + parse-options.c | 63 +++++++++++++++++++++++++++++-------------- parse-options.h | 6 +++++ t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 23 +++++++++++++++- 8 files changed, 79 insertions(+), 21 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 240cdb474bc..3b6aac2cf7f 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("populate log with at most <n> entries from shortlog"), .flags = PARSE_OPT_OPTARG, @@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "summary", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("alias for --log (deprecated)"), .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, diff --git a/builtin/merge.c b/builtin/merge.c index 21787d45165..9ab10c7db0a 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -254,6 +254,7 @@ static struct option builtin_merge_options[] = { .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("add (at most <n>) entries from shortlog to merge commit message"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/show-branch.c b/builtin/show-branch.c index dab37019d29..b549d8c3f5b 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -671,6 +671,7 @@ int cmd_show_branch(int ac, .type = OPTION_INTEGER, .long_name = "more", .value = &extra, + .precision = sizeof(extra), .argh = N_("n"), .help = N_("show <n> more commits after the common ancestor"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/tag.c b/builtin/tag.c index b266f12bb48..7597d93c71b 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -483,6 +483,7 @@ int cmd_tag(int argc, .type = OPTION_INTEGER, .short_name = 'n', .value = &filter.lines, + .precision = sizeof(filter.lines), .argh = N_("n"), .help = N_("print <n> lines of each tag message"), .flags = PARSE_OPT_OPTARG, diff --git a/parse-options.c b/parse-options.c index e8c08e55e02..2cb9bd3b5b9 100644 --- a/parse-options.c +++ b/parse-options.c @@ -172,33 +172,56 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, return (*opt->ll_callback)(p, opt, p_arg, p_unset); } case OPTION_INTEGER: + { + intmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision); + intmax_t lower_bound = -upper_bound - 1; + intmax_t value; + if (unset) { - *(int *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(int *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!*arg) + } else if (!*arg) { return error(_("%s expects a numerical value"), optname(opt, flags)); + } else { + errno = 0; + value = strtoimax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); + if (errno == ERANGE) + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + arg, optname(opt, flags), lower_bound, upper_bound); + if (errno) + return error_errno(_("value %s for %s cannot be parsed"), + arg, optname(opt, flags)); + } - errno = 0; - *(int *)opt->value = strtol(arg, (char **)&s, 10); - if (*s) - return error(_("%s expects a numerical value"), - optname(opt, flags)); - if (errno == ERANGE) + if (value < lower_bound || value > upper_bound) return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), - arg, optname(opt, flags), (intmax_t)LONG_MIN, (intmax_t)LONG_MAX); - if (errno) - return error_errno(_("value %s for %s cannot be parsed"), - arg, optname(opt, flags)); + arg, optname(opt, flags), lower_bound, upper_bound); - return 0; + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; + return 0; + case 2: + *(int16_t *)opt->value = value; + return 0; + case 4: + *(int32_t *)opt->value = value; + return 0; + case 8: + *(int64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } case OPTION_MAGNITUDE: if (unset) { *(unsigned long *)opt->value = 0; diff --git a/parse-options.h b/parse-options.h index 997ffbee805..8db96402c4d 100644 --- a/parse-options.h +++ b/parse-options.h @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, * `value`:: * stores pointers to the values to be filled. * + * `precision`:: + * precision of the integer pointed to by `value` in number of bytes. Should + * typically be its `sizeof()`. + * * `argh`:: * token to explain the kind of argument this option wants. Does not * begin in capital letter, and does not end with a full stop. @@ -151,6 +155,7 @@ struct option { int short_name; const char *long_name; void *value; + size_t precision; const char *argh; const char *help; @@ -214,6 +219,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = (f), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 997f55fd45b..b1275dfade4 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; 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"), @@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv) 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_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23), @@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv) } show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); + show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); show(&expect, &ret, "magnitude: %lu", magnitude); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 5eb1feb61b4..95951436cda 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -22,6 +22,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer + --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude --[no-]set23 set integer to 23 @@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 0 magnitude: 16384 timestamp: 0 string: 123 @@ -156,6 +158,7 @@ test_expect_success 'short options' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 9000 magnitude: 16384 timestamp: 0 string: 321 @@ -167,7 +170,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' cat >expect <<-EOF && boolean: 0 integer: 0 + i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' ' cat >expect <<\EOF boolean: 1 integer: 13 +i16: 0 magnitude: 0 timestamp: 0 string: 123 @@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' ' cat >expect <<\EOF boolean: 0 integer: 2 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -343,6 +349,7 @@ cat >expect <<\EOF Callback: "four", 0 boolean: 5 integer: 4 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' ' cat >expect <<\EOF boolean: 1 integer: 23 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' ' cat >expect <<\EOF boolean: 0 integer: 0 +i16: 0 magnitude: 0 timestamp: 0 string: (not set) @@ -789,4 +798,16 @@ test_expect_success 'overflowing integer' ' test_must_be_empty out ' +test_expect_success 'i16 limits range' ' + test-tool parse-options --i16 32767 >out && + test_grep "i16: 32767" out && + test_must_fail test-tool parse-options --i16 32768 2>err && + test_grep "value 32768 for option .i16. not in range \[-32768,32767\]" err && + + test-tool parse-options --i16 -32768 >out && + test_grep "i16: -32768" out && + test_must_fail test-tool parse-options --i16 -32769 2>err && + test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-16 10:02 ` [PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt @ 2025-04-16 17:29 ` Junio C Hamano 0 siblings, 0 replies; 46+ messages in thread From: Junio C Hamano @ 2025-04-16 17:29 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood Patrick Steinhardt <ps@pks.im> writes: > 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. > > Funny enough, even if the caller gets everything correct the parsing > logic is still insufficient because we use `strtol()` to parse the > argument, which returns a `long`. But as that value is implicitly cast > when assigning it to the `int` field we may still get invalid results. > > 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> > --- > ... > + { > + intmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision); > + intmax_t lower_bound = -upper_bound - 1; > + intmax_t value; > + > if (unset) { > ... > + if (value < lower_bound || value > upper_bound) > return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), When I imagined the case where precision is set to the same size as sizeof(intmax_t), this comparison made my head spin, but I think it should safely yield false for any "value", in which case it is fine. > + switch (opt->precision) { > + case 1: > + *(int8_t *)opt->value = value; > + return 0; > + case 2: > + *(int16_t *)opt->value = value; > + return 0; > + case 4: > + *(int32_t *)opt->value = value; > + return 0; > + case 8: > + *(int64_t *)opt->value = value; > + return 0; > + default: Good to have this "default" arm. As you cannot take an address of a bitfield, you cannot pass a bitfield member in a struct that is set to say 24-bit wide with .precision set to 3. IOW, limiting ourselves only to sizes of naturally occurring integral types is sufficient. > + BUG("invalid precision for option %s", > + optname(opt, flags)); > + } > + } > case OPTION_MAGNITUDE: > if (unset) { > *(unsigned long *)opt->value = 0; > diff --git a/parse-options.h b/parse-options.h > index 997ffbee805..8db96402c4d 100644 > --- a/parse-options.h > +++ b/parse-options.h > @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, > * `value`:: > * stores pointers to the values to be filled. > * > + * `precision`:: > + * precision of the integer pointed to by `value` in number of bytes. Should > + * typically be its `sizeof()`. > + * Nicer to see "bytes" in the description. I am still puzzled, and more importantly, I suspect readers will be puzzled, when told "Should typically be", as it implies there may be valid cases when it can be different from it, and wanting to know what these cases are. > @@ -214,6 +219,7 @@ struct option { > .short_name = (s), \ > .long_name = (l), \ > .value = (v), \ > + .precision = sizeof(*v), \ > .argh = N_("n"), \ > .help = (h), \ > .flags = (f), \ Nice. By doing this in OPT_INTEGER_F(), we cover OPT_INTEGER() as well automatically. Thanks. ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v3 4/7] parse-options: introduce precision handling for `OPTION_MAGNITUDE` 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (2 preceding siblings ...) 2025-04-16 10:02 ` [PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt ` (2 subsequent siblings) 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood This commit is the equivalent to the preceding commit, but instead of introducing precision handling for `OPTION_INTEGER` we introduce it for `OPTION_MAGNITUDE`. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 45 ++++++++++++++++++++++++++++++++----------- parse-options.h | 1 + t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 18 ++++++++++++++++- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/parse-options.c b/parse-options.c index 2cb9bd3b5b9..259716efb17 100644 --- a/parse-options.c +++ b/parse-options.c @@ -202,7 +202,7 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, if (value < lower_bound || value > upper_bound) return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), - arg, optname(opt, flags), lower_bound, upper_bound); + arg, optname(opt, flags), (intmax_t)lower_bound, (intmax_t)upper_bound); switch (opt->precision) { case 1: @@ -223,21 +223,44 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, } } case OPTION_MAGNITUDE: + { + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + unsigned long value; + if (unset) { - *(unsigned long *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(unsigned long *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!git_parse_ulong(arg, opt->value)) + } else if (!git_parse_ulong(arg, &value)) { return error(_("%s expects a non-negative integer value" " with an optional k/m/g suffix"), optname(opt, flags)); - return 0; + } + + if (value > upper_bound) + return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), + arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + + switch (opt->precision) { + case 1: + *(uint8_t *)opt->value = value; + return 0; + case 2: + *(uint16_t *)opt->value = value; + return 0; + case 4: + *(uint32_t *)opt->value = value; + return 0; + case 8: + *(uint64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } default: BUG("opt->type %d should not happen", opt->type); diff --git a/parse-options.h b/parse-options.h index 8db96402c4d..55c42faa29f 100644 --- a/parse-options.h +++ b/parse-options.h @@ -281,6 +281,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = PARSE_OPT_NONEG, \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index b1275dfade4..46deb4317ef 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; + uint16_t m16 = 0; int16_t i16 = 0; struct option options[] = { @@ -143,6 +144,7 @@ int cmd__parse_options(int argc, const char **argv) OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), + OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), 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), @@ -214,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); show(&expect, &ret, "magnitude: %lu", magnitude); + show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); show(&expect, &ret, "abbrev: %d", abbrev); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 95951436cda..8daaf568485 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -25,6 +25,7 @@ usage: test-tool parse-options <options> --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude + --m16 <n> get a 16 bit magnitude --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) @@ -139,6 +140,7 @@ boolean: 2 integer: 1729 i16: 0 magnitude: 16384 +m16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -160,6 +162,7 @@ boolean: 2 integer: 1729 i16: 9000 magnitude: 16384 +m16: 32768 timestamp: 0 string: 321 abbrev: 10 @@ -171,7 +174,7 @@ EOF test_expect_success 'long options' ' test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ - --boolean --string2=321 --verbose --verbose --no-dry-run \ + --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && test_must_be_empty output.err && @@ -184,6 +187,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' integer: 0 i16: 0 magnitude: 0 + m16: 0 timestamp: 0 string: (not set) abbrev: 100 @@ -259,6 +263,7 @@ boolean: 1 integer: 13 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -283,6 +288,7 @@ boolean: 0 integer: 2 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -351,6 +357,7 @@ boolean: 5 integer: 4 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -377,6 +384,7 @@ boolean: 1 integer: 23 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -457,6 +465,7 @@ boolean: 0 integer: 0 i16: 0 magnitude: 0 +m16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -810,4 +819,11 @@ test_expect_success 'i16 limits range' ' test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err ' +test_expect_success 'm16 limits range' ' + test-tool parse-options --m16 65535 >out && + test_grep "m16: 65535" out && + test_must_fail test-tool parse-options --m16 65536 2>err && + test_grep "value 65536 for option .m16. not in range \[0,65535\]" err +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED` 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (3 preceding siblings ...) 2025-04-16 10:02 ` [PATCH v3 4/7] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 18:50 ` Junio C Hamano 2025-04-16 10:02 ` [PATCH v3 6/7] parse-options: detect mismatches in integer signedness Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 7/7] parse-options: introduce bounded integer options Patrick Steinhardt 6 siblings, 1 reply; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood We have two generic ways to parse integers in the "parse-options" subsystem: - `OPTION_INTEGER` parses a signed integer. - `OPTION_MAGNITUDE` parses an unsigned integer, but it also interprets suffixes like "k" or "g". Notably missing is a middle ground that parses unsigned integers without interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to plug this gap. This option type will be used in subsequent commits. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 54 +++++++++++++++++++++++++++++++++++++++++++ parse-options.h | 12 ++++++++++ t/helper/test-parse-options.c | 4 +++- t/t0040-parse-options.sh | 24 ++++++++++++++++++- 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/parse-options.c b/parse-options.c index 259716efb17..e4dc22464b2 100644 --- a/parse-options.c +++ b/parse-options.c @@ -222,6 +222,60 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, optname(opt, flags)); } } + case OPTION_UNSIGNED: + { + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + uintmax_t value; + + if (unset) { + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { + return -1; + } else if (!*arg) { + return error(_("%s expects a numerical value"), + optname(opt, flags)); + } else if (*arg == '-') { + return error(_("%s does not accept negative values"), + optname(opt, flags)); + } else { + errno = 0; + value = strtoumax(arg, (char **)&s, 10); + if (*s) + return error(_("%s expects a numerical value"), + optname(opt, flags)); + if (errno == ERANGE) + return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), + arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + if (errno) + return error_errno(_("value %s for %s cannot be parsed"), + arg, optname(opt, flags)); + + } + + if (value > upper_bound) + return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), + arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + + switch (opt->precision) { + case 1: + *(uint8_t *)opt->value = value; + return 0; + case 2: + *(uint16_t *)opt->value = value; + return 0; + case 4: + *(uint32_t *)opt->value = value; + return 0; + case 8: + *(uint64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } case OPTION_MAGNITUDE: { uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); diff --git a/parse-options.h b/parse-options.h index 55c42faa29f..aa37134dc72 100644 --- a/parse-options.h +++ b/parse-options.h @@ -25,6 +25,7 @@ enum parse_opt_type { /* options with arguments (usually) */ OPTION_STRING, OPTION_INTEGER, + OPTION_UNSIGNED, OPTION_MAGNITUDE, OPTION_CALLBACK, OPTION_LOWLEVEL_CALLBACK, @@ -224,6 +225,16 @@ struct option { .help = (h), \ .flags = (f), \ } +#define OPT_UNSIGNED_F(s, l, v, h, f) { \ + .type = OPTION_UNSIGNED, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v), \ + .precision = sizeof(*v), \ + .argh = N_("n"), \ + .help = (h), \ + .flags = (f), \ +} #define OPT_END() { \ .type = OPTION_END, \ @@ -276,6 +287,7 @@ struct option { #define OPT_CMDMODE(s, l, v, h, i) OPT_CMDMODE_F(s, l, v, h, i, 0) #define OPT_INTEGER(s, l, v, h) OPT_INTEGER_F(s, l, v, h, 0) +#define OPT_UNSIGNED(s, l, v, h) OPT_UNSIGNED_F(s, l, v, h, 0) #define OPT_MAGNITUDE(s, l, v, h) { \ .type = OPTION_MAGNITUDE, \ .short_name = (s), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 46deb4317ef..0d559288d9c 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; - uint16_t m16 = 0; + uint16_t m16 = 0, u16 = 0; int16_t i16 = 0; struct option options[] = { @@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP(""), OPT_INTEGER('i', "integer", &integer, "get a integer"), OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), + OPT_UNSIGNED(0, "u16", &u16, "get a 16 bit unsigned integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), @@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); + show(&expect, &ret, "u16: %"PRIuMAX, (uintmax_t) u16); show(&expect, &ret, "magnitude: %lu", magnitude); show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 8daaf568485..66875ce0586 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -23,6 +23,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer --[no-]i16 <n> get a 16 bit integer + --[no-]u16 <n> get a 16 bit unsigned integer -j <n> get a integer, too -m, --magnitude <n> get a magnitude --m16 <n> get a 16 bit magnitude @@ -139,6 +140,7 @@ cat >expect <<\EOF boolean: 2 integer: 1729 i16: 0 +u16: 0 magnitude: 16384 m16: 0 timestamp: 0 @@ -161,6 +163,7 @@ cat >expect <<\EOF boolean: 2 integer: 1729 i16: 9000 +u16: 5432 magnitude: 16384 m16: 32768 timestamp: 0 @@ -173,7 +176,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \ --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' boolean: 0 integer: 0 i16: 0 + u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -262,6 +266,7 @@ cat >expect <<\EOF boolean: 1 integer: 13 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -287,6 +292,7 @@ cat >expect <<\EOF boolean: 0 integer: 2 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -356,6 +362,7 @@ Callback: "four", 0 boolean: 5 integer: 4 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -383,6 +390,7 @@ cat >expect <<\EOF boolean: 1 integer: 23 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -464,6 +472,7 @@ cat >expect <<\EOF boolean: 0 integer: 0 i16: 0 +u16: 0 magnitude: 0 m16: 0 timestamp: 0 @@ -826,4 +835,17 @@ test_expect_success 'm16 limits range' ' test_grep "value 65536 for option .m16. not in range \[0,65535\]" err ' +test_expect_success 'u16 limits range' ' + test-tool parse-options --u16 65535 >out && + test_grep "u16: 65535" out && + test_must_fail test-tool parse-options --u16 65536 2>err && + test_grep "value 65536 for option .u16. not in range \[0,65535\]" err +' + +test_expect_success 'u16 does not accept negative value' ' + test_must_fail test-tool parse-options --u16 -1 >out 2>err && + test_grep "option .u16. does not accept negative values" err && + test_must_be_empty out +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED` 2025-04-16 10:02 ` [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt @ 2025-04-16 18:50 ` Junio C Hamano 2025-04-17 8:15 ` Patrick Steinhardt 0 siblings, 1 reply; 46+ messages in thread From: Junio C Hamano @ 2025-04-16 18:50 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood Patrick Steinhardt <ps@pks.im> writes: > We have two generic ways to parse integers in the "parse-options" > subsystem: > > - `OPTION_INTEGER` parses a signed integer. > > - `OPTION_MAGNITUDE` parses an unsigned integer, but it also > interprets suffixes like "k" or "g". > > Notably missing is a middle ground that parses unsigned integers without > interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to > plug this gap. This option type will be used in subsequent commits. This takes a turn in a bit unexpected direction. Because the way to spell a scaled quantity is unambiguous in the sense that anything that used to be parsed with OPT_INTEGER() couldn't have had anything but "^-?[0-9]+$", an obvious alternative is to teach OPTION_INTEGER to always allow the scaling suffix if the user wants to use one, without adding a new "only numbers but this one does not even allow a sign" variant. Seriously, are there good candidates for an option where we want to absolutely refuse to take scaling suffix and insist only on a bare number? ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED` 2025-04-16 18:50 ` Junio C Hamano @ 2025-04-17 8:15 ` Patrick Steinhardt 0 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 8:15 UTC (permalink / raw) To: Junio C Hamano Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood On Wed, Apr 16, 2025 at 11:50:42AM -0700, Junio C Hamano wrote: > Patrick Steinhardt <ps@pks.im> writes: > > > We have two generic ways to parse integers in the "parse-options" > > subsystem: > > > > - `OPTION_INTEGER` parses a signed integer. > > > > - `OPTION_MAGNITUDE` parses an unsigned integer, but it also > > interprets suffixes like "k" or "g". > > > > Notably missing is a middle ground that parses unsigned integers without > > interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to > > plug this gap. This option type will be used in subsequent commits. > > This takes a turn in a bit unexpected direction. Because the way to > spell a scaled quantity is unambiguous in the sense that anything > that used to be parsed with OPT_INTEGER() couldn't have had anything > but "^-?[0-9]+$", an obvious alternative is to teach OPTION_INTEGER > to always allow the scaling suffix if the user wants to use one, > without adding a new "only numbers but this one does not even allow > a sign" variant. Yes, that would be the alternative indeed. I wanted to have this patch so that I can adapt callsites that accidentally pass unsigned integers to the signed option without introducing any kind of incompatibility. So I was mostly aiming for 1:1 compatibility. But I'm equally fine with extending `OPT_INTEGER()` to support parsing magnitudes. I guess it's even preferable: less overhead on our side, and more consistency for our users. > Seriously, are there good candidates for an option where we want to > absolutely refuse to take scaling suffix and insist only on a bare > number? I cannot think of any specific reason. Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v3 6/7] parse-options: detect mismatches in integer signedness 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (4 preceding siblings ...) 2025-04-16 10:02 ` [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 7/7] parse-options: introduce bounded integer options Patrick Steinhardt 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood It was reported that "t5620-backfill.sh" fails on s390x and sparc64 in a test that exercises the "--min-batch-size" command line option. The symptom was that the option didn't seem to have an effect: we didn't fetch objects with a batch size of 20, but instead fetched all objects at once. As it turns out, the root cause is that `--min-batch-size` uses `OPT_INTEGER()` to parse the command line option. While this macro expects the caller to pass a pointer to an integer, we instead pass a pointer to a `size_t`. This coincidentally works on most platforms, but it breaks apart on the mentioned platforms because they are big endian. This issue isn't specific to git-backfill(1): there are a couple of other places where we have the same type confusion going on. This indicates that the issue really is the interface that the parse-options subsystem provides -- it is simply too easy to get this wrong as there isn't any kind of compiler warning, and things just work on the most common systems. Address the systemic issue by introducing two new build asserts `BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names already hint at, those macros will cause a compiler error when passed a value that is not signed or unsigned, respectively. Adapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to use those asserts. This uncovers a small set of sites where we indeed have the same bug as in git-backfill(1). Adapt all of them to use the correct option. Reported-by: Todd Zullinger <tmz@pobox.com> Reported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de> Helped-by: SZEDER Gábor <szeder.dev@gmail.com> Helped-by: Jeff King <peff@peff.net> Signed-off-by: Patrick Steinhardt <ps@pks.im> --- apply.c | 4 ++-- builtin/backfill.c | 4 ++-- builtin/column.c | 2 +- builtin/grep.c | 4 ++-- git-compat-util.h | 7 +++++++ parse-options.h | 6 +++--- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apply.c b/apply.c index f274a379487..a850c7d75fe 100644 --- a/apply.c +++ b/apply.c @@ -5123,8 +5123,8 @@ int apply_parse_options(int argc, const char **argv, /* Think twice before adding "--nul" synonym to this */ OPT_SET_INT('z', NULL, &state->line_termination, N_("paths are separated with NUL character"), '\0'), - OPT_INTEGER('C', NULL, &state->p_context, - N_("ensure at least <n> lines of context match")), + OPT_UNSIGNED('C', NULL, &state->p_context, + N_("ensure at least <n> lines of context match")), OPT_CALLBACK(0, "whitespace", state, N_("action"), N_("detect new or modified lines that have whitespace errors"), apply_option_parse_whitespace), diff --git a/builtin/backfill.c b/builtin/backfill.c index 33e1ea2f84f..d95d7a2d4d6 100644 --- a/builtin/backfill.c +++ b/builtin/backfill.c @@ -123,8 +123,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit .sparse = 0, }; struct option options[] = { - OPT_INTEGER(0, "min-batch-size", &ctx.min_batch_size, - N_("Minimum number of objects to request at a time")), + OPT_UNSIGNED(0, "min-batch-size", &ctx.min_batch_size, + N_("Minimum number of objects to request at a time")), OPT_BOOL(0, "sparse", &ctx.sparse, N_("Restrict the missing objects to the current sparse-checkout")), OPT_END(), diff --git a/builtin/column.c b/builtin/column.c index 50314cc2559..ce6443d5fac 100644 --- a/builtin/column.c +++ b/builtin/column.c @@ -31,7 +31,7 @@ int cmd_column(int argc, struct option options[] = { OPT_STRING(0, "command", &real_command, N_("name"), N_("lookup config vars")), OPT_COLUMN(0, "mode", &colopts, N_("layout to use")), - OPT_INTEGER(0, "raw-mode", &colopts, N_("layout to use")), + OPT_UNSIGNED(0, "raw-mode", &colopts, N_("layout to use")), OPT_INTEGER(0, "width", &copts.width, N_("maximum width")), OPT_STRING(0, "indent", &copts.indent, N_("string"), N_("padding space on left border")), OPT_STRING(0, "nl", &copts.nl, N_("string"), N_("padding space on right border")), diff --git a/builtin/grep.c b/builtin/grep.c index c4869733e1b..f23a6f1dc86 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -983,9 +983,9 @@ int cmd_grep(int argc, OPT_CALLBACK('C', "context", &opt, N_("n"), N_("show <n> context lines before and after matches"), context_callback), - OPT_INTEGER('B', "before-context", &opt.pre_context, + OPT_UNSIGNED('B', "before-context", &opt.pre_context, N_("show <n> context lines before matches")), - OPT_INTEGER('A', "after-context", &opt.post_context, + OPT_UNSIGNED('A', "after-context", &opt.post_context, N_("show <n> context lines after matches")), OPT_INTEGER(0, "threads", &num_threads, N_("use <n> worker threads")), diff --git a/git-compat-util.h b/git-compat-util.h index cf733b38acd..1218fcf81a4 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -110,12 +110,19 @@ DISABLE_WARNING(-Wsign-compare) # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(__builtin_types_compatible_p(__typeof__(*(dst)), \ __typeof__(*(src)))) + +# define BARF_UNLESS_SIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) < 0) +# define BARF_UNLESS_UNSIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) > 0) #else # define BARF_UNLESS_AN_ARRAY(arr) 0 # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(0 ? ((*(dst) = *(src)), 0) : \ sizeof(*(dst)) == sizeof(*(src))) + +# define BARF_UNLESS_SIGNED(var) 0 +# define BARF_UNLESS_UNSIGNED(var) 0 #endif + /* * ARRAY_SIZE - get the number of elements in a visible array * @x: the array whose size you want. diff --git a/parse-options.h b/parse-options.h index aa37134dc72..168df642386 100644 --- a/parse-options.h +++ b/parse-options.h @@ -219,7 +219,7 @@ struct option { .type = OPTION_INTEGER, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_SIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ @@ -229,7 +229,7 @@ struct option { .type = OPTION_UNSIGNED, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ @@ -292,7 +292,7 @@ struct option { .type = OPTION_MAGNITUDE, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v3 7/7] parse-options: introduce bounded integer options 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (5 preceding siblings ...) 2025-04-16 10:02 ` [PATCH v3 6/7] parse-options: detect mismatches in integer signedness Patrick Steinhardt @ 2025-04-16 10:02 ` Patrick Steinhardt 2025-04-16 19:19 ` Junio C Hamano 6 siblings, 1 reply; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-16 10:02 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood In the preceding commits we have introduced integer precisions. The precision merely tracks bounds of the underlying data types so that we don't try to for example write a `size_t` into an `unsigned`, which could otherwise cause out-of-bounds writes. Some options may have bounds that are stricter than the underlying data type. Right now, users of any such options would have to manually verify that the value passed to such an option is inside the expected bounds. This is rather tedious, and it leads to code duplication across sites that wish to perform such bounds checks. Introduce `OPT_*_BOUNDED()` options that alleviate this issue. Users can optionally specify both a lower and upper bound, and if set we will verify that the value passed by the user is in that range. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 40 ++++++++++++++++++++++++++++----- parse-options.h | 52 +++++++++++++++++++++++++++++++++++++++++++ t/helper/test-parse-options.c | 5 +++++ t/t0040-parse-options.sh | 33 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/parse-options.c b/parse-options.c index e4dc22464b2..d1dffcfdf5f 100644 --- a/parse-options.c +++ b/parse-options.c @@ -177,6 +177,20 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, intmax_t lower_bound = -upper_bound - 1; intmax_t value; + if (opt->lower_bound) { + if (opt->lower_bound < lower_bound) + BUG("invalid lower bound for option %s", optname(opt, flags)); + if (opt->lower_bound > lower_bound) + lower_bound = opt->lower_bound; + } + + if (opt->upper_bound) { + if (opt->upper_bound > (uintmax_t)upper_bound) + BUG("invalid upper bound for option %s", optname(opt, flags)); + if (opt->upper_bound < (uintmax_t)upper_bound) + upper_bound = opt->upper_bound; + } + if (unset) { value = 0; } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { @@ -225,8 +239,16 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, case OPTION_UNSIGNED: { uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + uintmax_t lower_bound = 0; uintmax_t value; + if (opt->lower_bound < 0) + BUG("invalid lower bound for option %s", optname(opt, flags)); + if (opt->lower_bound > 0) + lower_bound = opt->lower_bound; + if (opt->upper_bound && opt->upper_bound < upper_bound) + upper_bound = opt->upper_bound; + if (unset) { value = 0; } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { @@ -247,16 +269,16 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, optname(opt, flags)); if (errno == ERANGE) return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), - arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + arg, optname(opt, flags), (uintmax_t)lower_bound, (uintmax_t)upper_bound); if (errno) return error_errno(_("value %s for %s cannot be parsed"), arg, optname(opt, flags)); } - if (value > upper_bound) + if (value < lower_bound || value > upper_bound) return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), - arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + arg, optname(opt, flags), (uintmax_t)lower_bound, (uintmax_t)upper_bound); switch (opt->precision) { case 1: @@ -279,8 +301,16 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, case OPTION_MAGNITUDE: { uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + uintmax_t lower_bound = 0; unsigned long value; + if (opt->lower_bound < 0) + BUG("invalid lower bound for option %s", optname(opt, flags)); + if (opt->lower_bound > 0) + lower_bound = opt->lower_bound; + if (opt->upper_bound && opt->upper_bound < upper_bound) + upper_bound = opt->upper_bound; + if (unset) { value = 0; } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { @@ -293,9 +323,9 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, optname(opt, flags)); } - if (value > upper_bound) + if (value < lower_bound || value > upper_bound) return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), - arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); + arg, optname(opt, flags), (uintmax_t)lower_bound, (uintmax_t)upper_bound); switch (opt->precision) { case 1: diff --git a/parse-options.h b/parse-options.h index 168df642386..c1ebdaf7639 100644 --- a/parse-options.h +++ b/parse-options.h @@ -97,6 +97,13 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, * precision of the integer pointed to by `value` in number of bytes. Should * typically be its `sizeof()`. * + * `lower_bound`,`upper_bound`:: + * lower and upper bound of the integer to further restrict the accepted + * range of integer values. `0` will use the minimum and maximum values for + * the integer type of the specified precision. Specifying a bound that does + * not fit into an integer type of the specified precision will trigger a + * bug. + * * `argh`:: * token to explain the kind of argument this option wants. Does not * begin in capital letter, and does not end with a full stop. @@ -157,6 +164,8 @@ struct option { const char *long_name; void *value; size_t precision; + intmax_t lower_bound; + uintmax_t upper_bound; const char *argh; const char *help; @@ -225,6 +234,19 @@ struct option { .help = (h), \ .flags = (f), \ } +#define OPT_INTEGER_BOUNDED_F(s, l, v, lower, upper, h, f) { \ + .type = OPTION_INTEGER, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v) + BARF_UNLESS_SIGNED(*(v)), \ + .precision = sizeof(*v), \ + .lower_bound = (lower), \ + .upper_bound = (upper), \ + .argh = N_("n"), \ + .help = (h), \ + .flags = (f), \ +} + #define OPT_UNSIGNED_F(s, l, v, h, f) { \ .type = OPTION_UNSIGNED, \ .short_name = (s), \ @@ -235,6 +257,18 @@ struct option { .help = (h), \ .flags = (f), \ } +#define OPT_UNSIGNED_BOUNDED_F(s, l, v, lower, upper, h, f) { \ + .type = OPTION_UNSIGNED, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ + .precision = sizeof(*v), \ + .lower_bound = (lower), \ + .upper_bound = (upper), \ + .argh = N_("n"), \ + .help = (h), \ + .flags = (f), \ +} #define OPT_END() { \ .type = OPTION_END, \ @@ -287,7 +321,12 @@ struct option { #define OPT_CMDMODE(s, l, v, h, i) OPT_CMDMODE_F(s, l, v, h, i, 0) #define OPT_INTEGER(s, l, v, h) OPT_INTEGER_F(s, l, v, h, 0) +#define OPT_INTEGER_BOUNDED(s, l, v, lower, upper, h) \ + OPT_INTEGER_BOUNDED_F(s, l, v, lower, upper, h, 0) #define OPT_UNSIGNED(s, l, v, h) OPT_UNSIGNED_F(s, l, v, h, 0) +#define OPT_UNSIGNED_BOUNDED(s, l, v, lower, upper, h) \ + OPT_UNSIGNED_BOUNDED_F(s, l, v, lower, upper, h, 0) + #define OPT_MAGNITUDE(s, l, v, h) { \ .type = OPTION_MAGNITUDE, \ .short_name = (s), \ @@ -298,6 +337,19 @@ struct option { .help = (h), \ .flags = PARSE_OPT_NONEG, \ } +#define OPT_MAGNITUDE_BOUNDED(s, l, v, lower, upper, h) { \ + .type = OPTION_MAGNITUDE, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ + .precision = sizeof(*v), \ + .lower_bound = (lower), \ + .upper_bound = (upper), \ + .argh = N_("n"), \ + .help = (h), \ + .flags = PARSE_OPT_NONEG, \ +} + #define OPT_STRING(s, l, v, a, h) OPT_STRING_F(s, l, v, a, h, 0) #define OPT_STRING_LIST(s, l, v, a, h) { \ .type = OPTION_CALLBACK, \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 0d559288d9c..0fcceec56a7 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,7 +120,9 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; + uint32_t mbounded = 0, ubounded = 0; uint16_t m16 = 0, u16 = 0; + int32_t ibounded = 0; int16_t i16 = 0; struct option options[] = { @@ -142,10 +144,13 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP(""), OPT_INTEGER('i', "integer", &integer, "get a integer"), OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), + OPT_INTEGER_BOUNDED(0, "ibounded", &ibounded, -10, 10, "get a bounded integer between [-10,10]"), OPT_UNSIGNED(0, "u16", &u16, "get a 16 bit unsigned integer"), + OPT_UNSIGNED_BOUNDED(0, "ubounded", &ubounded, 10, 100, "get a bounded unsigned integer between [10,100]"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), + OPT_MAGNITUDE_BOUNDED(0, "mbounded", &mbounded, 10, 100, "get a bounded magnitude between [10,100]"), 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), diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 66875ce0586..d76165c2053 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -23,10 +23,13 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer --[no-]i16 <n> get a 16 bit integer + --[no-]ibounded <n> get a bounded integer between [-10,10] --[no-]u16 <n> get a 16 bit unsigned integer + --[no-]ubounded <n> get a bounded unsigned integer between [10,100] -j <n> get a integer, too -m, --magnitude <n> get a magnitude --m16 <n> get a 16 bit magnitude + --mbounded <n> get a bounded magnitude between [10,100] --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) @@ -848,4 +851,34 @@ test_expect_success 'u16 does not accept negative value' ' test_must_be_empty out ' +test_expect_success 'ibounded does not accept outside range' ' + test_must_fail test-tool parse-options --ibounded -11 >out 2>err && + test_grep "value -11 for option .ibounded. not in range \[-10,10\]" err && + test_must_fail test-tool parse-options --ibounded 11 >out 2>err && + test_grep "value 11 for option .ibounded. not in range \[-10,10\]" err && + test-tool parse-options --ibounded -10 && + test-tool parse-options --ibounded 0 && + test-tool parse-options --ibounded 10 +' + +test_expect_success 'ubounded does not accept outside range' ' + test_must_fail test-tool parse-options --ubounded 9 >out 2>err && + test_grep "value 9 for option .ubounded. not in range \[10,100\]" err && + test_must_fail test-tool parse-options --ubounded 101 >out 2>err && + test_grep "value 101 for option .ubounded. not in range \[10,100\]" err && + test-tool parse-options --ubounded 10 && + test-tool parse-options --ubounded 50 && + test-tool parse-options --ubounded 100 +' + +test_expect_success 'mbounded does not accept outside range' ' + test_must_fail test-tool parse-options --mbounded 9 >out 2>err && + test_grep "value 9 for option .mbounded. not in range \[10,100\]" err && + test_must_fail test-tool parse-options --mbounded 101 >out 2>err && + test_grep "value 101 for option .mbounded. not in range \[10,100\]" err && + test-tool parse-options --mbounded 10 && + test-tool parse-options --mbounded 50 && + test-tool parse-options --mbounded 100 +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH v3 7/7] parse-options: introduce bounded integer options 2025-04-16 10:02 ` [PATCH v3 7/7] parse-options: introduce bounded integer options Patrick Steinhardt @ 2025-04-16 19:19 ` Junio C Hamano 2025-04-17 8:14 ` Patrick Steinhardt 0 siblings, 1 reply; 46+ messages in thread From: Junio C Hamano @ 2025-04-16 19:19 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood Patrick Steinhardt <ps@pks.im> writes: > In the preceding commits we have introduced integer precisions. The > precision merely tracks bounds of the underlying data types so that we > don't try to for example write a `size_t` into an `unsigned`, which > could otherwise cause out-of-bounds writes. > > Some options may have bounds that are stricter than the underlying data > type. Right now, users of any such options would have to manually verify > that the value passed to such an option is inside the expected bounds. > This is rather tedious, and it leads to code duplication across sites > that wish to perform such bounds checks. > > Introduce `OPT_*_BOUNDED()` options that alleviate this issue. Users > can optionally specify both a lower and upper bound, and if set we will > verify that the value passed by the user is in that range. > > Signed-off-by: Patrick Steinhardt <ps@pks.im> > --- > parse-options.c | 40 ++++++++++++++++++++++++++++----- > parse-options.h | 52 +++++++++++++++++++++++++++++++++++++++++++ > t/helper/test-parse-options.c | 5 +++++ > t/t0040-parse-options.sh | 33 +++++++++++++++++++++++++++ > 4 files changed, 125 insertions(+), 5 deletions(-) It is certainly cute, but unless there are plenty of existing users that use OPT_INTEGER() and friends and perform bounds checks themselves, I am not sure if this can withstand YAGNI criticism. And this step being at the end of the series, plus the above diffstat, tells us that there aren't any existing users converted to use this new mechanism. OPT_INTEGER that wants to track percentage may want to say the value is between 0 and 100 (inclusive), but instead we take it bounded not to exceed 100, without lower bound. Without a real callsite, we cannot even tell if it is acceptable compromise for the sake of simplicity to forbid 0 as lower or upper bound, for example. Thanks. ^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [PATCH v3 7/7] parse-options: introduce bounded integer options 2025-04-16 19:19 ` Junio C Hamano @ 2025-04-17 8:14 ` Patrick Steinhardt 0 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 8:14 UTC (permalink / raw) To: Junio C Hamano Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood On Wed, Apr 16, 2025 at 12:19:31PM -0700, Junio C Hamano wrote: > Patrick Steinhardt <ps@pks.im> writes: > > > In the preceding commits we have introduced integer precisions. The > > precision merely tracks bounds of the underlying data types so that we > > don't try to for example write a `size_t` into an `unsigned`, which > > could otherwise cause out-of-bounds writes. > > > > Some options may have bounds that are stricter than the underlying data > > type. Right now, users of any such options would have to manually verify > > that the value passed to such an option is inside the expected bounds. > > This is rather tedious, and it leads to code duplication across sites > > that wish to perform such bounds checks. > > > > Introduce `OPT_*_BOUNDED()` options that alleviate this issue. Users > > can optionally specify both a lower and upper bound, and if set we will > > verify that the value passed by the user is in that range. > > > > Signed-off-by: Patrick Steinhardt <ps@pks.im> > > --- > > parse-options.c | 40 ++++++++++++++++++++++++++++----- > > parse-options.h | 52 +++++++++++++++++++++++++++++++++++++++++++ > > t/helper/test-parse-options.c | 5 +++++ > > t/t0040-parse-options.sh | 33 +++++++++++++++++++++++++++ > > 4 files changed, 125 insertions(+), 5 deletions(-) > > It is certainly cute, but unless there are plenty of existing users > that use OPT_INTEGER() and friends and perform bounds checks > themselves, I am not sure if this can withstand YAGNI criticism. > And this step being at the end of the series, plus the above > diffstat, tells us that there aren't any existing users converted to > use this new mechanism. Yeah, that was also a bit of my feeling. I was on the lookout for callsites, but I ultimately didn't find too many. Which is basically the reason why I said that this patch is more of a PoC, and that I'm happy to drop it again. > OPT_INTEGER that wants to track percentage may want to say the value > is between 0 and 100 (inclusive), but instead we take it bounded not > to exceed 100, without lower bound. Without a real callsite, we > cannot even tell if it is acceptable compromise for the sake of > simplicity to forbid 0 as lower or upper bound, for example. Yes, `0` meaning "default" is restricting us here. But my counter argument is that a value that can only be between `0` and `100` should use `OPT_UNSIGNED` in the first place, which allows us to achieve exactly that. Let's just drop this patch for now. It was only a PoC anyway, and we can use it as inpiration if we ever see that this feature is something we want. Patrick ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v4 0/7] parse-options: harden handling of integer values 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt ` (6 preceding siblings ...) 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 1/7] parse: fix off-by-one for minimum signed values Patrick Steinhardt ` (6 more replies) 7 siblings, 7 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood Hi, this patch series addresses the issues raised in [1] and [2]. As discussed in [1], the series also introduces a couple of safeguards to make it harder to misuse `OPT_INTEGER()` and `OPT_MAGNITUDE()`: - We now track the precision of the underlying integer types. This makes it possible to pass arbitrarily-sized integers to those options, not only `int` and `unsigned long`, respectively. - We introduce a build assert to verify that the passed variable has correct signedness. Furthermore, the series introduces `OPT_UNSIGNED()` to adapt all callsites that previously used variables with the wrong signedness. Changes in v2: - Adapt computation of upper bounds to use similar logic to `maximum_signed_value_of_type()`. - Link to v1: https://lore.kernel.org/r/20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im Changes in v3: - Introduce `errno` checks for `strto{u,i}max()`. - Note that the precision is in bytes. - Reject leading '-' when parsing unsigned integers. - Introduce bounded integer options. This patch is mostly a proof of concept that demonstrates that precision and ranges are orthogonal to one another, so I consider it to be an optional patch. It may be useful in the future, but I haven't converted any callsites to use bounds yet. - Link to v2: https://lore.kernel.org/r/20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im Changes in v4: - Adapt `OPTION_INTEGER()` to also accept unit factors. Like this, we can avoid introducing `OPTION_UNSIGNED()` because now the behaviour of `OPTION_INTEGER()` and `OPTION_MAGNITUDE()` are the same, except of course the signedness. - Rename `OPTION_MAGNITUDE()` to clarify this new world order, as the main difference isn't unit factors anymore but only the signedness. - Drop the PoC patch that introduces bounded integer options. - Fix an off-by-one error for the lower boundary in `git_parse_signed()`. - Link to v3: https://lore.kernel.org/r/20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im Thanks! Patrick [1]: <89257ab82cd60d135cce02d51eacee7ec35c1c37.camel@physik.fu-berlin.de> [2]: <Z8HW6petWuMRWSXf@teonanacatl.net> --- Patrick Steinhardt (7): parse: fix off-by-one for minimum signed values global: use designated initializers for options parse-options: support unit factors in `OPT_INTEGER()` parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` parse-options: introduce precision handling for `OPTION_INTEGER` parse-options: introduce precision handling for `OPTION_UNSIGNED` parse-options: detect mismatches in integer signedness Documentation/technical/api-parse-options.adoc | 10 +- apply.c | 4 +- archive.c | 35 +++++-- builtin/am.c | 28 ++++-- builtin/backfill.c | 4 +- builtin/clone.c | 13 ++- builtin/column.c | 2 +- builtin/commit-tree.c | 12 ++- builtin/commit.c | 62 +++++++++--- builtin/config.c | 13 ++- builtin/describe.c | 24 +++-- builtin/fetch.c | 10 +- builtin/fmt-merge-msg.c | 27 +++-- builtin/gc.c | 16 ++- builtin/grep.c | 18 ++-- builtin/init-db.c | 13 ++- builtin/ls-remote.c | 11 ++- builtin/merge.c | 38 +++++-- builtin/multi-pack-index.c | 2 +- builtin/pack-objects.c | 8 +- builtin/read-tree.c | 11 ++- builtin/rebase.c | 25 +++-- builtin/repack.c | 8 +- builtin/revert.c | 12 ++- builtin/show-branch.c | 13 ++- builtin/tag.c | 24 +++-- builtin/update-index.c | 131 +++++++++++++++++-------- builtin/write-tree.c | 12 ++- diff.c | 13 ++- git-compat-util.h | 7 ++ parse-options.c | 102 ++++++++++++++----- parse-options.h | 17 +++- parse.c | 4 +- parse.h | 1 + ref-filter.h | 15 ++- t/helper/test-parse-options.c | 50 +++++++--- t/t0040-parse-options.sh | 93 +++++++++++++----- 37 files changed, 646 insertions(+), 242 deletions(-) Range-diff versus v3: -: ----------- > 1: 99a003e994b parse: fix off-by-one for minimum signed values 1: 7a3f09bbbbd = 2: 014d716fe08 global: use designated initializers for options 2: 526a1a2f2c4 < -: ----------- parse-options: check for overflow when parsing integers -: ----------- > 3: e3644d6825c parse-options: support unit factors in `OPT_INTEGER()` -: ----------- > 4: 5c10f8d5866 parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` 3: 2c2a2685455 ! 5: 4aa18d68c03 parse-options: introduce precision handling for `OPTION_INTEGER` @@ Commit message - 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. - Funny enough, even if the caller gets everything correct the parsing - logic is still insufficient because we use `strtol()` to parse the - argument, which returns a `long`. But as that value is implicitly cast - when assigning it to the `int` field we may still get invalid results. - 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 @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ + } else if (!*arg) { return error(_("%s expects a numerical value"), optname(opt, flags)); -+ } else { -+ errno = 0; -+ value = strtoimax(arg, (char **)&s, 10); -+ if (*s) -+ return error(_("%s expects a numerical value"), -+ optname(opt, flags)); +- if (!git_parse_int(arg, opt->value)) +- return error(_("%s expects an integer value" +- " with an optional k/m/g suffix"), ++ } else if (!git_parse_signed(arg, &value, upper_bound)) { + if (errno == ERANGE) + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + arg, optname(opt, flags), lower_bound, upper_bound); -+ if (errno) -+ return error_errno(_("value %s for %s cannot be parsed"), -+ arg, optname(opt, flags)); ++ ++ return error(_("%s expects an integer value with an optional k/m/g suffix"), + optname(opt, flags)); +- return 0; + } - -- errno = 0; -- *(int *)opt->value = strtol(arg, (char **)&s, 10); -- if (*s) -- return error(_("%s expects a numerical value"), -- optname(opt, flags)); -- if (errno == ERANGE) -+ if (value < lower_bound || value > upper_bound) - return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), -- arg, optname(opt, flags), (intmax_t)LONG_MIN, (intmax_t)LONG_MAX); -- if (errno) -- return error_errno(_("value %s for %s cannot be parsed"), -- arg, optname(opt, flags)); ++ ++ if (value < lower_bound) ++ return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + arg, optname(opt, flags), lower_bound, upper_bound); -- return 0; + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ + optname(opt, flags)); + } + } - case OPTION_MAGNITUDE: + case OPTION_UNSIGNED: if (unset) { *(unsigned long *)opt->value = 0; @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **arg 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_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), + OPT_UNSIGNED('u', "unsigned", &unsigned_integer, "get an unsigned integer"), OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23), @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv) } show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); + show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); - show(&expect, &ret, "magnitude: %lu", magnitude); + show(&expect, &ret, "unsigned: %lu", unsigned_integer); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); @@ t/t0040-parse-options.sh: usage: test-tool parse-options <options> get a integer + --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too - -m, --magnitude <n> get a magnitude + -u, --unsigned <n> get an unsigned integer --[no-]set23 set integer to 23 -@@ t/t0040-parse-options.sh: test_expect_success 'OPT_MAGNITUDE() 3giga' ' +@@ t/t0040-parse-options.sh: test_expect_success 'OPT_UNSIGNED() 3giga' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 0 - magnitude: 16384 + unsigned: 16384 timestamp: 0 string: 123 @@ t/t0040-parse-options.sh: test_expect_success 'short options' ' @@ t/t0040-parse-options.sh: test_expect_success 'short options' ' boolean: 2 integer: 1729 +i16: 9000 - magnitude: 16384 + unsigned: 16384 timestamp: 0 string: 321 @@ t/t0040-parse-options.sh: file: prefix/fi.le EOF test_expect_success 'long options' ' -- test-tool parse-options --boolean --integer 1729 --magnitude 16k \ -+ test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ +- test-tool parse-options --boolean --integer 1729 --unsigned 16k \ ++ test-tool parse-options --boolean --integer 1729 --i16 9000 --unsigned 16k \ --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ t/t0040-parse-options.sh: test_expect_success 'abbreviate to something longer th boolean: 0 integer: 0 + i16: 0 - magnitude: 0 + unsigned: 0 timestamp: 0 string: (not set) @@ t/t0040-parse-options.sh: test_expect_success 'superfluous value provided: cmdmode' ' @@ t/t0040-parse-options.sh: test_expect_success 'superfluous value provided: cmdmo boolean: 1 integer: 13 +i16: 0 - magnitude: 0 + unsigned: 0 timestamp: 0 string: 123 @@ t/t0040-parse-options.sh: test_expect_success 'intermingled arguments' ' @@ t/t0040-parse-options.sh: test_expect_success 'intermingled arguments' ' boolean: 0 integer: 2 +i16: 0 - magnitude: 0 + unsigned: 0 timestamp: 0 string: (not set) @@ t/t0040-parse-options.sh: cat >expect <<\EOF @@ t/t0040-parse-options.sh: cat >expect <<\EOF boolean: 5 integer: 4 +i16: 0 - magnitude: 0 + unsigned: 0 timestamp: 0 string: (not set) @@ t/t0040-parse-options.sh: test_expect_success 'OPT_CALLBACK() and callback errors work' ' @@ t/t0040-parse-options.sh: test_expect_success 'OPT_CALLBACK() and callback error boolean: 1 integer: 23 +i16: 0 - magnitude: 0 + unsigned: 0 timestamp: 0 string: (not set) @@ t/t0040-parse-options.sh: test_expect_success 'OPT_NUMBER_CALLBACK() works' ' @@ t/t0040-parse-options.sh: test_expect_success 'OPT_NUMBER_CALLBACK() works' ' boolean: 0 integer: 0 +i16: 0 - magnitude: 0 + unsigned: 0 timestamp: 0 string: (not set) -@@ t/t0040-parse-options.sh: test_expect_success 'overflowing integer' ' +@@ t/t0040-parse-options.sh: test_expect_success 'unsigned with units but no numbers' ' test_must_be_empty out ' 4: 9c1a42f8d3f ! 6: aa766336dd9 parse-options: introduce precision handling for `OPTION_MAGNITUDE` @@ Metadata Author: Patrick Steinhardt <ps@pks.im> ## Commit message ## - parse-options: introduce precision handling for `OPTION_MAGNITUDE` + parse-options: introduce precision handling for `OPTION_UNSIGNED` This commit is the equivalent to the preceding commit, but instead of introducing precision handling for `OPTION_INTEGER` we introduce it for - `OPTION_MAGNITUDE`. + `OPTION_UNSIGNED`. Signed-off-by: Patrick Steinhardt <ps@pks.im> ## parse-options.c ## @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, - if (value < lower_bound || value > upper_bound) + if (value < lower_bound) return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), - arg, optname(opt, flags), lower_bound, upper_bound); + arg, optname(opt, flags), (intmax_t)lower_bound, (intmax_t)upper_bound); @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, } } - case OPTION_MAGNITUDE: + case OPTION_UNSIGNED: + { + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); -+ unsigned long value; ++ uintmax_t value; + if (unset) { - *(unsigned long *)opt->value = 0; @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_ + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!git_parse_ulong(arg, opt->value)) -+ } else if (!git_parse_ulong(arg, &value)) { ++ } else if (!*arg) { ++ return error(_("%s expects a numerical value"), ++ optname(opt, flags)); ++ } else if (!git_parse_unsigned(arg, &value, upper_bound)) { ++ if (errno == ERANGE) ++ return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), ++ arg, optname(opt, flags), (uintmax_t) 0, upper_bound); ++ return error(_("%s expects a non-negative integer value" " with an optional k/m/g suffix"), optname(opt, flags)); - return 0; + } + -+ if (value > upper_bound) -+ return error(_("value %s for %s not in range [%"PRIuMAX",%"PRIuMAX"]"), -+ arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound); -+ + switch (opt->precision) { + case 1: + *(uint8_t *)opt->value = value; @@ parse-options.h: struct option { .help = (h), \ .flags = PARSE_OPT_NONEG, \ + ## parse.c ## +@@ parse.c: int git_parse_signed(const char *value, intmax_t *ret, intmax_t max) + return 0; + } + +-static int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max) ++int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max) + { + if (value && *value) { + char *end; + + ## parse.h ## +@@ + #define PARSE_H + + int git_parse_signed(const char *value, intmax_t *ret, intmax_t max); ++int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max); + int git_parse_ssize_t(const char *, ssize_t *); + int git_parse_ulong(const char *, unsigned long *); + int git_parse_int(const char *value, int *ret); + ## t/helper/test-parse-options.c ## @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; -+ uint16_t m16 = 0; ++ uint16_t u16 = 0; int16_t i16 = 0; struct option options[] = { @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv) OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), - OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), -+ OPT_MAGNITUDE(0, "m16", &m16, "get a 16 bit magnitude"), + OPT_UNSIGNED('u', "unsigned", &unsigned_integer, "get an unsigned integer"), ++ OPT_UNSIGNED(0, "u16", &u16, "get a 16 bit 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), @@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); - show(&expect, &ret, "magnitude: %lu", magnitude); -+ show(&expect, &ret, "m16: %"PRIuMAX, (uintmax_t) m16); + show(&expect, &ret, "unsigned: %lu", unsigned_integer); ++ show(&expect, &ret, "u16: %"PRIuMAX, (uintmax_t) u16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); show(&expect, &ret, "abbrev: %d", abbrev); @@ t/t0040-parse-options.sh @@ t/t0040-parse-options.sh: usage: test-tool parse-options <options> --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too - -m, --magnitude <n> get a magnitude -+ --m16 <n> get a 16 bit magnitude + -u, --unsigned <n> get an unsigned integer ++ --u16 <n> get a 16 bit unsigned integer --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) @@ t/t0040-parse-options.sh: boolean: 2 integer: 1729 i16: 0 - magnitude: 16384 -+m16: 0 + unsigned: 16384 ++u16: 0 timestamp: 0 string: 123 abbrev: 7 @@ t/t0040-parse-options.sh: boolean: 2 integer: 1729 i16: 9000 - magnitude: 16384 -+m16: 32768 + unsigned: 16384 ++u16: 32768 timestamp: 0 string: 321 abbrev: 10 @@ t/t0040-parse-options.sh: EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --unsigned 16k \ - --boolean --string2=321 --verbose --verbose --no-dry-run \ -+ --m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ ++ --u16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && test_must_be_empty output.err && @@ t/t0040-parse-options.sh: test_expect_success 'abbreviate to something longer than SHA1 length' ' integer: 0 i16: 0 - magnitude: 0 -+ m16: 0 + unsigned: 0 ++ u16: 0 timestamp: 0 string: (not set) abbrev: 100 @@ t/t0040-parse-options.sh: boolean: 1 integer: 13 i16: 0 - magnitude: 0 -+m16: 0 + unsigned: 0 ++u16: 0 timestamp: 0 string: 123 abbrev: 7 @@ t/t0040-parse-options.sh: boolean: 0 integer: 2 i16: 0 - magnitude: 0 -+m16: 0 + unsigned: 0 ++u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ t/t0040-parse-options.sh: boolean: 5 integer: 4 i16: 0 - magnitude: 0 -+m16: 0 + unsigned: 0 ++u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ t/t0040-parse-options.sh: boolean: 1 integer: 23 i16: 0 - magnitude: 0 -+m16: 0 + unsigned: 0 ++u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ t/t0040-parse-options.sh: boolean: 0 integer: 0 i16: 0 - magnitude: 0 -+m16: 0 + unsigned: 0 ++u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ t/t0040-parse-options.sh: test_expect_success 'i16 limits range' ' test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err ' -+test_expect_success 'm16 limits range' ' -+ test-tool parse-options --m16 65535 >out && -+ test_grep "m16: 65535" out && -+ test_must_fail test-tool parse-options --m16 65536 2>err && -+ test_grep "value 65536 for option .m16. not in range \[0,65535\]" err ++test_expect_success 'u16 limits range' ' ++ test-tool parse-options --u16 65535 >out && ++ test_grep "u16: 65535" out && ++ test_must_fail test-tool parse-options --u16 65536 2>err && ++ test_grep "value 65536 for option .u16. not in range \[0,65535\]" err +' + test_done 5: ef204776e85 < -: ----------- parse-options: introduce `OPTION_UNSIGNED` 6: 99e009d78c8 ! 7: 254e0f62a85 parse-options: detect mismatches in integer signedness @@ parse-options.h: struct option { .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ -@@ parse-options.h: struct option { - .type = OPTION_MAGNITUDE, \ - .short_name = (s), \ - .long_name = (l), \ -- .value = (v), \ -+ .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ - .precision = sizeof(*v), \ - .argh = N_("n"), \ - .help = (h), \ 7: ed5b28998af < -: ----------- parse-options: introduce bounded integer options --- base-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff change-id: 20250401-b4-pks-parse-options-integers-9b4bbcf21011 ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v4 1/7] parse: fix off-by-one for minimum signed values 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 2/7] global: use designated initializers for options Patrick Steinhardt ` (5 subsequent siblings) 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood We accept a maximum value in `git_parse_signed()` that restricts the range of accepted integers. As the intent is to pass `INT*_MAX` values here, this maximum doesn't only act as the upper bound, but also as the implicit lower bound of the accepted range. This lower bound is calculated by negating the maximum. But given that the maximum value of a signed integer with N bits is `2^(N-1)-1` whereas the minimum value is `-2^(N-1)` we have an off-by-one error in the lower bound. Fix this off-by-one error by using `-max - 1` as lower bound instead. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parse.c b/parse.c index 7a60a4f816c..3c47448ca67 100644 --- a/parse.c +++ b/parse.c @@ -38,7 +38,7 @@ int git_parse_signed(const char *value, intmax_t *ret, intmax_t max) errno = EINVAL; return 0; } - if ((val < 0 && -max / factor > val) || + if ((val < 0 && (-max - 1) / factor > val) || (val > 0 && max / factor < val)) { errno = ERANGE; return 0; -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v4 2/7] global: use designated initializers for options 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 1/7] parse: fix off-by-one for minimum signed values Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 3/7] parse-options: support unit factors in `OPT_INTEGER()` Patrick Steinhardt ` (4 subsequent siblings) 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood While we expose macros for most of our different option types understood by the "parse-options" subsystem, not every combination of fields that has one as that would otherwise quickly lead to an explosion of macros. Instead, we just initialize structures manually for those variants of fields that don't have a macro. Callsites that open-code these structure initialization don't use designated initializers though and instead just provide values for each of the fields that they want to initialize. This has three significant downsides: - Callsites need to specify all values up to the last field that they care about. This often includes fields that should simply be left at their default zero-initialized state, which adds distraction. - Any reader not deeply familiar with the layout of the structure has a hard time figuring out what the respective initializers mean. - Reordering or introducing new fields in the middle of the structure is impossible without adapting all callsites. Convert all sites to instead use designated initializers, which we have started using in our codebase quite a while ago. This allows us to skip any default-initialized fields, gives the reader context by specifying the field names and allows us to reorder or introduce new fields where we want to. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- archive.c | 35 ++++++++--- builtin/am.c | 28 ++++++--- builtin/clone.c | 13 ++++- builtin/commit-tree.c | 12 +++- builtin/commit.c | 62 +++++++++++++++----- builtin/config.c | 13 ++++- builtin/describe.c | 24 ++++++-- builtin/fetch.c | 10 +++- builtin/fmt-merge-msg.c | 25 +++++--- builtin/gc.c | 12 +++- builtin/grep.c | 14 +++-- builtin/init-db.c | 13 +++-- builtin/ls-remote.c | 11 +++- builtin/merge.c | 37 +++++++++--- builtin/read-tree.c | 11 +++- builtin/rebase.c | 25 ++++++-- builtin/revert.c | 12 +++- builtin/show-branch.c | 12 +++- builtin/tag.c | 23 ++++++-- builtin/update-index.c | 131 +++++++++++++++++++++++++++++------------- builtin/write-tree.c | 12 ++-- diff.c | 13 +++-- ref-filter.h | 15 +++-- t/helper/test-parse-options.c | 38 +++++++++--- 24 files changed, 443 insertions(+), 158 deletions(-) diff --git a/archive.c b/archive.c index 8be4e7ac8db..67bba3cd301 100644 --- a/archive.c +++ b/archive.c @@ -650,20 +650,37 @@ static int parse_archive_args(int argc, const char **argv, OPT_STRING(0, "format", &format, N_("fmt"), N_("archive format")), OPT_STRING(0, "prefix", &base, N_("prefix"), N_("prepend prefix to each pathname in the archive")), - { OPTION_CALLBACK, 0, "add-file", args, N_("file"), - N_("add untracked file to archive"), 0, add_file_cb, - (intptr_t)&base }, - { OPTION_CALLBACK, 0, "add-virtual-file", args, - N_("path:content"), N_("add untracked file to archive"), 0, - add_file_cb, (intptr_t)&base }, + { + .type = OPTION_CALLBACK, + .long_name = "add-file", + .value = args, + .argh = N_("file"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, + { + .type = OPTION_CALLBACK, + .long_name = "add-virtual-file", + .value = args, + .argh = N_("path:content"), + .help = N_("add untracked file to archive"), + .callback = add_file_cb, + .defval = (intptr_t) &base, + }, OPT_STRING('o', "output", &output, N_("file"), N_("write the archive to this file")), OPT_BOOL(0, "worktree-attributes", &worktree_attributes, N_("read .gitattributes in working directory")), OPT__VERBOSE(&verbose, N_("report archived files on stderr")), - { OPTION_STRING, 0, "mtime", &mtime_option, N_("time"), - N_("set modification time of archive entries"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "mtime", + .value = &mtime_option, + .argh = N_("time"), + .help = N_("set modification time of archive entries"), + .flags = PARSE_OPT_NONEG, + }, OPT_NUMBER_CALLBACK(&compression_level, N_("set compression level"), number_callback), OPT_GROUP(""), diff --git a/builtin/am.c b/builtin/am.c index 3b61bd4c333..4afb519830f 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -2400,11 +2400,16 @@ int cmd_am(int argc, OPT_CMDMODE(0, "quit", &resume_mode, N_("abort the patching operation but keep HEAD where it is"), RESUME_QUIT), - { OPTION_CALLBACK, 0, "show-current-patch", &resume_mode, - "(diff|raw)", - N_("show the patch being applied"), - PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW }, + { + .type = OPTION_CALLBACK, + .long_name = "show-current-patch", + .value = &resume_mode, + .argh = "(diff|raw)", + .help = N_("show the patch being applied"), + .flags = PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .callback = parse_opt_show_current_patch, + .defval = RESUME_SHOW_PATCH_RAW, + }, OPT_CMDMODE(0, "retry", &resume_mode, N_("try to apply current patch again"), RESUME_APPLY), @@ -2417,9 +2422,16 @@ int cmd_am(int argc, OPT_BOOL(0, "ignore-date", &state.ignore_date, N_("use current timestamp for author date")), OPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate), - { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &state.sign_commit, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_CALLBACK_F(0, "empty", &state.empty_type, "(stop|drop|keep)", N_("how to handle empty patches"), PARSE_OPT_NONEG, am_option_parse_empty), diff --git a/builtin/clone.c b/builtin/clone.c index 88276e5b7ab..9c3547f41e3 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -930,9 +930,16 @@ int cmd_clone(int argc, N_("don't use local hardlinks, always copy")), OPT_BOOL('s', "shared", &option_shared, N_("setup as shared repository")), - { OPTION_CALLBACK, 0, "recurse-submodules", &option_recurse_submodules, - N_("pathspec"), N_("initialize submodules in the clone"), - PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)"." }, + { + .type = OPTION_CALLBACK, + .long_name = "recurse-submodules", + .value = &option_recurse_submodules, + .argh = N_("pathspec"), + .help = N_("initialize submodules in the clone"), + .flags = PARSE_OPT_OPTARG, + .callback = recurse_submodules_cb, + .defval = (intptr_t)".", + }, OPT_ALIAS(0, "recursive", "recurse-submodules"), OPT_INTEGER('j', "jobs", &max_jobs, N_("number of submodules cloned in parallel")), diff --git a/builtin/commit-tree.c b/builtin/commit-tree.c index 38457600a4e..c787133d004 100644 --- a/builtin/commit-tree.c +++ b/builtin/commit-tree.c @@ -111,8 +111,16 @@ int cmd_commit_tree(int argc, OPT_CALLBACK_F('F', NULL, &buffer, N_("file"), N_("read commit log message from file"), PARSE_OPT_NONEG, parse_file_arg_callback), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; int ret; diff --git a/builtin/commit.c b/builtin/commit.c index 2f459682221..66bd91fd523 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1542,17 +1542,34 @@ struct repository *repo UNUSED) STATUS_FORMAT_LONG), OPT_BOOL('z', "null", &s.null_termination, N_("terminate entries with NUL")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, - N_("mode"), - N_("show untracked files, optional modes: all, normal, no. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, - { OPTION_STRING, 0, "ignored", &ignored_arg, - N_("mode"), - N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"traditional" }, - { OPTION_STRING, 0, "ignore-submodules", &ignore_submodule_arg, N_("when"), - N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), - PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, + { + .type = OPTION_STRING, + .long_name = "ignored", + .value = &ignored_arg, + .argh = N_("mode"), + .help = N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"traditional", + }, + { + .type = OPTION_STRING, + .long_name = "ignore-submodules", + .value = &ignore_submodule_arg, + .argh = N_("when"), + .help = N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")), OPT_BOOL(0, "no-renames", &no_renames, N_("do not detect renames")), OPT_CALLBACK_F('M', "find-renames", &rename_score_arg, @@ -1688,8 +1705,16 @@ int cmd_commit(int argc, OPT_BOOL('e', "edit", &edit_flag, N_("force edit of commit")), OPT_CLEANUP(&cleanup_arg), OPT_BOOL(0, "status", &include_status, N_("include status in commit message template")), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, /* end commit message options */ OPT_GROUP(N_("Commit contents options")), @@ -1714,7 +1739,16 @@ int cmd_commit(int argc, N_("terminate entries with NUL")), OPT_BOOL(0, "amend", &amend, N_("amend previous commit")), OPT_BOOL(0, "no-post-rewrite", &no_post_rewrite, N_("bypass post-rewrite hook")), - { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, N_("mode"), N_("show untracked files, optional modes: all, normal, no. (Default: all)"), PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { + .type = OPTION_STRING, + .short_name = 'u', + .long_name = "untracked-files", + .value = &untracked_files_arg, + .argh = N_("mode"), + .help = N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)"all", + }, OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), /* end commit contents options */ diff --git a/builtin/config.c b/builtin/config.c index 53a90094e31..f70d6354772 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -131,9 +131,16 @@ struct config_display_options { #define TYPE_COLOR 6 #define TYPE_BOOL_OR_STR 7 -#define OPT_CALLBACK_VALUE(s, l, v, h, i) \ - { OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \ - PARSE_OPT_NONEG, option_parse_type, (i) } +#define OPT_CALLBACK_VALUE(s, l, v, h, i) { \ + .type = OPTION_CALLBACK, \ + .short_name = (s), \ + .long_name = (l), \ + .value = (v), \ + .help = (h), \ + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, \ + .callback = option_parse_type, \ + .defval = (i), \ +} static int option_parse_type(const struct option *opt, const char *arg, int unset) diff --git a/builtin/describe.c b/builtin/describe.c index e2e73f3d757..2da9f4fed01 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -601,12 +601,24 @@ int cmd_describe(int argc, N_("do not consider tags matching <pattern>")), OPT_BOOL(0, "always", &always, N_("show abbreviated commit object as fallback")), - {OPTION_STRING, 0, "dirty", &dirty, N_("mark"), - N_("append <mark> on dirty working tree (default: \"-dirty\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-dirty"}, - {OPTION_STRING, 0, "broken", &broken, N_("mark"), - N_("append <mark> on broken working tree (default: \"-broken\")"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "-broken"}, + { + .type = OPTION_STRING, + .long_name = "dirty", + .value = &dirty, + .argh = N_("mark"), + .help = N_("append <mark> on dirty working tree (default: \"-dirty\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-dirty", + }, + { + .type = OPTION_STRING, + .long_name = "broken", + .value = &broken, + .argh = N_("mark"), + .help = N_("append <mark> on broken working tree (default: \"-broken\")"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "-broken", + }, OPT_END(), }; diff --git a/builtin/fetch.c b/builtin/fetch.c index 02af5054690..3a5159d9e69 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2359,8 +2359,14 @@ int cmd_fetch(int argc, OPT_SET_INT_F(0, "refetch", &refetch, N_("re-fetch without negotiating common commits"), 1, PARSE_OPT_NONEG), - { OPTION_STRING, 0, "submodule-prefix", &submodule_prefix, N_("dir"), - N_("prepend this to submodule path output"), PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "submodule-prefix", + .value = &submodule_prefix, + .argh = N_("dir"), + .help = N_("prepend this to submodule path output"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_CALLBACK_F(0, "recurse-submodules-default", &recurse_submodules_default, N_("on-demand"), N_("default for recursive fetching of submodules " diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 189cd1096a0..240cdb474bc 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -20,13 +20,24 @@ int cmd_fmt_merge_msg(int argc, char *into_name = NULL; int shortlog_len = -1; struct option options[] = { - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("populate log with at most <n> entries from shortlog"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, - { OPTION_INTEGER, 0, "summary", &shortlog_len, N_("n"), - N_("alias for --log (deprecated)"), - PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL, - DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("populate log with at most <n> entries from shortlog"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, + { + .type = OPTION_INTEGER, + .long_name = "summary", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("alias for --log (deprecated)"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_STRING('m', "message", &message, N_("text"), N_("use <text> as start of message")), OPT_STRING(0, "into-name", &into_name, N_("name"), diff --git a/builtin/gc.c b/builtin/gc.c index 99431fd4674..6707a26bc6e 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -699,9 +699,15 @@ struct repository *repo UNUSED) int ret; struct option builtin_gc_options[] = { OPT__QUIET(&quiet, N_("suppress progress reporting")), - { OPTION_STRING, 0, "prune", &prune_expire_arg, N_("date"), - N_("prune unreferenced objects"), - PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg }, + { + .type = OPTION_STRING, + .long_name = "prune", + .value = &prune_expire_arg, + .argh = N_("date"), + .help = N_("prune unreferenced objects"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t)prune_expire_arg, + }, OPT_BOOL(0, "cruft", &cfg.cruft_packs, N_("pack unreferenced objects separately")), OPT_MAGNITUDE(0, "max-cruft-size", &cfg.max_cruft_size, N_("with --cruft, limit the size of new cruft packs")), diff --git a/builtin/grep.c b/builtin/grep.c index d1427290f77..c4869733e1b 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -1017,10 +1017,16 @@ int cmd_grep(int argc, OPT_BOOL(0, "all-match", &opt.all_match, N_("show only matches from files that match all patterns")), OPT_GROUP(""), - { OPTION_STRING, 'O', "open-files-in-pager", &show_in_pager, - N_("pager"), N_("show matching files in the pager"), - PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, - NULL, (intptr_t)default_pager }, + { + .type = OPTION_STRING, + .short_name = 'O', + .long_name = "open-files-in-pager", + .value = &show_in_pager, + .argh = N_("pager"), + .help = N_("show matching files in the pager"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, + .defval = (intptr_t)default_pager, + }, OPT_BOOL_F(0, "ext-grep", &external_grep_allowed__ignored, N_("allow calling of grep(1) (ignored by this build)"), PARSE_OPT_NOCOMPLETE), diff --git a/builtin/init-db.c b/builtin/init-db.c index 196dccdd77a..4a950e44d8d 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -93,10 +93,15 @@ int cmd_init_db(int argc, N_("directory from which templates will be used")), OPT_SET_INT(0, "bare", &is_bare_repository_cfg, N_("create a bare repository"), 1), - { OPTION_CALLBACK, 0, "shared", &init_shared_repository, - N_("permissions"), - N_("specify that the git repository is to be shared amongst several users"), - PARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0}, + { + .type = OPTION_CALLBACK, + .long_name = "shared", + .value = &init_shared_repository, + .argh = N_("permissions"), + .help = N_("specify that the git repository is to be shared amongst several users"), + .flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + .callback = shared_callback + }, OPT_BIT('q', "quiet", &flags, N_("be quiet"), INIT_DB_QUIET), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), N_("separate git dir from working tree")), diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c index 42f34e12361..01a4d4daa1f 100644 --- a/builtin/ls-remote.c +++ b/builtin/ls-remote.c @@ -67,9 +67,14 @@ int cmd_ls_remote(int argc, OPT__QUIET(&quiet, N_("do not print remote URL")), OPT_STRING(0, "upload-pack", &uploadpack, N_("exec"), N_("path of git-upload-pack on the remote host")), - { OPTION_STRING, 0, "exec", &uploadpack, N_("exec"), - N_("path of git-upload-pack on the remote host"), - PARSE_OPT_HIDDEN }, + { + .type = OPTION_STRING, + .long_name = "exec", + .value = &uploadpack, + .argh = N_("exec"), + .help = N_("path of git-upload-pack on the remote host"), + .flags = PARSE_OPT_HIDDEN, + }, OPT_BIT('t', "tags", &flags, N_("limit to tags"), REF_TAGS), OPT_BIT('b', "branches", &flags, N_("limit to branches"), REF_BRANCHES), OPT_BIT_F('h', "heads", &flags, diff --git a/builtin/merge.c b/builtin/merge.c index ba9faf126aa..21787d45165 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -250,9 +250,15 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "stat", &show_diffstat, N_("show a diffstat at the end of the merge")), OPT_BOOL(0, "summary", &show_diffstat, N_("(synonym to --stat)")), - { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), - N_("add (at most <n>) entries from shortlog to merge commit message"), - PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, + { + .type = OPTION_INTEGER, + .long_name = "log", + .value = &shortlog_len, + .argh = N_("n"), + .help = N_("add (at most <n>) entries from shortlog to merge commit message"), + .flags = PARSE_OPT_OPTARG, + .defval = DEFAULT_MERGE_LOG_LEN, + }, OPT_BOOL(0, "squash", &squash, N_("create a single commit instead of doing a merge")), OPT_BOOL(0, "commit", &option_commit, @@ -274,9 +280,16 @@ static struct option builtin_merge_options[] = { OPT_CALLBACK('m', "message", &merge_msg, N_("message"), N_("merge commit message (for a non-fast-forward merge)"), option_parse_message), - { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"), - N_("read message from file"), PARSE_OPT_NONEG, - NULL, 0, option_read_message }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'F', + .long_name = "file", + .value = &merge_msg, + .argh = N_("path"), + .help = N_("read message from file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = option_read_message, + }, OPT_STRING(0, "into-name", &into_name, N_("name"), N_("use <name> instead of the real target")), OPT__VERBOSITY(&verbosity), @@ -289,8 +302,16 @@ static struct option builtin_merge_options[] = { OPT_BOOL(0, "allow-unrelated-histories", &allow_unrelated_histories, N_("allow merging unrelated histories")), OPT_SET_INT(0, "progress", &show_progress, N_("force progress reporting"), 1), - { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &sign_commit, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&autostash), OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")), OPT_BOOL(0, "signoff", &signoff, N_("add a Signed-off-by trailer")), diff --git a/builtin/read-tree.c b/builtin/read-tree.c index d2a807a828b..a8f352f7cd9 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -135,9 +135,14 @@ int cmd_read_tree(int argc, N_("3-way merge in presence of adds and removes")), OPT_BOOL(0, "reset", &opts.reset, N_("same as -m, but discard unmerged entries")), - { OPTION_STRING, 0, "prefix", &opts.prefix, N_("<subdirectory>/"), - N_("read the tree into the index under <subdirectory>/"), - PARSE_OPT_NONEG }, + { + .type = OPTION_STRING, + .long_name = "prefix", + .value = &opts.prefix, + .argh = N_("<subdirectory>/"), + .help = N_("read the tree into the index under <subdirectory>/"), + .flags = PARSE_OPT_NONEG, + }, OPT_BOOL('u', NULL, &opts.update, N_("update working tree with merge result")), OPT_CALLBACK_F(0, "exclude-per-directory", &opts, diff --git a/builtin/rebase.c b/builtin/rebase.c index d4715ed35d7..d4083350090 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1122,9 +1122,15 @@ int cmd_rebase(int argc, OPT_BIT('v', "verbose", &options.flags, N_("display a diffstat of what changed upstream"), REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), - {OPTION_NEGBIT, 'n', "no-stat", &options.flags, NULL, - N_("do not show diffstat of what changed upstream"), - PARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT }, + { + .type = OPTION_NEGBIT, + .short_name = 'n', + .long_name = "no-stat", + .value = &options.flags, + .help = N_("do not show diffstat of what changed upstream"), + .flags = PARSE_OPT_NOARG, + .defval = REBASE_DIFFSTAT, + }, OPT_BOOL(0, "signoff", &options.signoff, N_("add a Signed-off-by trailer to each commit")), OPT_BOOL(0, "committer-date-is-author-date", @@ -1190,9 +1196,16 @@ int cmd_rebase(int argc, OPT_BOOL(0, "update-refs", &options.update_refs, N_("update branches that point to commits " "that are being rebased")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG-sign commits"), - PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG-sign commits"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_AUTOSTASH(&options.autostash), OPT_STRING_LIST('x', "exec", &options.exec, N_("exec"), N_("add exec lines after each commit of the " diff --git a/builtin/revert.c b/builtin/revert.c index aca6c293cdf..4f5ef975494 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -132,8 +132,16 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, OPT_STRING(0, "strategy", &strategy, N_("strategy"), N_("merge strategy")), OPT_STRVEC('X', "strategy-option", &opts->xopts, N_("option"), N_("option for merge strategy")), - { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), - N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + { + .type = OPTION_STRING, + .short_name = 'S', + .long_name = "gpg-sign", + .value = &gpg_sign, + .argh = N_("key-id"), + .help = N_("GPG sign commit"), + .flags = PARSE_OPT_OPTARG, + .defval = (intptr_t) "", + }, OPT_END() }; struct option *options = base_options; diff --git a/builtin/show-branch.c b/builtin/show-branch.c index fce6b404e92..dab37019d29 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -667,9 +667,15 @@ int cmd_show_branch(int ac, N_("show remote-tracking branches")), OPT__COLOR(&showbranch_use_color, N_("color '*!+-' corresponding to the branch")), - { OPTION_INTEGER, 0, "more", &extra, N_("n"), - N_("show <n> more commits after the common ancestor"), - PARSE_OPT_OPTARG, NULL, (intptr_t)1 }, + { + .type = OPTION_INTEGER, + .long_name = "more", + .value = &extra, + .argh = N_("n"), + .help = N_("show <n> more commits after the common ancestor"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_SET_INT(0, "list", &extra, N_("synonym to more=-1"), -1), OPT_BOOL(0, "no-name", &no_name, N_("suppress naming strings")), OPT_BOOL(0, "current", &with_current_branch, diff --git a/builtin/tag.c b/builtin/tag.c index d3e0943b734..b266f12bb48 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -479,9 +479,15 @@ int cmd_tag(int argc, int edit_flag = 0; struct option options[] = { OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'), - { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"), - N_("print <n> lines of each tag message"), - PARSE_OPT_OPTARG, NULL, 1 }, + { + .type = OPTION_INTEGER, + .short_name = 'n', + .value = &filter.lines, + .argh = N_("n"), + .help = N_("print <n> lines of each tag message"), + .flags = PARSE_OPT_OPTARG, + .defval = 1, + }, OPT_CMDMODE('d', "delete", &cmdmode, N_("delete tags"), 'd'), OPT_CMDMODE('v', "verify", &cmdmode, N_("verify tags"), 'v'), @@ -513,9 +519,14 @@ int cmd_tag(int argc, N_("do not output a newline after empty formatted refs")), OPT_REF_SORT(&sorting_options), { - OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), - N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT, - parse_opt_object_name, (intptr_t) "HEAD" + .type = OPTION_CALLBACK, + .long_name = "points-at", + .value = &filter.points_at, + .argh = N_("object"), + .help = N_("print only tags of the object"), + .flags = PARSE_OPT_LASTARG_DEFAULT, + .callback = parse_opt_object_name, + .defval = (intptr_t) "HEAD", }, OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), diff --git a/builtin/update-index.c b/builtin/update-index.c index b2f6b1a3fbb..ee64b022679 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -964,29 +964,51 @@ int cmd_update_index(int argc, N_("like --refresh, but ignore assume-unchanged setting"), PARSE_OPT_NOARG | PARSE_OPT_NONEG, really_refresh_callback), - {OPTION_LOWLEVEL_CALLBACK, 0, "cacheinfo", NULL, - N_("<mode>,<object>,<path>"), - N_("add the specified entry to the index"), - PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ - PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, - NULL, 0, - cacheinfo_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "cacheinfo", + .argh = N_("<mode>,<object>,<path>"), + .help = N_("add the specified entry to the index"), + .flags = PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ + PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + .ll_callback = cacheinfo_callback, + }, OPT_CALLBACK_F(0, "chmod", &set_executable_bit, "(+|-)x", N_("override the executable bit of the listed files"), PARSE_OPT_NONEG, chmod_callback), - {OPTION_SET_INT, 0, "assume-unchanged", &mark_valid_only, NULL, - N_("mark files as \"not changing\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-assume-unchanged", &mark_valid_only, NULL, - N_("clear assumed-unchanged bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, - {OPTION_SET_INT, 0, "skip-worktree", &mark_skip_worktree_only, NULL, - N_("mark files as \"index-only\""), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-skip-worktree", &mark_skip_worktree_only, NULL, - N_("clear skip-worktree bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "assume-unchanged", + .value = &mark_valid_only, + .help = N_("mark files as \"not changing\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-assume-unchanged", + .value = &mark_valid_only, + .help = N_("clear assumed-unchanged bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("mark files as \"index-only\""), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-skip-worktree", + .value = &mark_skip_worktree_only, + .help = N_("clear skip-worktree bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_BOOL(0, "ignore-skip-worktree-entries", &ignore_skip_worktree_entries, N_("do not touch index-only entries")), OPT_SET_INT(0, "info-only", &info_only, @@ -995,22 +1017,39 @@ int cmd_update_index(int argc, N_("remove named paths even if present in worktree"), 1), OPT_BOOL('z', NULL, &nul_term_line, N_("with --stdin: input lines are terminated by null bytes")), - {OPTION_LOWLEVEL_CALLBACK, 0, "stdin", &read_from_stdin, NULL, - N_("read list of paths to be updated from standard input"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "index-info", &nul_term_line, NULL, - N_("add entries from standard input to the index"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, stdin_cacheinfo_callback}, - {OPTION_LOWLEVEL_CALLBACK, 0, "unresolve", &has_errors, NULL, - N_("repopulate stages #2 and #3 for the listed paths"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, unresolve_callback}, - {OPTION_LOWLEVEL_CALLBACK, 'g', "again", &has_errors, NULL, - N_("only update entries that differ from HEAD"), - PARSE_OPT_NONEG | PARSE_OPT_NOARG, - NULL, 0, reupdate_callback}, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "stdin", + .value = &read_from_stdin, + .help = N_("read list of paths to be updated from standard input"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "index-info", + .value = &nul_term_line, + .help = N_("add entries from standard input to the index"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = stdin_cacheinfo_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .long_name = "unresolve", + .value = &has_errors, + .help = N_("repopulate stages #2 and #3 for the listed paths"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = unresolve_callback, + }, + { + .type = OPTION_LOWLEVEL_CALLBACK, + .short_name = 'g', + .long_name = "again", + .value = &has_errors, + .help = N_("only update entries that differ from HEAD"), + .flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG, + .ll_callback = reupdate_callback, + }, OPT_BIT(0, "ignore-missing", &refresh_args.flags, N_("ignore files missing from worktree"), REFRESH_IGNORE_MISSING), @@ -1036,12 +1075,22 @@ int cmd_update_index(int argc, N_("write out the index even if is not flagged as changed"), 1), OPT_BOOL(0, "fsmonitor", &fsmonitor, N_("enable or disable file system monitor")), - {OPTION_SET_INT, 0, "fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("mark files as fsmonitor valid"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, - {OPTION_SET_INT, 0, "no-fsmonitor-valid", &mark_fsmonitor_only, NULL, - N_("clear fsmonitor valid bit"), - PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + { + .type = OPTION_SET_INT, + .long_name = "fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("mark files as fsmonitor valid"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = MARK_FLAG, + }, + { + .type = OPTION_SET_INT, + .long_name = "no-fsmonitor-valid", + .value = &mark_fsmonitor_only, + .help = N_("clear fsmonitor valid bit"), + .flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, + .defval = UNMARK_FLAG, + }, OPT_END() }; diff --git a/builtin/write-tree.c b/builtin/write-tree.c index 43f233e69b0..5a8dc377ec0 100644 --- a/builtin/write-tree.c +++ b/builtin/write-tree.c @@ -31,10 +31,14 @@ int cmd_write_tree(int argc, WRITE_TREE_MISSING_OK), OPT_STRING(0, "prefix", &tree_prefix, N_("<prefix>/"), N_("write tree object for a subdirectory <prefix>")), - { OPTION_BIT, 0, "ignore-cache-tree", &flags, NULL, - N_("only useful for debugging"), - PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL, - WRITE_TREE_IGNORE_CACHE_TREE }, + { + .type = OPTION_BIT, + .long_name = "ignore-cache-tree", + .value = &flags, + .help = N_("only useful for debugging"), + .flags = PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, + .defval = WRITE_TREE_IGNORE_CACHE_TREE, + }, OPT_END() }; diff --git a/diff.c b/diff.c index 08f5e00a2cc..f2fcc7f3c22 100644 --- a/diff.c +++ b/diff.c @@ -5892,10 +5892,15 @@ struct option *add_diff_options(const struct option *opts, OPT_CALLBACK_F(0, "diff-filter", options, N_("[(A|C|D|M|R|T|U|X|B)...[*]]"), N_("select files by diff type"), PARSE_OPT_NONEG, diff_opt_diff_filter), - { OPTION_CALLBACK, 0, "output", options, N_("<file>"), - N_("output to a specific file"), - PARSE_OPT_NONEG, NULL, 0, diff_opt_output }, - + { + .type = OPTION_CALLBACK, + .long_name = "output", + .value = options, + .argh = N_("<file>"), + .help = N_("output to a specific file"), + .flags = PARSE_OPT_NONEG, + .ll_callback = diff_opt_output, + }, OPT_END() }; diff --git a/ref-filter.h b/ref-filter.h index 013d4cfa64b..c98c4fbd4c1 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -114,11 +114,16 @@ struct ref_format { } /* Macros for checking --merged and --no-merged options */ -#define _OPT_MERGED_NO_MERGED(option, filter, h) \ - { OPTION_CALLBACK, 0, option, (filter), N_("commit"), (h), \ - PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ - parse_opt_merge_filter, (intptr_t) "HEAD" \ - } +#define _OPT_MERGED_NO_MERGED(option, filter, h) { \ + .type = OPTION_CALLBACK, \ + .long_name = option, \ + .value = (filter), \ + .argh = N_("commit"), \ + .help = (h), \ + .flags = PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ + .callback = parse_opt_merge_filter, \ + .defval = (intptr_t) "HEAD", \ +} #define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED("merged", f, h) #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h) diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index bfe45ec68b0..997f55fd45b 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -124,8 +124,15 @@ int cmd__parse_options(int argc, const char **argv) struct option options[] = { OPT_BOOL(0, "yes", &boolean, "get a boolean"), OPT_BOOL('D', "no-doubt", &boolean, "begins with 'no-'"), - { OPTION_SET_INT, 'B', "no-fear", &boolean, NULL, - "be brave", PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { + .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), @@ -155,12 +162,27 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP("Magic arguments"), OPT_NUMBER_CALLBACK(&integer, "set integer to NUM", number_callback), - { OPTION_COUNTUP, '+', NULL, &boolean, NULL, "same as -b", - PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH }, - { OPTION_COUNTUP, 0, "ambiguous", &ambiguous, NULL, - "positive ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, - { OPTION_COUNTUP, 0, "no-ambiguous", &ambiguous, NULL, - "negative ambiguity", PARSE_OPT_NOARG | PARSE_OPT_NONEG }, + { + .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"), -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v4 3/7] parse-options: support unit factors in `OPT_INTEGER()` 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 1/7] parse: fix off-by-one for minimum signed values Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 2/7] global: use designated initializers for options Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 4/7] parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` Patrick Steinhardt ` (3 subsequent siblings) 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood There are two main differences between `OPT_INTEGER()` and `OPT_MAGNITUDE()`: - The former parses signed integers whereas the latter parses unsigned integers. - The latter parses unit factors like 'k', 'm' or 'g'. While the first difference makes obvious sense, there isn't really a good reason why signed integers shouldn't support unit factors, too. This inconsistency will also become a bit of a problem with subsequent commits, where we will fix a couple of callsites that pass an unsigned integer to `OPT_INTEGER()`. There are three options: - We could adapt those users to instead pass a signed integer, but this would needlessly extend the range of accepted integer values. - We could convert them to use `OPT_MAGNITUDE()`, as it only accepts unsigned integers. But now we have the inconsistency that we also start to accept unit factors. - We could introduce `OPT_UNSIGNED()` as equivalent to `OPT_INTEGER()` so that it knows to only accept unsigned integers without unit suffix. Introducing a whole new option type feels a bit excessive. There also isn't really a good reason why `OPT_INTEGER()` cannot be extended to also accept unit factors: all valid values passed to such options cannot have a unit factors right now, so there wouldn't be any ambiguity. Refactor `OPT_INTEGER()` to use `git_parse_int()`, which knows to interpret unit factors. This removes the inconsistency between the signed and unsigned options so that we can easily fix up callsites that pass the wrong integer type right now. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- Documentation/technical/api-parse-options.adoc | 6 ++++-- parse-options.c | 8 ++++---- t/t0040-parse-options.sh | 4 +++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc index 61fa6ee1678..63acfb419bd 100644 --- a/Documentation/technical/api-parse-options.adoc +++ b/Documentation/technical/api-parse-options.adoc @@ -211,8 +211,10 @@ There are some macros to easily define options: Use of `--no-option` will clear the list of preceding values. `OPT_INTEGER(short, long, &int_var, description)`:: - Introduce an option with integer argument. - The integer is put into `int_var`. + Introduce an option with integer argument. The argument must be a + integer and may include a suffix of 'k', 'm' or 'g' to + scale the provided value by 1024, 1024^2 or 1024^3 respectively. + The scaled value is put into `int_var`. `OPT_MAGNITUDE(short, long, &unsigned_long_var, description)`:: Introduce an option with a size argument. The argument must be a diff --git a/parse-options.c b/parse-options.c index 35fbb3b0d63..b287436e81a 100644 --- a/parse-options.c +++ b/parse-options.c @@ -73,7 +73,7 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, enum opt_parsed flags, const char **argp) { - const char *s, *arg; + const char *arg; const int unset = flags & OPT_UNSET; int err; @@ -185,9 +185,9 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, if (!*arg) return error(_("%s expects a numerical value"), optname(opt, flags)); - *(int *)opt->value = strtol(arg, (char **)&s, 10); - if (*s) - return error(_("%s expects a numerical value"), + if (!git_parse_int(arg, opt->value)) + return error(_("%s expects an integer value" + " with an optional k/m/g suffix"), optname(opt, flags)); return 0; diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 2fe3522305f..0c538c4b437 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -111,7 +111,9 @@ test_expect_success 'OPT_BOOL() no negation #2' 'check_unknown_i18n --no-no-fear test_expect_success 'OPT_BOOL() positivation' 'check boolean: 0 -D --doubt' -test_expect_success 'OPT_INT() negative' 'check integer: -2345 -i -2345' +test_expect_success 'OPT_INTEGER() negative' 'check integer: -2345 -i -2345' +test_expect_success 'OPT_INTEGER() kilo' 'check integer: 239616 -i 234k' +test_expect_success 'OPT_INTEGER() negative kilo' 'check integer: -239616 -i -234k' test_expect_success 'OPT_MAGNITUDE() simple' ' check magnitude: 2345678 -m 2345678 -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v4 4/7] parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (2 preceding siblings ...) 2025-04-17 10:49 ` [PATCH v4 3/7] parse-options: support unit factors in `OPT_INTEGER()` Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 2025-04-17 15:17 ` Junio C Hamano 2025-04-17 10:49 ` [PATCH v4 5/7] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt ` (2 subsequent siblings) 6 siblings, 1 reply; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood With the preceding commit, `OPT_INTEGER()` has learned to support unit factors. Consequently, the major differencen between `OPT_INTEGER()` and `OPT_MAGNITUDE()` isn't the support of unit factors anymore, as both of them do support them now. Instead, the difference is that one handles signed and the other handles unsigned integers. Adapt the name of `OPT_MAGNITUDE()` accordingly by renaming it to `OPT_UNSIGNED()`. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- Documentation/technical/api-parse-options.adoc | 4 +-- builtin/gc.c | 4 +-- builtin/multi-pack-index.c | 2 +- builtin/pack-objects.c | 8 ++--- builtin/repack.c | 8 ++--- parse-options.c | 6 ++-- parse-options.h | 6 ++-- t/helper/test-parse-options.c | 6 ++-- t/t0040-parse-options.sh | 50 +++++++++++++------------- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc index 63acfb419bd..880eb946425 100644 --- a/Documentation/technical/api-parse-options.adoc +++ b/Documentation/technical/api-parse-options.adoc @@ -216,8 +216,8 @@ There are some macros to easily define options: scale the provided value by 1024, 1024^2 or 1024^3 respectively. The scaled value is put into `int_var`. -`OPT_MAGNITUDE(short, long, &unsigned_long_var, description)`:: - Introduce an option with a size argument. The argument must be a +`OPT_UNSIGNED(short, long, &unsigned_long_var, description)`:: + Introduce an option with an unsigned integer argument. The argument must be a non-negative integer and may include a suffix of 'k', 'm' or 'g' to scale the provided value by 1024, 1024^2 or 1024^3 respectively. The scaled value is put into `unsigned_long_var`. diff --git a/builtin/gc.c b/builtin/gc.c index 6707a26bc6e..b32cf937cdf 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -709,8 +709,8 @@ struct repository *repo UNUSED) .defval = (intptr_t)prune_expire_arg, }, OPT_BOOL(0, "cruft", &cfg.cruft_packs, N_("pack unreferenced objects separately")), - OPT_MAGNITUDE(0, "max-cruft-size", &cfg.max_cruft_size, - N_("with --cruft, limit the size of new cruft packs")), + OPT_UNSIGNED(0, "max-cruft-size", &cfg.max_cruft_size, + N_("with --cruft, limit the size of new cruft packs")), OPT_BOOL(0, "aggressive", &aggressive, N_("be more thorough (increased runtime)")), OPT_BOOL_F(0, "auto", &opts.auto_flag, N_("enable auto-gc mode"), PARSE_OPT_NOCOMPLETE), diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c index 2a938466f53..e4820fd721a 100644 --- a/builtin/multi-pack-index.c +++ b/builtin/multi-pack-index.c @@ -245,7 +245,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv, { struct option *options; static struct option builtin_multi_pack_index_repack_options[] = { - OPT_MAGNITUDE(0, "batch-size", &opts.batch_size, + OPT_UNSIGNED(0, "batch-size", &opts.batch_size, N_("during repack, collect pack-files of smaller size into a batch that is larger than this size")), OPT_BIT(0, "progress", &opts.flags, N_("force progress reporting"), MIDX_PROGRESS), diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 79e1e6fb52b..9328812e286 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4483,16 +4483,16 @@ int cmd_pack_objects(int argc, OPT_CALLBACK_F(0, "index-version", &pack_idx_opts, N_("<version>[,<offset>]"), N_("write the pack index file in the specified idx format version"), PARSE_OPT_NONEG, option_parse_index_version), - OPT_MAGNITUDE(0, "max-pack-size", &pack_size_limit, - N_("maximum size of each output pack file")), + OPT_UNSIGNED(0, "max-pack-size", &pack_size_limit, + N_("maximum size of each output pack file")), OPT_BOOL(0, "local", &local, N_("ignore borrowed objects from alternate object store")), OPT_BOOL(0, "incremental", &incremental, N_("ignore packed objects")), OPT_INTEGER(0, "window", &window, N_("limit pack window by objects")), - OPT_MAGNITUDE(0, "window-memory", &window_memory_limit, - N_("limit pack window by memory in addition to object limit")), + OPT_UNSIGNED(0, "window-memory", &window_memory_limit, + N_("limit pack window by memory in addition to object limit")), OPT_INTEGER(0, "depth", &depth, N_("maximum length of delta chain allowed in the resulting pack")), OPT_BOOL(0, "reuse-delta", &reuse_delta, diff --git a/builtin/repack.c b/builtin/repack.c index 75e3752353a..8bf9941b2c2 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -1202,8 +1202,8 @@ int cmd_repack(int argc, PACK_CRUFT), OPT_STRING(0, "cruft-expiration", &cruft_expiration, N_("approxidate"), N_("with --cruft, expire objects older than this")), - OPT_MAGNITUDE(0, "max-cruft-size", &cruft_po_args.max_pack_size, - N_("with --cruft, limit the size of new cruft packs")), + OPT_UNSIGNED(0, "max-cruft-size", &cruft_po_args.max_pack_size, + N_("with --cruft, limit the size of new cruft packs")), OPT_BOOL('d', NULL, &delete_redundant, N_("remove redundant packs, and run git-prune-packed")), OPT_BOOL('f', NULL, &po_args.no_reuse_delta, @@ -1233,8 +1233,8 @@ int cmd_repack(int argc, N_("limits the maximum delta depth")), OPT_STRING(0, "threads", &opt_threads, N_("n"), N_("limits the maximum number of threads")), - OPT_MAGNITUDE(0, "max-pack-size", &po_args.max_pack_size, - N_("maximum size of each packfile")), + OPT_UNSIGNED(0, "max-pack-size", &po_args.max_pack_size, + N_("maximum size of each packfile")), OPT_PARSE_LIST_OBJECTS_FILTER(&po_args.filter_options), OPT_BOOL(0, "pack-kept-objects", &pack_kept_objects, N_("repack objects in packs marked with .keep")), diff --git a/parse-options.c b/parse-options.c index b287436e81a..d23e587e98b 100644 --- a/parse-options.c +++ b/parse-options.c @@ -191,7 +191,7 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, optname(opt, flags)); return 0; - case OPTION_MAGNITUDE: + case OPTION_UNSIGNED: if (unset) { *(unsigned long *)opt->value = 0; return 0; @@ -656,7 +656,7 @@ static void show_negated_gitcomp(const struct option *opts, int show_all, case OPTION_STRING: case OPTION_FILENAME: case OPTION_INTEGER: - case OPTION_MAGNITUDE: + case OPTION_UNSIGNED: case OPTION_CALLBACK: case OPTION_BIT: case OPTION_NEGBIT: @@ -708,7 +708,7 @@ static int show_gitcomp(const struct option *opts, int show_all) case OPTION_STRING: case OPTION_FILENAME: case OPTION_INTEGER: - case OPTION_MAGNITUDE: + case OPTION_UNSIGNED: case OPTION_CALLBACK: if (opts->flags & PARSE_OPT_NOARG) break; diff --git a/parse-options.h b/parse-options.h index 997ffbee805..14e4df1ee21 100644 --- a/parse-options.h +++ b/parse-options.h @@ -25,7 +25,7 @@ enum parse_opt_type { /* options with arguments (usually) */ OPTION_STRING, OPTION_INTEGER, - OPTION_MAGNITUDE, + OPTION_UNSIGNED, OPTION_CALLBACK, OPTION_LOWLEVEL_CALLBACK, OPTION_FILENAME @@ -270,8 +270,8 @@ struct option { #define OPT_CMDMODE(s, l, v, h, i) OPT_CMDMODE_F(s, l, v, h, i, 0) #define OPT_INTEGER(s, l, v, h) OPT_INTEGER_F(s, l, v, h, 0) -#define OPT_MAGNITUDE(s, l, v, h) { \ - .type = OPTION_MAGNITUDE, \ +#define OPT_UNSIGNED(s, l, v, h) { \ + .type = OPTION_UNSIGNED, \ .short_name = (s), \ .long_name = (l), \ .value = (v), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 997f55fd45b..fc3e2861c26 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -6,7 +6,7 @@ static int boolean = 0; static int integer = 0; -static unsigned long magnitude = 0; +static unsigned long unsigned_integer = 0; static timestamp_t timestamp; static int abbrev = 7; static int verbose = -1; /* unspecified */ @@ -140,7 +140,7 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP(""), OPT_INTEGER('i', "integer", &integer, "get a integer"), OPT_INTEGER('j', NULL, &integer, "get a integer, too"), - OPT_MAGNITUDE('m', "magnitude", &magnitude, "get a magnitude"), + 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), @@ -210,7 +210,7 @@ int cmd__parse_options(int argc, const char **argv) } show(&expect, &ret, "boolean: %d", boolean); show(&expect, &ret, "integer: %d", integer); - show(&expect, &ret, "magnitude: %lu", magnitude); + 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); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 0c538c4b437..65a11c8dbc8 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -23,7 +23,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer -j <n> get a integer, too - -m, --magnitude <n> get a magnitude + -u, --unsigned <n> get an unsigned integer --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) @@ -115,30 +115,30 @@ test_expect_success 'OPT_INTEGER() negative' 'check integer: -2345 -i -2345' test_expect_success 'OPT_INTEGER() kilo' 'check integer: 239616 -i 234k' test_expect_success 'OPT_INTEGER() negative kilo' 'check integer: -239616 -i -234k' -test_expect_success 'OPT_MAGNITUDE() simple' ' - check magnitude: 2345678 -m 2345678 +test_expect_success 'OPT_UNSIGNED() simple' ' + check unsigned: 2345678 -u 2345678 ' -test_expect_success 'OPT_MAGNITUDE() kilo' ' - check magnitude: 239616 -m 234k +test_expect_success 'OPT_UNSIGNED() kilo' ' + check unsigned: 239616 -u 234k ' -test_expect_success 'OPT_MAGNITUDE() mega' ' - check magnitude: 104857600 -m 100m +test_expect_success 'OPT_UNSIGNED() mega' ' + check unsigned: 104857600 -u 100m ' -test_expect_success 'OPT_MAGNITUDE() giga' ' - check magnitude: 1073741824 -m 1g +test_expect_success 'OPT_UNSIGNED() giga' ' + check unsigned: 1073741824 -u 1g ' -test_expect_success 'OPT_MAGNITUDE() 3giga' ' - check magnitude: 3221225472 -m 3g +test_expect_success 'OPT_UNSIGNED() 3giga' ' + check unsigned: 3221225472 -u 3g ' cat >expect <<\EOF boolean: 2 integer: 1729 -magnitude: 16384 +unsigned: 16384 timestamp: 0 string: 123 abbrev: 7 @@ -149,7 +149,7 @@ file: prefix/my.file EOF test_expect_success 'short options' ' - test-tool parse-options -s123 -b -i 1729 -m 16k -b -vv -n -F my.file \ + test-tool parse-options -s123 -b -i 1729 -u 16k -b -vv -n -F my.file \ >output 2>output.err && test_cmp expect output && test_must_be_empty output.err @@ -158,7 +158,7 @@ test_expect_success 'short options' ' cat >expect <<\EOF boolean: 2 integer: 1729 -magnitude: 16384 +unsigned: 16384 timestamp: 0 string: 321 abbrev: 10 @@ -169,7 +169,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --magnitude 16k \ + test-tool parse-options --boolean --integer 1729 --unsigned 16k \ --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -181,7 +181,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' cat >expect <<-EOF && boolean: 0 integer: 0 - magnitude: 0 + unsigned: 0 timestamp: 0 string: (not set) abbrev: 100 @@ -255,7 +255,7 @@ test_expect_success 'superfluous value provided: cmdmode' ' cat >expect <<\EOF boolean: 1 integer: 13 -magnitude: 0 +unsigned: 0 timestamp: 0 string: 123 abbrev: 7 @@ -278,7 +278,7 @@ test_expect_success 'intermingled arguments' ' cat >expect <<\EOF boolean: 0 integer: 2 -magnitude: 0 +unsigned: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -345,7 +345,7 @@ cat >expect <<\EOF Callback: "four", 0 boolean: 5 integer: 4 -magnitude: 0 +unsigned: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -370,7 +370,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' ' cat >expect <<\EOF boolean: 1 integer: 23 -magnitude: 0 +unsigned: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -449,7 +449,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' ' cat >expect <<\EOF boolean: 0 integer: 0 -magnitude: 0 +unsigned: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -773,14 +773,14 @@ test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in c grep ^BUG err ' -test_expect_success 'negative magnitude' ' - test_must_fail test-tool parse-options --magnitude -1 >out 2>err && +test_expect_success 'negative unsigned' ' + test_must_fail test-tool parse-options --unsigned -1 >out 2>err && grep "non-negative integer" err && test_must_be_empty out ' -test_expect_success 'magnitude with units but no numbers' ' - test_must_fail test-tool parse-options --magnitude m >out 2>err && +test_expect_success 'unsigned with units but no numbers' ' + test_must_fail test-tool parse-options --unsigned m >out 2>err && grep "non-negative integer" err && test_must_be_empty out ' -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* Re: [PATCH v4 4/7] parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` 2025-04-17 10:49 ` [PATCH v4 4/7] parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` Patrick Steinhardt @ 2025-04-17 15:17 ` Junio C Hamano 0 siblings, 0 replies; 46+ messages in thread From: Junio C Hamano @ 2025-04-17 15:17 UTC (permalink / raw) To: Patrick Steinhardt Cc: git, John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood Patrick Steinhardt <ps@pks.im> writes: > With the preceding commit, `OPT_INTEGER()` has learned to support unit > factors. Consequently, the major differencen between `OPT_INTEGER()` and > `OPT_MAGNITUDE()` isn't the support of unit factors anymore, as both of > them do support them now. Instead, the difference is that one handles > signed and the other handles unsigned integers. > > Adapt the name of `OPT_MAGNITUDE()` accordingly by renaming it to > `OPT_UNSIGNED()`. Very good move. It would have been very confusing if a new developer had to choose between INTEGER and MAGNITUDE, and this change nicely removes that "Huh?" factor. Thanks. ^ permalink raw reply [flat|nested] 46+ messages in thread
* [PATCH v4 5/7] parse-options: introduce precision handling for `OPTION_INTEGER` 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (3 preceding siblings ...) 2025-04-17 10:49 ` [PATCH v4 4/7] parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 6/7] parse-options: introduce precision handling for `OPTION_UNSIGNED` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 7/7] parse-options: detect mismatches in integer signedness Patrick Steinhardt 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood 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> --- builtin/fmt-merge-msg.c | 2 ++ builtin/merge.c | 1 + builtin/show-branch.c | 1 + builtin/tag.c | 1 + parse-options.c | 52 ++++++++++++++++++++++++++++++++----------- parse-options.h | 6 +++++ t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 23 ++++++++++++++++++- 8 files changed, 75 insertions(+), 14 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 240cdb474bc..3b6aac2cf7f 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("populate log with at most <n> entries from shortlog"), .flags = PARSE_OPT_OPTARG, @@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "summary", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("alias for --log (deprecated)"), .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, diff --git a/builtin/merge.c b/builtin/merge.c index 21787d45165..9ab10c7db0a 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -254,6 +254,7 @@ static struct option builtin_merge_options[] = { .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len, + .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("add (at most <n>) entries from shortlog to merge commit message"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/show-branch.c b/builtin/show-branch.c index dab37019d29..b549d8c3f5b 100644 --- a/builtin/show-branch.c +++ b/builtin/show-branch.c @@ -671,6 +671,7 @@ int cmd_show_branch(int ac, .type = OPTION_INTEGER, .long_name = "more", .value = &extra, + .precision = sizeof(extra), .argh = N_("n"), .help = N_("show <n> more commits after the common ancestor"), .flags = PARSE_OPT_OPTARG, diff --git a/builtin/tag.c b/builtin/tag.c index b266f12bb48..7597d93c71b 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -483,6 +483,7 @@ int cmd_tag(int argc, .type = OPTION_INTEGER, .short_name = 'n', .value = &filter.lines, + .precision = sizeof(filter.lines), .argh = N_("n"), .help = N_("print <n> lines of each tag message"), .flags = PARSE_OPT_OPTARG, diff --git a/parse-options.c b/parse-options.c index d23e587e98b..768718a3972 100644 --- a/parse-options.c +++ b/parse-options.c @@ -172,25 +172,51 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, return (*opt->ll_callback)(p, opt, p_arg, p_unset); } case OPTION_INTEGER: + { + intmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision); + intmax_t lower_bound = -upper_bound - 1; + intmax_t value; + if (unset) { - *(int *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(int *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!*arg) + } else if (!*arg) { return error(_("%s expects a numerical value"), optname(opt, flags)); - if (!git_parse_int(arg, opt->value)) - return error(_("%s expects an integer value" - " with an optional k/m/g suffix"), + } else if (!git_parse_signed(arg, &value, upper_bound)) { + if (errno == ERANGE) + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + arg, optname(opt, flags), lower_bound, upper_bound); + + return error(_("%s expects an integer value with an optional k/m/g suffix"), optname(opt, flags)); - return 0; + } + + if (value < lower_bound) + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + arg, optname(opt, flags), lower_bound, upper_bound); + switch (opt->precision) { + case 1: + *(int8_t *)opt->value = value; + return 0; + case 2: + *(int16_t *)opt->value = value; + return 0; + case 4: + *(int32_t *)opt->value = value; + return 0; + case 8: + *(int64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } case OPTION_UNSIGNED: if (unset) { *(unsigned long *)opt->value = 0; diff --git a/parse-options.h b/parse-options.h index 14e4df1ee21..4c430c7273c 100644 --- a/parse-options.h +++ b/parse-options.h @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, * `value`:: * stores pointers to the values to be filled. * + * `precision`:: + * precision of the integer pointed to by `value` in number of bytes. Should + * typically be its `sizeof()`. + * * `argh`:: * token to explain the kind of argument this option wants. Does not * begin in capital letter, and does not end with a full stop. @@ -151,6 +155,7 @@ struct option { int short_name; const char *long_name; void *value; + size_t precision; const char *argh; const char *help; @@ -214,6 +219,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = (f), \ diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index fc3e2861c26..3689aee8315 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; 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"), @@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv) 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), @@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv) } 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)"); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index 65a11c8dbc8..be785547ead 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -22,6 +22,7 @@ usage: test-tool parse-options <options> -i, --[no-]integer <n> get a integer + --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -u, --unsigned <n> get an unsigned integer --[no-]set23 set integer to 23 @@ -138,6 +139,7 @@ test_expect_success 'OPT_UNSIGNED() 3giga' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 0 unsigned: 16384 timestamp: 0 string: 123 @@ -158,6 +160,7 @@ test_expect_success 'short options' ' cat >expect <<\EOF boolean: 2 integer: 1729 +i16: 9000 unsigned: 16384 timestamp: 0 string: 321 @@ -169,7 +172,7 @@ file: prefix/fi.le EOF test_expect_success 'long options' ' - test-tool parse-options --boolean --integer 1729 --unsigned 16k \ + test-tool parse-options --boolean --integer 1729 --i16 9000 --unsigned 16k \ --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && @@ -181,6 +184,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' cat >expect <<-EOF && boolean: 0 integer: 0 + i16: 0 unsigned: 0 timestamp: 0 string: (not set) @@ -255,6 +259,7 @@ test_expect_success 'superfluous value provided: cmdmode' ' cat >expect <<\EOF boolean: 1 integer: 13 +i16: 0 unsigned: 0 timestamp: 0 string: 123 @@ -278,6 +283,7 @@ test_expect_success 'intermingled arguments' ' cat >expect <<\EOF boolean: 0 integer: 2 +i16: 0 unsigned: 0 timestamp: 0 string: (not set) @@ -345,6 +351,7 @@ cat >expect <<\EOF Callback: "four", 0 boolean: 5 integer: 4 +i16: 0 unsigned: 0 timestamp: 0 string: (not set) @@ -370,6 +377,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' ' cat >expect <<\EOF boolean: 1 integer: 23 +i16: 0 unsigned: 0 timestamp: 0 string: (not set) @@ -449,6 +457,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' ' cat >expect <<\EOF boolean: 0 integer: 0 +i16: 0 unsigned: 0 timestamp: 0 string: (not set) @@ -785,4 +794,16 @@ test_expect_success 'unsigned with units but no numbers' ' test_must_be_empty out ' +test_expect_success 'i16 limits range' ' + test-tool parse-options --i16 32767 >out && + test_grep "i16: 32767" out && + test_must_fail test-tool parse-options --i16 32768 2>err && + test_grep "value 32768 for option .i16. not in range \[-32768,32767\]" err && + + test-tool parse-options --i16 -32768 >out && + test_grep "i16: -32768" out && + test_must_fail test-tool parse-options --i16 -32769 2>err && + test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v4 6/7] parse-options: introduce precision handling for `OPTION_UNSIGNED` 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (4 preceding siblings ...) 2025-04-17 10:49 ` [PATCH v4 5/7] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 7/7] parse-options: detect mismatches in integer signedness Patrick Steinhardt 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood This commit is the equivalent to the preceding commit, but instead of introducing precision handling for `OPTION_INTEGER` we introduce it for `OPTION_UNSIGNED`. Signed-off-by: Patrick Steinhardt <ps@pks.im> --- parse-options.c | 48 +++++++++++++++++++++++++++++++++---------- parse-options.h | 1 + parse.c | 2 +- parse.h | 1 + t/helper/test-parse-options.c | 3 +++ t/t0040-parse-options.sh | 18 +++++++++++++++- 6 files changed, 60 insertions(+), 13 deletions(-) diff --git a/parse-options.c b/parse-options.c index 768718a3972..a9a39ecaef6 100644 --- a/parse-options.c +++ b/parse-options.c @@ -197,7 +197,7 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, if (value < lower_bound) return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), - arg, optname(opt, flags), lower_bound, upper_bound); + arg, optname(opt, flags), (intmax_t)lower_bound, (intmax_t)upper_bound); switch (opt->precision) { case 1: @@ -218,21 +218,47 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, } } case OPTION_UNSIGNED: + { + uintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision); + uintmax_t value; + if (unset) { - *(unsigned long *)opt->value = 0; - return 0; - } - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { - *(unsigned long *)opt->value = opt->defval; - return 0; - } - if (get_arg(p, opt, flags, &arg)) + value = 0; + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { + value = opt->defval; + } else if (get_arg(p, opt, flags, &arg)) { return -1; - if (!git_parse_ulong(arg, opt->value)) + } else if (!*arg) { + return error(_("%s expects a numerical value"), + optname(opt, flags)); + } else if (!git_parse_unsigned(arg, &value, upper_bound)) { + if (errno == ERANGE) + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), + arg, optname(opt, flags), (uintmax_t) 0, upper_bound); + return error(_("%s expects a non-negative integer value" " with an optional k/m/g suffix"), optname(opt, flags)); - return 0; + } + + switch (opt->precision) { + case 1: + *(uint8_t *)opt->value = value; + return 0; + case 2: + *(uint16_t *)opt->value = value; + return 0; + case 4: + *(uint32_t *)opt->value = value; + return 0; + case 8: + *(uint64_t *)opt->value = value; + return 0; + default: + BUG("invalid precision for option %s", + optname(opt, flags)); + } + } default: BUG("opt->type %d should not happen", opt->type); diff --git a/parse-options.h b/parse-options.h index 4c430c7273c..dc460a26ff1 100644 --- a/parse-options.h +++ b/parse-options.h @@ -281,6 +281,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \ + .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = PARSE_OPT_NONEG, \ diff --git a/parse.c b/parse.c index 3c47448ca67..48313571aab 100644 --- a/parse.c +++ b/parse.c @@ -51,7 +51,7 @@ int git_parse_signed(const char *value, intmax_t *ret, intmax_t max) return 0; } -static int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max) +int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max) { if (value && *value) { char *end; diff --git a/parse.h b/parse.h index 6bb9a54d9ac..ea32de9a91f 100644 --- a/parse.h +++ b/parse.h @@ -2,6 +2,7 @@ #define PARSE_H int git_parse_signed(const char *value, intmax_t *ret, intmax_t max); +int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max); int git_parse_ssize_t(const char *, ssize_t *); int git_parse_ulong(const char *, unsigned long *); int git_parse_int(const char *value, int *ret); diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 3689aee8315..f2663dd0c07 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv) }; struct string_list expect = STRING_LIST_INIT_NODUP; struct string_list list = STRING_LIST_INIT_NODUP; + uint16_t u16 = 0; int16_t i16 = 0; struct option options[] = { @@ -143,6 +144,7 @@ int cmd__parse_options(int argc, const char **argv) 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_UNSIGNED(0, "u16", &u16, "get a 16 bit 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), @@ -214,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv) show(&expect, &ret, "integer: %d", integer); show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); show(&expect, &ret, "unsigned: %lu", unsigned_integer); + show(&expect, &ret, "u16: %"PRIuMAX, (uintmax_t) u16); show(&expect, &ret, "timestamp: %"PRItime, timestamp); show(&expect, &ret, "string: %s", string ? string : "(not set)"); show(&expect, &ret, "abbrev: %d", abbrev); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index be785547ead..ca55ea8228c 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -25,6 +25,7 @@ usage: test-tool parse-options <options> --[no-]i16 <n> get a 16 bit integer -j <n> get a integer, too -u, --unsigned <n> get an unsigned integer + --u16 <n> get a 16 bit unsigned integer --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) @@ -141,6 +142,7 @@ boolean: 2 integer: 1729 i16: 0 unsigned: 16384 +u16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -162,6 +164,7 @@ boolean: 2 integer: 1729 i16: 9000 unsigned: 16384 +u16: 32768 timestamp: 0 string: 321 abbrev: 10 @@ -173,7 +176,7 @@ EOF test_expect_success 'long options' ' test-tool parse-options --boolean --integer 1729 --i16 9000 --unsigned 16k \ - --boolean --string2=321 --verbose --verbose --no-dry-run \ + --u16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \ --abbrev=10 --file fi.le --obsolete \ >output 2>output.err && test_must_be_empty output.err && @@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' ' integer: 0 i16: 0 unsigned: 0 + u16: 0 timestamp: 0 string: (not set) abbrev: 100 @@ -261,6 +265,7 @@ boolean: 1 integer: 13 i16: 0 unsigned: 0 +u16: 0 timestamp: 0 string: 123 abbrev: 7 @@ -285,6 +290,7 @@ boolean: 0 integer: 2 i16: 0 unsigned: 0 +u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -353,6 +359,7 @@ boolean: 5 integer: 4 i16: 0 unsigned: 0 +u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -379,6 +386,7 @@ boolean: 1 integer: 23 i16: 0 unsigned: 0 +u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -459,6 +467,7 @@ boolean: 0 integer: 0 i16: 0 unsigned: 0 +u16: 0 timestamp: 0 string: (not set) abbrev: 7 @@ -806,4 +815,11 @@ test_expect_success 'i16 limits range' ' test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err ' +test_expect_success 'u16 limits range' ' + test-tool parse-options --u16 65535 >out && + test_grep "u16: 65535" out && + test_must_fail test-tool parse-options --u16 65536 2>err && + test_grep "value 65536 for option .u16. not in range \[0,65535\]" err +' + test_done -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
* [PATCH v4 7/7] parse-options: detect mismatches in integer signedness 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt ` (5 preceding siblings ...) 2025-04-17 10:49 ` [PATCH v4 6/7] parse-options: introduce precision handling for `OPTION_UNSIGNED` Patrick Steinhardt @ 2025-04-17 10:49 ` Patrick Steinhardt 6 siblings, 0 replies; 46+ messages in thread From: Patrick Steinhardt @ 2025-04-17 10:49 UTC (permalink / raw) To: git Cc: John Paul Adrian Glaubitz, Todd Zullinger, René Scharfe, SZEDER Gábor, Derrick Stolee, Jeff King, Phillip Wood It was reported that "t5620-backfill.sh" fails on s390x and sparc64 in a test that exercises the "--min-batch-size" command line option. The symptom was that the option didn't seem to have an effect: we didn't fetch objects with a batch size of 20, but instead fetched all objects at once. As it turns out, the root cause is that `--min-batch-size` uses `OPT_INTEGER()` to parse the command line option. While this macro expects the caller to pass a pointer to an integer, we instead pass a pointer to a `size_t`. This coincidentally works on most platforms, but it breaks apart on the mentioned platforms because they are big endian. This issue isn't specific to git-backfill(1): there are a couple of other places where we have the same type confusion going on. This indicates that the issue really is the interface that the parse-options subsystem provides -- it is simply too easy to get this wrong as there isn't any kind of compiler warning, and things just work on the most common systems. Address the systemic issue by introducing two new build asserts `BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names already hint at, those macros will cause a compiler error when passed a value that is not signed or unsigned, respectively. Adapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to use those asserts. This uncovers a small set of sites where we indeed have the same bug as in git-backfill(1). Adapt all of them to use the correct option. Reported-by: Todd Zullinger <tmz@pobox.com> Reported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de> Helped-by: SZEDER Gábor <szeder.dev@gmail.com> Helped-by: Jeff King <peff@peff.net> Signed-off-by: Patrick Steinhardt <ps@pks.im> --- apply.c | 4 ++-- builtin/backfill.c | 4 ++-- builtin/column.c | 2 +- builtin/grep.c | 4 ++-- git-compat-util.h | 7 +++++++ parse-options.h | 4 ++-- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apply.c b/apply.c index f274a379487..a850c7d75fe 100644 --- a/apply.c +++ b/apply.c @@ -5123,8 +5123,8 @@ int apply_parse_options(int argc, const char **argv, /* Think twice before adding "--nul" synonym to this */ OPT_SET_INT('z', NULL, &state->line_termination, N_("paths are separated with NUL character"), '\0'), - OPT_INTEGER('C', NULL, &state->p_context, - N_("ensure at least <n> lines of context match")), + OPT_UNSIGNED('C', NULL, &state->p_context, + N_("ensure at least <n> lines of context match")), OPT_CALLBACK(0, "whitespace", state, N_("action"), N_("detect new or modified lines that have whitespace errors"), apply_option_parse_whitespace), diff --git a/builtin/backfill.c b/builtin/backfill.c index 33e1ea2f84f..d95d7a2d4d6 100644 --- a/builtin/backfill.c +++ b/builtin/backfill.c @@ -123,8 +123,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit .sparse = 0, }; struct option options[] = { - OPT_INTEGER(0, "min-batch-size", &ctx.min_batch_size, - N_("Minimum number of objects to request at a time")), + OPT_UNSIGNED(0, "min-batch-size", &ctx.min_batch_size, + N_("Minimum number of objects to request at a time")), OPT_BOOL(0, "sparse", &ctx.sparse, N_("Restrict the missing objects to the current sparse-checkout")), OPT_END(), diff --git a/builtin/column.c b/builtin/column.c index 50314cc2559..ce6443d5fac 100644 --- a/builtin/column.c +++ b/builtin/column.c @@ -31,7 +31,7 @@ int cmd_column(int argc, struct option options[] = { OPT_STRING(0, "command", &real_command, N_("name"), N_("lookup config vars")), OPT_COLUMN(0, "mode", &colopts, N_("layout to use")), - OPT_INTEGER(0, "raw-mode", &colopts, N_("layout to use")), + OPT_UNSIGNED(0, "raw-mode", &colopts, N_("layout to use")), OPT_INTEGER(0, "width", &copts.width, N_("maximum width")), OPT_STRING(0, "indent", &copts.indent, N_("string"), N_("padding space on left border")), OPT_STRING(0, "nl", &copts.nl, N_("string"), N_("padding space on right border")), diff --git a/builtin/grep.c b/builtin/grep.c index c4869733e1b..f23a6f1dc86 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -983,9 +983,9 @@ int cmd_grep(int argc, OPT_CALLBACK('C', "context", &opt, N_("n"), N_("show <n> context lines before and after matches"), context_callback), - OPT_INTEGER('B', "before-context", &opt.pre_context, + OPT_UNSIGNED('B', "before-context", &opt.pre_context, N_("show <n> context lines before matches")), - OPT_INTEGER('A', "after-context", &opt.post_context, + OPT_UNSIGNED('A', "after-context", &opt.post_context, N_("show <n> context lines after matches")), OPT_INTEGER(0, "threads", &num_threads, N_("use <n> worker threads")), diff --git a/git-compat-util.h b/git-compat-util.h index cf733b38acd..1218fcf81a4 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -110,12 +110,19 @@ DISABLE_WARNING(-Wsign-compare) # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(__builtin_types_compatible_p(__typeof__(*(dst)), \ __typeof__(*(src)))) + +# define BARF_UNLESS_SIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) < 0) +# define BARF_UNLESS_UNSIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) > 0) #else # define BARF_UNLESS_AN_ARRAY(arr) 0 # define BARF_UNLESS_COPYABLE(dst, src) \ BUILD_ASSERT_OR_ZERO(0 ? ((*(dst) = *(src)), 0) : \ sizeof(*(dst)) == sizeof(*(src))) + +# define BARF_UNLESS_SIGNED(var) 0 +# define BARF_UNLESS_UNSIGNED(var) 0 #endif + /* * ARRAY_SIZE - get the number of elements in a visible array * @x: the array whose size you want. diff --git a/parse-options.h b/parse-options.h index dc460a26ff1..91c3e3c29b3 100644 --- a/parse-options.h +++ b/parse-options.h @@ -218,7 +218,7 @@ struct option { .type = OPTION_INTEGER, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_SIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ @@ -280,7 +280,7 @@ struct option { .type = OPTION_UNSIGNED, \ .short_name = (s), \ .long_name = (l), \ - .value = (v), \ + .value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ -- 2.49.0.805.g082f7c87e0.dirty ^ permalink raw reply related [flat|nested] 46+ messages in thread
end of thread, other threads:[~2025-04-17 15:17 UTC | newest] Thread overview: 46+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2025-04-01 15:01 [PATCH 0/5] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 1/5] global: use designated initializers for options Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt 2025-04-01 18:47 ` René Scharfe 2025-04-15 10:26 ` Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt 2025-04-01 15:01 ` [PATCH 5/5] parse-options: detect mismatches in integer signedness Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 0/5] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 1/5] global: use designated initializers for options Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt 2025-04-15 15:51 ` Phillip Wood 2025-04-16 10:28 ` Patrick Steinhardt 2025-04-15 16:59 ` Junio C Hamano 2025-04-16 10:28 ` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt 2025-04-15 15:52 ` Phillip Wood 2025-04-16 10:27 ` Patrick Steinhardt 2025-04-16 13:31 ` phillip.wood123 2025-04-15 17:38 ` René Scharfe 2025-04-16 10:28 ` Patrick Steinhardt 2025-04-15 12:14 ` [PATCH v2 5/5] parse-options: detect mismatches in integer signedness Patrick Steinhardt 2025-04-15 17:02 ` Junio C Hamano 2025-04-16 10:02 ` [PATCH v3 0/7] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 1/7] global: use designated initializers for options Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 2/7] parse-options: check for overflow when parsing integers Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt 2025-04-16 17:29 ` Junio C Hamano 2025-04-16 10:02 ` [PATCH v3 4/7] parse-options: introduce precision handling for `OPTION_MAGNITUDE` Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED` Patrick Steinhardt 2025-04-16 18:50 ` Junio C Hamano 2025-04-17 8:15 ` Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 6/7] parse-options: detect mismatches in integer signedness Patrick Steinhardt 2025-04-16 10:02 ` [PATCH v3 7/7] parse-options: introduce bounded integer options Patrick Steinhardt 2025-04-16 19:19 ` Junio C Hamano 2025-04-17 8:14 ` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 0/7] parse-options: harden handling of integer values Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 1/7] parse: fix off-by-one for minimum signed values Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 2/7] global: use designated initializers for options Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 3/7] parse-options: support unit factors in `OPT_INTEGER()` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 4/7] parse-options: rename `OPT_MAGNITUDE()` to `OPT_UNSIGNED()` Patrick Steinhardt 2025-04-17 15:17 ` Junio C Hamano 2025-04-17 10:49 ` [PATCH v4 5/7] parse-options: introduce precision handling for `OPTION_INTEGER` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 6/7] parse-options: introduce precision handling for `OPTION_UNSIGNED` Patrick Steinhardt 2025-04-17 10:49 ` [PATCH v4 7/7] parse-options: detect mismatches in integer signedness Patrick Steinhardt
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for NNTP newsgroup(s).