mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
git-utils: block selected patch filters
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::io;
|
||||
use std::io::Seek;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
use crate::git_command::GitRunner;
|
||||
use crate::git_config::GitConfigEntry;
|
||||
use crate::git_config::parse_effective_config;
|
||||
use crate::git_config::parse_effective_config_with_origins;
|
||||
|
||||
pub(crate) const DISABLED_HOOKS_PATH: &str = if cfg!(windows) { "NUL" } else { "/dev/null" };
|
||||
pub(crate) const EXECUTABLE_FILTER_CONFIG_PATTERN: &str = r"^filter\..*\.(clean|smudge|process)$";
|
||||
|
||||
const ISOLATED_GIT_ENVIRONMENT: [&str; 11] = [
|
||||
"GIT_DIR",
|
||||
@@ -28,3 +41,242 @@ pub(crate) fn isolate_git_command_environment(command: &mut Command) {
|
||||
command.env_remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_no_selected_executable_git_filters(
|
||||
git: &GitRunner,
|
||||
cwd: &Path,
|
||||
paths: &[String],
|
||||
git_config_args: &[String],
|
||||
) -> io::Result<()> {
|
||||
let entries = read_filter_config(git, cwd, git_config_args)?;
|
||||
if !entries.values().any(|entry| !entry.value.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
let paths = paths
|
||||
.iter()
|
||||
.map(|path| path.as_bytes().to_vec())
|
||||
.collect::<Vec<_>>();
|
||||
let attributes = read_filter_attributes(git, cwd, &paths, git_config_args)?;
|
||||
if let Some((driver, path)) = selected_executable_filter(&entries, &attributes)? {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
format!(
|
||||
"refusing to run an internal Git worktree operation with executable filter {driver:?} selected for {}",
|
||||
String::from_utf8_lossy(&path)
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_filter_config(
|
||||
git: &GitRunner,
|
||||
cwd: &Path,
|
||||
git_config_args: &[String],
|
||||
) -> io::Result<BTreeMap<String, GitConfigEntry>> {
|
||||
read_effective_config_with_fallback(
|
||||
git,
|
||||
cwd,
|
||||
git_config_args,
|
||||
EXECUTABLE_FILTER_CONFIG_PATTERN,
|
||||
"filter",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn read_effective_config_with_fallback(
|
||||
git: &GitRunner,
|
||||
cwd: &Path,
|
||||
git_config_args: &[String],
|
||||
pattern: &str,
|
||||
probe: &str,
|
||||
) -> io::Result<BTreeMap<String, GitConfigEntry>> {
|
||||
let scoped =
|
||||
run_effective_config_query(git, cwd, git_config_args, pattern, /*show_scope*/ true)?;
|
||||
if scoped
|
||||
.status
|
||||
.code()
|
||||
.is_some_and(|code| code == 0 || code == 1)
|
||||
{
|
||||
return parse_effective_config(&scoped.stdout);
|
||||
}
|
||||
|
||||
let legacy = run_effective_config_query(
|
||||
git,
|
||||
cwd,
|
||||
git_config_args,
|
||||
pattern,
|
||||
/*show_scope*/ false,
|
||||
)?;
|
||||
if !legacy
|
||||
.status
|
||||
.code()
|
||||
.is_some_and(|code| code == 0 || code == 1)
|
||||
{
|
||||
return Err(io::Error::other(format!(
|
||||
"git {probe} config probe failed with status {}: {}",
|
||||
legacy.status,
|
||||
String::from_utf8_lossy(&legacy.stderr).trim()
|
||||
)));
|
||||
}
|
||||
parse_effective_config_with_origins(&legacy.stdout)
|
||||
}
|
||||
|
||||
fn run_effective_config_query(
|
||||
git: &GitRunner,
|
||||
cwd: &Path,
|
||||
git_config_args: &[String],
|
||||
pattern: &str,
|
||||
show_scope: bool,
|
||||
) -> io::Result<std::process::Output> {
|
||||
let mut command = git.command();
|
||||
command
|
||||
.env("GIT_OPTIONAL_LOCKS", "0")
|
||||
.args(git_config_args)
|
||||
.args(["config", "--null"]);
|
||||
if show_scope {
|
||||
command.arg("--show-scope");
|
||||
}
|
||||
command
|
||||
.args(["--show-origin", "--includes", "--get-regexp", pattern])
|
||||
.current_dir(cwd);
|
||||
git.output(command)
|
||||
}
|
||||
|
||||
fn read_filter_attributes(
|
||||
git: &GitRunner,
|
||||
cwd: &Path,
|
||||
paths: &[Vec<u8>],
|
||||
git_config_args: &[String],
|
||||
) -> io::Result<BTreeMap<Vec<u8>, String>> {
|
||||
if paths.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
let mut input = tempfile::tempfile()?;
|
||||
write_nul_paths(&mut input, paths)?;
|
||||
input.rewind()?;
|
||||
|
||||
let mut command = git.command();
|
||||
command
|
||||
.env("GIT_OPTIONAL_LOCKS", "0")
|
||||
.args(git_config_args)
|
||||
.args([
|
||||
"-c",
|
||||
&format!("core.hooksPath={DISABLED_HOOKS_PATH}"),
|
||||
"-c",
|
||||
"core.fsmonitor=false",
|
||||
"check-attr",
|
||||
"--stdin",
|
||||
"-z",
|
||||
"filter",
|
||||
])
|
||||
.current_dir(cwd)
|
||||
.stdin(Stdio::from(input));
|
||||
let output = git.output(command)?;
|
||||
if !output.status.success() {
|
||||
return Err(io::Error::other(format!(
|
||||
"git filter attribute probe failed with status {}: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
)));
|
||||
}
|
||||
parse_filter_attributes(&output.stdout, paths)
|
||||
}
|
||||
|
||||
fn selected_executable_filter(
|
||||
entries: &BTreeMap<String, GitConfigEntry>,
|
||||
attributes: &BTreeMap<Vec<u8>, String>,
|
||||
) -> io::Result<Option<(String, Vec<u8>)>> {
|
||||
let mut executable_drivers = BTreeSet::new();
|
||||
for entry in entries.values() {
|
||||
let driver = filter_driver_name(&entry.key)?;
|
||||
if !entry.value.is_empty() {
|
||||
executable_drivers.insert(driver);
|
||||
}
|
||||
}
|
||||
for (path, driver) in attributes {
|
||||
if executable_drivers.contains(driver) {
|
||||
return Ok(Some((driver.clone(), path.clone())));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn filter_driver_name(key: &str) -> io::Result<String> {
|
||||
let Some(remainder) = key.strip_prefix("filter.") else {
|
||||
return Err(invalid_filter_output("malformed filter config key"));
|
||||
};
|
||||
let driver = [".clean", ".smudge", ".process"]
|
||||
.into_iter()
|
||||
.find_map(|suffix| remainder.strip_suffix(suffix))
|
||||
.filter(|driver| !driver.is_empty())
|
||||
.ok_or_else(|| invalid_filter_output("malformed filter config key"))?;
|
||||
Ok(driver.to_string())
|
||||
}
|
||||
|
||||
fn write_nul_paths(input: &mut std::fs::File, paths: &[Vec<u8>]) -> io::Result<()> {
|
||||
let mut unique = BTreeSet::new();
|
||||
for path in paths {
|
||||
if path.is_empty() || path.contains(&0) {
|
||||
return Err(invalid_filter_output("invalid Git path"));
|
||||
}
|
||||
if unique.insert(path.as_slice()) {
|
||||
input.write_all(path)?;
|
||||
input.write_all(&[0])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_filter_attributes(
|
||||
output: &[u8],
|
||||
expected_paths: &[Vec<u8>],
|
||||
) -> io::Result<BTreeMap<Vec<u8>, String>> {
|
||||
let expected = expected_paths
|
||||
.iter()
|
||||
.map(Vec::as_slice)
|
||||
.collect::<BTreeSet<_>>();
|
||||
if expected.is_empty() && output.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
let Some(body) = output.strip_suffix(&[0]) else {
|
||||
return Err(invalid_filter_output(
|
||||
"unterminated Git filter attribute output",
|
||||
));
|
||||
};
|
||||
let fields = body.split(|byte| *byte == 0).collect::<Vec<_>>();
|
||||
if fields.len() % 3 != 0 {
|
||||
return Err(invalid_filter_output(
|
||||
"incomplete Git filter attribute record",
|
||||
));
|
||||
}
|
||||
let mut attributes = BTreeMap::new();
|
||||
for record in fields.chunks_exact(3) {
|
||||
if !expected.contains(record[0]) || record[1] != b"filter" {
|
||||
return Err(invalid_filter_output(
|
||||
"unexpected Git filter attribute record",
|
||||
));
|
||||
}
|
||||
let driver = std::str::from_utf8(record[2])
|
||||
.map_err(|_| invalid_filter_output("non-UTF-8 Git filter attribute value"))?;
|
||||
if attributes
|
||||
.insert(record[0].to_vec(), driver.to_string())
|
||||
.is_some()
|
||||
{
|
||||
return Err(invalid_filter_output(
|
||||
"duplicate Git filter attribute record",
|
||||
));
|
||||
}
|
||||
}
|
||||
if attributes.len() != expected.len() {
|
||||
return Err(invalid_filter_output("missing Git filter attribute record"));
|
||||
}
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
fn invalid_filter_output(message: &str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, message)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "safe_git_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user