git_policy_cli/global_arguments.rs
1//! What:
2//! Locate Git 2.56.0 command boundaries without reconstructing native arguments.
3//! Why:
4//! Policy stages need the command index,
5//! but Git remains authoritative for option validation and -C semantics.
6//!
7//! In TS you'd write (pseudocode):
8//! ```ts
9//! // Scan argument boundaries; forward the original global prefix to every repository query.
10//! ```
11
12/// Import native argument storage;
13/// no lossy UTF-8 conversion occurs.
14use std::ffi::OsString;
15
16/// Which native Git path follows the global prefix.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum GlobalOutcome {
19 /// A subcommand starts at prefix_len.
20 Command,
21 /// Git prints help/version/path/command inventory without executing a repository command.
22 /// `--version`, `-v`, `--help` and `-h` run Git as a child and add the wrapper's text after Git's answer
23 /// (see `announced_query`); every other query is forwarded by replacing the process.
24 Query,
25 /// A value-taking global option has no following token;
26 /// Git must render its own error.
27 MissingValue,
28 /// Git 2.56.0 does not recognize this global option;
29 /// do not reinterpret it as a subcommand.
30 InvalidOption,
31 /// The argument list ended without a subcommand.
32 NoCommand,
33}
34
35/// Immutable positions into the caller's untouched argument vector.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct GlobalLayout {
38 /// Count of complete native globals before the command/query/error token.
39 pub prefix_len: usize,
40 /// Native interpretation after the completed global prefix.
41 pub outcome: GlobalOutcome,
42}
43
44/// Complete separated-value globals accepted by Git 2.56.0 git.c::handle_options.
45pub(crate) const VALUE_OPTIONS: &[&[u8]] = &[
46 b"-C",
47 b"-c",
48 b"--git-dir",
49 b"--work-tree",
50 b"--namespace",
51 b"--config-env",
52 b"--shallow-file",
53 b"--attr-source",
54];
55
56/// Complete no-value globals accepted by that same release,
57/// not a cross-version fallback list.
58const FLAG_OPTIONS: &[&[u8]] = &[
59 b"-p",
60 b"--paginate",
61 b"-P",
62 b"--no-pager",
63 b"--no-lazy-fetch",
64 b"--no-replace-objects",
65 b"--bare",
66 b"--literal-pathspecs",
67 b"--no-literal-pathspecs",
68 b"--glob-pathspecs",
69 b"--noglob-pathspecs",
70 b"--icase-pathspecs",
71 b"--no-optional-locks",
72 b"--no-advice",
73];
74
75/// Inline-value forms explicitly recognized by the native release.
76const INLINE_OPTIONS: &[&[u8]] = &[
77 b"--git-dir=",
78 b"--work-tree=",
79 b"--namespace=",
80 b"--config-env=",
81 b"--attr-source=",
82];
83
84/// What:
85/// Find a command/query/error boundary with one forward scan.
86/// Why:
87/// Values such as '-h',
88/// empty -C paths and non-UTF-8 filenames remain original opaque argument values.
89/// Symlink-sensitive -C chaining is not approximated lexically;
90/// repository queries forward the complete prefix.
91///
92/// In TS you'd write (pseudocode):
93/// ```ts
94/// function globalLayout(nativeArgs): { prefixLen: number; outcome: GlobalOutcome };
95/// ```
96pub fn global_layout(arguments: &[OsString]) -> GlobalLayout {
97 // True while the token being visited is the value of the separated-value option before it.
98 let mut is_value: bool = false;
99 // `.iter().enumerate()` visits every argument once with its position, so the scan
100 // always ends: there is no hand-stepped index that a mistake could leave standing still.
101 for (index, token) in arguments.iter().enumerate() {
102 if is_value {
103 is_value = false;
104 continue;
105 }
106 // Borrow native encoded bytes solely to recognize Git's ASCII option syntax.
107 let argument: &[u8] = token.as_encoded_bytes();
108 if !argument.starts_with(b"-") {
109 return GlobalLayout {
110 prefix_len: index,
111 outcome: GlobalOutcome::Command,
112 };
113 }
114 if [
115 b"--help".as_slice(),
116 b"-h",
117 b"--version",
118 b"-v",
119 b"--html-path",
120 b"--man-path",
121 b"--info-path",
122 ]
123 .contains(&argument)
124 || argument.starts_with(b"--list-cmds=")
125 {
126 return GlobalLayout {
127 prefix_len: index,
128 outcome: GlobalOutcome::Query,
129 };
130 }
131 // Git's own prefix branch treats every --exec-path suffix except '=' as a query.
132 if let Some(remainder) = argument.strip_prefix(b"--exec-path") {
133 if remainder.first() != Some(&b'=') {
134 return GlobalLayout {
135 prefix_len: index,
136 outcome: GlobalOutcome::Query,
137 };
138 }
139 continue;
140 }
141 if VALUE_OPTIONS.contains(&argument) {
142 if index + 1 == arguments.len() {
143 return GlobalLayout {
144 prefix_len: index,
145 outcome: GlobalOutcome::MissingValue,
146 };
147 }
148 is_value = true;
149 continue;
150 }
151 if FLAG_OPTIONS.contains(&argument) {
152 continue;
153 }
154 let mut inline: bool = false;
155 for prefix in INLINE_OPTIONS {
156 if argument.starts_with(prefix) {
157 inline = true;
158 break;
159 }
160 }
161 if inline {
162 continue;
163 }
164 return GlobalLayout {
165 prefix_len: index,
166 outcome: GlobalOutcome::InvalidOption,
167 };
168 }
169 return GlobalLayout {
170 prefix_len: arguments.len(),
171 outcome: GlobalOutcome::NoCommand,
172 };
173}
174
175/// What:
176/// The command word and the tokens after it,
177/// when the arguments name a command.
178/// `Option<(&OsString, &[OsString])>` is "a pair of borrowed views,
179/// or nothing":
180/// the word,
181/// and the list of everything after it.
182/// Why:
183/// Every rule that reads a command's own options needs exactly this split.
184/// Taking it
185/// from one function means no rule computes an offset of its own,
186/// so no rule can
187/// read the command word as one of its options by an arithmetic slip.
188///
189/// In TS you'd write (pseudocode):
190/// ```ts
191/// function commandTokens(args: string[]): [word: string, region: string[]] | undefined;
192/// ```
193pub fn command_tokens(arguments: &[OsString]) -> Option<(&OsString, &[OsString])> {
194 let layout: GlobalLayout = global_layout(arguments);
195 if layout.outcome != GlobalOutcome::Command {
196 // `None` is the "absent" case of `Option`.
197 return None;
198 }
199 // `&arguments[n..]` borrows from the command word on; `.split_first()` separates the
200 // first item from the rest, or gives `None` for an empty list.
201 return arguments[layout.prefix_len..].split_first();
202}
203
204/// Boundary controls include real Git probes and native non-UTF-8 arguments.
205#[cfg(test)]
206#[path = "global_arguments_tests.rs"]
207mod tests;