diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-21 20:38:51 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-21 21:05:13 +0900 |
| commit | 195e1d0ec893f4977a37ebb35dc7745dc8c8d3b6 (patch) | |
| tree | 221016ba98d3a64ba02e5c45e52e78044bdae700 | |
| parent | 40a9b8c4e5f80b1e4dc8df8f5af1b7b4903e69df (diff) | |
| download | php-shirabe-195e1d0ec893f4977a37ebb35dc7745dc8c8d3b6.tar.gz php-shirabe-195e1d0ec893f4977a37ebb35dc7745dc8c8d3b6.tar.zst php-shirabe-195e1d0ec893f4977a37ebb35dc7745dc8c8d3b6.zip | |
feat(php-shim): implement exec/shell_exec/system/escapeshell*/posix_isatty
Run exec()/shell_exec()/system() through /bin/sh -c with PHP-faithful
line handling and return values. Port escapeshellcmd() (Unix branch,
including paired-quote handling) and escapeshellarg(). Implement
posix_isatty() via std::io::IsTerminal.
uid/signal/process-title helpers and proc_open-dependent status calls
remain TODO(phase-d): they need a libc/syscall crate or the deferred
process/signal subsystems.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| -rw-r--r-- | crates/shirabe-php-shim/src/process.rs | 150 |
1 files changed, 136 insertions, 14 deletions
diff --git a/crates/shirabe-php-shim/src/process.rs b/crates/shirabe-php-shim/src/process.rs index cb5aa50..8551d27 100644 --- a/crates/shirabe-php-shim/src/process.rs +++ b/crates/shirabe-php-shim/src/process.rs @@ -7,27 +7,127 @@ pub const SIGUSR1: i64 = 10; pub const SIGUSR2: i64 = 12; pub fn exec( - _command: &str, - _output: Option<&mut Vec<String>>, - _exit_code: Option<&mut i64>, + command: &str, + output: Option<&mut Vec<String>>, + exit_code: Option<&mut i64>, ) -> Option<String> { - todo!() + let result = std::process::Command::new("/bin/sh") + .arg("-c") + .arg(command) + .output() + .ok()?; + if let Some(code) = exit_code { + *code = result.status.code().unwrap_or(-1) as i64; + } + let stdout = String::from_utf8_lossy(&result.stdout); + let mut lines: Vec<String> = stdout + .split('\n') + .map(|l| l.strip_suffix('\r').unwrap_or(l).to_string()) + .collect(); + // Drop the single trailing empty line produced by a terminating newline. + if lines.last().map(String::is_empty).unwrap_or(false) { + lines.pop(); + } + let last = lines.last().cloned().unwrap_or_default(); + if let Some(out) = output { + // PHP appends to the array rather than replacing it. + out.extend(lines); + } + Some(last) } -pub fn shell_exec(_command: &str) -> Option<String> { - todo!() +pub fn shell_exec(command: &str) -> Option<String> { + let result = std::process::Command::new("/bin/sh") + .arg("-c") + .arg(command) + .output() + .ok()?; + let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); + // PHP returns null when the command produces no output. + if stdout.is_empty() { + None + } else { + Some(stdout) + } } -pub fn system(_command: &str, _result_code: Option<&mut i64>) -> Option<String> { - todo!() +pub fn system(command: &str, result_code: Option<&mut i64>) -> Option<String> { + use std::io::Write; + let result = std::process::Command::new("/bin/sh") + .arg("-c") + .arg(command) + .output() + .ok()?; + if let Some(code) = result_code { + *code = result.status.code().unwrap_or(-1) as i64; + } + // PHP system() passes the command output straight through to the script's output. + // TODO(phase-d): PHP flushes line by line as the command runs; here the whole output is captured + // and emitted once the command finishes, which changes interleaving/streaming timing. + let _ = std::io::stdout().write_all(&result.stdout); + let _ = std::io::stdout().flush(); + let stdout = String::from_utf8_lossy(&result.stdout); + let mut lines: Vec<String> = stdout + .split('\n') + .map(|l| l.strip_suffix('\r').unwrap_or(l).to_string()) + .collect(); + if lines.last().map(String::is_empty).unwrap_or(false) { + lines.pop(); + } + Some(lines.last().cloned().unwrap_or_default()) } -pub fn escapeshellcmd(_command: &str) -> String { - todo!() +// Port of PHP's escapeshellcmd (Unix branch). Shell metacharacters are backslash-escaped; quote +// characters are escaped only when unpaired, paired quotes being left intact. +pub fn escapeshellcmd(command: &str) -> String { + let bytes = command.as_bytes(); + let len = bytes.len(); + let mut out: Vec<u8> = Vec::with_capacity(len); + // Byte index of the matching closing quote while inside a paired quote run. + let mut paired: Option<usize> = None; + let mut x = 0; + while x < len { + let c = bytes[x]; + match c { + b'"' | b'\'' => { + if paired.is_none() { + if let Some(rel) = bytes[x + 1..].iter().position(|&b| b == c) { + paired = Some(x + 1 + rel); + } else { + out.push(b'\\'); + } + } else if paired == Some(x) { + paired = None; + } else { + out.push(b'\\'); + } + out.push(c); + } + b'#' | b'&' | b';' | b'`' | b'|' | b'*' | b'?' | b'~' | b'<' | b'>' | b'^' | b'(' + | b')' | b'[' | b']' | b'{' | b'}' | b'$' | b'\\' | 0x0A | 0xFF => { + out.push(b'\\'); + out.push(c); + } + _ => out.push(c), + } + x += 1; + } + String::from_utf8_lossy(&out).into_owned() } -pub fn escapeshellarg(_arg: &str) -> String { - todo!() +// Unix branch of PHP's escapeshellarg: wrap in single quotes, escaping embedded single quotes. +pub fn escapeshellarg(arg: &str) -> String { + let mut out = String::with_capacity(arg.len() + 2); + out.push('\''); + for c in arg.chars() { + if c == '\'' { + out.push_str("'\\''"); + } else { + out.push(c); + } + } + out.push('\''); + out } // TODO(phase-c): reports proc_open as unavailable (returns false), so callers fall back to @@ -49,10 +149,13 @@ pub fn proc_close(_process: PhpMixed) -> i64 { } pub fn proc_get_status(_process: &PhpMixed) -> IndexMap<String, PhpMixed> { + // TODO(phase-d): depends on proc_open returning a real process handle, which is itself deferred + // (see proc_open above). Without a live child there is no status to report. todo!() } pub fn proc_terminate(_process: &PhpMixed, _signal: i64) -> bool { + // TODO(phase-d): depends on proc_open returning a real process handle (see proc_open above). todo!() } @@ -61,10 +164,13 @@ pub fn getmypid() -> i64 { } pub fn cli_set_process_title(_title: &str) -> bool { + // TODO(phase-d): changing the process title visible to ps(1) requires platform-specific calls + // (prctl/setproctitle) not available without a libc/syscall crate. todo!() } pub fn setproctitle(_title: &str) { + // TODO(phase-d): see cli_set_process_title; requires platform-specific process-title support. todo!() } @@ -73,33 +179,49 @@ pub fn setproctitle(_title: &str) { pub fn pcntl_async_signals(_enable: bool) {} pub fn pcntl_signal(_signal: i64, _handler: PhpMixed) -> bool { + // TODO(phase-d): registering a signal handler requires the signal-handling subsystem to be wired + // up (cf. SignalRegistry / the TODO(plugin) notes), plus a syscall crate for sigaction. todo!() } pub fn pcntl_signal_get_handler(_signal: i64) -> PhpMixed { + // TODO(phase-d): see pcntl_signal; needs the signal-handling subsystem. todo!() } pub fn posix_getuid() -> i64 { + // TODO(phase-d): getuid(2) is not reachable without a libc/syscall crate. todo!() } pub fn posix_geteuid() -> i64 { + // TODO(phase-d): geteuid(2) is not reachable without a libc/syscall crate. todo!() } pub fn posix_getpwuid(_uid: i64) -> PhpMixed { + // TODO(phase-d): getpwuid(3) is not reachable without a libc/syscall crate. todo!() } -pub fn posix_isatty(_stream: PhpResource) -> bool { - todo!() +pub fn posix_isatty(stream: PhpResource) -> bool { + use std::io::IsTerminal; + match stream { + PhpResource::Stdin => std::io::stdin().is_terminal(), + PhpResource::Stdout => std::io::stdout().is_terminal(), + PhpResource::Stderr => std::io::stderr().is_terminal(), + // A regular file is never a tty. + PhpResource::File(_) => false, + } } pub fn posix_kill(_pid: i64, _signal: i64) -> bool { + // TODO(phase-d): kill(2) is not reachable without a libc/syscall crate. todo!() } pub fn get_current_user() -> String { + // TODO(phase-d): PHP returns the owner name of the running script file, which needs stat(2) plus + // getpwuid(3); neither is reachable without a libc/syscall crate. todo!() } |
