Skip to main content

git_policy_cli/
git_system_config.rs

1//! What:
2//!  The system configuration file that one real Git executable reads when `GIT_CONFIG_SYSTEM` is not set,
3//!  learned once per executable identity and cached beside the `git version` cache.
4//! Why:
5//!  Git compiles the path in (`ETC_GITCONFIG`, `/etc/gitconfig` for a build with prefix `/usr`, and under the prefix
6//!  for any other build, resolved against the executable's own location in a runtime-prefix build), so no reader may
7//!  assume `/etc/gitconfig`.
8//!  `git var GIT_CONFIG_SYSTEM` prints the path the executable would read, with the environment's own overrides
9//!  removed from its start, so the path is a property of the executable's bytes and location, which the identity of
10//!  `git_version_cache` captures.
11//!  An executable without an identity (a script, a shim) or an answer that is not an absolute path is unknown, and
12//!  the readers then leave the question to Git.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const path = cached(identity, 'git-system-config') ?? (await run(realGit, ['var', 'GIT_CONFIG_SYSTEM'])); // undefined: unknown
17//! ```
18
19use super::child_environment::{FORWARD_TARGET_VARIABLE, environment_value};
20use super::git_version_cache::{BinaryIdentity, binary_identity, cache_directory, read_cached_text, store_cached_text};
21use std::ffi::OsString;
22use std::path::{Path, PathBuf};
23
24/// The kind of the cache file that holds the compiled-in system configuration path.
25pub const SYSTEM_CONFIG_KIND: &str = "git-system-config";
26
27/// The longest path the cache keeps and the question accepts.
28const PATH_LIMIT: usize = 4096;
29
30/// What:
31///  The path text `git var GIT_CONFIG_SYSTEM` printed, when it is one absolute path on one line.
32/// Why:
33///   Any other output (nothing, a relative path, several lines) is not a fact the readers can use.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// const path = stdout.endsWith('\n') && !stdout.slice(0, -1).includes('\n') && isAbsolute(stdout.slice(0, -1)) ? stdout.slice(0, -1) : undefined;
38/// ```
39pub fn parse_path_line(stdout: &[u8]) -> Option<PathBuf> {
40    let line: &[u8] = stdout.strip_suffix(b"\n")?;
41    if line.is_empty() || line.len() > PATH_LIMIT || line.contains(&b'\n') || line.contains(&0) {
42        return None;
43    }
44    let path: PathBuf = PathBuf::from(std::str::from_utf8(line).ok()?);
45    return if path.is_absolute() { Some(path) } else { None };
46}
47
48/// Start the executable at `real_git` with `var GIT_CONFIG_SYSTEM` and the environment's overrides removed.
49fn ask_git(real_git: &Path) -> Option<Vec<u8>> {
50    let output = std::process::Command::new(real_git)
51        .args(["var", "GIT_CONFIG_SYSTEM"])
52        .env_clear()
53        .env("LC_ALL", "C")
54        .current_dir("/")
55        .stdin(std::process::Stdio::null())
56        .stderr(std::process::Stdio::null())
57        .output()
58        .ok()?;
59    return if output.status.success() { Some(output.stdout) } else { None };
60}
61
62/// What:
63///  The system configuration path of the executable at `real_git`: from the cache when the identity matches,
64///  otherwise from `ask`, recorded for the next start.
65/// Why:
66///   The question runs once per executable; `ask` is a parameter so a control can count the starts and give a Git
67///  whose prefix is not `/usr`.
68///
69/// In TS you'd write (pseudocode):
70/// ```ts
71/// const path = (await readCache(dir, identity, kind)) ?? (await ask(realGit)); // undefined: unknown
72/// ```
73pub fn system_config_of(real_git: &Path, cache: Option<&Path>, ask: &dyn Fn(&Path) -> Option<Vec<u8>>) -> Option<PathBuf> {
74    let identity: BinaryIdentity = binary_identity(real_git)?;
75    if let Some(directory) = cache
76        && let Some(path) = read_cached_text(directory, &identity, SYSTEM_CONFIG_KIND).and_then(|text| { return parse_path_line(&[text.as_slice(), b"\n"].concat()); })
77    {
78        return Some(path);
79    }
80    let answered: Vec<u8> = ask(real_git)?;
81    let path: PathBuf = parse_path_line(answered.as_slice())?;
82    // The identity is taken again after the start, so an executable replaced during it is never recorded under the old one.
83    if let Some(directory) = cache
84        && binary_identity(real_git).as_ref() == Some(&identity)
85    {
86        store_cached_text(directory, &identity, SYSTEM_CONFIG_KIND, path.as_os_str().as_encoded_bytes());
87    }
88    return Some(path);
89}
90
91/// What:
92///  `variables` with the forward-target variable added for `real_git` when the variables do not name one.
93/// Why:
94///   The wrapper process learns the system configuration path from the real Git named by that variable.
95///  The environment `entry.rs` builds always carries it, but a resident process (the service) and callers that hold only a
96///  Git path and a bare overlay do not, and without it every read there would decline; the caller knows the Git it runs.
97///
98/// In TS you'd write (pseudocode):
99/// ```ts
100/// const named = { [FORWARD_TARGET]: realGit, ...variables }; // the variables win when they name one
101/// ```
102pub fn with_real_git(variables: &[(OsString, OsString)], real_git: &Path) -> Vec<(OsString, OsString)> {
103    let mut named: Vec<(OsString, OsString)> = variables.to_vec();
104    if environment_value(variables, FORWARD_TARGET_VARIABLE).is_none() {
105        named.push((OsString::from(FORWARD_TARGET_VARIABLE), real_git.as_os_str().to_os_string()));
106    }
107    return named;
108}
109
110/// What:
111///  The system configuration path the Git a child would run reads, learned from the forward-target variable the wrapper
112///  puts in every child's environment.
113/// Why:
114///   `None` (no variable, no identity, no answer) makes the readers decline whenever the system file would be read.
115///
116/// In TS you'd write (pseudocode):
117/// ```ts
118/// const path = systemConfigFor(env); // undefined: unknown
119/// ```
120pub fn system_config_for(variables: &[(OsString, OsString)]) -> Option<PathBuf> {
121    let target: OsString = environment_value(variables, FORWARD_TARGET_VARIABLE)?;
122    return system_config_of(Path::new(&target), cache_directory(variables).as_deref(), &ask_git);
123}
124
125#[cfg(test)]
126#[path = "git_system_config_tests.rs"]
127mod tests;