diff options
Diffstat (limited to 'crates/shirabe-php-shim/src')
| -rw-r--r-- | crates/shirabe-php-shim/src/fs.rs | 8 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 36 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/stream.rs | 141 |
3 files changed, 171 insertions, 14 deletions
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 |
