Skip to main content

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;