Skip to main content

git_policy_cli/
command_options.rs

1//! What:
2//!  Read one Git command's arguments the way Git 2.56.0 `parse-options.c` reads them.
3//! Why:
4//!  A policy that guesses which token is an option,
5//!  a value or a path can be bypassed by a
6//!      spelling Git accepts.
7//!  Every per-command module declares Git's own option table and
8//!      this module walks the arguments exactly once with Git's rules.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! // parseOptions({ args, table, mode, wrapperFlags }): ParsedOptions  (throws OptionError)
13//! ```
14
15/// What:
16///  `use` brings names from sibling files into this file;
17///  `super::` means "the parent
18///       module",
19///  which is where every sibling file of this crate is declared.
20/// Why:
21///   Long and short spellings follow different Git functions,
22///  so each lives in its own file.
23///
24/// In TS you'd write (pseudocode):
25/// ```ts
26/// import { scanLongOption } from './command_options_long.ts';
27/// import { scanShortCluster } from './command_options_short.ts';
28/// ```
29use super::command_options_long::scan_long_option;
30use super::command_options_short::scan_short_cluster;
31/// What:
32///  `OsString` is owned operating-system text of raw bytes.
33///  Sibling the reader might
34///       expect:
35///  `String`,
36///  which must be valid UTF-8.
37/// Why:
38///   File names and messages may hold bytes that are not UTF-8;
39///  `String` would reject them.
40///
41/// In TS you'd write (pseudocode):
42/// ```ts
43/// // string, but byte-preserving.
44/// ```
45use std::ffi::OsString;
46
47/// What:
48///  `enum Arity` lists how an option relates to a value.
49///  An `enum` is a closed set of
50///       named alternatives.
51///  `#[derive(...)]` asks the compiler to generate copying (`Clone`,
52///       `Copy`),
53///  debug printing (`Debug`) and `==` (`Eq`,
54///  `PartialEq`).
55/// Why:
56///   Value consumption decides whether the next token is a path or an option value.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// type Arity = 'none' | 'required' | 'optional' | 'last-argument-default';
61/// ```
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum Arity {
64    /// `PARSE_OPT_NOARG`:
65    ///  never takes a value;
66    ///  `--name=value` is an error.
67    None,
68    /// Takes the attached value,
69    ///  otherwise the next token (`get_arg`,
70    ///  parse-options.c:47-62).
71    Required,
72    /// `PARSE_OPT_OPTARG`:
73    ///  takes an attached value only,
74    ///  never the next token (203-204).
75    Optional,
76    /// `PARSE_OPT_LASTARG_DEFAULT`:
77    ///  like `Required`,
78    ///  but valid without a value when last (54-55).
79    LastArgDefault,
80}
81
82/// What:
83///  `struct OptionSpec` is one row of a Git option table.
84///       `u16` is an unsigned 16-bit integer (siblings:
85///  `u8`,
86///  `u32`,
87///  `usize`).
88///       `Option<u8>` is "a byte or nothing";
89///  `&'static str` is text baked into the program.
90/// Why:
91///   `id` lets a command module ask about an option by a named constant instead of by
92///       table position.
93///  `u16` (not `usize`) keeps rows small;
94///  no table has 65 thousand rows.
95///
96/// In TS you'd write (pseudocode):
97/// ```ts
98/// type OptionSpec = { id: number; short?: string; long?: string; arity: Arity; negatable: boolean };
99/// ```
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct OptionSpec {
102    /// Identifier chosen by the command module;
103    ///  `UNREAD` when no fact reads the option.
104    pub id: u16,
105    /// Git `short_name`:
106    ///  the single letter after one dash.
107    pub short: Option<u8>,
108    /// Git `long_name`:
109    ///  the spelling after two dashes;
110    ///  it may itself start with `no-`.
111    pub long: Option<&'static str>,
112    /// Value relationship.
113    pub arity: Arity,
114    /// Whether `--no-<long>` is accepted (false for `PARSE_OPT_NONEG`).
115    pub negatable: bool,
116}
117
118/// Identifier of every row no command fact reads;
119///  such rows only supply arity.
120pub const UNREAD: u16 = 0;
121
122/// What:
123///  `const fn` is a function the compiler can run while building a `const` table.
124/// Why:
125///   One short call per row keeps each Git table readable beside the C source.
126///
127/// In TS you'd write (pseudocode):
128/// ```ts
129/// const row = (id, short, long, arity, negatable): OptionSpec => ({ id, short, long, arity, negatable });
130/// ```
131pub const fn row(
132    id: u16,
133    short: Option<u8>,
134    long: Option<&'static str>,
135    arity: Arity,
136    negatable: bool,
137) -> OptionSpec {
138    return OptionSpec {
139        id,
140        short,
141        long,
142        arity,
143        negatable,
144    };
145}
146
147/// What:
148///  Per-call switches matching Git's `enum parse_opt_flags`.
149///  `bool` is true or false.
150/// Why:
151///   `git stash` keeps unknown options and stops at the first non-option;
152///  most commands
153///       do neither.
154///  `PARSE_OPT_KEEP_DASHDASH` needs no switch:
155///  the `--` index is always reported.
156///
157/// In TS you'd write (pseudocode):
158/// ```ts
159/// type ParseMode = { keepUnknown: boolean; stopAtNonOption: boolean };
160/// ```
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub struct ParseMode {
163    /// `PARSE_OPT_KEEP_UNKNOWN_OPT`:
164    ///  unknown options are kept,
165    ///  abbreviations are disabled.
166    pub keep_unknown: bool,
167    /// `PARSE_OPT_STOP_AT_NON_OPTION`:
168    ///  the first non-option ends option parsing.
169    pub stop_at_non_option: bool,
170}
171
172/// Flags `0`,
173///  the mode of `commit`,
174///  `add`,
175///  `push`,
176///  `clean`,
177///  `branch` and `status`.
178pub const DEFAULT_MODE: ParseMode = ParseMode {
179    keep_unknown: false,
180    stop_at_non_option: false,
181};
182
183/// What:
184///  Where an option's value sits.
185///  `usize` is the index type of lists (siblings:
186///  `u32`,
187///       `u64`);
188///  every standard list API indexes with `usize`.
189/// Why:
190///   A value is reported by position,
191///  never copied,
192///  so its bytes stay exactly as given.
193///
194/// In TS you'd write (pseudocode):
195/// ```ts
196/// type OptionValue = { token: number; offset: number } | { token: number };
197/// ```
198#[derive(Clone, Copy, Debug, Eq, PartialEq)]
199pub enum OptionValue {
200    /// `-mvalue` or `--message=value`:
201    ///  the value starts at byte `offset` of `token`.
202    Attached { token: usize, offset: usize },
203    /// `-m value`:
204    ///  the whole following token is the value.
205    Detached { token: usize },
206}
207
208/// What:
209///  One accepted use of a declared option.
210/// Why:
211///   Git lets the last use win,
212///  so order and negation are kept for the command module.
213///
214/// In TS you'd write (pseudocode):
215/// ```ts
216/// type Occurrence = { id: number; negated: boolean; value?: OptionValue; token: number };
217/// ```
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
219pub struct Occurrence {
220    /// The matched row's identifier.
221    pub id: u16,
222    /// True for a `--no-` form.
223    pub negated: bool,
224    /// Position of the value,
225    ///  when one was taken.
226    pub value: Option<OptionValue>,
227    /// Index of the token that spelled the option.
228    pub token: usize,
229}
230
231/// What:
232///  One wrapper-only flag found where Git would have looked for an option.
233/// Why:
234///   Only such positions may be removed before forwarding;
235///  values and paths stay.
236///
237/// In TS you'd write (pseudocode):
238/// ```ts
239/// type WrapperOccurrence = { flag: number; token: number };
240/// ```
241#[derive(Clone, Copy, Debug, Eq, PartialEq)]
242pub struct WrapperOccurrence {
243    /// Index into the caller's wrapper flag list.
244    pub flag: usize,
245    /// Index of the token that spelled the flag.
246    pub token: usize,
247}
248
249/// What:
250///  How option parsing ended;
251///  each variant holds the index of the ending token.
252/// Why:
253///   Tokens after the boundary are never options,
254///  and commands treat `--` specially.
255///
256/// In TS you'd write (pseudocode):
257/// ```ts
258/// type Boundary = { kind: 'end' } | { kind: 'dash-dash' | 'end-of-options' | 'non-option'; token: number };
259/// ```
260#[derive(Clone, Copy, Debug, Eq, PartialEq)]
261pub enum Boundary {
262    /// Every token was read.
263    End,
264    /// `--` at this index (parse-options.c:1113-1118).
265    DashDash(usize),
266    /// `--end-of-options` at this index (1119-1125).
267    EndOfOptions(usize),
268    /// First non-option at this index,
269    ///  under `stop_at_non_option` (1015-1016).
270    NonOption(usize),
271}
272
273/// What:
274///  Everything learned from one argument region.
275///  `Vec<T>` is an owned,
276///  growable list
277///       (siblings:
278///  `&[T]`,
279///  a borrowed view,
280///  and `[T; N]`,
281///  a fixed-size array).
282/// Why:
283///   The result outlives the scan and its lengths are unknown beforehand,
284///  so it owns lists.
285///
286/// In TS you'd write (pseudocode):
287/// ```ts
288/// type ParsedOptions = { occurrences: Occurrence[]; wrapper: WrapperOccurrence[];
289///   unknown: number[]; leading: number[]; boundary: Boundary };
290/// ```
291#[derive(Clone, Debug, Eq, PartialEq)]
292pub struct ParsedOptions {
293    /// Declared options in the order Git applies them.
294    pub occurrences: Vec<Occurrence>,
295    /// Wrapper-only flags found in option position.
296    pub wrapper: Vec<WrapperOccurrence>,
297    /// Tokens holding an undeclared option,
298    ///  only under `keep_unknown`.
299    pub unknown: Vec<usize>,
300    /// Non-option tokens before the boundary.
301    pub leading: Vec<usize>,
302    /// How option parsing ended.
303    pub boundary: Boundary,
304}
305
306/// What:
307///  Why Git would refuse the region before running the command.
308/// Why:
309///   The caller reports or forwards such a command line;
310///  it must never guess its facts.
311///
312/// In TS you'd write (pseudocode):
313/// ```ts
314/// type OptionErrorKind = 'unknown-option' | 'ambiguous-option' | 'missing-value'
315///   | 'unexpected-value' | 'single-dash-long-option' | 'help-requested';
316/// ```
317#[derive(Clone, Copy, Debug, Eq, PartialEq)]
318pub enum OptionErrorKind {
319    /// Git prints `unknown option` or `unknown switch` and exits 129 (parse-options.c:1224-1233).
320    UnknownOption,
321    /// An abbreviation matches several long options (578-587).
322    AmbiguousOption,
323    /// A value-taking option ends the region (60).
324    MissingValue,
325    /// `--flag=value` or `--no-option=value` (138-143).
326    UnexpectedValue,
327    /// `-amend`:
328    ///  letters that spell a long option;
329    ///  Git asks about two dashes (622-640).
330    SingleDashLongOption,
331    /// `-h`,
332    ///  `--help`,
333    ///  `--help-all` or a lone completion helper:
334    ///  Git prints and exits 0.
335    HelpRequested,
336}
337
338/// What:
339///  One refusal,
340///  with the index of the token that caused it.
341/// Why:
342///   A diagnostic can name the exact argument without this module rendering text for it.
343///
344/// In TS you'd write (pseudocode):
345/// ```ts
346/// class OptionError extends Error { kind: OptionErrorKind; token: number }
347/// ```
348#[derive(Clone, Copy, Debug, Eq, PartialEq)]
349pub struct OptionError {
350    /// The refusal category.
351    pub kind: OptionErrorKind,
352    /// Index of the offending token within the region.
353    pub token: usize,
354}
355
356/// What:
357///  `impl std::fmt::Display for OptionError` supplies Rust's "print me" interface.
358/// Why:
359///   Callers can show the refusal with ordinary formatting.
360///
361/// In TS you'd write (pseudocode):
362/// ```ts
363/// toString(): string { return `${kind} at argument ${token}`; }
364/// ```
365impl std::fmt::Display for OptionError {
366    /// `&self` borrows the error read-only;
367    ///  `&mut` lends the formatter for writing.
368    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        // `write!` is the standard formatting macro; `{:?}` prints the variant name.
370        return write!(
371            formatter,
372            "Git 2.56.0 refuses argument {} of this command region: {:?}",
373            self.token, self.kind
374        );
375    }
376}
377
378/// An empty `impl` marks the type as a standard error value for generic handling.
379impl std::error::Error for OptionError {}
380
381/// What:
382///  Find a wrapper-only flag by exact byte spelling.
383///  `&[u8]` borrows bytes;
384///  `&[&[u8]]`
385///       borrows a list of byte spellings.
386///  The result is its list index or nothing.
387/// Why:
388///   Wrapper flags are never abbreviated and never carry `=value`.
389///
390/// In TS you'd write (pseudocode):
391/// ```ts
392/// const flag = wrapperFlags.indexOf(token); // -1 becomes "nothing"
393/// ```
394fn wrapper_flag_index(token: &[u8], wrapper_flags: &[&[u8]]) -> Option<usize> {
395    let mut index: usize = 0;
396    while index < wrapper_flags.len() {
397        if wrapper_flags[index] == token {
398            // `Some(x)` is the "present" case of `Option`.
399            return Some(index);
400        }
401        index += 1;
402    }
403    // `None` is the "absent" case of `Option`.
404    return None;
405}
406
407/// What:
408///  Tokenize one region in a single forward pass (`parse_options_step`,
409///       parse-options.c:995-1169).
410///  `Result<A, B>` is "either success `A` or failure `B`".
411/// Why:
412///   One faithful scan replaces per-command guesses about option arity.
413/// Gotcha:
414///  Indexes in the result are relative to `arguments`,
415///  the region after the subcommand.
416///
417/// In TS you'd write (pseudocode):
418/// ```ts
419/// function parseOptions({ args, table, mode, wrapperFlags }): ParsedOptions; // throws OptionError
420/// ```
421pub fn parse_options(
422    arguments: &[OsString],
423    table: &[OptionSpec],
424    mode: ParseMode,
425    wrapper_flags: &[&[u8]],
426) -> Result<ParsedOptions, OptionError> {
427    // `mut` allows the lists to grow; `Vec::new()` is an empty list.
428    let mut parsed: ParsedOptions = ParsedOptions {
429        occurrences: Vec::<Occurrence>::new(),
430        wrapper: Vec::<WrapperOccurrence>::new(),
431        unknown: Vec::<usize>::new(),
432        leading: Vec::<usize>::new(),
433        boundary: Boundary::End,
434    };
435    // True while the token being visited is the value of the option before it.
436    let mut is_value: bool = false;
437    // `.iter().enumerate()` visits every token once with its position, so the scan always
438    // ends: an option only says whether the next token is its value.
439    for (index, argument) in arguments.iter().enumerate() {
440        if is_value {
441            is_value = false;
442            continue;
443        }
444        // `.as_encoded_bytes()` lends the raw bytes; nothing is decoded or copied.
445        let token: &[u8] = argument.as_encoded_bytes();
446        // `if let Some(flag) = ...` runs the block only when a value is present.
447        if let Some(flag) = wrapper_flag_index(token, wrapper_flags) {
448            parsed
449                .wrapper
450                .push(WrapperOccurrence { flag, token: index });
451            continue;
452        }
453        // A token not starting with `-`, the empty token and a lone `-` are not options (1011).
454        if token.first() != Some(&b'-') || token.len() == 1 {
455            if mode.stop_at_non_option {
456                parsed.boundary = Boundary::NonOption(index);
457                break;
458            }
459            parsed.leading.push(index);
460            continue;
461        }
462        // A lone `-h` asks for usage (1050); a lone completion request prints the table
463        // (1057-1060). Git exits 0 for both without running the command.
464        if arguments.len() == 1
465            && (token == b"-h"
466                || token == b"--git-completion-helper"
467                || token == b"--git-completion-helper-all")
468        {
469            // `Err(x)` is the failure case of `Result`.
470            return Err(OptionError {
471                kind: OptionErrorKind::HelpRequested,
472                token: index,
473            });
474        }
475        if token[1] != b'-' {
476            // `?` returns the failure to our caller, or unwraps the success value.
477            is_value = scan_short_cluster(arguments, index, table, mode, &mut parsed)?;
478            continue;
479        }
480        if token.len() == 2 {
481            parsed.boundary = Boundary::DashDash(index);
482            break;
483        }
484        // `&token[2..]` borrows the bytes after the two dashes.
485        let name: &[u8] = &token[2..];
486        if name == b"end-of-options" {
487            parsed.boundary = Boundary::EndOfOptions(index);
488            break;
489        }
490        if name == b"help" || name == b"help-all" {
491            return Err(OptionError {
492                kind: OptionErrorKind::HelpRequested,
493                token: index,
494            });
495        }
496        is_value = scan_long_option(arguments, index, table, mode, &mut parsed)?;
497    }
498    // `Ok(x)` is the success case of `Result`.
499    return Ok(parsed);
500}
501
502/// Synthetic-table cases,
503///  ported incumbent cases and typed refusals.
504#[cfg(test)]
505#[path = "command_options_tests.rs"]
506mod tests;
507
508/// Differential controls against `git rev-parse --parseopt` of the real Git 2.56.0.
509#[cfg(test)]
510#[path = "command_options_git_tests.rs"]
511mod git_tests;