mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
shell-command: parse literal PowerShell reads
This commit is contained in:
@@ -2,6 +2,7 @@ use crate::bash::extract_bash_command;
|
||||
use crate::bash::try_parse_shell;
|
||||
use crate::bash::try_parse_word_only_commands_sequence;
|
||||
use crate::powershell::extract_powershell_command;
|
||||
use crate::powershell::extract_powershell_literal_read_path;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use shlex::split as shlex_split;
|
||||
use shlex::try_join as shlex_try_join;
|
||||
@@ -1257,19 +1258,68 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn powershell_with_path_is_stripped() {
|
||||
let command = if cfg!(windows) {
|
||||
"C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
||||
} else {
|
||||
"/usr/local/bin/powershell.exe"
|
||||
};
|
||||
|
||||
assert_parsed(
|
||||
&vec_str(&[command, "-NoProfile", "-c", "Write-Host hi"]),
|
||||
&vec_str(&[
|
||||
r"C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe",
|
||||
"-NoProfile",
|
||||
"-c",
|
||||
"Write-Host hi",
|
||||
]),
|
||||
vec![ParsedCommand::Unknown {
|
||||
cmd: "Write-Host hi".to_string(),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powershell_literal_get_content_is_read_without_the_host_shell() {
|
||||
let script = r"Get-Content 'C:\codex runtime\AGENTS.md' -ErrorAction Stop";
|
||||
assert_parsed(
|
||||
&vec_str(&[
|
||||
r"C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
script,
|
||||
]),
|
||||
vec![ParsedCommand::Read {
|
||||
cmd: script.to_string(),
|
||||
name: "AGENTS.md".to_string(),
|
||||
path: PathBuf::from(r"C:\codex runtime\AGENTS.md"),
|
||||
}],
|
||||
);
|
||||
|
||||
let alias_script = r"gc '.\input.txt'";
|
||||
assert_parsed(
|
||||
&vec_str(&["pwsh.exe", "-Command", alias_script]),
|
||||
vec![ParsedCommand::Read {
|
||||
cmd: alias_script.to_string(),
|
||||
name: "input.txt".to_string(),
|
||||
path: PathBuf::from(r".\input.txt"),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powershell_dynamic_or_compound_get_content_stays_unknown() {
|
||||
for script in [
|
||||
"Get-Content $path",
|
||||
r#"Get-Content "$(Get-Location)\AGENTS.md""#,
|
||||
"Get-Content AGENTS.md | Select-Object -First 1",
|
||||
"Get-Content AGENTS.md; Remove-Item AGENTS.md",
|
||||
"Get-Content *.md",
|
||||
"Get-Content Env:PATH",
|
||||
r"'Get-Content' 'C:\input.txt'",
|
||||
r"Get-Content '-Path' 'C:\input.txt'",
|
||||
r#"Get-Content "C:\input.txt""#,
|
||||
] {
|
||||
assert_parsed(
|
||||
&vec_str(&["powershell.exe", "-Command", script]),
|
||||
vec![ParsedCommand::Unknown {
|
||||
cmd: script.to_string(),
|
||||
}],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_command_impl(command: &[String]) -> Vec<ParsedCommand> {
|
||||
@@ -1278,9 +1328,14 @@ pub fn parse_command_impl(command: &[String]) -> Vec<ParsedCommand> {
|
||||
}
|
||||
|
||||
if let Some((_, script)) = extract_powershell_command(command) {
|
||||
return vec![ParsedCommand::Unknown {
|
||||
let parsed = extract_powershell_literal_read_path(script).map(|path| ParsedCommand::Read {
|
||||
cmd: script.to_string(),
|
||||
}];
|
||||
name: short_display_path(path),
|
||||
path: PathBuf::from(path),
|
||||
});
|
||||
return vec![parsed.unwrap_or_else(|| ParsedCommand::Unknown {
|
||||
cmd: script.to_string(),
|
||||
})];
|
||||
}
|
||||
|
||||
let normalized = normalize_tokens(command);
|
||||
|
||||
@@ -70,6 +70,68 @@ pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Extracts the literal path from the narrow `Get-Content` shape that can be recognized without
|
||||
/// launching the selected execution environment's PowerShell on the Codex host.
|
||||
pub(crate) fn extract_powershell_literal_read_path(script: &str) -> Option<&str> {
|
||||
if script
|
||||
.chars()
|
||||
.any(|character| matches!(character, '\0' | '\r' | '\n'))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let script = script.trim();
|
||||
let command_end = script.find(char::is_whitespace)?;
|
||||
let command = &script[..command_end];
|
||||
if !command.eq_ignore_ascii_case("Get-Content") && !command.eq_ignore_ascii_case("gc") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let quoted_path = script[command_end..].trim_start().strip_prefix('\'')?;
|
||||
let path_end = quoted_path.find('\'')?;
|
||||
let path = "ed_path[..path_end];
|
||||
if path.is_empty()
|
||||
|| path
|
||||
.chars()
|
||||
.any(|character| matches!(character, '*' | '?' | '[' | ']'))
|
||||
|| is_powershell_provider_path(path)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let trailing = quoted_path[path_end + 1..].trim();
|
||||
if !trailing.is_empty() {
|
||||
let mut words = trailing.split_whitespace();
|
||||
let flag = words.next()?;
|
||||
let value = words.next()?;
|
||||
if !flag.eq_ignore_ascii_case("-ErrorAction")
|
||||
|| !value.eq_ignore_ascii_case("Stop")
|
||||
|| words.next().is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn is_powershell_provider_path(path: &str) -> bool {
|
||||
if path.contains("::") {
|
||||
return true;
|
||||
}
|
||||
|
||||
let bytes = path.as_bytes();
|
||||
match bytes.iter().position(|byte| *byte == b':') {
|
||||
None => false,
|
||||
Some(1) => !matches!(
|
||||
bytes,
|
||||
[drive, b':', separator, ..]
|
||||
if drive.is_ascii_alphabetic() && matches!(separator, b'\\' | b'/')
|
||||
),
|
||||
Some(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the script body from a top-level PowerShell wrapper into argv-like commands.
|
||||
///
|
||||
/// This is intentionally narrower than the Windows safe-command parser: it only unwraps the
|
||||
|
||||
Reference in New Issue
Block a user