use rstest::rstest;
use super::*;
use crate::config::{ConfigInputs, ConfigScope};
fn decode(input: &[u8], pairs: usize, bytes: usize) -> Result<Config, ConfigError> {
Config::from_command_environment(
|key| (key == "GIT_CONFIG_PARAMETERS").then(|| input.to_vec()),
pairs,
bytes,
)
}
#[rstest]
#[case::old(b"'fixture.value=a=b'", Some(b"a=b".as_slice()))]
#[case::new(b"'fixture.value'='a=b'", Some(b"a=b".as_slice()))]
#[case::implicit(b"'fixture.value'", None)]
#[case::old_empty(b"'fixture.value='", Some(b"".as_slice()))]
#[case::new_empty(b"'fixture.value'=''", Some(b"".as_slice()))]
#[case::unquoted_empty(b"'fixture.value'=", Some(b"".as_slice()))]
#[case::quote(b"'fixture.value=a'\\''b'", Some(b"a'b".as_slice()))]
#[case::bang(b"'fixture.value'='a'\\!'b'", Some(b"a!b".as_slice()))]
#[case::literal_escape(b"'fixture.value=a\\nb'", Some(b"a\\nb".as_slice()))]
#[case::newline(b"'fixture.value=a\nb'", Some(b"a\nb".as_slice()))]
#[case::bytes(b"'fixture.value=\xff'", Some(b"\xff".as_slice()))]
fn command_environment_values(#[case] input: &[u8], #[case] expected: Option<&[u8]>) {
let config = decode(input, 1, 4096).unwrap();
assert_eq!(config.value("fixture", None, "value"), Some(expected));
}
#[rstest]
#[case::bare(b"fixture.value=x")]
#[case::double_quote(b"\"fixture.value=x\"")]
#[case::leading_space(b" 'fixture.value=x'")]
#[case::leading_tab(b"\t'fixture.value=x'")]
#[case::unclosed(b"'fixture.value=x")]
#[case::unquoted_value(b"'fixture.value'=x")]
#[case::adjacent(b"'fixture.value'='a''b'")]
#[case::trailing(b"'fixture.value=x'junk")]
#[case::spaced_equal(b"'fixture.value' = 'x'")]
#[case::no_section(b"'value=x'")]
#[case::no_name(b"'fixture.=x'")]
#[case::numeric_name(b"'fixture.1bad=x'")]
#[case::nul_value(b"'fixture.value=\0'")]
fn command_environment_rejects_invalid(#[case] input: &[u8]) {
assert!(decode(input, 1, 4096).is_err());
}
#[rstest]
#[case::lone_quote(b"'".as_slice(), "invalid command environment quoting")]
#[case::escape_at_end(b"'fixture.value=x'\\", "invalid command environment quoting")]
#[case::escape_without_reopen(b"'fixture.value=x'\\'", "invalid command environment quoting")]
#[case::bang_without_reopen(b"'fixture.value=x'\\!", "invalid command environment quoting")]
#[case::dots_only(b"'.=x'", "invalid command environment key or value")]
fn command_environment_reports_quoting_and_key_reasons(#[case] input: &[u8], #[case] reason: &str) {
assert!(matches!(decode(input, 1, 4096), Err(error) if error.reason == reason));
}
#[test]
fn command_environment_preserves_sections_and_runtime_provenance() {
let environment = Config::from_command_environment(
|key| match key {
"GIT_CONFIG_COUNT" => Some(b"1".to_vec()),
"GIT_CONFIG_KEY_0" => Some(b"fixture.value".to_vec()),
"GIT_CONFIG_VALUE_0" => Some(b"counted".to_vec()),
"GIT_CONFIG_PARAMETERS" => {
Some(b"'fixture.value=legacy'\t'fixture.value' \r\n".to_vec())
}
_ => None,
},
3,
4096,
)
.unwrap();
let config = Config::resolve(&ConfigInputs {
environment: Some(environment),
command: Some(Config::parse(b"[fixture]\nvalue=cli\n").unwrap()),
..Default::default()
})
.unwrap();
assert_eq!(
config.values("fixture", None, "value").collect::<Vec<_>>(),
[
Some(b"counted".as_slice()),
Some(b"legacy"),
None,
Some(b"cli")
]
);
assert_eq!(
config
.section_occurrences()
.map(|s| s.entry_indices())
.collect::<Vec<_>>(),
[&[0][..], &[1], &[2], &[3]]
);
assert_eq!(
config.entries()[2].origin.as_ref().unwrap().scope,
ConfigScope::Environment
);
assert_eq!(
config.entries()[2].origin.as_ref().unwrap().location.line,
3
);
assert_eq!(
config.entries()[3].origin.as_ref().unwrap().scope,
ConfigScope::Command
);
}
#[test]
fn command_environment_limits_counted_and_legacy_together() {
let get = |key: &str| match key {
"GIT_CONFIG_COUNT" => Some(b"1".to_vec()),
"GIT_CONFIG_KEY_0" => Some(b"a.b".to_vec()),
"GIT_CONFIG_VALUE_0" => Some(b"c".to_vec()),
"GIT_CONFIG_PARAMETERS" => Some(b"'a.b=d'".to_vec()),
_ => None,
};
let exact = 1 + 3 + 1 + 7 + 2 * ASSIGNMENT_BYTES;
assert!(Config::from_command_environment(get, 2, exact).is_ok());
assert_eq!(
Config::from_command_environment(get, 2, exact - 1)
.unwrap_err()
.reason,
"environment byte limit"
);
assert_eq!(
Config::from_command_environment(get, 1, exact)
.unwrap_err()
.reason,
"environment pair limit"
);
assert_eq!(
Config::from_command_environment(get, 2, 4)
.unwrap_err()
.reason,
"environment byte limit"
);
}
#[test]
fn command_environment_checks_limits_before_decoding_next_assignment() {
assert_eq!(
decode(b"'a.b=x' 'unterminated", 1, 4096)
.unwrap_err()
.reason,
"environment pair limit"
);
assert_eq!(
decode(b"'unterminated", 1, 1).unwrap_err().reason,
"environment byte limit"
);
assert!(decode(b"", 0, 0).is_ok());
assert_eq!(
decode(b"'a.b=x'", 0, 4096).unwrap_err().reason,
"environment pair limit"
);
}
#[test]
fn command_environment_preserves_counted_failure_order() {
let mut legacy_requested = false;
let error = Config::from_command_environment(
|key| {
legacy_requested |= key == "GIT_CONFIG_PARAMETERS";
(key == "GIT_CONFIG_COUNT").then(|| b"invalid".to_vec())
},
10,
4096,
)
.unwrap_err();
assert_eq!(error.reason, "invalid environment count");
assert!(!legacy_requested);
}
#[test]
fn counted_environment_still_ignores_legacy() {
let config = Config::from_environment(
|key| {
assert_ne!(key, "GIT_CONFIG_PARAMETERS");
None
},
0,
)
.unwrap();
assert!(config.entries().is_empty());
}
#[rstest]
#[case::both_encodings(b"'fixture.value=old' 'fixture.value'='new'")]
#[case::implicit_and_empty(b"'fixture.value' 'fixture.value=' 'fixture.value'=''")]
#[case::quoted_bytes(b"'fixture.value=a'\\''b' 'fixture.value'='a'\\!'b'")]
#[case::whitespace(b"'fixture.value=a'\t'fixture.value=b'\n'fixture.value=c' \r")]
#[case::subsection(b"'fixture.Sub section.value=x'")]
fn command_environment_matches_git(#[case] input: &[u8]) {
let temp = tempfile::tempdir().unwrap();
let empty_config = temp.path().join("empty-config");
std::fs::write(&empty_config, []).unwrap();
let input = std::str::from_utf8(input).unwrap();
let output = std::process::Command::new("git")
.env_clear()
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", &empty_config)
.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "fixture.value")
.env("GIT_CONFIG_VALUE_0", "counted")
.env("GIT_CONFIG_PARAMETERS", input)
.args(["-c", "fixture.value=cli", "config", "--null", "--list"])
.current_dir(temp.path())
.output()
.unwrap();
let parsed = Config::from_command_environment(
|key| match key {
"GIT_CONFIG_COUNT" => Some(b"1".to_vec()),
"GIT_CONFIG_KEY_0" => Some(b"fixture.value".to_vec()),
"GIT_CONFIG_VALUE_0" => Some(b"counted".to_vec()),
"GIT_CONFIG_PARAMETERS" => Some(input.as_bytes().to_vec()),
_ => None,
},
32,
4096,
);
assert!(output.status.success());
let config = Config::resolve(&ConfigInputs {
environment: Some(parsed.unwrap()),
command: Some(Config::parse(b"[fixture]\nvalue=cli\n").unwrap()),
..Default::default()
})
.unwrap();
assert_eq!(git_list_bytes(&config), output.stdout);
}
fn git_list_bytes(config: &Config) -> Vec<u8> {
let mut result = Vec::new();
for entry in config.entries() {
result.extend(entry.section.iter().map(u8::to_ascii_lowercase));
result.push(b'.');
if let Some(subsection) = &entry.subsection {
result.extend_from_slice(subsection);
result.push(b'.');
}
result.extend(entry.name.iter().map(u8::to_ascii_lowercase));
if let Some(value) = &entry.value {
result.push(b'\n');
result.extend_from_slice(value);
}
result.push(0);
}
result
}
#[rstest]
#[case::leading(b" 'fixture.value=x'")]
#[case::unquoted(b"fixture.value=x")]
#[case::key(b"'fixture.1bad=x'")]
#[case::quotes(b"'fixture.value'='a''b'")]
fn command_environment_rejection_matches_git(#[case] input: &[u8]) {
let temp = tempfile::tempdir().unwrap();
let output = std::process::Command::new("git")
.env_clear()
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_PARAMETERS", std::str::from_utf8(input).unwrap())
.args(["config", "--null", "--list"])
.current_dir(temp.path())
.output()
.unwrap();
assert!(!output.status.success());
assert!(decode(input, 1, 4096).is_err());
}