Git development
 help / color / mirror / Atom feed
From: Christian Couder <christian.couder@gmail.com>
To: git@vger.kernel.org
Cc: Junio C Hamano <gitster@pobox.com>,
	Kaartic Sivaraam <kaartic.sivaraam@gmail.com>,
	Cynthia Uche <herroyalpriestess@gmail.com>,
	Patrick Steinhardt <ps@pks.im>, Elijah Newren <newren@gmail.com>,
	Jeff King <peff@peff.net>,
	"brian m . carlson" <sandals@crustytoothpaste.net>,
	Johannes Schindelin <Johannes.Schindelin@gmx.de>,
	Justin Tobler <jltobler@gmail.com>,
	Christian Couder <christian.couder@gmail.com>
Subject: [PATCH v4 0/4] Standardize early option scanning
Date: Mon,  2 Nov 2026 16:14:48 +0100	[thread overview]
Message-ID: <20261102151447.734680-6-christian.couder@gmail.com> (raw)
In-Reply-To: <20261012074413.337183-1-christian.couder@gmail.com>

A number of commands perform an early scan of their arguments to look
for specific flags or structural separators (like `--`).

These hand-rolled early scans are often fragile. They especially fail
to account for options that take their value as a separate
argument. This leads to disagreements between the early scan and the
actual parse_options() pass. For example, the early scanner might miss
a special option entirely, or mistakenly treat an option's value as
the `--` path separator.

To allow these commands to safely skip option values during their
early scans, this series introduces a new "early-scan" sub-API into
the existing "parse-options" API.

This is deliberately implemented as a new simple and fast scan. A full
'dry run' of parse_options() would have to avoid writing through
`opt->value`, calling callbacks or dying. Instead, the scan reuses
only the long option lookup of parse_options(), and stops when unsure.

This allows negated options ("--no-..."), unambiguous abbreviated
options and aliases to be properly handled. The early scan also agrees
with parse_options() on which options take a value, including those
with PARSE_OPT_OPTARG or PARSE_OPT_LASTARG_DEFAULT.

To never misinterpret an option's value as an option though, it stops
at any argument it cannot reliably skip:

 1. a short option, or a bundle of them,

 2. an unknown option, or an ambiguous or disallowed abbreviation,

 3. an option with a low-level callback, as such a callback may
 consume any number of arguments,

 4. an option with a missing or stuck value that parse_options() would
 reject, like "--no-<name>=<value>".

So the scan may fail to see an option that parse_options() would
accept, but it doesn't report an option that parse_options() would
not, and callers can safely err on the side of not having seen it.

This holds as long as the scan is told how the later parse_options()
call treats its arguments: EARLY_SCAN_STOP_AT_NON_OPTION should be
passed when parse_options() stops at non-options or has subcommands,
and the scan should not be used with PARSE_OPT_KEEP_UNKNOWN_OPT, which
disables abbreviations in parse_options() only.

The early scan is performed by a new early_scan_options() function
which takes a regular `const struct option *option` array as
argument.

This requires that the command already uses `struct option` and the
parse-options API to parse its arguments. As the majority of commands
performing an early scan don't use the parse-options API yet, they
will have to be converted to use it before they can use
early_scan_options().

In this series, only `git fast-import` is converted to the early-scan
API. Its hand-rolled scan disagreed with parse_options() in both
directions:

  - `git fast-import --depth 5 --allow-unsafe-features` silently
    ignored `--allow-unsafe-features`, refusing unsafe features from
    the stream, and so did `--allow-unsafe`, its abbreviation,

  - `git fast-import --quiet --export-pack --allow-unsafe-features`
    and `git fast-import --end-of-options --allow-unsafe-features`
    allowed unsafe features from the stream, while parse_options()
    takes `--allow-unsafe-features` as the value of
    `--export-pack-edges` in the first case, and as a non-option
    argument in the second one.

All these cases now agree with parse_options().

