Skip to main content

git_policy_cli/
command_config.rs

1//! What:
2//!  The form and file scope of a `git config` region,
3//!  read as Git 2.56.0 reads it.
4//! Why:
5//!  The require-root policy exempts `git config` when it works on the per-user or
6//!      system file,
7//!  or lists.
8//!  Whether `--global` is that option or a value being stored
9//!      depends on the form:
10//!  `git config user.name --global` writes the text `--global`
11//!      into the repository's own configuration.
12//!
13//! Gotcha:
14//!  Git also refuses two different actions of the form without a subcommand word
15//!         (`--list --get`);
16//!  that conflict is not modeled,
17//!  so such a region still reports
18//!         its listing.
19//!  Git then runs nothing.
20//!
21//! In TS you'd write (pseudocode):
22//! ```ts
23//! // parseConfigRegion(['--global', 'user.name']).global === true
24//! // parseConfigRegion(['user.name', '--global']).global === false
25//! ```
26
27/// What:
28///  Bring the option groups,
29///  the tokenizer and its questions into this file.
30/// Why:
31///   This module only picks the form's groups and interprets what the tokenizer found.
32///
33/// In TS you'd write (pseudocode):
34/// ```ts
35/// import { parseOptions } from './command_options.ts';
36/// ```
37use super::command_config_table::{
38    CONFIG_STOP_MODE, DISPLAY_ROWS, GET_FILTER_ROWS, GET_OTHER_ROWS, GLOBAL, LEGACY_ACTION_ROWS,
39    LEGACY_OTHER_ROWS, LIST, LIST_OTHER_ROWS, LOCATION_ROWS, SET_ROWS, SYSTEM, TYPE_ROWS,
40    UNSET_ROWS,
41};
42use super::command_options::{
43    DEFAULT_MODE, OptionError, OptionSpec, ParseMode, ParsedOptions, WrapperOccurrence,
44    parse_options,
45};
46use super::command_options_query::{is_enabled, is_stated};
47/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
48use std::ffi::OsString;
49
50/// What:
51///  The form `cmd_config` dispatches to (builtin/config.c:1633-1660).
52///  An `enum` is a
53///       closed set of named alternatives.
54/// Why:
55///   `Legacy` is the form without a subcommand word,
56///  where actions are options such as
57///       `--list` and `--get`.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// type ConfigForm = 'list' | 'get' | 'set' | 'unset' | 'rename-section' | 'remove-section' | 'edit' | 'legacy';
62/// ```
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum ConfigForm {
65    /// `git config list`.
66    List,
67    /// `git config get`.
68    Get,
69    /// `git config set`.
70    Set,
71    /// `git config unset`.
72    Unset,
73    /// `git config rename-section`.
74    RenameSection,
75    /// `git config remove-section`.
76    RemoveSection,
77    /// `git config edit`.
78    Edit,
79    /// No subcommand word:
80    ///  the action is an option or implied by the argument count.
81    Legacy,
82}
83
84/// What:
85///  Facts of one `git config` region.
86///  `Vec<WrapperOccurrence>` is an owned growable
87///       list of wrapper-only flags.
88/// Why:
89///   The require-root decision reads the scope and the listing action;
90///  the caller
91///       removes its wrapper-only flags by the positions reported here.
92///
93/// In TS you'd write (pseudocode):
94/// ```ts
95/// type ConfigRegion = { form: ConfigForm; global: boolean; system: boolean; lists: boolean; wrapper: WrapperOccurrence[] };
96/// ```
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct ConfigRegion {
99    /// The form Git dispatches to.
100    pub form: ConfigForm,
101    /// `--global` is on after every use was applied.
102    pub global: bool,
103    /// `--system` is on after every use was applied.
104    pub system: bool,
105    /// The command lists variables:
106    ///  `git config list`,
107    ///  or `-l`/`--list` without a
108    /// subcommand word.
109    pub lists: bool,
110    /// Wrapper-only flags in option position;
111    ///  `token` indexes the region.
112    pub wrapper: Vec<WrapperOccurrence>,
113}
114
115/// What:
116///  The form a first region token selects,
117///  or nothing.
118///  `&[u8]` borrows the token.
119/// Why:
120///   Git matches the seven words exactly,
121///  without abbreviation (parse-options.c
122///       `parse_subcommand`).
123///
124/// In TS you'd write (pseudocode):
125/// ```ts
126/// const form = FORMS.get(word);
127/// ```
128fn form_of_word(word: &[u8]) -> Option<ConfigForm> {
129    // `(&[u8], ConfigForm)` is a pair of a spelling and the form it selects.
130    let words: [(&[u8], ConfigForm); 7] = [
131        (b"list", ConfigForm::List),
132        (b"get", ConfigForm::Get),
133        (b"set", ConfigForm::Set),
134        (b"unset", ConfigForm::Unset),
135        (b"rename-section", ConfigForm::RenameSection),
136        (b"remove-section", ConfigForm::RemoveSection),
137        (b"edit", ConfigForm::Edit),
138    ];
139    for (spelling, form) in words {
140        if spelling == word {
141            // `Some(x)` is the "present" case of `Option`.
142            return Some(form);
143        }
144    }
145    // `None` is the "absent" case of `Option`.
146    return None;
147}
148
149/// What:
150///  The complete option table of one form,
151///  assembled from Git's groups in Git's
152///       order.
153///  `Vec<OptionSpec>` is an owned list,
154///  built once per parse.
155/// Why:
156///   The completion control compares each assembled table with the binary's.
157///
158/// In TS you'd write (pseudocode):
159/// ```ts
160/// function configTable(form: ConfigForm): OptionSpec[] { return groups[form].flat(); }
161/// ```
162pub fn config_table(form: ConfigForm) -> Vec<OptionSpec> {
163    // `&[&[OptionSpec]]` borrows a list of borrowed groups; `match` picks one per form.
164    let groups: &[&[OptionSpec]] = match form {
165        ConfigForm::List => &[LOCATION_ROWS, DISPLAY_ROWS, TYPE_ROWS, LIST_OTHER_ROWS],
166        ConfigForm::Get => &[
167            LOCATION_ROWS,
168            GET_FILTER_ROWS,
169            DISPLAY_ROWS,
170            TYPE_ROWS,
171            GET_OTHER_ROWS,
172        ],
173        ConfigForm::Set => &[LOCATION_ROWS, TYPE_ROWS, SET_ROWS],
174        ConfigForm::Unset => &[LOCATION_ROWS, UNSET_ROWS],
175        ConfigForm::RenameSection | ConfigForm::RemoveSection | ConfigForm::Edit => {
176            &[LOCATION_ROWS]
177        }
178        ConfigForm::Legacy => &[
179            LOCATION_ROWS,
180            LEGACY_ACTION_ROWS,
181            DISPLAY_ROWS,
182            TYPE_ROWS,
183            LEGACY_OTHER_ROWS,
184        ],
185    };
186    let mut table: Vec<OptionSpec> = Vec::<OptionSpec>::new();
187    for group in groups {
188        // `extend_from_slice` copies the group's rows onto the end of the table.
189        table.extend_from_slice(group);
190    }
191    return table;
192}
193
194/// What:
195///  Parse the region after `git config`.
196///  `Result<A, B>` is "either success `A` or
197///       failure `B`";
198///  `wrapper_flags` are the caller's wrapper-only spellings.
199/// Why:
200///   Git looks at the first token only to pick the form.
201///  A subcommand word selects its
202///       table;
203///  a leading `--` is dropped by that first look,
204///  so the form without a
205///       subcommand then reads the rest as options again (config.c:1643-1660).
206///  Leading
207///       wrapper flags are skipped first,
208///  because the wrapper removes them before Git
209///       runs:
210///  `git config --cli-git-keep-going list` reaches Git as `git config list`.
211///
212/// In TS you'd write (pseudocode):
213/// ```ts
214/// function parseConfigRegion(region: string[], wrapperFlags: string[]): ConfigRegion; // throws OptionError
215/// ```
216pub fn parse_config_region(
217    region: &[OsString],
218    wrapper_flags: &[&[u8]],
219) -> Result<ConfigRegion, OptionError> {
220    let mut wrapper: Vec<WrapperOccurrence> = Vec::<WrapperOccurrence>::new();
221    // Region index of the first token that is not a wrapper flag.
222    let mut first: usize = 0;
223    while first < region.len() {
224        let token: &[u8] = region[first].as_encoded_bytes();
225        // Index of the matching wrapper flag; the list length means "none".
226        let mut flag: usize = 0;
227        while flag < wrapper_flags.len() && wrapper_flags[flag] != token {
228            flag += 1;
229        }
230        if flag == wrapper_flags.len() {
231            break;
232        }
233        wrapper.push(WrapperOccurrence { flag, token: first });
234        first += 1;
235    }
236    let mut form: ConfigForm = ConfigForm::Legacy;
237    // Region index of the first token the form's own parser reads.
238    let mut offset: usize = first;
239    // `if let Some(found) = ...` runs only when a token follows the wrapper flags.
240    if let Some(found) = region.get(first) {
241        let word: &[u8] = found.as_encoded_bytes();
242        if let Some(named) = form_of_word(word) {
243            form = named;
244            offset = first + 1;
245        } else if word == b"--" {
246            offset = first + 1;
247        }
248    }
249    let mode: ParseMode = if form == ConfigForm::List || form == ConfigForm::Edit {
250        DEFAULT_MODE
251    } else {
252        CONFIG_STOP_MODE
253    };
254    let table: Vec<OptionSpec> = config_table(form);
255    // `&region[offset..]` borrows the tokens from `offset` to the end.
256    let outcome: Result<ParsedOptions, OptionError> =
257        parse_options(&region[offset..], table.as_slice(), mode, wrapper_flags);
258    // `match` on a `Result` handles both cases; token indexes are moved back to the region.
259    let parsed: ParsedOptions = match outcome {
260        Ok(found) => found,
261        Err(refusal) => {
262            // `Err(x)` is the failure case of `Result`.
263            return Err(OptionError {
264                kind: refusal.kind,
265                token: refusal.token + offset,
266            });
267        }
268    };
269    for occurrence in &parsed.wrapper {
270        wrapper.push(WrapperOccurrence {
271            flag: occurrence.flag,
272            token: occurrence.token + offset,
273        });
274    }
275    // `Ok(x)` is the success case of `Result`.
276    return Ok(ConfigRegion {
277        form,
278        global: is_enabled(&parsed, GLOBAL),
279        system: is_enabled(&parsed, SYSTEM),
280        lists: form == ConfigForm::List || (form == ConfigForm::Legacy && is_stated(&parsed, LIST)),
281        wrapper,
282    });
283}
284
285/// Forms,
286///  scopes and option positions,
287///  with real Git 2.56.0 controls.
288#[cfg(test)]
289#[path = "command_config_tests.rs"]
290mod tests;