aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/process.rs6
-rw-r--r--crates/shirabe-php-shim/src/fs.rs8
-rw-r--r--crates/shirabe-php-shim/src/lib.rs36
-rw-r--r--crates/shirabe-php-shim/src/stream.rs141
-rw-r--r--crates/shirabe/tests/util/process_executor_test.rs6
5 files changed, 177 insertions, 20 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/process/process.rs b/crates/shirabe-external-packages/src/symfony/process/process.rs
index 3550f09..1b58972 100644
--- a/crates/shirabe-external-packages/src/symfony/process/process.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/process.rs
@@ -222,7 +222,11 @@ impl Process {
this.cwd = cwd;
// on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
- if this.cwd.is_none() && php::DIRECTORY_SEPARATOR == "\\" {
+ // PHP: null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)
+ // `\defined('ZEND_THREAD_SAFE')` is unconditionally true in modern PHP (the constant always
+ // exists; its value merely reflects the NTS/ZTS build), so the disjunction is always true and
+ // the cwd is defaulted to getcwd() whenever it was null.
+ if this.cwd.is_none() {
this.cwd = php::getcwd();
}
if let Some(env) = env {
diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs
index 5558f58..2fb0dc5 100644
--- a/crates/shirabe-php-shim/src/fs.rs
+++ b/crates/shirabe-php-shim/src/fs.rs
@@ -343,7 +343,13 @@ pub fn fread(stream: &PhpResource, length: i64) -> Option<String> {
return None;
}
let mut buf = vec![0u8; cap];
- let n = state.backing.as_rws().read(&mut buf).ok()?;
+ let n = match state.backing.as_rws().read(&mut buf) {
+ Ok(n) => n,
+ // A non-blocking stream with no data ready is not an error in PHP: fread() returns
+ // the empty string and feof() stays false. std surfaces this as WouldBlock.
+ Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Some(String::new()),
+ Err(_) => return None,
+ };
if cap > 0 && n == 0 {
state.eof = true;
}
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index 113c9d7..8c8073f 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -349,6 +349,32 @@ pub enum PhpResource {
Process(std::rc::Rc<std::cell::RefCell<process::ProcessState>>),
}
+impl PhpResource {
+ /// Returns the underlying OS file descriptor backing this resource, when it has one.
+ /// Used by `stream_set_blocking`/`stream_select` to drive `fcntl(2)`/`select(2)`. In-memory
+ /// streams (`php://memory`/`php://temp`) and process handles have no fd and return `None`.
+ pub(crate) fn raw_fd(&self) -> Option<std::os::unix::io::RawFd> {
+ use std::os::unix::io::AsRawFd;
+ match self {
+ PhpResource::Stdin => Some(std::io::stdin().as_raw_fd()),
+ PhpResource::Stdout => Some(std::io::stdout().as_raw_fd()),
+ PhpResource::Stderr => Some(std::io::stderr().as_raw_fd()),
+ PhpResource::Process(_) => None,
+ PhpResource::Stream(state) => {
+ let state = state.borrow();
+ if state.closed {
+ return None;
+ }
+ match &state.backing {
+ StreamBacking::File(f) => Some(f.as_raw_fd()),
+ StreamBacking::Pipe(p) => Some(p.as_raw_fd()),
+ StreamBacking::Memory(_) => None,
+ }
+ }
+ }
+ }
+}
+
/// Combined capability of every seekable byte stream backing. Both `std::fs::File`
/// and `std::io::Cursor<Vec<u8>>` satisfy it, so a stream can be driven uniformly.
pub trait ReadWriteSeek: std::io::Read + std::io::Write + std::io::Seek {}
@@ -420,6 +446,16 @@ impl std::io::Seek for ChildPipe {
}
}
+impl std::os::unix::io::AsRawFd for ChildPipe {
+ fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
+ match self {
+ ChildPipe::In(i) => i.as_raw_fd(),
+ ChildPipe::Out(o) => o.as_raw_fd(),
+ ChildPipe::Err(e) => e.as_raw_fd(),
+ }
+ }
+}
+
#[derive(Debug)]
pub struct StreamState {
pub(crate) backing: StreamBacking,
diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs
index a8ccbe5..606b202 100644
--- a/crates/shirabe-php-shim/src/stream.rs
+++ b/crates/shirabe-php-shim/src/stream.rs
@@ -259,24 +259,139 @@ fn build_meta_data(
map
}
-pub fn stream_set_blocking(_resource: &PhpResource, _enable: bool) -> bool {
- // TODO(phase-d): toggling O_NONBLOCK requires fcntl(2); a syscall crate is intentionally not
- // introduced here.
- todo!("stream_set_blocking requires fcntl(2) (syscall crate not available)")
+// libc is already linked into every binary, so these can be declared directly without an extra
+// crate (mirrors the `getuid`/`geteuid` declarations in process.rs).
+const F_GETFL: i32 = 3;
+const F_SETFL: i32 = 4;
+const O_NONBLOCK: i32 = 0o4000;
+const FD_SETSIZE: usize = 1024;
+
+unsafe extern "C" {
+ fn fcntl(fd: i32, cmd: i32, ...) -> i32;
+ fn select(
+ nfds: i32,
+ readfds: *mut FdSet,
+ writefds: *mut FdSet,
+ exceptfds: *mut FdSet,
+ timeout: *mut TimeVal,
+ ) -> i32;
+}
+
+#[repr(C)]
+struct TimeVal {
+ tv_sec: i64,
+ tv_usec: i64,
+}
+
+// `fd_set` is a bitmap of `FD_SETSIZE` bits laid out as an array of `long` words.
+#[repr(C)]
+struct FdSet {
+ fds_bits: [i64; FD_SETSIZE / (8 * std::mem::size_of::<i64>())],
+}
+
+impl FdSet {
+ fn zero() -> Self {
+ FdSet {
+ fds_bits: [0; FD_SETSIZE / (8 * std::mem::size_of::<i64>())],
+ }
+ }
+
+ fn set(&mut self, fd: i32) {
+ let bits = 8 * std::mem::size_of::<i64>();
+ self.fds_bits[fd as usize / bits] |= 1i64 << (fd as usize % bits);
+ }
+
+ fn is_set(&self, fd: i32) -> bool {
+ let bits = 8 * std::mem::size_of::<i64>();
+ (self.fds_bits[fd as usize / bits] & (1i64 << (fd as usize % bits))) != 0
+ }
+}
+
+/// PHP `stream_set_blocking()`: toggle `O_NONBLOCK` on the resource's underlying fd via `fcntl(2)`.
+/// Returns `false` (PHP failure) when the resource has no fd or the syscall fails.
+pub fn stream_set_blocking(resource: &PhpResource, enable: bool) -> bool {
+ let Some(fd) = resource.raw_fd() else {
+ return false;
+ };
+ let flags = unsafe { fcntl(fd, F_GETFL) };
+ if flags < 0 {
+ return false;
+ }
+ // `enable` means blocking, i.e. clear O_NONBLOCK.
+ let new_flags = if enable {
+ flags & !O_NONBLOCK
+ } else {
+ flags | O_NONBLOCK
+ };
+ unsafe { fcntl(fd, F_SETFL, new_flags) >= 0 }
}
/// PHP `stream_select`. Returns the number of changed streams, or `None` for the PHP `false`
-/// returned when the underlying `select` is interrupted/fails.
+/// returned when the underlying `select` is interrupted/fails. On success the `read`/`write`/
+/// `except` vectors are narrowed to only the resources that became ready, matching PHP's
+/// in-place rewrite of the passed arrays.
pub fn stream_select(
- _read: &mut Vec<PhpResource>,
- _write: &mut Vec<PhpResource>,
- _except: &mut Vec<PhpResource>,
- _seconds: i64,
- _microseconds: Option<i64>,
+ read: &mut Vec<PhpResource>,
+ write: &mut Vec<PhpResource>,
+ except: &mut Vec<PhpResource>,
+ seconds: i64,
+ microseconds: Option<i64>,
) -> Option<i64> {
- // TODO(phase-d): multiplexing readiness requires select(2)/poll(2); a syscall crate is
- // intentionally not introduced here.
- todo!("stream_select requires select(2)/poll(2) (syscall crate not available)")
+ let mut readfds = FdSet::zero();
+ let mut writefds = FdSet::zero();
+ let mut exceptfds = FdSet::zero();
+ let mut nfds = 0i32;
+
+ // Resources without an fd (in-memory streams, process handles) cannot be waited on; PHP would
+ // emit a warning for them. We skip them here, leaving them out of the ready set.
+ let mut prepare = |set: &mut FdSet, resources: &[PhpResource]| {
+ for resource in resources {
+ if let Some(fd) = resource.raw_fd() {
+ if (fd as usize) < FD_SETSIZE {
+ set.set(fd);
+ if fd + 1 > nfds {
+ nfds = fd + 1;
+ }
+ }
+ }
+ }
+ };
+ prepare(&mut readfds, read);
+ prepare(&mut writefds, write);
+ prepare(&mut exceptfds, except);
+
+ let mut timeout = TimeVal {
+ tv_sec: seconds,
+ tv_usec: microseconds.unwrap_or(0),
+ };
+
+ let ret = unsafe {
+ select(
+ nfds,
+ &mut readfds,
+ &mut writefds,
+ &mut exceptfds,
+ &mut timeout,
+ )
+ };
+
+ if ret < 0 {
+ // select(2) failed (e.g. EINTR). PHP returns false.
+ return None;
+ }
+
+ // Narrow each array in place to the resources whose fd is still set in the result bitmap.
+ let narrow = |set: &FdSet, resources: &mut Vec<PhpResource>| {
+ resources.retain(|resource| match resource.raw_fd() {
+ Some(fd) if (fd as usize) < FD_SETSIZE => set.is_set(fd),
+ _ => false,
+ });
+ };
+ narrow(&readfds, read);
+ narrow(&writefds, write);
+ narrow(&exceptfds, except);
+
+ Some(ret as i64)
}
/// PHP `stream_get_contents($stream, $maxlength, $offset)`. A non-negative `offset` seeks there
diff --git a/crates/shirabe/tests/util/process_executor_test.rs b/crates/shirabe/tests/util/process_executor_test.rs
index 342c7ff..01e37ed 100644
--- a/crates/shirabe/tests/util/process_executor_test.rs
+++ b/crates/shirabe/tests/util/process_executor_test.rs
@@ -20,7 +20,6 @@ use shirabe_external_packages::symfony::console::output::output_interface::{
};
use shirabe_php_shim::{PHP_EOL, trim};
-#[ignore]
#[test]
fn test_execute_captures_output() {
let mut process = ProcessExecutor::new(None);
@@ -41,7 +40,7 @@ fn test_use_io_is_not_null_and_if_not_captured() {
todo!()
}
-#[ignore]
+#[ignore = "stderr capture works, but the test cwd (crates/shirabe) contains a `foo/` fixture dir, so `cat foo` reports \"Is a directory\" instead of \"No such file or directory\""]
#[test]
fn test_execute_captures_stderr() {
let mut process = ProcessExecutor::new(None);
@@ -89,7 +88,6 @@ fn hide_password_provider() -> Vec<(&'static str, &'static str)> {
]
}
-#[ignore]
#[test]
fn test_hide_passwords() {
for (command, expected_command_output) in hide_password_provider() {
@@ -107,7 +105,6 @@ fn test_hide_passwords() {
}
}
-#[ignore]
#[test]
fn test_doesnt_hide_ports() {
let buffer = Rc::new(RefCell::new(
@@ -130,7 +127,6 @@ fn test_split_lines() {
todo!()
}
-#[ignore]
#[test]
fn test_console_io_does_not_format_symfony_console_style() {
let output = Rc::new(RefCell::new(BufferedOutput::new(