Overview of the patches
=======================

 - Patch 1/4 refactors some existing code into a new
   parse_options_takes_argument() helper that will be used in patch
   3/4.

 - Patch 2/4 extracts the long option lookup code from
   parse_long_opt() into a new lookup_long_opt() function, so that the
   function can be reused in the next patch.

 - Patch 3/4 introduces early_scan_options(), the early scanner that
   will be used instead of hand-rolled ones, along with the
   PARSE_OPT_EARLY flag and tests.

 - Patch 4/4 uses early_scan_options() to fix the early scan for
   `--allow-unsafe-features` in `git fast-import`.

Changes since v3
================

Thanks a lot to Junio, Kaartic and Cynthia who reviewed previous
versions.

There are a number of small changes since v3:

 - In patch 2/4, a `switch` statement is now used in parse_long_opt()
   to process the result from lookup_long_opt().

 - In patch 3/4, a parse_options_check() call has been added to
   early_scan_options(), after aliases are preprocessed, so that the
   options are checked before the scan relies on them, rather than
   only later by parse_options().

 - In patch 3/4, in "parse-options.h":

   - the comment before early_scan_fn() has been improved to mention
     that the `option` argument may point into a temporary copy of the
     options, and

   - the end of the comment before early_scan_options() has been made
     clearer and now mentions that EARLY_SCAN_STOP_AT_NON_OPTION is
     safe to use even if PARSE_OPT_STOP_AT_NON_OPTION is not passed to
     parse_options().

 - In patch 4/4:

   - the comment before the call to early_scan_options() in
     cmd_fast_import() has been improved to explain why
     EARLY_SCAN_STOP_AT_NON_OPTION is passed,

   - a new test has been added to check that unsafe "feature" commands
     from the stream are refused when `--allow-unsafe-features` comes
     after a non-option argument, which parse_argv() rejects anyway.

CI tests:
=========

Some failed, see:

https://github.com/chriscool/git/actions/runs/41450228024

but the failures in t5550 and t5540 on ubuntu:rolling and
alpine:latest are unrelated. They come from recent curl stripping
credentials from CURLINFO_EFFECTIVE_URL, and are fixed on master by
jk/http-curl-strip-creds.

Patches
=======

Christian Couder (4):
  parse-options: add parse_options_takes_argument()
  parse-options: extract lookup_long_opt() from parse_long_opt()
  parse-options: add early_scan_options()
  fast-import: use early_scan_options() for --allow-unsafe-features

Diffstat
========

 Documentation/git-fast-import.adoc            |   7 -
 .../technical/api-parse-options.adoc          |   5 +
 builtin/fast-import.c                         |  42 ++-
 parse-options.c                               | 291 +++++++++++++---
 parse-options.h                               | 109 ++++++
 t/helper/test-parse-options.c                 |  96 ++++++
 t/helper/test-tool.c                          |   1 +
 t/helper/test-tool.h                          |   1 +
 t/t0040-parse-options.sh                      | 321 ++++++++++++++++++
 t/t9300-fast-import.sh                        |  36 ++
 10 files changed, 844 insertions(+), 65 deletions(-)

Range-diff since v3
===================

