mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why Sourced snapshots parse options and aliases as a group before restored options take effect. Aliases serialized with `RC_QUOTES` enabled can therefore be misinterpreted by the replay shell. ## What changed Serialize zsh aliases for sourced snapshots with `NO_RC_QUOTES` in a subshell, preserving the captured shell options and avoiding a dependency on the optional `zsh/parameter` module. ## Testing Extend regression coverage to execute restored aliases with `RC_QUOTES` enabled and disabled across source and eval replay. Expand macOS zsh concurrent replay tests to cover quoted aliases, preserved options, and blocked descriptor paths with and without a TTY. GitOrigin-RevId: 48086f380c21318bece9226dfe6486c179c2a440
2308 lines
90 KiB
Rust
2308 lines
90 KiB
Rust
use super::CapturedSnapshot;
|
|
use super::PreparedSnapshot;
|
|
use super::SnapshotCaptureOptions;
|
|
use super::SnapshotCredentialEnvironment;
|
|
use super::SnapshotStartup;
|
|
use super::prepare_snapshot_credentials;
|
|
use super::snapshot_capture_script;
|
|
use crate::shell_detect::ShellType;
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::HashMap;
|
|
use std::os::unix::process::CommandExt;
|
|
use std::process::Command;
|
|
use tempfile::tempdir;
|
|
|
|
fn empty_snapshot_credentials<'a>(
|
|
empty: &'a HashMap<String, String>,
|
|
is_allowed_unset: &'a dyn Fn(&str) -> bool,
|
|
) -> SnapshotCredentialEnvironment<'a> {
|
|
SnapshotCredentialEnvironment {
|
|
original: empty,
|
|
restored: empty,
|
|
configured: empty,
|
|
discovered: empty,
|
|
allowed: empty,
|
|
is_allowed_unset,
|
|
brokered_keys: &[],
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &[],
|
|
}
|
|
}
|
|
|
|
const CAPTURE_ALL: SnapshotCaptureOptions = SnapshotCaptureOptions {
|
|
startup: SnapshotStartup::Interactive,
|
|
declarations: true,
|
|
environment: true,
|
|
};
|
|
|
|
const CAPTURE_NON_INTERACTIVE: SnapshotCaptureOptions = SnapshotCaptureOptions {
|
|
startup: SnapshotStartup::NonInteractive,
|
|
..CAPTURE_ALL
|
|
};
|
|
|
|
fn snapshot_script(shell_type: ShellType) -> Option<String> {
|
|
snapshot_capture_script(shell_type, CAPTURE_ALL)
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_capture_requires_marker_and_complete_records() {
|
|
for captured in [
|
|
"missing header\0\0\0",
|
|
"# Snapshot file\n\0",
|
|
"# Snapshot file\n\0\0NAME\0export NAME=value\n",
|
|
] {
|
|
assert!(CapturedSnapshot::parse(ShellType::Bash, captured.as_bytes()).is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_capture_selects_declarations_and_environment() -> Result<()> {
|
|
let dir = tempdir()?;
|
|
for (shell, shell_type) in [
|
|
("/bin/bash", ShellType::Bash),
|
|
("/bin/sh", ShellType::Sh),
|
|
("/bin/zsh", ShellType::Zsh),
|
|
] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
for (declarations, environment) in
|
|
[(true, false), (false, true), (true, true), (false, false)]
|
|
{
|
|
let script = snapshot_capture_script(
|
|
shell_type,
|
|
SnapshotCaptureOptions {
|
|
startup: SnapshotStartup::NonInteractive,
|
|
declarations,
|
|
environment,
|
|
},
|
|
)
|
|
.unwrap();
|
|
let output = Command::new(shell)
|
|
.args(["-c", &script])
|
|
.env_clear()
|
|
.env("HOME", dir.path())
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.env("APP_SETTING", "value")
|
|
.output()?;
|
|
assert!(output.status.success(), "{shell}: {output:?}");
|
|
let captured = CapturedSnapshot::parse(shell_type, &output.stdout).unwrap();
|
|
assert_eq!(
|
|
(
|
|
captured
|
|
.exports
|
|
.iter()
|
|
.any(|export| export.key == "APP_SETTING"),
|
|
captured
|
|
.environment
|
|
.split(|byte| *byte == 0)
|
|
.any(|entry| entry == b"APP_SETTING=value"),
|
|
),
|
|
(declarations, environment),
|
|
"{shell}"
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn bash_snapshot_filters_invalid_exports() -> Result<()> {
|
|
let output = Command::new("/bin/bash")
|
|
.arg("-c")
|
|
.arg(snapshot_script(ShellType::Bash).expect("bash supports snapshots"))
|
|
.env("BASH_ENV", "/dev/null")
|
|
.env("VALID_NAME", "ok")
|
|
.env("PWD", "/tmp/stale")
|
|
.env("NEXTEST_BIN_EXE_codex-write-config-schema", "/path/to/bin")
|
|
.env("BAD-NAME", "broken")
|
|
.output()?;
|
|
|
|
assert!(output.status.success());
|
|
|
|
let stdout = captured_script(ShellType::Bash, &String::from_utf8(output.stdout)?)?;
|
|
assert!(stdout.contains("VALID_NAME"));
|
|
assert!(!stdout.contains("PWD=/tmp/stale"));
|
|
assert!(!stdout.contains("NEXTEST_BIN_EXE_codex-write-config-schema"));
|
|
assert!(!stdout.contains("BAD-NAME"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn bash_snapshot_preserves_multiline_exports() -> Result<()> {
|
|
let multiline_cert = "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----";
|
|
let output = Command::new("/bin/bash")
|
|
.arg("-c")
|
|
.arg(snapshot_script(ShellType::Bash).expect("bash supports snapshots"))
|
|
.env("BASH_ENV", "/dev/null")
|
|
.env("MULTILINE_CERT", multiline_cert)
|
|
.output()?;
|
|
|
|
assert!(output.status.success());
|
|
|
|
let stdout = captured_script(ShellType::Bash, &String::from_utf8(output.stdout)?)?;
|
|
assert!(
|
|
stdout.contains("MULTILINE_CERT=") || stdout.contains("MULTILINE_CERT"),
|
|
"snapshot should include the multiline export name"
|
|
);
|
|
|
|
let dir = tempdir()?;
|
|
let snapshot_path = dir.path().join("snapshot.sh");
|
|
std::fs::write(&snapshot_path, stdout.as_bytes())?;
|
|
|
|
let validate = Command::new("/bin/bash")
|
|
.arg("-c")
|
|
.arg("set -e; . \"$1\"")
|
|
.arg("bash")
|
|
.arg(&snapshot_path)
|
|
.env("BASH_ENV", "/dev/null")
|
|
.output()?;
|
|
|
|
assert!(
|
|
validate.status.success(),
|
|
"snapshot validation failed: {}",
|
|
String::from_utf8_lossy(&validate.stderr)
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_environment_preserves_native_path_exports_and_shell_level() -> Result<()> {
|
|
let dir = tempdir()?;
|
|
for (shell, shell_type, setup) in [
|
|
("/bin/bash", ShellType::Bash, "export -n PATH"),
|
|
("/bin/zsh", ShellType::Zsh, "typeset +x PATH"),
|
|
] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
let run = |script: &str| {
|
|
// Keep Bash's final-command exec optimization from decrementing SHLVL.
|
|
Command::new(shell)
|
|
.args(["-c", &format!("{setup}\n{script}\n:")])
|
|
.env_clear()
|
|
.env("HOME", dir.path())
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.env("SHLVL", "3")
|
|
.output()
|
|
};
|
|
let native = run("/usr/bin/env -0")?;
|
|
let script = snapshot_capture_script(
|
|
shell_type,
|
|
SnapshotCaptureOptions {
|
|
startup: SnapshotStartup::NonInteractive,
|
|
..CAPTURE_ALL
|
|
},
|
|
)
|
|
.unwrap();
|
|
let output = run(&script)?;
|
|
assert!(native.status.success() && output.status.success());
|
|
let captured = CapturedSnapshot::parse(shell_type, &output.stdout).unwrap();
|
|
let metadata = |environment: &[u8]| {
|
|
environment
|
|
.split(|byte| *byte == 0)
|
|
.filter(|entry| entry.starts_with(b"PATH=") || entry.starts_with(b"SHLVL="))
|
|
.map(<[u8]>::to_vec)
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(
|
|
metadata(captured.environment),
|
|
metadata(&native.stdout),
|
|
"{shell}"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn sh_declarations_preserve_path_export_state() -> Result<()> {
|
|
let dir = tempdir()?;
|
|
let snapshot_path = dir.path().join("snapshot.sh");
|
|
let script = snapshot_capture_script(
|
|
ShellType::Sh,
|
|
SnapshotCaptureOptions {
|
|
startup: SnapshotStartup::NonInteractive,
|
|
..CAPTURE_ALL
|
|
},
|
|
)
|
|
.expect("sh supports snapshots");
|
|
for shell in [
|
|
"/bin/sh",
|
|
#[cfg(target_os = "macos")]
|
|
"/bin/dash",
|
|
] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
for (setup, expected) in [
|
|
("PATH=/enterprise/bin; export PATH", "x:/enterprise/bin"),
|
|
("PATH=; export PATH", "x:"),
|
|
("unset PATH; export PATH", ":"),
|
|
("unset PATH; PATH=/enterprise/bin", ":"),
|
|
] {
|
|
let captured = Command::new(shell)
|
|
.args(["-uc", &format!("{setup}\n{script}")])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(captured.status.success(), "capture failed: {captured:?}");
|
|
std::fs::write(
|
|
&snapshot_path,
|
|
captured_script(ShellType::Sh, &String::from_utf8(captured.stdout)?)?,
|
|
)?;
|
|
let restored = Command::new(shell)
|
|
.args([
|
|
"-c",
|
|
"unset PATH; . \"$1\"; printf '%s:%s' \"${PATH+x}\" \"${PATH-}\"",
|
|
"snapshot",
|
|
])
|
|
.arg(&snapshot_path)
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(restored.status.success(), "replay failed: {restored:?}");
|
|
assert_eq!(restored.stdout, expected.as_bytes(), "{setup}");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn posix_startup_path_expansion_preserves_supported_forms() -> Result<()> {
|
|
let script = format!(
|
|
"{}\n__codex_snapshot_expand_env \"$ENV\"",
|
|
super::posix_env_path_expansion_function()
|
|
);
|
|
let output = Command::new("/bin/sh")
|
|
.arg("-uc")
|
|
.arg(&script)
|
|
.env("ENV", "$MISSING")
|
|
.output()?;
|
|
|
|
assert!(output.status.success(), "snapshot failed: {output:?}");
|
|
assert_eq!(output.stdout, b"$MISSING");
|
|
|
|
let dir = tempdir()?;
|
|
let startup = dir.path().join("startup.sh");
|
|
for (env_value, expansion_env) in [
|
|
(
|
|
startup.display().to_string(),
|
|
Vec::<(String, String)>::new(),
|
|
),
|
|
(
|
|
"$STARTUP_FILE".to_string(),
|
|
vec![("STARTUP_FILE".to_string(), startup.display().to_string())],
|
|
),
|
|
(
|
|
"${STARTUP_FILE}".to_string(),
|
|
vec![("STARTUP_FILE".to_string(), startup.display().to_string())],
|
|
),
|
|
(
|
|
"${STARTUP_DIR}/startup.sh".to_string(),
|
|
vec![("STARTUP_DIR".to_string(), dir.path().display().to_string())],
|
|
),
|
|
(
|
|
"${PATH%%:*}/startup.sh".to_string(),
|
|
vec![(
|
|
"PATH".to_string(),
|
|
format!("{}:/usr/bin:/bin", dir.path().display()),
|
|
)],
|
|
),
|
|
(
|
|
"$PATH/startup.sh".to_string(),
|
|
vec![("PATH".to_string(), dir.path().display().to_string())],
|
|
),
|
|
(
|
|
"${PATH}/startup.sh".to_string(),
|
|
vec![("PATH".to_string(), dir.path().display().to_string())],
|
|
),
|
|
(
|
|
format!("$PATH{}", startup.display()),
|
|
vec![("PATH".to_string(), String::new())],
|
|
),
|
|
(
|
|
format!("${{PATH}}{}", startup.display()),
|
|
vec![("PATH".to_string(), String::new())],
|
|
),
|
|
(
|
|
"~/startup.sh".to_string(),
|
|
vec![("HOME".to_string(), dir.path().display().to_string())],
|
|
),
|
|
] {
|
|
let output = Command::new("/bin/sh")
|
|
.arg("-uc")
|
|
.arg(&script)
|
|
.env("ENV", &env_value)
|
|
.envs(expansion_env)
|
|
.output()?;
|
|
|
|
assert!(output.status.success(), "snapshot failed: {output:?}");
|
|
assert_eq!(
|
|
String::from_utf8(output.stdout)?,
|
|
startup.display().to_string(),
|
|
"ENV={env_value}"
|
|
);
|
|
}
|
|
|
|
let default_startup_dir = dir.path().join(".config");
|
|
for xdg_config_home in [
|
|
None,
|
|
Some(std::ffi::OsStr::new("")),
|
|
Some(default_startup_dir.as_os_str()),
|
|
] {
|
|
let mut command = Command::new("/bin/sh");
|
|
command
|
|
.arg("-uc")
|
|
.arg(&script)
|
|
.env("HOME", dir.path())
|
|
.env("ENV", "${XDG_CONFIG_HOME:-$HOME/.config}/startup.sh");
|
|
if let Some(xdg_config_home) = xdg_config_home {
|
|
command.env("XDG_CONFIG_HOME", xdg_config_home);
|
|
} else {
|
|
command.env_remove("XDG_CONFIG_HOME");
|
|
}
|
|
let output = command.output()?;
|
|
|
|
assert!(output.status.success(), "snapshot failed: {output:?}");
|
|
assert_eq!(
|
|
String::from_utf8(output.stdout)?,
|
|
default_startup_dir.join("startup.sh").display().to_string()
|
|
);
|
|
}
|
|
|
|
let injected_marker = dir.path().join("env-injected");
|
|
let injected_env = format!(
|
|
"\"; touch '{}'; __codex_env_file=\"",
|
|
injected_marker.display()
|
|
);
|
|
let output = Command::new("/bin/sh")
|
|
.arg("-uc")
|
|
.arg(&script)
|
|
.env("ENV", injected_env)
|
|
.output()?;
|
|
assert!(output.status.success(), "snapshot failed: {output:?}");
|
|
assert!(
|
|
!injected_marker.exists(),
|
|
"snapshot interpreted ENV as shell source"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshots_filter_complete_multiline_exports() -> Result<()> {
|
|
let dir = tempdir()?;
|
|
for (shell, shell_type) in [
|
|
("/bin/bash", ShellType::Bash),
|
|
("/bin/sh", ShellType::Sh),
|
|
("/bin/dash", ShellType::Sh),
|
|
("/bin/zsh", ShellType::Zsh),
|
|
] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
for value in [
|
|
"before\nexport NOT_A_REAL_VARIABLE=value\nafter",
|
|
"before'\ndeclare -x NOT_A_REAL_VARIABLE=\"quoted\"\ntypeset OTHER=value\nafter\\",
|
|
"before\t\n# exports 99\nexport NOT_A_REAL_VARIABLE=value\nafter",
|
|
"certificate\nwith trailing newlines\n\n",
|
|
"before\nexport NOT_A_REAL_VARIABLE\nafter",
|
|
"before\nexport name=not_the_actual_value\nafter",
|
|
] {
|
|
let original = HashMap::from([
|
|
("APP_SCRIPT".to_string(), value.to_string()),
|
|
("EXCLUDED_SCRIPT".to_string(), value.to_string()),
|
|
("ZZZ_AFTER".to_string(), "kept".to_string()),
|
|
("name".to_string(), "original name".to_string()),
|
|
("value".to_string(), "original value".to_string()),
|
|
("escaped".to_string(), "original escaped".to_string()),
|
|
]);
|
|
let mut allowed = original.clone();
|
|
allowed.remove("EXCLUDED_SCRIPT");
|
|
let function_value = "before\n# exports 1\nexport NOT_A_REAL_VARIABLE=value\nafter";
|
|
let capture_script = snapshot_capture_script(
|
|
shell_type,
|
|
SnapshotCaptureOptions {
|
|
startup: SnapshotStartup::NonInteractive,
|
|
..CAPTURE_ALL
|
|
},
|
|
)
|
|
.expect("shell supports snapshots");
|
|
let capture = if matches!(shell_type, ShellType::Sh) {
|
|
capture_script
|
|
} else {
|
|
format!("emit_source() {{ cat <<'EOF'\n{function_value}\nEOF\n}}\n{capture_script}")
|
|
};
|
|
let capture = if matches!(shell_type, ShellType::Sh | ShellType::Zsh) {
|
|
format!("export APP_SETTING EXCLUDED_SETTING\n{capture}")
|
|
} else {
|
|
capture
|
|
};
|
|
let captured = Command::new(shell)
|
|
.args(["-c", &capture])
|
|
.env_clear()
|
|
.envs(&original)
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.output()?;
|
|
assert!(captured.status.success(), "{shell}: {captured:?}");
|
|
let mut snapshot = String::from_utf8(captured.stdout)?;
|
|
let records = CapturedSnapshot::parse(shell_type, snapshot.as_bytes()).unwrap();
|
|
assert_eq!(
|
|
records
|
|
.exports
|
|
.iter()
|
|
.filter(|export| export.key == "name")
|
|
.count(),
|
|
1,
|
|
"{shell}: duplicate export record"
|
|
);
|
|
if matches!(shell_type, ShellType::Sh) {
|
|
let raw_path = dir.path().join("raw-snapshot.sh");
|
|
std::fs::write(&raw_path, captured_script(shell_type, &snapshot)?)?;
|
|
let replayed = Command::new(shell)
|
|
.args(["-c", ". \"$1\"; printf '%s\\n' \"${APP_SETTING-unset}\"; APP_SETTING=production; /usr/bin/printenv APP_SETTING; NOT_A_REAL_VARIABLE=unexpected; /usr/bin/printenv NOT_A_REAL_VARIABLE", "snapshot"])
|
|
.arg(&raw_path)
|
|
.env_clear()
|
|
.output()?;
|
|
assert_eq!(
|
|
replayed.stdout, b"unset\nproduction\n",
|
|
"{shell}: {snapshot}"
|
|
);
|
|
assert_eq!(replayed.status.code(), Some(1));
|
|
}
|
|
rewrite_snapshot_credentials(
|
|
shell_type,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|key| key == "APP_SETTING",
|
|
brokered_keys: &[],
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &[],
|
|
},
|
|
|_| true,
|
|
);
|
|
let path = dir.path().join("snapshot.sh");
|
|
std::fs::write(&path, snapshot)?;
|
|
let replayed = Command::new(shell)
|
|
.args(["-c", ". \"$1\"; printf '%s\\0%s\\0%s\\0%s\\0%s\\0%s' \"$APP_SCRIPT\" \"${EXCLUDED_SCRIPT-unset}\" \"$ZZZ_AFTER\" \"$name\" \"$value\" \"$escaped\"; if command -v emit_source >/dev/null; then emit_source; fi; APP_SETTING=production; EXCLUDED_SETTING=denied; /usr/bin/printenv APP_SETTING; /usr/bin/printenv EXCLUDED_SETTING || :", "snapshot"])
|
|
.arg(&path)
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(replayed.status.success(), "{shell}: {replayed:?}");
|
|
let function_value = if matches!(shell_type, ShellType::Sh) {
|
|
String::new()
|
|
} else {
|
|
format!("{function_value}\n")
|
|
};
|
|
let exported_setting = if matches!(shell_type, ShellType::Sh | ShellType::Zsh) {
|
|
"production\n"
|
|
} else {
|
|
""
|
|
};
|
|
assert_eq!(
|
|
replayed.stdout,
|
|
format!("{value}\0unset\0kept\0original name\0original value\0original escaped{function_value}{exported_setting}").as_bytes(),
|
|
"{shell}"
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_zsh_snapshot_preserves_tied_credential_aliases() -> Result<()> {
|
|
if !std::path::Path::new("/bin/zsh").exists() {
|
|
return Ok(());
|
|
}
|
|
let dir = tempdir()?;
|
|
let real = "password$abcdefghijklmno";
|
|
let dummy = "dummy-0123456789abcdef";
|
|
let last = "ordinary'quote\nnext";
|
|
let brokered_keys = vec!["VENDOR_PASSWORD".to_string()];
|
|
for separator in [":", "|", "'", "\t", "$", "_", "x"] {
|
|
let appended = format!("ordinary{separator}other");
|
|
for (rcquotes, captured_credential) in [
|
|
(false, real),
|
|
(true, real),
|
|
(false, "captured$0123456789abcdef"),
|
|
(true, "pin_ab$cd"),
|
|
] {
|
|
let script = snapshot_capture_script(ShellType::Zsh, CAPTURE_NON_INTERACTIVE).unwrap();
|
|
let setup = format!(
|
|
"{}\nexport -UT SERVICE_URLS service_urls=( \"https://user:$VENDOR_PASSWORD@example.invalid/path\" '' {} ) {}\nservice_urls+=({})\n{script}",
|
|
if rcquotes {
|
|
"setopt rcquotes rcexpandparam"
|
|
} else {
|
|
":"
|
|
},
|
|
shlex::try_quote(last)?,
|
|
shlex::try_quote(separator)?,
|
|
shlex::try_quote(&appended)?,
|
|
);
|
|
let original = HashMap::from([
|
|
(
|
|
"VENDOR_PASSWORD".to_string(),
|
|
captured_credential.to_string(),
|
|
),
|
|
(
|
|
"SERVICE_URLS".to_string(),
|
|
[
|
|
format!("https://user:{captured_credential}@example.invalid/path"),
|
|
String::new(),
|
|
last.to_string(),
|
|
appended.clone(),
|
|
]
|
|
.join(separator),
|
|
),
|
|
]);
|
|
let captured = Command::new("/bin/zsh")
|
|
.args(["-fc", &setup])
|
|
.env_clear()
|
|
.env("VENDOR_PASSWORD", captured_credential)
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.output()?;
|
|
assert!(captured.status.success(), "{captured:?}");
|
|
let mut snapshot = String::from_utf8(captured.stdout)?;
|
|
let allowed = original
|
|
.iter()
|
|
.map(|(key, value)| (key.clone(), value.replace(captured_credential, dummy)))
|
|
.collect::<HashMap<_, _>>();
|
|
let restored = original
|
|
.iter()
|
|
.map(|(key, value)| (key.clone(), value.replace(captured_credential, real)))
|
|
.collect::<HashMap<_, _>>();
|
|
let rewritten = rewrite_snapshot_credentials(
|
|
ShellType::Zsh,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &restored,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| true,
|
|
brokered_keys: &brokered_keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &brokered_keys,
|
|
},
|
|
|text| {
|
|
*text = text
|
|
.replace(real, dummy)
|
|
.replace(captured_credential, dummy);
|
|
true
|
|
},
|
|
)
|
|
.expect("tied export can be brokered");
|
|
assert_eq!(
|
|
rewritten.aliases,
|
|
HashMap::from([("SERVICE_URLS".to_string(), allowed["SERVICE_URLS"].clone())])
|
|
);
|
|
assert!(!snapshot.contains(real));
|
|
let path = dir.path().join("snapshot.sh");
|
|
std::fs::write(&path, &snapshot)?;
|
|
for live in [dummy, real] {
|
|
let replayed = Command::new("/bin/zsh")
|
|
.args(["-fc", "set -e; . \"$1\"; printf '%s\\0' \"$SERVICE_URLS\" \"${service_urls[@]}\" \"$options[rcquotes]\"", "snapshot"])
|
|
.arg(&path)
|
|
.env_clear()
|
|
.env("VENDOR_PASSWORD", live)
|
|
.output()?;
|
|
assert!(replayed.status.success(), "{replayed:?}");
|
|
let scalar = original["SERVICE_URLS"].replace(captured_credential, live);
|
|
let mut array = vec![
|
|
format!("https://user:{live}@example.invalid/path"),
|
|
String::new(),
|
|
last.to_string(),
|
|
];
|
|
array.push(appended.clone());
|
|
assert_eq!(
|
|
replayed.stdout,
|
|
format!(
|
|
"{scalar}\0{}\0{}\0",
|
|
array.join("\0"),
|
|
if rcquotes { "on" } else { "off" }
|
|
)
|
|
.into_bytes()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
for unique in [false, true] {
|
|
for separator in [":", "|", "'", "\t"] {
|
|
let flags = if unique { "-UT" } else { "-T" };
|
|
let separator = shlex::try_quote(separator)?;
|
|
let binding = format!("export {flags} SERVICE_URLS service_urls {separator}");
|
|
let script = snapshot_capture_script(ShellType::Zsh, CAPTURE_NON_INTERACTIVE).unwrap();
|
|
let captured = Command::new("/bin/zsh")
|
|
.args([
|
|
"-fc",
|
|
&format!(
|
|
"export {flags} SERVICE_URLS service_urls=( '' \"$VENDOR_PASSWORD\" '' plain plain '' ) {separator}\nunset VENDOR_PASSWORD\n{script}"
|
|
),
|
|
])
|
|
.env_clear()
|
|
.env("VENDOR_PASSWORD", real)
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.output()?;
|
|
assert!(captured.status.success(), "{captured:?}");
|
|
let mut snapshot = String::from_utf8(captured.stdout)?;
|
|
let original = CapturedSnapshot::parse(ShellType::Zsh, snapshot.as_bytes())
|
|
.unwrap()
|
|
.environment
|
|
.split(|byte| *byte == 0)
|
|
.filter_map(|entry| std::str::from_utf8(entry).ok()?.split_once('='))
|
|
.map(|(key, value)| (key.to_string(), value.to_string()))
|
|
.collect::<HashMap<_, _>>();
|
|
assert!(!original.contains_key("VENDOR_PASSWORD"));
|
|
let allowed = original
|
|
.iter()
|
|
.map(|(key, value)| (key.clone(), value.replace(real, dummy)))
|
|
.collect::<HashMap<_, _>>();
|
|
let alias_keys = vec!["SERVICE_URLS".to_string()];
|
|
let rewritten = rewrite_snapshot_credentials(
|
|
ShellType::Zsh,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| true,
|
|
brokered_keys: &[],
|
|
brokered_alias_keys: &alias_keys,
|
|
allowed_brokered_keys: &[],
|
|
},
|
|
|text| {
|
|
*text = text.replace(real, dummy);
|
|
true
|
|
},
|
|
)
|
|
.expect("source-less tied alias can be brokered");
|
|
assert_eq!(rewritten.aliases, HashMap::new());
|
|
assert!(!snapshot.contains(real));
|
|
assert!(!snapshot.contains(dummy));
|
|
let path = dir.path().join("source-less-snapshot.sh");
|
|
std::fs::write(&path, &snapshot)?;
|
|
let inspect = "printf '%s\\0' \"${SERVICE_URLS-unset}\" \"${service_urls[@]}\" \"${(t)service_urls}\" \"${VENDOR_PASSWORD-unset}\"; if [ -n \"${SERVICE_URLS+x}\" ]; then SERVICE_URLS=$UPDATE; printf '%s\\0' \"$SERVICE_URLS\" \"${service_urls[@]}\"; unset SERVICE_URLS; fi; printf '%s:%s' \"${+SERVICE_URLS}\" \"${+service_urls}\"";
|
|
for live in [Some(allowed["SERVICE_URLS"].as_str()), Some(""), None] {
|
|
for source in [None, Some("different-live-source")] {
|
|
let mut outputs = Vec::new();
|
|
for script in [
|
|
format!("set -e; . \"$1\"; {inspect}"),
|
|
format!(
|
|
"set -e; if [ -n \"${{SERVICE_URLS+x}}\" ]; then {binding}; fi; {inspect}"
|
|
),
|
|
] {
|
|
let mut command = Command::new("/bin/zsh");
|
|
command
|
|
.args(["-fc", &script, "snapshot"])
|
|
.arg(&path)
|
|
.env_clear()
|
|
.env(
|
|
"UPDATE",
|
|
allowed["SERVICE_URLS"].replace(dummy, "new-live-dummy"),
|
|
);
|
|
if let Some(live) = live {
|
|
command.env("SERVICE_URLS", live);
|
|
}
|
|
if let Some(source) = source {
|
|
command.env("VENDOR_PASSWORD", source);
|
|
}
|
|
let output = command.output()?;
|
|
assert!(output.status.success(), "{script}: {output:?}");
|
|
outputs.push(String::from_utf8(output.stdout)?);
|
|
}
|
|
assert_eq!(outputs[0], outputs[1], "{binding}, {live:?}, {source:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshot_rejects_credentials_crossing_array_elements() {
|
|
let real = "password$abcdefghijklmno";
|
|
let dummy = "dummy-0123456789abcdef";
|
|
let keys = vec!["VENDOR_PASSWORD".to_string()];
|
|
let restored = HashMap::from([(keys[0].clone(), real.to_string())]);
|
|
for (captured_value, allowed_value) in [(real, dummy), (real, real), (dummy, dummy)] {
|
|
let original = HashMap::from([
|
|
(keys[0].clone(), captured_value.to_string()),
|
|
("AUTH_HEADER".to_string(), captured_value.to_string()),
|
|
]);
|
|
let allowed = HashMap::from([
|
|
(keys[0].clone(), allowed_value.to_string()),
|
|
("AUTH_HEADER".to_string(), allowed_value.to_string()),
|
|
]);
|
|
let (first, last) = captured_value.split_at(/*mid*/ 10);
|
|
let data = format!(
|
|
"# Snapshot file\n\0\0AUTH_HEADER\0typeset -xT AUTH_HEADER header=( '{first}' '{last}' ) ''\n\0\0"
|
|
);
|
|
let capture = CapturedSnapshot::parse(ShellType::Zsh, data.as_bytes()).unwrap();
|
|
let prepared = prepare_snapshot_credentials(
|
|
&capture,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &restored,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| true,
|
|
brokered_keys: &keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &keys,
|
|
},
|
|
|text| {
|
|
*text = text.replace(real, allowed_value);
|
|
true
|
|
},
|
|
);
|
|
assert!(prepared.is_none(), "{captured_value}, {allowed_value}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zsh_literal_bytes_match_native_modifiers() -> Result<()> {
|
|
if !std::path::Path::new("/bin/zsh").exists() {
|
|
return Ok(());
|
|
}
|
|
for word in [
|
|
r"$'\M-C\M-)'",
|
|
r"$'\xC3'$'\M-)'",
|
|
r"$'\M-\C-?'",
|
|
r"$'\C-\M-?'",
|
|
r"$'\C-\xC3'",
|
|
r"$'\M\x41'",
|
|
r"$'\M-\u0041X'",
|
|
r"$'\M-\q'",
|
|
] {
|
|
let script = format!("printf '%s' {word}");
|
|
let native = Command::new("/bin/zsh")
|
|
.args(["-fc", &script])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(native.status.success(), "{word}: {native:?}");
|
|
let tree = crate::bash::try_parse_shell(&script).unwrap();
|
|
let node = tree.root_node().named_child(0).unwrap();
|
|
let mut cursor = node.walk();
|
|
let argument = node
|
|
.children_by_field_name("argument", &mut cursor)
|
|
.last()
|
|
.unwrap();
|
|
let decoded = super::literals::literal_word_bytes(
|
|
argument,
|
|
&script,
|
|
ShellType::Zsh,
|
|
super::literals::Quoting::Posix,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(decoded, native.stdout, "{word}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshots_check_complete_alias_literals() -> Result<()> {
|
|
let real = "token_ab'cdefghijklmnopqrstuvwx";
|
|
let original = HashMap::from([("VENDOR_PASSWORD".to_string(), real.to_string())]);
|
|
let allowed = HashMap::from([(
|
|
"VENDOR_PASSWORD".to_string(),
|
|
"token_dummy_abcdefghijklmnopqr".to_string(),
|
|
)]);
|
|
let brokered_keys = vec!["VENDOR_PASSWORD".to_string()];
|
|
let controls = (1u8..=31)
|
|
.map(char::from)
|
|
.chain(['\u{7f}', '\u{e9}'])
|
|
.collect::<String>();
|
|
for (shell, aliases) in [("/bin/bash", "alias -p"), ("/bin/zsh", "alias -L")] {
|
|
let shell_type = crate::shell_detect::detect_shell_type(shell).unwrap();
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
for (value, accepted) in [
|
|
(format!("printf '%s' \"{real}\""), false),
|
|
(format!("printf '%s' \"{real}\"\n:"), false),
|
|
(
|
|
"printf %s token_ab\\'cdefghijklmnopqrstuvwx\n:".to_string(),
|
|
false,
|
|
),
|
|
(
|
|
"printf %s 'token_ab'\\''cdefghijklmnopqrstuvwx'\n:".to_string(),
|
|
false,
|
|
),
|
|
(
|
|
"printf '%s' \"ordinary'quoted\"\n# tab\tbell\u{7}escape\u{1b}backslash\\\n:"
|
|
.to_string(),
|
|
true,
|
|
),
|
|
(format!("printf '%s' \"{controls}\""), true),
|
|
] {
|
|
let output = Command::new(shell)
|
|
.args(["-c", &format!("alias helper=\"$ALIAS_VALUE\"; {aliases}")])
|
|
.env_clear()
|
|
.env("ALIAS_VALUE", &value)
|
|
.output()?;
|
|
assert!(output.status.success(), "{shell}: {output:?}");
|
|
let mut snapshot = format!(
|
|
"# aliases\n{}\n# exports 0\n",
|
|
String::from_utf8(output.stdout)?
|
|
);
|
|
let before = snapshot.clone();
|
|
let rewritten = rewrite_snapshot_credentials(
|
|
shell_type,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &brokered_keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &brokered_keys,
|
|
},
|
|
|text| {
|
|
*text = text.replace(real, &allowed["VENDOR_PASSWORD"]);
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(rewritten.is_some(), accepted, "{shell}: {before}");
|
|
if accepted {
|
|
assert_eq!(snapshot, before);
|
|
let expected = if shell_type == ShellType::Zsh {
|
|
let replay = Command::new(shell)
|
|
.args(["-fc", &format!("{snapshot}\nprint -rn -- $aliases[helper]")])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(replay.status.success(), "{replay:?}");
|
|
String::from_utf8(replay.stdout)?
|
|
} else {
|
|
value.clone()
|
|
};
|
|
assert!(
|
|
super::literals::literal_words(&snapshot, shell_type)
|
|
.unwrap()
|
|
.contains(&format!("helper={expected}")),
|
|
"{shell}: {value:?}; {snapshot}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshots_check_prefixed_alias_declarations() -> Result<()> {
|
|
let empty = HashMap::new();
|
|
let real = "token_ab'cd\"efghijklmnopqrstuvwx";
|
|
let body = format!("printf '%s' {}", shlex::try_quote(real)?);
|
|
for (shell, shell_type, setup) in [
|
|
("/bin/bash", ShellType::Bash, "shopt -s expand_aliases"),
|
|
("/bin/zsh", ShellType::Zsh, "setopt posixbuiltins"),
|
|
] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
for (prefix, creates_alias, name) in [
|
|
("alias", true),
|
|
("builtin alias", true),
|
|
("builtin builtin alias", true),
|
|
("time builtin alias", true),
|
|
("command alias", true),
|
|
("command -p -- alias", true),
|
|
("command -pp alias", true),
|
|
("command builtin alias", true),
|
|
("builtin command -p alias", true),
|
|
("command -v alias", false),
|
|
("command -pV alias", false),
|
|
]
|
|
.into_iter()
|
|
.chain((shell_type == ShellType::Bash).then_some(("builtin -- alias", true)))
|
|
.chain((shell_type == ShellType::Bash).then_some(("time -p builtin alias", true)))
|
|
.chain(
|
|
(shell_type == ShellType::Zsh)
|
|
.then_some([
|
|
("noglob builtin alias", true),
|
|
("builtin - alias", true),
|
|
("- builtin alias", true),
|
|
])
|
|
.into_iter()
|
|
.flatten(),
|
|
)
|
|
.flat_map(|(prefix, creates_alias)| {
|
|
["captured", "\"$name\""].map(|name| (prefix, creates_alias, name))
|
|
}) {
|
|
let capture = format!(
|
|
"helper() {{ local name=captured; {prefix} {name}={}; }}\ntypeset -f helper",
|
|
shlex::try_quote(&body)?
|
|
);
|
|
let output = Command::new(shell)
|
|
.args(["-fc", &capture])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(output.status.success(), "{shell}: {output:?}");
|
|
let original = String::from_utf8(output.stdout)?;
|
|
assert!(!original.contains(real), "{shell}: {original}");
|
|
for contains_credential in [false, true] {
|
|
let mut snapshot = original.clone();
|
|
let result = rewrite_snapshot_credentials(
|
|
shell_type,
|
|
&mut snapshot,
|
|
empty_snapshot_credentials(&empty, &|_| false),
|
|
|text| {
|
|
if contains_credential {
|
|
*text = text.replace(real, "dummy");
|
|
}
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(
|
|
result.is_some(),
|
|
!contains_credential || !creates_alias,
|
|
"{shell}, {prefix}: {snapshot}"
|
|
);
|
|
assert_eq!(snapshot, original);
|
|
if !contains_credential && creates_alias {
|
|
let replay = Command::new(shell)
|
|
.args([
|
|
"-fc",
|
|
&format!("{setup}\n{snapshot}\nhelper\neval captured"),
|
|
])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(replay.status.success(), "{shell}, {prefix}: {replay:?}");
|
|
assert_eq!(String::from_utf8(replay.stdout)?, real);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshots_check_array_initializers_and_binary_literals() {
|
|
let empty = HashMap::new();
|
|
let real = "password$abcdefghijklmno";
|
|
for (shell_type, original, has_credential) in [
|
|
(
|
|
ShellType::Bash,
|
|
r#"declare -ax API_HEADERS='([0]="Bearer password\$abcdefghijklmno")'"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"builtin declare -ax API_HEADERS='([0]="Bearer password\$abcdefghijklmno")'"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"command -p typeset -ax API_HEADERS='([0]="Bearer password\$abcdefghijklmno")'"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"declare -Ax API_HEADERS=([auth]="Bearer password\$abcdefghijklmno")"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"helper() { local -a headers; local headers='([0]="Bearer password\$abcdefghijklmno")'; }"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"declare -a headers; builtin declare headers='([0]="Bearer password\$abcdefghijklmno")'"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"declare -a headers; command -p typeset headers='([0]="Bearer password\$abcdefghijklmno")'"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"declare SCALAR_TEMPLATE='([0]="Bearer password\$abcdefghijklmno")'"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"helper() { local name=headers; local -a headers; local "${name}"='([0]="Bearer password\$abcdefghijklmno")'; }"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"helper() { local -a headers; local headers='([0]="Bearer password\$abcdefghijklmno" [1]="'"$OTHER"'")'; }"#,
|
|
),
|
|
(
|
|
ShellType::Bash,
|
|
r#"helper() { local -a headers; local headers=$PREFIX'([0]="Bearer password\$abcdefghijklmno")'$SUFFIX; }"#,
|
|
),
|
|
(
|
|
ShellType::Zsh,
|
|
r#"helper() { printf '%s' $'\x89password\x24abcdefghijklmno\xff'; }"#,
|
|
),
|
|
]
|
|
.into_iter()
|
|
.map(|(shell_type, original)| (shell_type, original, true))
|
|
.chain(
|
|
[
|
|
r#"local TEMPLATE="(\$'\\xZZ')""#,
|
|
r#"local TEMPLATE="(\$'\\uD800')""#,
|
|
r#"local TEMPLATE="(\$'\\UFFFFFFFF')""#,
|
|
r#"local TEMPLATE="(\$'\\c')""#,
|
|
r#"local TEMPLATE="(\$'\\')""#,
|
|
"local TEMPLATE=\"(\\$'\\\\c\u{e9}')\"",
|
|
r#"local TEMPLATE="(\$'incomplete)""#,
|
|
r#"local -a headers; local headers='([0]="Bearer '$CREDENTIAL'")'"#,
|
|
]
|
|
.map(|original| (ShellType::Bash, original, false)),
|
|
) {
|
|
for contains_credential in [false, true] {
|
|
let mut snapshot = original.to_string();
|
|
let result = rewrite_snapshot_credentials(
|
|
shell_type,
|
|
&mut snapshot,
|
|
empty_snapshot_credentials(&empty, &|_| true),
|
|
|text| {
|
|
if contains_credential {
|
|
*text = text.replace(real, "dummy");
|
|
}
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(
|
|
result.is_some(),
|
|
!contains_credential || !has_credential,
|
|
"{original}"
|
|
);
|
|
assert_eq!(snapshot, original);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_sh_snapshots_inspect_the_captured_shell_dialect() -> Result<()> {
|
|
let empty = HashMap::new();
|
|
let real = "password$abcdefghijklmno";
|
|
let script = snapshot_capture_script(ShellType::Sh, CAPTURE_NON_INTERACTIVE).unwrap();
|
|
for (shell, bash_backed) in [("/bin/bash", true), ("/bin/dash", false)] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
for (setup, contains_credential) in [
|
|
(
|
|
r#"helper() { local -a headers; local headers='([0]="Bearer password\$abcdefghijklmno")'; }"#,
|
|
true,
|
|
),
|
|
(
|
|
r#"helper() { time -p builtin alias captured='printf "%s" "password\$abcdefghijklmno"'; }"#,
|
|
true,
|
|
),
|
|
(
|
|
r#"helper() { local name=captured; alias "$name"='printf "%s" "password\$abcdefghijklmno"'; }"#,
|
|
true,
|
|
),
|
|
(
|
|
r#"export TEMPLATE='([0]="Bearer password\$abcdefghijklmno")'"#,
|
|
true,
|
|
),
|
|
(r#"export TEMPLATE="(\$'incomplete)""#, false),
|
|
(
|
|
r#"helper() { local -a headers; local headers='([0]="ordinary\$value")'; }; export TEMPLATE=ordinary"#,
|
|
false,
|
|
),
|
|
] {
|
|
let capture = Command::new(shell)
|
|
.arg0("sh")
|
|
.args(["-c", &format!("{setup}\n{script}")])
|
|
.env_clear()
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.output()?;
|
|
assert!(capture.status.success(), "{shell}: {capture:?}");
|
|
let original = String::from_utf8(capture.stdout)?;
|
|
assert_eq!(
|
|
original.starts_with(super::BASH_SH_SNAPSHOT_HEADER),
|
|
bash_backed
|
|
);
|
|
let mut snapshot = original.clone();
|
|
let result = rewrite_snapshot_credentials(
|
|
ShellType::Sh,
|
|
&mut snapshot,
|
|
empty_snapshot_credentials(&empty, &|_| true),
|
|
|text| {
|
|
*text = text.replace(real, "dummy");
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(
|
|
result.is_some(),
|
|
!bash_backed || !contains_credential,
|
|
"{shell}: {setup}"
|
|
);
|
|
assert_eq!(
|
|
snapshot,
|
|
if result.is_some() {
|
|
captured_script(ShellType::Sh, &original)?
|
|
} else {
|
|
original
|
|
}
|
|
);
|
|
if result.is_some() {
|
|
let replay = Command::new(shell)
|
|
.arg0("sh")
|
|
.args(["-c", &format!("{snapshot}\nprintf '%s' \"${{TEMPLATE-}}\"")])
|
|
.env_clear()
|
|
.output()?;
|
|
let control = Command::new(shell)
|
|
.arg0("sh")
|
|
.args(["-c", &format!("{setup}\nprintf '%s' \"${{TEMPLATE-}}\"")])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(replay.status.success(), "{shell}: {replay:?}");
|
|
assert!(control.status.success(), "{shell}: {control:?}");
|
|
assert_eq!(replay.stdout, control.stdout);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshots_check_native_quoting() -> Result<()> {
|
|
let empty = HashMap::new();
|
|
for (shell, shell_type, aliases) in [
|
|
("/bin/bash", ShellType::Bash, "alias -p"),
|
|
("/bin/zsh", ShellType::Zsh, "alias -L"),
|
|
] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
let mut expressions = vec![
|
|
r#""`printf '%s' 'password\$abcdefghijklmno'`""#,
|
|
r#"`printf '%s' 'password\$abcdefghijklmno'`"#,
|
|
r#""`printf '%s' \`printf '%s' 'password\\\$abcdefghijklmno'\``""#,
|
|
r#""${EMPTY-`printf '%s' 'password\"abcdefghijklmno'`}""#,
|
|
r#"$"token_ab'cdefghijklmnopqrstuvwx""#,
|
|
r#"prefix$"token_ab'cdefghijklmnopqrstuvwx""#,
|
|
r#"$"token_ab'cdefghijklmnopqrstuvwx""${EMPTY-}""#,
|
|
r#"$'token_\c\\abcdefghijklmnopqrstuvwx'"#,
|
|
r#""$(printf '%s' $'token_\c\\abcdefghijklmnopqrstuvwx')""#,
|
|
r#"$'token_\C-Aabcdefghijklmnopqrstuvwx'"#,
|
|
r#"$'token_\C-\x41abcdefghijklmnopqrstuvwx'"#,
|
|
r#"$'token_\C-\101abcdefghijklmnopqrstuvwx'"#,
|
|
r#"$'token_\C-\u0041abcdefghijklmnopqrstuvwx'"#,
|
|
r#"$'token_\C\101abcdefghijklmnopqrstuvwx'"#,
|
|
];
|
|
let mut array_expressions = Vec::new();
|
|
if shell_type == ShellType::Bash {
|
|
expressions.extend([
|
|
r#""$(array_value() { local -a headers; local headers='([0]="password\$abcdefghijklmno")'; printf '%s' "${headers[0]}"; }; array_value)""#,
|
|
r#""$(declare -a headers; builtin declare headers='([0]="password\$abcdefghijklmno")'; printf '%s' "${headers[0]}")""#,
|
|
r#""$(declare -a headers; command -p typeset headers='([0]="password\$abcdefghijklmno")'; printf '%s' "${headers[0]}")""#,
|
|
r#""$(array_value() { local name=headers; local -a headers; local "${name}"='([0]="password\$abcdefghijklmno")'; printf '%s' "${headers[0]}"; }; array_value)""#,
|
|
r#""$(array_value() { local -a headers; local headers='([0]="password\$abcdefghijklmno" [1]="'"$OTHER"'")'; printf '%s' "${headers[0]}"; }; array_value)""#,
|
|
r#""$(array_value() { local -a headers; local headers=$PREFIX'([0]="password\$abcdefghijklmno")'$SUFFIX; printf '%s' "${headers[0]}"; }; array_value)""#,
|
|
]);
|
|
for escape in [r"\xZZ", r"\uD800", r"\UFFFFFFFF", r"\c"] {
|
|
let initializer = format!(r#"([0]=$'{escape}' [1]="password\$abcdefghijklmno")"#);
|
|
let initializer = shlex::try_quote(&initializer)?;
|
|
array_expressions.push(format!(
|
|
"\"$(declare -a headers; declare headers={initializer}; printf '%s' \"${{headers[1]}}\")\""
|
|
));
|
|
}
|
|
} else if shell_type == ShellType::Zsh {
|
|
expressions.extend([
|
|
"password_a$'\\xc3'$'\\xa9'bcdefghijklmno\"${EMPTY-}\"",
|
|
"password_a$'\\M-C'$'\\M-)'bcdefghijklmno\"${EMPTY-}\"",
|
|
"password_a$'\\xc3'\\\n$'\\xa9'bcdefghijklmno\"${EMPTY-}\"",
|
|
"password\\\n\\$abcdefghijklmno",
|
|
"pass\\\nword\\\n\\\n\\$abcdefghijklmno",
|
|
"'password'\\\n'$abcdefghijklmno'",
|
|
"\"password\"\\\n'$abcdefghijklmno'",
|
|
"$'pass'\"word\"\\\n'$abcdefghijklmno'",
|
|
]);
|
|
}
|
|
for expression in expressions
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.chain(array_expressions)
|
|
{
|
|
let body = format!("printf '%s' {expression}");
|
|
let native = Command::new(shell)
|
|
.args(["-fc", &body])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(native.status.success(), "{shell}, {expression}: {native:?}");
|
|
let real = String::from_utf8(native.stdout)?;
|
|
assert!(real.len() >= 16, "{shell}, {expression}: {real:?}");
|
|
let mut captures = vec![
|
|
format!("helper() {{ {body}; }}\ntypeset -f helper"),
|
|
format!("alias helper=\"$ALIAS_VALUE\"\n{aliases}"),
|
|
];
|
|
if shell_type == ShellType::Zsh {
|
|
captures.push(format!("alias -g helper=\"$ALIAS_VALUE\"\n{aliases}"));
|
|
let script = snapshot_capture_script(shell_type, CAPTURE_NON_INTERACTIVE)
|
|
.expect("zsh supports snapshots");
|
|
captures.push(format!(
|
|
"quoted() {{ cat <<'EOF'\n\" ${{(@f)EMPTY:-password\\$abcdefghijklmno}}\nEOF\n}}\nhelper() {{ {body}; }}\n{script}"
|
|
));
|
|
}
|
|
for capture in captures {
|
|
let output = Command::new(shell)
|
|
.args(["-fc", &capture])
|
|
.env_clear()
|
|
.env("ALIAS_VALUE", &body)
|
|
.output()?;
|
|
assert!(output.status.success(), "{shell}: {output:?}");
|
|
let captured = String::from_utf8(output.stdout)?;
|
|
let original = if captured.contains('\0') {
|
|
let capture = CapturedSnapshot::parse(shell_type, captured.as_bytes()).unwrap();
|
|
format!("{}{}", capture.state, capture.aliases)
|
|
} else {
|
|
captured
|
|
};
|
|
for contains_credential in [false, true] {
|
|
let mut snapshot = original.clone();
|
|
let result = rewrite_snapshot_credentials(
|
|
shell_type,
|
|
&mut snapshot,
|
|
empty_snapshot_credentials(&empty, &|_| false),
|
|
|text| {
|
|
if contains_credential {
|
|
*text = text.replace(&real, "dummy");
|
|
}
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(
|
|
result.is_some(),
|
|
!contains_credential,
|
|
"{shell}, {expression}: {snapshot}"
|
|
);
|
|
assert_eq!(snapshot, original);
|
|
if !contains_credential {
|
|
let setup = if shell_type == ShellType::Bash {
|
|
"shopt -s expand_aliases\n"
|
|
} else {
|
|
""
|
|
};
|
|
let replay = Command::new(shell)
|
|
.args(["-fc", &format!("{setup}{snapshot}\neval helper")])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(
|
|
replay.status.success(),
|
|
"{shell}, {expression}: {snapshot}\n{replay:?}"
|
|
);
|
|
assert_eq!(String::from_utf8(replay.stdout)?, real);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn zsh_snapshot_checks_functions_before_restoring_quote_options() -> Result<()> {
|
|
if !std::path::Path::new("/bin/zsh").exists() {
|
|
return Ok(());
|
|
}
|
|
let dir = tempdir()?;
|
|
let real = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
|
|
let original = HashMap::from([("GH_TOKEN".to_string(), real.to_string())]);
|
|
let allowed = HashMap::from([("GH_TOKEN".to_string(), "dummy-github-token".to_string())]);
|
|
let keys = vec!["GH_TOKEN".to_string()];
|
|
let script = snapshot_capture_script(ShellType::Zsh, CAPTURE_NON_INTERACTIVE)
|
|
.expect("zsh supports snapshots");
|
|
for option in ["setopt rcquotes", "unsetopt rcquotes"] {
|
|
for (value, accepted) in [(real, false), ("ordinaryvalue", true)] {
|
|
let (first, second) = value.split_at(value.len() / 2);
|
|
let startup = dir.path().join("startup.sh");
|
|
std::fs::write(
|
|
&startup,
|
|
format!("helper() {{ printf '%s' '{first}''{second}'; }}\n{option}\n"),
|
|
)?;
|
|
let output = Command::new("/bin/zsh")
|
|
.args(["-fc", &format!(". \"$1\"\n{script}"), "snapshot"])
|
|
.arg(startup)
|
|
.env_clear()
|
|
.envs(&original)
|
|
.env("HOME", dir.path())
|
|
.output()?;
|
|
assert!(output.status.success(), "{output:?}");
|
|
let mut snapshot = String::from_utf8(output.stdout)?;
|
|
let rewritten = rewrite_snapshot_credentials(
|
|
ShellType::Zsh,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &keys,
|
|
},
|
|
|text| {
|
|
*text = text.replace(real, &allowed["GH_TOKEN"]);
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(rewritten.is_some(), accepted, "{option}: {snapshot}");
|
|
if accepted {
|
|
let path = dir.path().join("snapshot.sh");
|
|
std::fs::write(&path, &snapshot)?;
|
|
let replay = Command::new("/bin/zsh")
|
|
.args(["-fc", ". \"$1\"; helper", "snapshot"])
|
|
.arg(path)
|
|
.env_clear()
|
|
.envs(&allowed)
|
|
.output()?;
|
|
assert!(replay.status.success(), "{replay:?}");
|
|
assert_eq!(String::from_utf8(replay.stdout)?, value);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn zsh_snapshot_checks_deferred_quote_modes() -> Result<()> {
|
|
if !std::path::Path::new("/bin/zsh").exists() {
|
|
return Ok(());
|
|
}
|
|
let dir = tempdir()?;
|
|
let script = snapshot_capture_script(ShellType::Zsh, CAPTURE_NON_INTERACTIVE)
|
|
.expect("zsh supports snapshots");
|
|
for (real, startup) in [
|
|
(
|
|
"token_ab'cdefghijklmnopqrstuvwx",
|
|
"helper() { printf '%s' \"$(printf '%s' 'token_ab''cdefghijklmnopqrstuvwx')\"; }\nsetopt rcquotes\n",
|
|
),
|
|
(
|
|
"ghp_abcdefghijklmnopqrstuvwxyz0123456789",
|
|
"alias helper=\"setopt rcquotes; printf '%s' 'ghp_abcdefghijklmno''pqrstuvwxyz0123456789'\"\n",
|
|
),
|
|
(
|
|
"ghp_abcdefghijklmnopqrstuvwxyz0123456789",
|
|
"alias helper=\"printf '%s' 'ghp_abcdefghijkl'\\\\\n'mnopqrstuvwxyz0123456789'\"\nsetopt rcquotes\n",
|
|
),
|
|
] {
|
|
let startup_path = dir.path().join("startup.sh");
|
|
std::fs::write(&startup_path, startup)?;
|
|
let output = Command::new("/bin/zsh")
|
|
.args(["-fc", &format!(". \"$1\"\n{script}"), "snapshot"])
|
|
.arg(startup_path)
|
|
.env_clear()
|
|
.env("HOME", dir.path())
|
|
.output()?;
|
|
assert!(output.status.success(), "{output:?}");
|
|
let snapshot = captured_script(ShellType::Zsh, &String::from_utf8(output.stdout)?)?;
|
|
let path = dir.path().join("snapshot.sh");
|
|
std::fs::write(&path, &snapshot)?;
|
|
let replay = Command::new("/bin/zsh")
|
|
.args(["-fc", ". \"$1\"; eval helper", "snapshot"])
|
|
.arg(path)
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(replay.status.success(), "{replay:?}");
|
|
assert_eq!(String::from_utf8(replay.stdout)?, real);
|
|
assert!(
|
|
super::literals::literal_words(&snapshot, ShellType::Zsh)
|
|
.expect("valid snapshot")
|
|
.contains(&real.to_string()),
|
|
"runtime literal was not checked: {snapshot}"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn zsh_snapshot_checks_rcquotes_without_changing_alias_behavior() -> Result<()> {
|
|
if !std::path::Path::new("/bin/zsh").exists() {
|
|
return Ok(());
|
|
}
|
|
let dir = tempdir()?;
|
|
let real = "token_ab'cdefghijklmnopqrstuvwx";
|
|
let original = HashMap::from([("VENDOR_PASSWORD".to_string(), real.to_string())]);
|
|
let allowed = HashMap::from([(
|
|
"VENDOR_PASSWORD".to_string(),
|
|
"token_dummy_abcdefghijklmnopqr".to_string(),
|
|
)]);
|
|
let brokered_keys = vec!["VENDOR_PASSWORD".to_string()];
|
|
let script = snapshot_capture_script(ShellType::Zsh, CAPTURE_NON_INTERACTIVE)
|
|
.expect("zsh supports snapshots");
|
|
for (option, option_value) in [("setopt rcquotes", "on"), ("unsetopt rcquotes", "off")] {
|
|
for (value, accepted) in [(real, false), ("ordinary'quoted", true)] {
|
|
for alias in [
|
|
r#"alias helper="printf '%s' \"$ALIAS_VALUE\"""#,
|
|
r#"alias helper="printf '%s' ${(qq)ALIAS_VALUE}""#,
|
|
r#"alias helper="printf '%s' ${(qq)ALIAS_VALUE}\"\${EMPTY-}\"""#,
|
|
] {
|
|
let output = Command::new("/bin/zsh")
|
|
.args([
|
|
"-fc",
|
|
&format!("{option}\n{alias}\nunset ALIAS_VALUE\n{script}"),
|
|
])
|
|
.env_clear()
|
|
.envs(&original)
|
|
.env("HOME", dir.path())
|
|
.env("ALIAS_VALUE", value)
|
|
.output()?;
|
|
assert!(output.status.success(), "{output:?}");
|
|
let mut snapshot = String::from_utf8(output.stdout)?;
|
|
let rewritten = rewrite_snapshot_credentials(
|
|
ShellType::Zsh,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &brokered_keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &brokered_keys,
|
|
},
|
|
|text| {
|
|
*text = text.replace(real, &allowed["VENDOR_PASSWORD"]);
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(
|
|
rewritten.is_some(),
|
|
accepted,
|
|
"{option}, {alias}: {snapshot}"
|
|
);
|
|
if accepted {
|
|
let path = dir.path().join("snapshot.sh");
|
|
std::fs::write(&path, &snapshot)?;
|
|
let replay = Command::new("/bin/zsh")
|
|
.args([
|
|
"-fc",
|
|
". \"$1\"; eval helper; printf '\\n%s\\n' $options[rcquotes]",
|
|
"snapshot",
|
|
])
|
|
.arg(path)
|
|
.env_clear()
|
|
.envs(&allowed)
|
|
.output()?;
|
|
assert!(replay.status.success(), "{replay:?}");
|
|
assert_eq!(
|
|
String::from_utf8(replay.stdout)?,
|
|
format!("{value}\n{option_value}\n")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshots_check_static_fragments_next_to_expansions() -> Result<()> {
|
|
let real = "password$abcdefghijklmno";
|
|
let dir = tempdir()?;
|
|
let invocation = dir.path().join("invoke.sh");
|
|
std::fs::write(&invocation, "helper\n")?;
|
|
let inner_invocation = dir.path().join("inner-invoke.sh");
|
|
std::fs::write(&inner_invocation, "inner\n")?;
|
|
for (shell, aliases) in [("/bin/bash", "alias -p"), ("/bin/zsh", "alias -L")] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
for (body, accepted) in [
|
|
(r#"printf '%s' "${USER}:password\$abcdefghijklmno""#, false),
|
|
(r#"printf '%s' "${USER}:pass"'word$abcdefghijklmno'"#, false),
|
|
(r#"printf '%s' "password\$abcdefghijklmno:${USER}""#, false),
|
|
(
|
|
r#"printf '%s' "${USER}:"$'password\x24abcdefghijklmno'"#,
|
|
false,
|
|
),
|
|
(r#"printf '%s' "${USER}:$VENDOR_PASSWORD""#, true),
|
|
(r#"printf '%s' "${USER}:ordinary\$value""#, true),
|
|
(
|
|
"cat <<EOF\nAuthorization: password\\$abcdefghijklmno\nEOF\n",
|
|
false,
|
|
),
|
|
(
|
|
"cat <<EOF\nAuthorization: pass\\\nword\\$abcdefghijklmno\nEOF\n",
|
|
false,
|
|
),
|
|
(
|
|
"cat <<EOF\n${USER}:password\\$abcdefghijklmno\nEOF\n",
|
|
false,
|
|
),
|
|
("cat <<-EOF\n\tpassword\\$abcdefghijklmno\n\tEOF\n", false),
|
|
("cat <<'EOF'\npassword\\$abcdefghijklmno\nEOF\n", true),
|
|
("cat <<EOF\n${USER}:$VENDOR_PASSWORD\nEOF\n", true),
|
|
("cat <<EOF\nordinary\\$value\nEOF\n", true),
|
|
]
|
|
.into_iter()
|
|
.chain(
|
|
(shell == "/bin/zsh")
|
|
.then_some([
|
|
(
|
|
"export GH_COPY=password\\\n\\$abcdefghijklmno; printf '%s' \"$GH_COPY\"",
|
|
false,
|
|
),
|
|
(
|
|
"GH_COPY=password\\\n\\$abcdefghijklmno; printf '%s' \"$GH_COPY\"",
|
|
false,
|
|
),
|
|
(
|
|
"password=''\\\n'$abcdefghijklmno'; printf '%s' \"$password\"",
|
|
true,
|
|
),
|
|
(
|
|
"ali\\\nas inner='printf \"%s\" \"password\\$abcdefghijklmno\"'",
|
|
false,
|
|
),
|
|
(
|
|
"builtin ali\\\nas inner='printf \"%s\" \"password\\$abcdefghijklmno\"'",
|
|
false,
|
|
),
|
|
(
|
|
"alias inner='printf \"%s\" \"password'\\\n'\\$abcdefghijklmno\"'",
|
|
false,
|
|
),
|
|
(
|
|
"alias inner='printf \"%s\" \"password'\\\n'\\$abcdefghijklmno\"'\"${EMPTY}\"",
|
|
false,
|
|
),
|
|
(
|
|
"alias inner=\"${EMPTY}\"'printf \"%s\" \"password'\\\n'\\$abcdefghijklmno\"'",
|
|
false,
|
|
),
|
|
(
|
|
"alias inner='printf \"%s\" \"password'\"${EMPTY}\"\\\n'\\$abcdefghijklmno\"'",
|
|
true,
|
|
),
|
|
(
|
|
"alias inner='printf \"%s\" \"ordinary'\\\n'value\"'\"${EMPTY}\"",
|
|
true,
|
|
),
|
|
(
|
|
"printf '%s' '${USER}:pass'\\\n'word$abcdefghijklmno'",
|
|
false,
|
|
),
|
|
(
|
|
"printf '%s' \"${USER}:pass\"\\\n'word$abcdefghijklmno'",
|
|
false,
|
|
),
|
|
(
|
|
"printf '%s' 'password'\\\n\"${USER}\\$abcdefghijklmno\"",
|
|
true,
|
|
),
|
|
("printf '%s' 'password\n$abcdefghijklmno'", true),
|
|
("printf '%s' 'password\\\n$abcdefghijklmno'", true),
|
|
("printf '%s' \"password\n\\$abcdefghijklmno\"", true),
|
|
("printf '%s' password\\\\\\\n\\$abcdefghijklmno", true),
|
|
("printf '%s ' password \\\n\\$abcdefghijklmno", true),
|
|
("printf '%s ' password\\\n \\$abcdefghijklmno", true),
|
|
])
|
|
.into_iter()
|
|
.flatten(),
|
|
) {
|
|
for script in [
|
|
format!("helper() {{\n{body}\n}}; typeset -f helper"),
|
|
format!("alias helper=\"$ALIAS_VALUE\"; {aliases}"),
|
|
] {
|
|
let output = Command::new(shell)
|
|
.args(["-c", &script])
|
|
.env_clear()
|
|
.env("ALIAS_VALUE", body)
|
|
.output()?;
|
|
assert!(output.status.success(), "{shell}: {output:?}");
|
|
let mut snapshot = String::from_utf8(output.stdout)?;
|
|
let original = snapshot.clone();
|
|
let result = rewrite_snapshot_credentials(
|
|
crate::shell_detect::detect_shell_type(shell).unwrap(),
|
|
&mut snapshot,
|
|
empty_snapshot_credentials(&HashMap::new(), &|_| false),
|
|
|text| {
|
|
*text = text.replace(real, "dummy");
|
|
true
|
|
},
|
|
);
|
|
assert_eq!(result.is_some(), accepted, "{shell}: {original}");
|
|
assert_eq!(snapshot, original);
|
|
let invokes_inner = body.contains("inner=");
|
|
if shell == "/bin/zsh" && (accepted || invokes_inner) {
|
|
let path = dir.path().join("snapshot.sh");
|
|
std::fs::write(&path, &snapshot)?;
|
|
// Source each invocation after its alias has been defined, without eval.
|
|
let replay_command = if invokes_inner {
|
|
". \"$1\"; . \"$2\"; . \"$3\""
|
|
} else {
|
|
". \"$1\"; . \"$2\""
|
|
};
|
|
let replay = Command::new(shell)
|
|
.args(["-c", replay_command, "snapshot"])
|
|
.args([&path, &invocation, &inner_invocation])
|
|
.env_clear()
|
|
.output()?;
|
|
let native_command = if invokes_inner {
|
|
format!("{body}\n. \"$1\"")
|
|
} else {
|
|
body.to_string()
|
|
};
|
|
let native = Command::new(shell)
|
|
.args(["-c", &native_command, "native"])
|
|
.arg(&inner_invocation)
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(replay.status.success(), "{shell}: {replay:?}");
|
|
assert!(native.status.success(), "{shell}: {native:?}");
|
|
assert_eq!(replay.stdout, native.stdout, "{shell}: {body}");
|
|
if !accepted && invokes_inner {
|
|
assert_eq!(native.stdout, real.as_bytes(), "{body}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_bash_snapshot_does_not_rewrite_function_syntax() -> Result<()> {
|
|
let dir = tempdir()?;
|
|
let startup = dir.path().join("startup.sh");
|
|
let real = "ghp_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH";
|
|
let dummy = "ghp_brokered_dummy_abcdefghijklmnopqrstuvwxyz0123";
|
|
let credential_shaped_function = "ghp_0123456789abcdefghijklmnopqrstuvwxyzABCD";
|
|
std::fs::write(
|
|
&startup,
|
|
format!(
|
|
"helper() {{\n cat <<'EOF'\nexport LEGACY_SETTING=production\nAuthorization: {real}\nEOF\n}}\n{credential_shaped_function}() {{ printf UNEXPECTED_EXECUTION; }}\nexport -f helper\nexport GH_TOKEN='{real}'\n"
|
|
),
|
|
)?;
|
|
let output = Command::new("/bin/bash")
|
|
.arg("-c")
|
|
.arg(
|
|
snapshot_capture_script(ShellType::Bash, CAPTURE_ALL).expect("bash supports snapshots"),
|
|
)
|
|
.env_clear()
|
|
.env("HOME", dir.path())
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.env("BASH_ENV", &startup)
|
|
.output()?;
|
|
assert!(output.status.success());
|
|
|
|
let mut snapshot = String::from_utf8(output.stdout)?;
|
|
let original = HashMap::from([("GH_TOKEN".to_string(), real.to_string())]);
|
|
let discovered = HashMap::from([("GH_TOKEN".to_string(), dummy.to_string())]);
|
|
let brokered_keys = vec!["GH_TOKEN".to_string()];
|
|
let snapshot_is_safe = rewrite_snapshot_credentials(
|
|
ShellType::Bash,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &HashMap::new(),
|
|
discovered: &discovered,
|
|
allowed: &discovered,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &brokered_keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &brokered_keys,
|
|
},
|
|
|text| {
|
|
if text.contains(credential_shaped_function) {
|
|
*text = text.replace(credential_shaped_function, "");
|
|
return false;
|
|
}
|
|
*text = text.replace(real, dummy);
|
|
true
|
|
},
|
|
);
|
|
assert!(snapshot_is_safe.is_none());
|
|
assert!(snapshot.contains(real));
|
|
assert!(snapshot.contains(credential_shaped_function));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_zsh_tied_alias_respects_policy_override() -> Result<()> {
|
|
if !std::path::Path::new("/bin/zsh").exists() {
|
|
return Ok(());
|
|
}
|
|
let real = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
|
|
let dummy = "ghp_dummy_abcdefghijklmnopqrstuvwxyz0123";
|
|
let keys = vec!["GH_TOKEN".to_string()];
|
|
let script = snapshot_capture_script(ShellType::Zsh, CAPTURE_NON_INTERACTIVE).unwrap();
|
|
for (replacement, source_present, source_allowed) in ["", "replacement|plain|replacement"]
|
|
.into_iter()
|
|
.flat_map(|replacement| {
|
|
[
|
|
(replacement, true, true),
|
|
(replacement, true, false),
|
|
(replacement, false, false),
|
|
]
|
|
})
|
|
{
|
|
let unset_source = if source_present { "" } else { "unset GH_TOKEN" };
|
|
let output = Command::new("/bin/zsh")
|
|
.args([
|
|
"-fc",
|
|
&format!(
|
|
"export -UT AUTH_HEADER auth_header=(\"Bearer $GH_TOKEN\" plain) '|'\n{unset_source}\n{script}"
|
|
),
|
|
])
|
|
.env_clear()
|
|
.env("GH_TOKEN", real)
|
|
.output()?;
|
|
assert!(output.status.success(), "{output:?}");
|
|
let mut snapshot = String::from_utf8(output.stdout)?;
|
|
let mut original = HashMap::from([
|
|
("GH_TOKEN".to_string(), real.to_string()),
|
|
("AUTH_HEADER".to_string(), format!("Bearer {real}|plain")),
|
|
]);
|
|
let brokered_keys = if source_present {
|
|
keys.as_slice()
|
|
} else {
|
|
original.remove("GH_TOKEN");
|
|
&[]
|
|
};
|
|
let discovered = original
|
|
.iter()
|
|
.map(|(key, value)| (key.clone(), value.replace(real, dummy)))
|
|
.collect::<HashMap<_, _>>();
|
|
let configured = HashMap::from([("AUTH_HEADER".to_string(), replacement.to_string())]);
|
|
let mut allowed = discovered.clone();
|
|
allowed.extend(configured.clone());
|
|
let allowed_keys = if source_allowed {
|
|
keys.as_slice()
|
|
} else {
|
|
allowed.remove("GH_TOKEN");
|
|
&[]
|
|
};
|
|
let prepared = rewrite_snapshot_credentials(
|
|
ShellType::Zsh,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &configured,
|
|
discovered: &discovered,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| true,
|
|
brokered_keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: allowed_keys,
|
|
},
|
|
|value| {
|
|
if !source_allowed && (value.contains(real) || value.contains(dummy)) {
|
|
return false;
|
|
}
|
|
*value = value.replace(real, dummy);
|
|
true
|
|
},
|
|
)
|
|
.expect("policy override keeps the tied alias usable");
|
|
assert_eq!(
|
|
prepared.aliases,
|
|
if source_present {
|
|
configured
|
|
} else {
|
|
HashMap::new()
|
|
}
|
|
);
|
|
assert!(!snapshot.contains(real));
|
|
let inspect = "printf '%s\\0' \"$AUTH_HEADER\" \"${auth_header[@]}\"; AUTH_HEADER='next|next|last'; printf '%s\\0' \"$AUTH_HEADER\" \"${auth_header[@]}\"";
|
|
let mut outputs = Vec::new();
|
|
for setup in [snapshot.as_str(), "export -UT AUTH_HEADER auth_header '|'"] {
|
|
let output = Command::new("/bin/zsh")
|
|
.args(["-fc", &format!("set -e; {setup}\n{inspect}")])
|
|
.env_clear()
|
|
.env("AUTH_HEADER", replacement)
|
|
.output()?;
|
|
assert!(output.status.success(), "{output:?}");
|
|
outputs.push(output.stdout);
|
|
}
|
|
assert_eq!(outputs[0], outputs[1]);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshot_alias_values_follow_allowed_credential_overrides() {
|
|
let real = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
|
|
let discovered_dummy = "ghp_discovered_dummy_abcdefghijklmnopqr";
|
|
let configured_dummy = "ghp_configured_dummy_abcdefghijklmnopqr";
|
|
let original = HashMap::from([
|
|
("GH_TOKEN".to_string(), real.to_string()),
|
|
("AUTH_HEADER".to_string(), format!("Bearer {real}")),
|
|
]);
|
|
let discovered = HashMap::from([("GH_TOKEN".to_string(), discovered_dummy.to_string())]);
|
|
let brokered_keys = vec!["GH_TOKEN".to_string()];
|
|
for (source_allowed, alias, expected) in [
|
|
(
|
|
true,
|
|
format!("Bearer {discovered_dummy}"),
|
|
Some((
|
|
format!("Bearer {configured_dummy}"),
|
|
r#"export AUTH_HEADER='Bearer '"${GH_TOKEN-}""#,
|
|
)),
|
|
),
|
|
(
|
|
true,
|
|
String::new(),
|
|
Some((String::new(), "export AUTH_HEADER=''")),
|
|
),
|
|
(false, format!("Bearer {discovered_dummy}"), None),
|
|
] {
|
|
let mut allowed = HashMap::from([
|
|
("GH_TOKEN".to_string(), configured_dummy.to_string()),
|
|
("AUTH_HEADER".to_string(), alias),
|
|
]);
|
|
let allowed_brokered_keys = if source_allowed {
|
|
brokered_keys.as_slice()
|
|
} else {
|
|
allowed.remove("GH_TOKEN");
|
|
&[]
|
|
};
|
|
let mut snapshot =
|
|
format!("# Snapshot file\n\0\0AUTH_HEADER\0export AUTH_HEADER='Bearer {real}'\n\0\0");
|
|
let prepared = rewrite_snapshot_credentials(
|
|
ShellType::Bash,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &original,
|
|
configured: &HashMap::new(),
|
|
discovered: &discovered,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &brokered_keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys,
|
|
},
|
|
|text| {
|
|
*text = text.replace(real, discovered_dummy);
|
|
true
|
|
},
|
|
);
|
|
let expected = match expected {
|
|
Some((value, assignment)) => (
|
|
format!("# Snapshot file\n# exports (native declarations)\n{assignment}\n"),
|
|
HashMap::from([("AUTH_HEADER".to_string(), value)]),
|
|
Vec::new(),
|
|
),
|
|
None => (
|
|
"# Snapshot file\n".to_string(),
|
|
HashMap::new(),
|
|
vec!["AUTH_HEADER".to_string()],
|
|
),
|
|
};
|
|
assert_eq!(
|
|
prepared.map(|rewrite| (rewrite.script, rewrite.aliases, rewrite.rejected_alias_keys)),
|
|
Some(expected),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_short_alias_respects_policy_override() -> Result<()> {
|
|
let real = "pin_abcdefgh";
|
|
let dummy = "pin_qrstuvwx";
|
|
let keys = vec!["VENDOR_PASSWORD".to_string()];
|
|
for captured in [real, "pin_ijklmnop"] {
|
|
let original = HashMap::from([
|
|
("VENDOR_PASSWORD".to_string(), captured.to_string()),
|
|
("AUTH_HEADER".to_string(), format!("Bearer {captured}")),
|
|
]);
|
|
let restored = HashMap::from([
|
|
("VENDOR_PASSWORD".to_string(), real.to_string()),
|
|
("AUTH_HEADER".to_string(), format!("Bearer {real}")),
|
|
]);
|
|
let discovered = HashMap::from([
|
|
("VENDOR_PASSWORD".to_string(), dummy.to_string()),
|
|
("AUTH_HEADER".to_string(), format!("Bearer {dummy}")),
|
|
]);
|
|
for replacement in ["", "replacement"] {
|
|
let configured = HashMap::from([("AUTH_HEADER".to_string(), replacement.to_string())]);
|
|
let mut allowed = discovered.clone();
|
|
allowed.extend(configured.clone());
|
|
let mut snapshot = format!(
|
|
"# Snapshot file\n\0\0AUTH_HEADER\0export AUTH_HEADER='Bearer {captured}'\n\0\0"
|
|
);
|
|
let prepared = rewrite_snapshot_credentials(
|
|
ShellType::Bash,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &restored,
|
|
configured: &configured,
|
|
discovered: &discovered,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &keys,
|
|
},
|
|
|value| {
|
|
*value = value.replace(real, dummy).replace(captured, dummy);
|
|
true
|
|
},
|
|
)
|
|
.expect("policy override keeps short aliases usable");
|
|
assert_eq!(prepared.aliases, configured);
|
|
assert!(!snapshot.contains(real));
|
|
assert!(!snapshot.contains(captured));
|
|
let output = Command::new("/bin/bash")
|
|
.args(["-c", &format!("{snapshot}\nprintf '%s' \"$AUTH_HEADER\"")])
|
|
.env_clear()
|
|
.output()?;
|
|
assert!(output.status.success(), "{output:?}");
|
|
assert_eq!(output.stdout, replacement.as_bytes());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshot_aliases_follow_captured_dummies() -> Result<()> {
|
|
for (real, captured, dummy) in [
|
|
(
|
|
"ghp_real_abcdefghijklmnopqrstuvwxyz012345",
|
|
"ghp_old_abcdefghijklmnopqrstuvwxyz0123456",
|
|
"ghp_new_abcdefghijklmnopqrstuvwxyz0123456",
|
|
),
|
|
("pin_abcdefgh", "pin_ijklmnop", "pin_ijklmnop"),
|
|
("pin_abcdefgh", "pin_ijklmnop", "pin_qrstuvwx"),
|
|
] {
|
|
let original = HashMap::from([
|
|
("VENDOR_PASSWORD".to_string(), captured.to_string()),
|
|
("AUTH_HEADER".to_string(), format!("Bearer {captured}")),
|
|
("READONLY_HEADER".to_string(), format!("Bearer {captured}")),
|
|
]);
|
|
let mut restored = original.clone();
|
|
restored.insert("VENDOR_PASSWORD".to_string(), real.to_string());
|
|
let allowed = HashMap::from([
|
|
("VENDOR_PASSWORD".to_string(), dummy.to_string()),
|
|
("AUTH_HEADER".to_string(), format!("Bearer {dummy}")),
|
|
("READONLY_HEADER".to_string(), format!("Bearer {dummy}")),
|
|
]);
|
|
let keys = vec!["VENDOR_PASSWORD".to_string()];
|
|
let mut snapshot = format!(
|
|
"# Snapshot file\n\0\0AUTH_HEADER\0export AUTH_HEADER='Bearer {captured}'\n\0READONLY_HEADER\0declare -rx READONLY_HEADER='Bearer {captured}'\n\0\0"
|
|
);
|
|
let rewrite = rewrite_snapshot_credentials(
|
|
ShellType::Bash,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &restored,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &keys,
|
|
},
|
|
|text| {
|
|
*text = text
|
|
.replace(&format!("Bearer {real}"), &format!("Bearer {dummy}"))
|
|
.replace(&format!("Bearer {captured}"), &format!("Bearer {dummy}"));
|
|
true
|
|
},
|
|
)
|
|
.expect("captured dummy aliases should be rewritten");
|
|
assert_eq!(
|
|
rewrite.aliases,
|
|
HashMap::from([
|
|
("AUTH_HEADER".to_string(), format!("Bearer {dummy}")),
|
|
("READONLY_HEADER".to_string(), format!("Bearer {dummy}")),
|
|
])
|
|
);
|
|
let replay = Command::new("/bin/bash")
|
|
.args([
|
|
"-c",
|
|
&format!("{snapshot}\nprintf '%s\\n%s' \"$AUTH_HEADER\" \"$READONLY_HEADER\""),
|
|
])
|
|
.env_clear()
|
|
.env("VENDOR_PASSWORD", real)
|
|
.output()?;
|
|
assert!(replay.status.success(), "{replay:?}");
|
|
assert_eq!(
|
|
replay.stdout,
|
|
format!("Bearer {real}\nBearer {real}").as_bytes()
|
|
);
|
|
if std::path::Path::new("/bin/zsh").exists() {
|
|
let script = snapshot_capture_script(ShellType::Zsh, CAPTURE_NON_INTERACTIVE).unwrap();
|
|
let capture_output = Command::new("/bin/zsh")
|
|
.args(["-fc", &format!("readonly READONLY_HEADER\n{script}")])
|
|
.env_clear()
|
|
.envs(&original)
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.output()?;
|
|
assert!(capture_output.status.success(), "{capture_output:?}");
|
|
let mut snapshot = String::from_utf8(capture_output.stdout)?;
|
|
let rewritten = rewrite_snapshot_credentials(
|
|
ShellType::Zsh,
|
|
&mut snapshot,
|
|
SnapshotCredentialEnvironment {
|
|
original: &original,
|
|
restored: &restored,
|
|
configured: &HashMap::new(),
|
|
discovered: &allowed,
|
|
allowed: &allowed,
|
|
is_allowed_unset: &|_| false,
|
|
brokered_keys: &keys,
|
|
brokered_alias_keys: &[],
|
|
allowed_brokered_keys: &keys,
|
|
},
|
|
|text| {
|
|
*text = text.replace(real, dummy).replace(captured, dummy);
|
|
true
|
|
},
|
|
)
|
|
.expect("omitted readonly exports retain their alias metadata");
|
|
assert_eq!(rewritten.aliases, rewrite.aliases);
|
|
let replay = Command::new("/bin/zsh")
|
|
.args(["-fc", &format!("readonly READONLY_HEADER\n{snapshot}\n[[ $parameters[READONLY_HEADER] == *readonly* ]] || exit 1\nprintf '%s' \"$READONLY_HEADER\"")])
|
|
.env_clear()
|
|
.env("VENDOR_PASSWORD", real)
|
|
.env("READONLY_HEADER", format!("Bearer {real}"))
|
|
.output()?;
|
|
assert!(replay.status.success(), "{replay:?}");
|
|
assert_eq!(replay.stdout, format!("Bearer {real}").as_bytes());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn brokered_snapshot_rejects_shell_escaped_credentials() {
|
|
let real = "token_ab'cdefgh";
|
|
let dummy = "token_xy'zabcde";
|
|
let mut snapshot = format!(
|
|
"# Snapshot file\n# aliases 1\nalias helper='tool {escaped}'\n# exports 0\n",
|
|
escaped = real.replace('\'', r#"'\''"#),
|
|
);
|
|
let original = snapshot.clone();
|
|
|
|
let snapshot_is_safe = rewrite_snapshot_credentials(
|
|
ShellType::Bash,
|
|
&mut snapshot,
|
|
empty_snapshot_credentials(&HashMap::new(), &|_| false),
|
|
|text| {
|
|
*text = text.replace(real, dummy);
|
|
true
|
|
},
|
|
);
|
|
|
|
assert!(snapshot_is_safe.is_none());
|
|
assert_eq!(snapshot, original);
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn zsh_snapshot_restores_tied_path() -> Result<()> {
|
|
let dir = tempdir()?;
|
|
let path_with_spaces = dir.path().join("path with spaces").join("bin");
|
|
let plain_path = dir.path().join("plain-path").join("bin");
|
|
let expected_path = format!(
|
|
"{}:{}:/usr/bin:/bin",
|
|
path_with_spaces.display(),
|
|
plain_path.display()
|
|
);
|
|
let zshrc = format!(
|
|
"export -UT PATH path=('{}' '{}' '{}' /usr/bin /bin)\n",
|
|
path_with_spaces.display(),
|
|
plain_path.display(),
|
|
plain_path.display()
|
|
);
|
|
std::fs::write(dir.path().join(".zshrc"), zshrc)?;
|
|
|
|
let snapshot = Command::new("/bin/zsh")
|
|
.arg("-f")
|
|
.arg("-c")
|
|
.arg(snapshot_script(ShellType::Zsh).expect("zsh supports snapshots"))
|
|
.env_clear()
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.env("ZDOTDIR", dir.path())
|
|
.output()?;
|
|
assert!(snapshot.status.success());
|
|
|
|
let snapshot_path = dir.path().join("snapshot.sh");
|
|
let snapshot = captured_script(ShellType::Zsh, &String::from_utf8(snapshot.stdout)?)?;
|
|
std::fs::write(&snapshot_path, &snapshot)?;
|
|
|
|
let restored = Command::new("/bin/zsh")
|
|
.arg("-f")
|
|
.arg("-c")
|
|
.arg("set -e; . \"$1\"; print -r -- \"$PATH\"")
|
|
.arg("zsh")
|
|
.arg(&snapshot_path)
|
|
.env_clear()
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.output()?;
|
|
assert!(restored.status.success());
|
|
assert_eq!(
|
|
String::from_utf8(restored.stdout)?.trim_end(),
|
|
expected_path
|
|
);
|
|
|
|
assert!(
|
|
snapshot
|
|
.lines()
|
|
.any(|line| line.starts_with("export -UT PATH path=")),
|
|
"snapshot should capture the tied PATH export"
|
|
);
|
|
|
|
std::fs::write(dir.path().join(".zshrc"), "readonly PATH\n")?;
|
|
let readonly_snapshot = Command::new("/bin/zsh")
|
|
.arg("-f")
|
|
.arg("-c")
|
|
.arg(snapshot_script(ShellType::Zsh).expect("zsh supports snapshots"))
|
|
.env_clear()
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.env("ZDOTDIR", dir.path())
|
|
.output()?;
|
|
assert!(readonly_snapshot.status.success());
|
|
let readonly_snapshot = captured_script(
|
|
ShellType::Zsh,
|
|
&String::from_utf8(readonly_snapshot.stdout)?,
|
|
)?;
|
|
std::fs::write(&snapshot_path, &readonly_snapshot)?;
|
|
|
|
let readonly_restored = Command::new("/bin/zsh")
|
|
.arg("-f")
|
|
.arg("-c")
|
|
.arg("set -e; . \"$1\"; export PATH='/codex-path':\"$PATH\"; print -r -- \"$PATH\"")
|
|
.arg("zsh")
|
|
.arg(&snapshot_path)
|
|
.env_clear()
|
|
.env("PATH", "/usr/bin:/bin")
|
|
.output()?;
|
|
assert!(readonly_restored.status.success());
|
|
assert_eq!(
|
|
String::from_utf8(readonly_restored.stdout)?.trim_end(),
|
|
"/codex-path:/usr/bin:/bin"
|
|
);
|
|
|
|
assert!(
|
|
!readonly_snapshot
|
|
.lines()
|
|
.any(|line| line.starts_with("export -rT PATH path=")),
|
|
"snapshot should not capture the readonly tied PATH export"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Literal-only fixtures are shell source rather than capture output. Give those fixtures
|
|
// empty alias/export sections; real shell fixtures exercise the full framed capture.
|
|
fn rewrite_snapshot_credentials(
|
|
shell_type: ShellType,
|
|
snapshot: &mut String,
|
|
environment: SnapshotCredentialEnvironment<'_>,
|
|
virtualize_text: impl FnMut(&mut String) -> bool,
|
|
) -> Option<PreparedSnapshot> {
|
|
let source_only = !snapshot.contains('\0');
|
|
let framed = if source_only {
|
|
format!("# Snapshot file\n{snapshot}\0\0\0")
|
|
} else {
|
|
snapshot.clone()
|
|
};
|
|
let capture = CapturedSnapshot::parse(shell_type, framed.as_bytes())?;
|
|
let prepared = prepare_snapshot_credentials(&capture, environment, virtualize_text)?;
|
|
*snapshot = if source_only {
|
|
prepared
|
|
.script
|
|
.strip_prefix("# Snapshot file\n")?
|
|
.to_string()
|
|
} else {
|
|
prepared.script.clone()
|
|
};
|
|
Some(prepared)
|
|
}
|
|
|
|
fn captured_script(shell_type: ShellType, source: &str) -> Result<String> {
|
|
let captured =
|
|
CapturedSnapshot::parse(shell_type, source.as_bytes()).context("invalid native capture")?;
|
|
Ok(captured.render_script(&codex_protocol::config_types::ShellEnvironmentPolicy::default()))
|
|
}
|
|
|
|
#[test]
|
|
fn streamed_snapshot_preserves_alias_and_option_parse_order() -> Result<()> {
|
|
for (shell, shell_type, options, expected) in [
|
|
(
|
|
"/bin/bash",
|
|
ShellType::Bash,
|
|
"shopt -s expand_aliases",
|
|
"onetwohelper",
|
|
),
|
|
(
|
|
"/bin/zsh",
|
|
ShellType::Zsh,
|
|
"module_path=()\nsetopt RC_QUOTES",
|
|
"one'twohelper",
|
|
),
|
|
(
|
|
"/bin/zsh",
|
|
ShellType::Zsh,
|
|
"module_path=()\nsetopt NO_RC_QUOTES",
|
|
"onetwohelper",
|
|
),
|
|
] {
|
|
if !std::path::Path::new(shell).exists() {
|
|
continue;
|
|
}
|
|
let dir = tempdir()?;
|
|
let setup = format!(
|
|
"{options}\nhelper() {{ printf helper; }}\nfunction : () {{ exit 41; }}\nalias snapshot_alias=\"printf 'one''two'\"\n"
|
|
);
|
|
let mut results = Vec::new();
|
|
// Cover V1's native source, V2's source, and V2's environment fallback.
|
|
for (source_capture, source_replay) in [(false, true), (true, true), (true, false)] {
|
|
let capture = if source_capture {
|
|
super::snapshot_source_capture_script(shell_type, CAPTURE_NON_INTERACTIVE)
|
|
} else {
|
|
snapshot_capture_script(shell_type, CAPTURE_NON_INTERACTIVE)
|
|
}
|
|
.unwrap();
|
|
let shadow = if source_capture && shell_type == ShellType::Zsh {
|
|
"alias alias=false\n"
|
|
} else {
|
|
""
|
|
};
|
|
let output = Command::new(shell)
|
|
.args(["-c", &format!("{setup}{shadow}{capture}")])
|
|
.output()?;
|
|
assert!(output.status.success(), "{shell}: {output:?}");
|
|
let captured = CapturedSnapshot::parse(shell_type, &output.stdout).unwrap();
|
|
let path = dir.path().join("state");
|
|
std::fs::write(&path, captured.render_state())?;
|
|
let restore = if source_replay {
|
|
". \"$1\""
|
|
} else {
|
|
"eval \"$(cat \"$1\")\""
|
|
};
|
|
let command =
|
|
format!("{restore}\n\\alias snapshot_alias\neval snapshot_alias\nhelper\n");
|
|
let output = Command::new(shell)
|
|
.args(["-c", &command, "snapshot-test"])
|
|
.arg(&path)
|
|
.output()?;
|
|
assert!(output.status.success(), "{shell}: {output:?}");
|
|
assert!(String::from_utf8_lossy(&output.stdout).contains("snapshot_alias="));
|
|
assert!(String::from_utf8_lossy(&output.stdout).ends_with(expected));
|
|
results.push((output.stdout, output.stderr));
|
|
}
|
|
assert_eq!(results[0], results[1], "{shell}");
|
|
assert_eq!(results[0], results[2], "{shell}");
|
|
}
|
|
Ok(())
|
|
}
|