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;