1:  57c48ab0fe = 1:  57c48ab0fe parse-options: add parse_options_takes_argument()
2:  38243ce8bf ! 2:  1c001e577e parse-options: extract lookup_long_opt() from parse_long_opt()
    @@ Commit message
         when disallowed only if there is no exact match, and the ambiguity
         error message is unchanged.
     
    +    Helped-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
         Signed-off-by: Christian Couder <christian.couder@gmail.com>
     
      ## parse-options.c ##
    @@ parse-options.c: static enum parse_opt_result parse_long_opt(
      	}
      
     -	if (disallow_abbreviated_options && (ambiguous.option || abbrev.option))
    +-		die("disallowed abbreviated or ambiguous option '%.*s'",
    +-		    (int)(arg_end - arg), arg);
    +-
    +-	if (ambiguous.option) {
    +-		error(_("ambiguous option: %s "
    +-			"(could be --%s%s or --%s%s)"),
    +-			arg,
    +-			(ambiguous.flags & OPT_UNSET) ?  "no-" : "",
    +-			ambiguous.option->long_name,
    +-			(abbrev.flags & OPT_UNSET) ?  "no-" : "",
    +-			abbrev.option->long_name);
    +-		return PARSE_OPT_HELP_ERROR;
    +-	}
    +-	if (abbrev.option) {
     +	if (other->option)
     +		return LONG_OPT_AMBIGUOUS;
     +
     +	if (found->option) {
    -+		if (*arg_end)
    + 		if (*arg_end)
    +-			p->opt = arg_end + 1;
    +-		return get_value(p, abbrev.option, abbrev.flags);
     +			*stuck = arg_end + 1;
     +		return LONG_OPT_ABBREV;
    -+	}
    + 	}
    +-	return PARSE_OPT_UNKNOWN;
     +
     +	return LONG_OPT_UNKNOWN;
     +}
    @@ parse-options.c: static enum parse_opt_result parse_long_opt(
     +	enum long_opt_lookup result = lookup_long_opt(p, arg, options,
     +						      &found, &other, &stuck);
     +
    -+	if (result == LONG_OPT_UNKNOWN)
    ++	switch (result) {
    ++	case LONG_OPT_UNKNOWN:
     +		return PARSE_OPT_UNKNOWN;
    -+
    -+	if (disallow_abbreviated_options &&
    -+	    (result == LONG_OPT_ABBREV || result == LONG_OPT_AMBIGUOUS)) {
    -+		const char *arg_end = strchrnul(arg, '=');
    - 		die("disallowed abbreviated or ambiguous option '%.*s'",
    - 		    (int)(arg_end - arg), arg);
    -+	}
    - 
    --	if (ambiguous.option) {
    --		error(_("ambiguous option: %s "
    --			"(could be --%s%s or --%s%s)"),
    --			arg,
    --			(ambiguous.flags & OPT_UNSET) ?  "no-" : "",
    --			ambiguous.option->long_name,
    --			(abbrev.flags & OPT_UNSET) ?  "no-" : "",
    --			abbrev.option->long_name);
    -+	if (result == LONG_OPT_AMBIGUOUS) {
    -+		error(_("ambiguous option: %s (could be --%s%s or --%s%s)"), arg,
    -+		      (other.flags & OPT_UNSET) ? "no-" : "", other.option->long_name,
    -+		      (found.flags & OPT_UNSET) ? "no-" : "", found.option->long_name);
    - 		return PARSE_OPT_HELP_ERROR;
    - 	}
    --	if (abbrev.option) {
    --		if (*arg_end)
    --			p->opt = arg_end + 1;
    --		return get_value(p, abbrev.option, abbrev.flags);
    -+
    -+	if (result == LONG_OPT_ABBREV || result == LONG_OPT_EXACT) {
    ++	case LONG_OPT_ABBREV:
    ++	case LONG_OPT_AMBIGUOUS:
    ++		if (disallow_abbreviated_options) {
    ++			const char *arg_end = strchrnul(arg, '=');
    ++			die("disallowed abbreviated or ambiguous option '%.*s'",
    ++			    (int)(arg_end - arg), arg);
    ++		}
    ++		if (result == LONG_OPT_AMBIGUOUS) {
    ++			error(_("ambiguous option: %s "
    ++				"(could be --%s%s or --%s%s)"),
    ++			      arg,
    ++			      (other.flags & OPT_UNSET) ? "no-" : "",
    ++			      other.option->long_name,
    ++			      (found.flags & OPT_UNSET) ? "no-" : "",
    ++			      found.option->long_name);
    ++			return PARSE_OPT_HELP_ERROR;
    ++		}
    ++		/* fallthru */
    ++	case LONG_OPT_EXACT:
     +		p->opt = stuck;
     +		return get_value(p, found.option, found.flags);
    - 	}
    --	return PARSE_OPT_UNKNOWN;
    ++	}
     +
     +	BUG("unknown enum long_opt_lookup value '%d'", result);
      }
3:  c36cae4e44 ! 3:  9e2d7fc5d6 parse-options: add early_scan_options()
    @@ parse-options.c: int parse_options(int argc, const char **argv,
     +	int i;
     +
     +	s.option = real_option ? real_option : option;
    ++	parse_options_check(s.option);
     +
     +	for (i = 0; i < argc; i++)
     +		if (!early_scan_one_option(&s, argc, argv, &i))
    @@ parse-options.h: static inline void die_for_incompatible_opt2(int opt1, const ch
     + * Called by early_scan_options() for each argument matching a
     + * `struct option` with PARSE_OPT_EARLY set.
     + *
    -+ * - `option` is the matching option,
    ++ * - `option` is the matching option, (may point into a temporary
    ++ *   copy of the options, so must not be used after the scan,)
     + * - `value` its value, or NULL if it doesn't take one or if its value
     + *   is optional and wasn't given, as with PARSE_OPT_OPTARG, or with
     + *   PARSE_OPT_LASTARG_DEFAULT when the option is the last argument,
    @@ parse-options.h: static inline void die_for_incompatible_opt2(int opt1, const ch
     + * parse_options() would not, so that callers err on the safe side.
     + *
     + * This requires the scan to be told about the way the later
    -+ * parse_options() call treats its arguments though: callers should
    -+ * pass EARLY_SCAN_STOP_AT_NON_OPTION when parse_options() is called
    -+ * with PARSE_OPT_STOP_AT_NON_OPTION or with subcommands, and should
    -+ * not use the scan when parse_options() is called with
    -+ * PARSE_OPT_KEEP_UNKNOWN_OPT, which disables abbreviations there but
    -+ * not here.
    ++ * parse_options() call treats its arguments though:
    ++ *
    ++ *  - when parse_options() is called with PARSE_OPT_STOP_AT_NON_OPTION
    ++ *    or with subcommands, callers should pass
    ++ *    EARLY_SCAN_STOP_AT_NON_OPTION,
    ++ *
    ++ *  - when parse_options() is called with PARSE_OPT_KEEP_UNKNOWN_OPT,
    ++ *    which disables abbreviations there but not here, callers should
    ++ *    not use the scan.
    ++ *
    ++ * Passing EARLY_SCAN_STOP_AT_NON_OPTION in other cases is safe, and
    ++ * useful when the caller rejects non-option arguments itself.
     + */
     +int early_scan_options(int argc, const char **argv,
     +		       const struct option *option,
4:  515f13df83 ! 4:  ceeb21e89e fast-import: use early_scan_options() for --allow-unsafe-features
    @@ Commit message
         about it.
     
         Helped-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
    +    Helped-by: Cynthia Uche <herroyalpriestess@gmail.com>
         Signed-off-by: Christian Couder <christian.couder@gmail.com>
     
      ## Documentation/git-fast-import.adoc ##
    @@ builtin/fast-import.c: int cmd_fast_import(int argc,
     +	 * two agree on which options take a value and on abbreviations. In
     +	 * case of doubt, the scan stops early, so unsafe features are then
     +	 * refused rather than wrongly allowed.
    ++	 *
    ++	 * EARLY_SCAN_STOP_AT_NON_OPTION is passed even though parse_options()
    ++	 * is not called with PARSE_OPT_STOP_AT_NON_OPTION, as any non-option
    ++	 * argument makes parse_argv() reject the command line, and the stream
    ++	 * should not have been processed with unsafe features by then.
      	 */
     -	for (int i = 1; i < argc; i++) {
     -		const char *arg = argv[i];
    @@ t/t9300-fast-import.sh: test_expect_success 'R: export-marks options can be over
     +		--allow-unsafe-features <input 2>err &&
     +	test_grep "forbidden in input without --allow-unsafe-features" err
     +'
    ++
    ++test_expect_success 'R: --allow-unsafe-features after a non-option' '
    ++	echo "feature import-marks-if-exists=nonexistent.marks" >input &&
    ++	test_must_fail git fast-import --depth 5 extra \
    ++		--allow-unsafe-features <input 2>err &&
    ++	test_grep "forbidden in input without --allow-unsafe-features" err
    ++'
     +
      test_expect_success 'R: catch typo in marks file name' '
      	test_must_fail git fast-import --import-marks=nonexistent.marks </dev/null &&

base-commit: a018953688f1b10bddf91bff8747068f5f4746a4
-- 
2.56.0.4.gceeb21e89e


  parent reply	other threads:[~2026-11-02 15:15 UTC|newest]

Thread overview: 41+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-02 16:10 [PATCH 0/6] Standardize early option scanning to fix argument parsing bugs Christian Couder
2026-09-02 16:10 ` [PATCH 1/6] parse-options: add early_scan_options() Christian Couder
2026-09-02 22:11   ` Junio C Hamano
2026-09-23  8:10     ` Christian Couder
2026-09-23 17:25       ` Junio C Hamano
2026-09-02 16:10 ` [PATCH 2/6] bisect: fix "--" detection when a term name is "--" Christian Couder
2026-09-02 22:30   ` Junio C Hamano
2026-09-23  8:11     ` Christian Couder
2026-09-23 17:27       ` Junio C Hamano
2026-09-02 16:10 ` [PATCH 3/6] rev-parse: fix "--" detection when it is an option value Christian Couder
2026-09-02 16:10 ` [PATCH 4/6] parse-options: add parse_options_takes_argument() Christian Couder
2026-09-02 16:10 ` [PATCH 5/6] parse-options: build early scan options from a struct option array Christian Couder
2026-09-02 16:10 ` [PATCH 6/6] fast-import: use early_scan_options() for --allow-unsafe-features Christian Couder
2026-09-04  3:38   ` Junio C Hamano
2026-09-02 18:52 ` [PATCH 0/6] Standardize early option scanning to fix argument parsing bugs Junio C Hamano
2026-09-23  8:10   ` Christian Couder
2026-09-23  8:09 ` [PATCH v2 0/3] Standardize early option scanning Christian Couder
2026-09-23  8:09   ` [PATCH v2 1/3] parse-options: add parse_options_takes_argument() Christian Couder
2026-09-23  8:09   ` [PATCH v2 2/3] parse-options: add early_scan_options() Christian Couder
2026-09-30  7:13     ` Kaartic Sivaraam
2026-10-12  8:10       ` Christian Couder
2026-09-23  8:09   ` [PATCH v2 3/3] fast-import: use early_scan_options() for --allow-unsafe-features Christian Couder
2026-10-12  7:44   ` [PATCH v3 0/4] Standardize early option scanning Christian Couder
2026-10-12  7:44     ` [PATCH v3 1/4] parse-options: add parse_options_takes_argument() Christian Couder
2026-10-12  7:44     ` [PATCH v3 2/4] parse-options: extract lookup_long_opt() from parse_long_opt() Christian Couder
2026-10-30 21:08       ` Kaartic Sivaraam
2026-11-02 15:20         ` Christian Couder
2026-10-12  7:44     ` [PATCH v3 3/4] parse-options: add early_scan_options() Christian Couder
2026-10-30 21:27       ` Kaartic Sivaraam
2026-11-02 15:25         ` Christian Couder
2026-10-12  7:44     ` [PATCH v3 4/4] fast-import: use early_scan_options() for --allow-unsafe-features Christian Couder
2026-10-31 14:22       ` Cynthia Uche
2026-11-02 15:36         ` Christian Couder
2026-10-30 21:33     ` [PATCH v3 0/4] Standardize early option scanning Kaartic Sivaraam
2026-11-02 15:14     ` Christian Couder [this message]
2026-11-02 15:14       ` [PATCH v4 1/4] parse-options: add parse_options_takes_argument() Christian Couder
2026-11-02 15:14       ` [PATCH v4 2/4] parse-options: extract lookup_long_opt() from parse_long_opt() Christian Couder
2026-11-02 15:14       ` [PATCH v4 3/4] parse-options: add early_scan_options() Christian Couder
2026-11-02 15:14       ` [PATCH v4 4/4] fast-import: use early_scan_options() for --allow-unsafe-features Christian Couder
2026-11-03  7:54       ` [PATCH v4 0/4] Standardize early option scanning Kaartic Sivaraam
2026-11-03 17:20         ` Junio C Hamano

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261102151447.734680-6-christian.couder@gmail.com \
    --to=christian.couder@gmail.com \
    --cc=Johannes.Schindelin@gmx.de \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=herroyalpriestess@gmail.com \
    --cc=jltobler@gmail.com \
    --cc=kaartic.sivaraam@gmail.com \
    --cc=newren@gmail.com \
    --cc=peff@peff.net \
    --cc=ps@pks.im \
    --cc=sandals@crustytoothpaste.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox