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