mbox series

[v4,0/7] parse-options: harden handling of integer values

Message ID 20250417-b4-pks-parse-options-integers-v4-0-9cbc76b61cfe@pks.im (mailing list archive)
Headers show
Series parse-options: harden handling of integer values | expand

Message

Patrick Steinhardt April 17, 2025, 10:49 a.m. UTC
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