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;