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    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;