mod array; mod compress; mod curl; mod datetime; mod env; mod exception; mod filter; mod fs; mod hash; mod json; mod math; mod net; mod openssl; mod output; mod phar; mod preg; mod process; mod random; mod rar; mod runtime; mod stream; mod string; mod url; mod var; mod xml; mod zip; pub use array::*; pub use compress::*; pub use curl::*; pub use datetime::*; pub use env::*; pub use exception::*; pub use filter::*; pub use fs::*; pub use hash::*; pub use json::*; pub use math::*; pub use net::*; pub use openssl::*; pub use output::*; pub use phar::*; pub use preg::*; pub use process::*; pub use random::*; pub use rar::*; pub use runtime::*; pub use stream::*; pub use string::*; pub use url::*; pub use var::*; pub use xml::*; pub use zip::*; use indexmap::IndexMap; #[derive(Debug, Clone, Default)] pub enum PhpMixed { #[default] Null, Bool(bool), Int(i64), Float(f64), String(String), List(Vec), Array(IndexMap), // TODO: consolidate Object to Array. Object(IndexMap), // Resources, arbitrary objects and callables are intentionally excluded. Do not add these // things to this type. } impl serde::Serialize for PhpMixed { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { use serde::ser::{SerializeMap, SerializeSeq}; match self { PhpMixed::Null => serializer.serialize_none(), PhpMixed::Bool(b) => serializer.serialize_bool(*b), PhpMixed::Int(i) => serializer.serialize_i64(*i), PhpMixed::Float(f) => serializer.serialize_f64(*f), PhpMixed::String(s) => serializer.serialize_str(s), PhpMixed::List(items) => { let mut seq = serializer.serialize_seq(Some(items.len()))?; for item in items { seq.serialize_element(item)?; } seq.end() } PhpMixed::Array(entries) => { // PHP arrays do not distinguish an empty map from an empty list, and // `json_encode([])` always emits `[]`. Mirror that so an empty associative // array encodes as `[]` rather than `{}`. if entries.is_empty() { return serializer.serialize_seq(Some(0))?.end(); } let mut map = serializer.serialize_map(Some(entries.len()))?; for (k, v) in entries { map.serialize_entry(k, v)?; } map.end() } PhpMixed::Object(entries) => { let mut map = serializer.serialize_map(Some(entries.len()))?; for (k, v) in entries { map.serialize_entry(k, v)?; } map.end() } } } } /// PHP `===` semantics: type-strict and, for arrays, order-sensitive. impl PartialEq for PhpMixed { fn eq(&self, other: &Self) -> bool { match (self, other) { (PhpMixed::Null, PhpMixed::Null) => true, (PhpMixed::Bool(a), PhpMixed::Bool(b)) => a == b, (PhpMixed::Int(a), PhpMixed::Int(b)) => a == b, (PhpMixed::Float(a), PhpMixed::Float(b)) => a == b, (PhpMixed::String(a), PhpMixed::String(b)) => a == b, (PhpMixed::List(a), PhpMixed::List(b)) => a == b, (PhpMixed::Array(a), PhpMixed::Array(b)) => { a.len() == b.len() && a.iter() .zip(b.iter()) .all(|((ka, va), (kb, vb))| ka == kb && va == vb) } (PhpMixed::Object(a), PhpMixed::Object(b)) => { a.len() == b.len() && a.iter() .zip(b.iter()) .all(|((ka, va), (kb, vb))| ka == kb && va == vb) } _ => false, } } } impl PhpMixed { pub fn as_bool(&self) -> Option { match self { PhpMixed::Bool(b) => Some(*b), _ => None, } } pub fn as_int(&self) -> Option { match self { PhpMixed::Int(i) => Some(*i), _ => None, } } pub fn as_float(&self) -> Option { match self { PhpMixed::Float(f) => Some(*f), _ => None, } } pub fn as_string(&self) -> Option<&str> { match self { PhpMixed::String(s) => Some(s.as_str()), _ => None, } } pub fn as_list(&self) -> Option<&Vec> { match self { PhpMixed::List(l) => Some(l), _ => None, } } pub fn as_array(&self) -> Option<&IndexMap> { match self { PhpMixed::Array(a) => Some(a), _ => None, } } pub fn as_array_mut(&mut self) -> Option<&mut IndexMap> { match self { PhpMixed::Array(a) => Some(a), _ => None, } } pub fn as_list_mut(&mut self) -> Option<&mut Vec> { match self { PhpMixed::List(l) => Some(l), _ => None, } } pub fn as_object(&self) -> Option<&IndexMap> { match self { PhpMixed::Object(o) => Some(o), _ => None, } } pub fn is_null(&self) -> bool { matches!(self, PhpMixed::Null) } /// PHP loose boolean cast `(bool) $value`. pub fn to_bool(&self) -> bool { php_truthy(self) } pub fn get(&self, key: &str) -> Option<&PhpMixed> { self.as_array().and_then(|m| m.get(key)) } /// Treats PhpMixed::Null as None, everything else as Some. pub fn as_opt(&self) -> Option<&PhpMixed> { if self.is_null() { None } else { Some(self) } } pub fn unwrap_or(self, default: PhpMixed) -> PhpMixed { if self.is_null() { default } else { self } } pub fn unwrap_or_default(self) -> PhpMixed { if self.is_null() { PhpMixed::Null } else { self } } pub fn unwrap(self) -> PhpMixed { if self.is_null() { panic!("called `PhpMixed::unwrap()` on a `Null` value"); } self } /// Treats PhpMixed::Null as None and applies the function for chaining. pub fn and_then Option>(&self, f: F) -> Option { self.as_opt().and_then(f) } /// Treats `Null` and `Bool(false)` as the falsy case, anything else as Some. pub fn ok_or_else E>(self, err: F) -> Result { match self { PhpMixed::Null | PhpMixed::Bool(false) => Err(err()), v => Ok(v), } } } impl From<()> for PhpMixed { fn from(_value: ()) -> Self { PhpMixed::Null } } impl From for PhpMixed { fn from(value: bool) -> Self { PhpMixed::Bool(value) } } /// Blanket downcast helper so trait objects (`dyn Command`, `dyn OutputInterface`, /// etc.) can be downcast to their concrete type, mirroring PHP `instanceof`. pub trait AsAny { fn as_any(&self) -> &dyn std::any::Any; fn as_any_mut(&mut self) -> &mut dyn std::any::Any; } impl AsAny for T { fn as_any(&self) -> &dyn std::any::Any { self } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } } impl From for PhpMixed { fn from(value: i64) -> Self { PhpMixed::Int(value) } } impl From for PhpMixed { fn from(value: f64) -> Self { PhpMixed::Float(value) } } impl From for PhpMixed { fn from(value: String) -> Self { PhpMixed::String(value) } } impl From<&str> for PhpMixed { fn from(value: &str) -> Self { PhpMixed::String(value.to_string()) } } impl From> for PhpMixed where T: Into, { fn from(value: IndexMap) -> Self { PhpMixed::Array(value.into_iter().map(|(k, v)| (k, v.into())).collect()) } } impl From> for PhpMixed where T: Into, { fn from(value: Vec) -> Self { PhpMixed::List(value.into_iter().map(|v| v.into()).collect()) } } impl From> for PhpMixed where T: Into, { fn from(value: Option) -> Self { match value { Some(v) => v.into(), None => PhpMixed::Null, } } } impl From> for PhpMixed where T: Into, { fn from(value: Box) -> Self { (*value).into() } } impl std::fmt::Display for PhpMixed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.pad(&php_to_string(self)) } } #[derive(Debug, Clone)] pub enum PhpResource { Stdin, Stdout, Stderr, Stream(std::rc::Rc>), Process(std::rc::Rc>), } 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 { 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>` satisfy it, so a stream can be driven uniformly. pub trait ReadWriteSeek: std::io::Read + std::io::Write + std::io::Seek {} impl ReadWriteSeek for T {} #[derive(Debug)] pub enum StreamBacking { /// A real file on disk (also `/dev/null`); the OS tracks the position. File(std::fs::File), /// `php://memory` and `php://temp` — an in-memory growable buffer. /// TODO(phase-d): `php://temp/maxmemory:N` spills to a temp file past N bytes; /// the threshold is ignored here and everything stays in memory. Memory(std::io::Cursor>), /// A child process pipe created by `proc_open`. Half-duplex and not seekable. Pipe(ChildPipe), } impl StreamBacking { pub(crate) fn as_rws(&mut self) -> &mut dyn ReadWriteSeek { match self { StreamBacking::File(f) => f, StreamBacking::Memory(c) => c, StreamBacking::Pipe(p) => p, } } } /// One end of a child process pipe. Each variant supports only the direction PHP /// allows for it; the unsupported operations return `ErrorKind::Unsupported` so the /// `ReadWriteSeek` contract is satisfied without pretending pipes are seekable. #[derive(Debug)] pub enum ChildPipe { In(std::process::ChildStdin), Out(std::process::ChildStdout), Err(std::process::ChildStderr), } impl std::io::Read for ChildPipe { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { match self { ChildPipe::Out(o) => o.read(buf), ChildPipe::Err(e) => e.read(buf), ChildPipe::In(_) => Err(std::io::Error::from(std::io::ErrorKind::Unsupported)), } } } impl std::io::Write for ChildPipe { fn write(&mut self, buf: &[u8]) -> std::io::Result { match self { ChildPipe::In(i) => i.write(buf), ChildPipe::Out(_) | ChildPipe::Err(_) => { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) } } } fn flush(&mut self) -> std::io::Result<()> { match self { ChildPipe::In(i) => i.flush(), ChildPipe::Out(_) | ChildPipe::Err(_) => Ok(()), } } } impl std::io::Seek for ChildPipe { fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) } } 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, /// Whether the mode opened the stream for reading. pub(crate) readable: bool, /// Whether the mode opened the stream for writing. pub(crate) writable: bool, /// Set once a read attempt sees end-of-stream, mirroring PHP's `feof()` which /// only reports true after a read has hit the end; cleared by a seek. pub(crate) eof: bool, pub(crate) closed: bool, /// The mode string passed to `fopen`, reported back by `stream_get_meta_data`. pub(crate) mode: String, /// The path/URI the stream was opened from, reported back by `stream_get_meta_data`. pub(crate) uri: String, } impl StreamState { pub(crate) fn new( backing: StreamBacking, readable: bool, writable: bool, mode: String, uri: String, ) -> StreamState { StreamState { backing, readable, writable, eof: false, closed: false, mode, uri, } } }