diff options
Diffstat (limited to 'crates/shirabe-php-shim/src/lib.rs')
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 3680 |
1 files changed, 48 insertions, 3632 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 170fe5a..a359275 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -1,8 +1,56 @@ +mod apcu; +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 zip; +pub use apcu::*; +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 zip::*; use indexmap::IndexMap; @@ -315,3085 +363,6 @@ impl std::fmt::Display for PhpMixed { } } -#[derive(Debug)] -pub struct Exception { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for Exception { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for Exception {} - -#[derive(Debug)] -pub struct RuntimeException { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for RuntimeException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for RuntimeException {} - -#[derive(Debug)] -pub struct UnexpectedValueException { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for UnexpectedValueException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for UnexpectedValueException {} - -#[derive(Debug)] -pub struct InvalidArgumentException { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for InvalidArgumentException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for InvalidArgumentException {} - -#[derive(Debug)] -pub struct TypeError { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for TypeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for TypeError {} - -#[derive(Debug)] -pub struct LogicException { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for LogicException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for LogicException {} - -#[derive(Debug)] -pub struct BadMethodCallException { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for BadMethodCallException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for BadMethodCallException {} - -#[derive(Debug)] -pub struct OutOfBoundsException { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for OutOfBoundsException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for OutOfBoundsException {} - -#[derive(Debug)] -pub struct ErrorException { - pub message: String, - pub code: i64, - pub severity: i64, - pub filename: String, - pub lineno: i64, -} - -impl std::fmt::Display for ErrorException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for ErrorException {} - -pub fn is_bool(_value: &PhpMixed) -> bool { - matches!(_value, PhpMixed::Bool(_)) -} - -pub fn is_string(_value: &PhpMixed) -> bool { - matches!(_value, PhpMixed::String(_)) -} - -pub fn is_int(_value: &PhpMixed) -> bool { - matches!(_value, PhpMixed::Int(_)) -} - -pub fn is_scalar(_value: &PhpMixed) -> bool { - matches!( - _value, - PhpMixed::Bool(_) | PhpMixed::Int(_) | PhpMixed::Float(_) | PhpMixed::String(_) - ) -} - -pub fn is_numeric(value: &PhpMixed) -> bool { - match value { - PhpMixed::Int(_) | PhpMixed::Float(_) => true, - PhpMixed::String(s) => is_numeric_string(s), - _ => false, - } -} - -pub fn strtotime(_time: &str) -> Option<i64> { - todo!() -} - -pub fn strcasecmp(_s1: &str, _s2: &str) -> i64 { - _s1.to_ascii_lowercase().cmp(&_s2.to_ascii_lowercase()) as i64 -} - -pub fn sprintf(_format: &str, _args: &[PhpMixed]) -> String { - todo!() -} - -pub fn array_values<V: Clone>(_array: &IndexMap<String, V>) -> Vec<V> { - _array.values().cloned().collect() -} - -pub fn array_keys<V>(_array: &IndexMap<String, V>) -> Vec<String> { - _array.keys().cloned().collect() -} - -pub fn str_replace(search: &str, replace: &str, subject: &str) -> String { - // PHP returns the subject unchanged when the search string is empty, whereas Rust's - // `str::replace` would insert `replace` between every character. - if search.is_empty() { - return subject.to_string(); - } - - subject.replace(search, replace) -} - -pub fn php_to_string(value: &PhpMixed) -> String { - match value { - PhpMixed::Null => String::new(), - PhpMixed::Bool(true) => "1".to_string(), - PhpMixed::Bool(false) => String::new(), - PhpMixed::Int(i) => i.to_string(), - PhpMixed::Float(f) => f.to_string(), - PhpMixed::String(s) => s.clone(), - // PHP renders any array as the literal string "Array". - PhpMixed::List(_) | PhpMixed::Array(_) => "Array".to_string(), - PhpMixed::Object(_) => todo!(), - } -} - -// Byte-based, matching PHP's substr. A negative start/length counts from the end. -// The result is reinterpreted as UTF-8 (lossily), which only matters when a slice -// boundary falls inside a multibyte sequence. -pub fn substr(s: &str, start: i64, length: Option<i64>) -> String { - let bytes = s.as_bytes(); - let len = bytes.len() as i64; - let start = if start < 0 { - (len + start).max(0) - } else { - start.min(len) - }; - let end = match length { - None => len, - Some(l) if l < 0 => (len + l).max(start), - Some(l) => (start + l).min(len), - }; - String::from_utf8_lossy(&bytes[start as usize..end as usize]).into_owned() -} - -pub const FILTER_VALIDATE_EMAIL: i64 = 274; - -pub const PATH_SEPARATOR: &str = ":"; - -pub fn spl_autoload_functions() -> Vec<PhpMixed> { - todo!() -} - -pub fn array_push(_array: &mut Vec<String>, _value: String) -> i64 { - _array.push(_value); - _array.len() as i64 -} - -pub fn array_search_in_vec(_needle: &str, _haystack: &[String]) -> Option<usize> { - _haystack.iter().position(|s| s.as_str() == _needle) -} - -pub fn array_map_str_fn<F: Fn(&str) -> String>(_callback: F, _array: &[String]) -> Vec<String> { - _array.iter().map(|s| _callback(s)).collect() -} - -pub fn is_callable(_value: &PhpMixed) -> bool { - todo!() -} - -pub fn is_object(_value: &PhpMixed) -> bool { - matches!(_value, PhpMixed::Object(_)) -} - -pub fn is_a(_object_or_class: &PhpMixed, _class: &str, _allow_string: bool) -> bool { - todo!() -} - -pub fn str_contains(_haystack: &str, _needle: &str) -> bool { - _haystack.contains(_needle) -} - -pub fn str_starts_with(_haystack: &str, _needle: &str) -> bool { - _haystack.starts_with(_needle) -} - -pub fn str_ends_with(_haystack: &str, _needle: &str) -> bool { - _haystack.ends_with(_needle) -} - -pub fn strpos(_haystack: &str, _needle: &str) -> Option<usize> { - _haystack.find(_needle) -} - -pub fn strtoupper(_s: &str) -> String { - _s.to_ascii_uppercase() -} - -pub fn strlen(_s: &str) -> i64 { - _s.len() as i64 -} - -pub fn krsort<V>(_array: &mut IndexMap<i64, V>) { - todo!() -} - -pub fn max_i64(_a: i64, _b: i64) -> i64 { - _a.max(_b) -} - -pub fn count_mixed(value: &PhpMixed) -> i64 { - count(value) as i64 -} - -/// Resolve PHP array_slice/substr-style (offset, length) into a `[start, end)` -/// pair of indices, honouring negative offsets and lengths. -fn php_slice_bounds(len: i64, offset: i64, length: Option<i64>) -> (usize, usize) { - let start = if offset < 0 { - (len + offset).max(0) - } else { - offset.min(len) - }; - let end = match length { - None => len, - Some(l) if l < 0 => (len + l).max(start), - Some(l) => (start + l).min(len), - }; - (start as usize, end as usize) -} - -pub fn array_slice_mixed(value: &PhpMixed, offset: i64, length: Option<i64>) -> PhpMixed { - match value { - PhpMixed::List(items) => { - let (start, end) = php_slice_bounds(items.len() as i64, offset, length); - PhpMixed::List(items[start..end].to_vec()) - } - PhpMixed::Array(map) => { - let (start, end) = php_slice_bounds(map.len() as i64, offset, length); - PhpMixed::Array( - map.iter() - .skip(start) - .take(end - start) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - ) - } - _ => panic!("array_slice(): Argument #1 ($array) must be of type array"), - } -} - -pub fn array_slice_strs(value: &[String], offset: i64, length: Option<i64>) -> Vec<String> { - let (start, end) = php_slice_bounds(value.len() as i64, offset, length); - value[start..end].to_vec() -} - -pub fn empty(value: &PhpMixed) -> bool { - match value { - PhpMixed::Null => true, - PhpMixed::Bool(b) => !*b, - PhpMixed::Int(i) => *i == 0, - PhpMixed::Float(f) => *f == 0.0, - PhpMixed::String(s) => s.is_empty() || s == "0", - PhpMixed::List(v) => v.is_empty(), - PhpMixed::Array(m) => m.is_empty(), - PhpMixed::Object(_) => false, - } -} - -pub fn method_exists(_object: &PhpMixed, _method_name: &str) -> bool { - todo!() -} - -pub fn get_class(_object: &PhpMixed) -> String { - todo!() -} - -// Overload accepting an `anyhow::Error` (PHP's `get_class($e)` is commonly used on exceptions). -pub fn get_class_err(_e: &anyhow::Error) -> String { - todo!() -} - -/// Overload accepting any object reference. PHP's `get_class($obj)` returns the -/// class name; in Rust we don't have a runtime class name, so this stub is left -/// as `todo!()`. -pub fn get_class_obj<T: ?Sized>(_object: &T) -> String { - todo!() -} - -pub fn get_debug_type(_value: &PhpMixed) -> String { - todo!() -} - -// Models the constants defined in a standard modern PHP CLI environment on a -// non-Windows platform with the common extensions loaded (curl, openssl, json). -// Windows-only, HHVM and Composer-bootstrap constants are reported undefined. -pub fn defined(name: &str) -> bool { - matches!( - name, - "CURLMOPT_MAX_HOST_CONNECTIONS" - | "CURL_HTTP_VERSION_2_0" - | "CURL_HTTP_VERSION_3" - | "CURL_VERSION_HTTP2" - | "CURL_VERSION_HTTP3" - | "CURL_VERSION_HTTPS_PROXY" - | "CURL_VERSION_LIBZ" - | "CURL_VERSION_ZSTD" - | "GLOB_BRACE" - | "JSON_ERROR_UTF8" - | "OPENSSL_VERSION_TEXT" - | "PHP_BINARY" - | "SIGINT" - | "STDIN" - | "STDOUT" - ) -} - -pub fn hash(_algo: &str, _data: &str) -> String { - todo!() -} - -pub fn hash_raw(_algo: &str, _data: &str) -> Vec<u8> { - todo!() -} - -pub fn pack(_format: &str, _values: &[PhpMixed]) -> Vec<u8> { - todo!() -} - -pub fn unpack(_format: &str, _data: &[u8]) -> Option<IndexMap<String, PhpMixed>> { - todo!() -} - -pub const PHP_VERSION_ID: i64 = 80100; - -// Models the extensions loaded in a standard PHP CLI environment running Composer. -// Opt-in extensions (apcu, xdebug, ionCube, uopz) are reported absent. -pub fn extension_loaded(name: &str) -> bool { - matches!( - name, - "Phar" - | "curl" - | "filter" - | "hash" - | "iconv" - | "intl" - | "mbstring" - | "openssl" - | "zip" - | "zlib" - ) -} - -pub fn gzopen(_file: &str, _mode: &str) -> PhpMixed { - todo!() -} - -pub fn gzread(_file: PhpMixed, _length: i64) -> String { - todo!() -} - -pub fn gzclose(_file: PhpMixed) { - todo!() -} - -pub fn fseek(_stream: PhpMixed, _offset: i64) -> i64 { - todo!() -} - -pub fn rewind(_stream: PhpMixed) -> bool { - todo!() -} - -pub fn strip_tags(_str: &str) -> String { - todo!() -} - -pub const PHP_EOL: &str = "\n"; - -pub const FILE_APPEND: i64 = 8; - -pub const STDIN: PhpResource = PhpResource::Stdin; - -pub fn fopen(_file: &str, _mode: &str) -> PhpMixed { - todo!() -} - -pub fn fwrite(_file: PhpMixed, _data: &str, _length: i64) -> Option<i64> { - todo!() -} - -pub fn fclose(_file: PhpMixed) { - todo!() -} - -pub fn parse_url(url: &str, component: i64) -> PhpMixed { - let all = parse_url_all(url); - let map = match all.as_array() { - Some(map) => map, - // parse_url_all already collapsed a malformed URL to false; propagate it. - None => return all, - }; - let key = match component { - PHP_URL_SCHEME => "scheme", - PHP_URL_HOST => "host", - PHP_URL_PORT => "port", - PHP_URL_USER => "user", - PHP_URL_PASS => "pass", - PHP_URL_PATH => "path", - PHP_URL_QUERY => "query", - PHP_URL_FRAGMENT => "fragment", - _ => return PhpMixed::Null, - }; - map.get(key).cloned().unwrap_or(PhpMixed::Null) -} - -pub fn parse_url_all(url: &str) -> PhpMixed { - // TODO(phase-c): PHP's parse_url uses php_url_parse_ex, which accepts relative - // and partial URLs and leaves an absent component absent. reqwest::Url - // (WHATWG/RFC 3986) requires an absolute URL, lowercases the host of special - // schemes, and normalizes the path (e.g. "http://host" yields path "/"). This - // is therefore not a byte-for-byte compatible port of parse_url. - let parsed = match reqwest::Url::parse(url) { - Ok(parsed) => parsed, - Err(_) => return PhpMixed::Bool(false), - }; - let mut map: IndexMap<String, PhpMixed> = IndexMap::new(); - map.insert( - "scheme".to_string(), - PhpMixed::String(parsed.scheme().to_string()), - ); - if let Some(host) = parsed.host_str() { - map.insert("host".to_string(), PhpMixed::String(host.to_string())); - } - if let Some(port) = parsed.port() { - map.insert("port".to_string(), PhpMixed::Int(port as i64)); - } - if !parsed.username().is_empty() { - map.insert( - "user".to_string(), - PhpMixed::String(parsed.username().to_string()), - ); - } - if let Some(pass) = parsed.password() { - map.insert("pass".to_string(), PhpMixed::String(pass.to_string())); - } - let path = parsed.path(); - if !path.is_empty() { - map.insert("path".to_string(), PhpMixed::String(path.to_string())); - } - if let Some(query) = parsed.query() { - map.insert("query".to_string(), PhpMixed::String(query.to_string())); - } - if let Some(fragment) = parsed.fragment() { - map.insert( - "fragment".to_string(), - PhpMixed::String(fragment.to_string()), - ); - } - PhpMixed::Array(map) -} - -pub fn pathinfo(path: PhpMixed, option: i64) -> PhpMixed { - let path = path.as_string().unwrap_or(""); - let component = match option { - PATHINFO_DIRNAME => dirname(path), - PATHINFO_BASENAME => basename(path), - PATHINFO_EXTENSION => { - let base = basename(path); - match base.rfind('.') { - Some(index) => base[index + 1..].to_string(), - None => String::new(), - } - } - PATHINFO_FILENAME => { - let base = basename(path); - match base.rfind('.') { - Some(index) => base[..index].to_string(), - None => base, - } - } - _ => unreachable!("pathinfo called with an unsupported single-component option"), - }; - PhpMixed::String(component) -} - -pub fn strtr(str: &str, from: &str, to: &str) -> String { - let from: Vec<char> = from.chars().collect(); - let to: Vec<char> = to.chars().collect(); - let n = from.len().min(to.len()); - str.chars() - .map(|c| match from[..n].iter().position(|&f| f == c) { - Some(i) => to[i], - None => c, - }) - .collect() -} - -pub fn implode(_glue: &str, _pieces: &[String]) -> String { - _pieces.join(_glue) -} - -pub fn version_compare(_v1: &str, _v2: &str, _op: &str) -> bool { - todo!() -} - -pub fn version_compare_2(_v1: &str, _v2: &str) -> i64 { - todo!() -} - -pub fn microtime(_get_as_float: bool) -> f64 { - todo!() -} - -static ERROR_REPORTING_LEVEL: std::sync::atomic::AtomicI64 = - std::sync::atomic::AtomicI64::new(E_ALL); - -pub fn error_reporting(level: Option<i64>) -> i64 { - let old = ERROR_REPORTING_LEVEL.load(std::sync::atomic::Ordering::Relaxed); - if let Some(level) = level { - ERROR_REPORTING_LEVEL.store(level, std::sync::atomic::Ordering::Relaxed); - } - old -} - -pub const E_ALL: i64 = 32767; -pub const E_WARNING: i64 = 2; -pub const E_NOTICE: i64 = 8; -pub const E_USER_WARNING: i64 = 512; -pub const E_USER_NOTICE: i64 = 1024; -pub const E_DEPRECATED: i64 = 8192; -pub const E_USER_DEPRECATED: i64 = 16384; - -pub const PHP_URL_SCHEME: i64 = 0; -pub const PHP_URL_HOST: i64 = 1; -pub const PHP_URL_PORT: i64 = 2; -pub const PHP_URL_USER: i64 = 3; -pub const PHP_URL_PASS: i64 = 4; -pub const PHP_URL_PATH: i64 = 5; -pub const PHP_URL_QUERY: i64 = 6; -pub const PHP_URL_FRAGMENT: i64 = 7; -pub const PATHINFO_FILENAME: i64 = 64; -pub const PATHINFO_EXTENSION: i64 = 4; -pub const PATHINFO_DIRNAME: i64 = 1; -pub const PATHINFO_BASENAME: i64 = 2; -pub const DIRECTORY_SEPARATOR: &str = "/"; - -pub const HHVM_VERSION: Option<&str> = None; - -#[derive(Debug)] -pub struct Phar { - path: String, -} - -impl Phar { - pub const ZIP: i64 = 1; - pub const TAR: i64 = 2; - pub const GZ: i64 = 4096; - pub const BZ2: i64 = 8192; - - pub fn new(_a: String) -> Self { - todo!() - } - - pub fn extract_to(&self, _a: &str, _b: Option<()>, _c: bool) { - todo!() - } - - pub fn running(_return_full: bool) -> String { - todo!() - } -} - -#[derive(Debug)] -pub struct PharException { - pub message: String, - pub code: i64, -} - -impl std::fmt::Display for PharException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for PharException {} - -#[derive(Debug)] -pub struct PharFileInfo; - -impl PharFileInfo { - pub fn get_content(&self) -> String { - todo!() - } - - pub fn get_basename(&self) -> String { - todo!() - } - - pub fn is_dir(&self) -> bool { - todo!() - } -} - -#[derive(Debug)] -pub struct PharData { - path: String, -} - -impl PharData { - pub fn new(_a: String) -> Self { - todo!() - } - - pub fn new_with_format(_path: String, _flags: i64, _alias: &str, _format: i64) -> Self { - todo!() - } - - pub fn can_compress(_algo: i64) -> bool { - todo!() - } - - pub fn valid(&self) -> bool { - todo!() - } - - pub fn get(&self, _key: &str) -> Option<PharFileInfo> { - todo!() - } - - pub fn iter(&self) -> impl Iterator<Item = PharFileInfo> { - todo!(); - std::iter::empty() - } - - pub fn extract_to(&self, _a: &str, _b: Option<()>, _c: bool) { - todo!() - } - - pub fn add_empty_dir(&self, _a: &str) { - todo!() - } - - pub fn build_from_iterator( - &self, - _iter: &mut dyn Iterator<Item = std::path::PathBuf>, - _base: &str, - ) { - todo!() - } - - pub fn compress(&self, _algo: i64) { - todo!() - } -} - -#[derive(Debug)] -pub struct ZipArchive { - pub num_files: i64, -} - -impl Default for ZipArchive { - fn default() -> Self { - Self::new() - } -} - -impl ZipArchive { - pub fn new() -> Self { - todo!() - } - - pub fn open(&mut self, _filename: &str, _flags: i64) -> Result<(), i64> { - todo!() - } - - pub fn close(&self) -> bool { - todo!() - } - - pub fn count(&self) -> i64 { - todo!() - } - - pub fn stat_index(&self, _index: i64) -> Option<IndexMap<String, PhpMixed>> { - todo!() - } - - pub fn extract_to(&self, _path: &str) -> bool { - todo!() - } - - pub fn locate_name(&self, _name: &str) -> Option<i64> { - todo!() - } - - pub fn get_from_index(&self, _index: i64) -> Option<String> { - todo!() - } - - pub fn get_name_index(&self, _index: i64) -> String { - todo!() - } - - pub fn get_stream(&self, _name: &str) -> Option<PhpMixed> { - todo!() - } - - pub fn add_empty_dir(&self, _local_name: &str) -> bool { - todo!() - } - - pub fn add_file(&self, _filepath: &str, _local_name: &str) -> bool { - todo!() - } - - pub fn set_external_attributes_name(&self, _name: &str, _opsys: i64, _attr: i64) -> bool { - todo!() - } - - pub fn get_status_string(&self) -> String { - todo!() - } -} - -impl ZipArchive { - pub const CREATE: i64 = 1; - pub const OPSYS_UNIX: i64 = 3; - pub const ER_SEEK: i64 = 4; - pub const ER_READ: i64 = 5; - pub const ER_NOENT: i64 = 9; - pub const ER_EXISTS: i64 = 10; - pub const ER_OPEN: i64 = 11; - pub const ER_MEMORY: i64 = 14; - pub const ER_INVAL: i64 = 18; - pub const ER_NOZIP: i64 = 19; - pub const ER_INCONS: i64 = 21; -} - -pub trait JsonSerializable { - fn json_serialize(&self) -> PhpMixed; -} - -pub fn in_array(needle: PhpMixed, haystack: &PhpMixed, strict: bool) -> bool { - let values: Vec<&PhpMixed> = match haystack { - PhpMixed::List(items) => items.iter().collect(), - PhpMixed::Array(map) => map.values().collect(), - _ => return false, - }; - - if !strict { - // TODO(phase-c): non-strict in_array needs PHP's loose `==` comparison semantics. Only the - // strict path is implemented; loose comparison is deferred rather than approximated. - todo!("non-strict in_array (PHP loose comparison)"); - } - - values.iter().any(|value| **value == needle) -} - -// TODO(phase-c): takes &Path and returns Option<PathBuf> -pub fn realpath(path: &str) -> Option<String> { - std::path::Path::new(path) - .canonicalize() - .ok() - .and_then(|p| p.to_str().map(ToOwned::to_owned)) -} - -pub const JSON_UNESCAPED_UNICODE: i64 = 256; -pub const JSON_UNESCAPED_SLASHES: i64 = 64; -pub const JSON_PRETTY_PRINT: i64 = 128; -pub const JSON_THROW_ON_ERROR: i64 = 4194304; - -pub fn json_encode<T: serde::Serialize + ?Sized>(value: &T) -> Option<String> { - // PHP's json_encode() with no flags escapes slashes and non-ASCII characters. - json_encode_ex(value, 0) -} - -pub fn dirname(path: &str) -> String { - if path.is_empty() { - return String::new(); - } - match std::path::Path::new(path).parent() { - // No parent: the root itself, or a path made up solely of slashes. - None => "/".to_string(), - // Path::parent yields an empty path where PHP's dirname returns ".". - Some(parent) if parent.as_os_str().is_empty() => ".".to_string(), - Some(parent) => parent.to_str().expect("input was valid UTF-8").to_string(), - } -} - -pub fn stream_get_contents(_stream: PhpMixed) -> Option<String> { - todo!() -} - -// Models the classes available in a standard PHP CLI environment running Composer: -// the common bundled extensions (zip, Phar) plus Composer's own runtime classes. -pub fn class_exists(name: &str) -> bool { - matches!(name, "Composer\\InstalledVersions" | "Phar" | "ZipArchive") -} - -// Models the functions available in a standard modern PHP CLI environment on a -// non-Windows platform with the common extensions loaded (curl, mbstring, iconv, -// zlib, posix, pcntl). Opt-in or Windows-only functions are reported absent. -pub fn function_exists(name: &str) -> bool { - matches!( - name, - "bzcompress" - | "cli_set_process_title" - | "curl_multi_exec" - | "curl_multi_init" - | "curl_multi_setopt" - | "curl_share_init" - | "curl_strerror" - | "date_default_timezone_get" - | "date_default_timezone_set" - | "disk_free_space" - | "exec" - | "filter_var" - | "getmypid" - | "gzcompress" - | "iconv" - | "ini_set" - | "json_decode" - | "mb_check_encoding" - | "mb_convert_encoding" - | "mb_strlen" - | "pcntl_async_signals" - | "pcntl_signal" - | "php_strip_whitespace" - | "php_uname" - | "posix_geteuid" - | "posix_getpwuid" - | "posix_getuid" - | "posix_isatty" - | "proc_open" - | "putenv" - | "shell_exec" - | "stream_isatty" - | "symlink" - ) -} - -/// Normalizes an mbstring encoding label to a canonical spelling (e.g. `utf8` -> `UTF-8`). -fn canonical_encoding(name: &str) -> String { - match name.to_ascii_uppercase().replace('-', "").as_str() { - "UTF8" => "UTF-8".to_string(), - "ASCII" | "USASCII" => "ASCII".to_string(), - _ => name.to_ascii_uppercase(), - } -} - -pub fn mb_convert_encoding(_string: Vec<u8>, _to_encoding: &str, _from_encoding: &str) -> String { - let to = canonical_encoding(_to_encoding); - let from = canonical_encoding(_from_encoding); - // ASCII is a subset of UTF-8, so converting among ASCII/UTF-8 is a byte-level no-op. Other - // encodings need conversion tables that have not been ported yet. - if matches!(to.as_str(), "UTF-8" | "ASCII") && matches!(from.as_str(), "UTF-8" | "ASCII") { - return String::from_utf8_lossy(&_string).into_owned(); - } - todo!("mb_convert_encoding {} -> {}", from, to) -} - -pub fn touch(_path: &str) -> bool { - todo!() -} - -pub fn touch2(_path: &str, _mtime: i64) -> bool { - todo!() -} - -pub fn touch3(_path: &str, _mtime: i64, _atime: i64) -> bool { - todo!() -} - -/// PHP `PHP_OS_FAMILY` constant: the family of the host OS. -/// One of "Windows", "BSD", "Darwin", "Solaris", "Linux", "Unknown". -pub fn php_os_family() -> &'static str { - match std::env::consts::OS { - "linux" | "android" => "Linux", - "macos" | "ios" => "Darwin", - "windows" => "Windows", - "freebsd" | "dragonfly" | "netbsd" | "openbsd" => "BSD", - "solaris" | "illumos" => "Solaris", - _ => "Unknown", - } -} - -pub fn chmod(_path: &str, _mode: u32) -> bool { - todo!() -} - -pub fn strpbrk(haystack: &str, char_list: &str) -> Option<String> { - let set = char_list.as_bytes(); - let bytes = haystack.as_bytes(); - for i in 0..bytes.len() { - if set.contains(&bytes[i]) { - return Some(String::from_utf8_lossy(&bytes[i..]).into_owned()); - } - } - None -} - -fn hex_digit_value(b: u8) -> Option<u8> { - match b { - b'0'..=b'9' => Some(b - b'0'), - b'a'..=b'f' => Some(b - b'a' + 10), - b'A'..=b'F' => Some(b - b'A' + 10), - _ => None, - } -} - -pub fn rawurldecode(s: &str) -> String { - let bytes = s.as_bytes(); - let mut out: Vec<u8> = Vec::with_capacity(bytes.len()); - let mut i = 0; - while i < bytes.len() { - if bytes[i] == b'%' && i + 2 < bytes.len() { - if let (Some(h), Some(l)) = - (hex_digit_value(bytes[i + 1]), hex_digit_value(bytes[i + 2])) - { - out.push((h << 4) | l); - i += 3; - continue; - } - } - out.push(bytes[i]); - i += 1; - } - String::from_utf8_lossy(&out).into_owned() -} - -pub fn rawurlencode(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - for &b in s.as_bytes() { - if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') { - out.push(b as char); - } else { - out.push_str(&format!("%{:02X}", b)); - } - } - out -} - -pub fn urlencode(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - for &b in s.as_bytes() { - if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.') { - out.push(b as char); - } else if b == b' ' { - out.push('+'); - } else { - out.push_str(&format!("%{:02X}", b)); - } - } - out -} - -pub fn base64_encode(_data: &str) -> String { - todo!() -} - -pub fn base64_decode(_data: &str) -> Option<Vec<u8>> { - todo!() -} - -pub fn substr_count(haystack: &str, needle: &str) -> i64 { - if needle.is_empty() { - panic!("substr_count(): Argument #2 ($needle) cannot be empty"); - } - // str::matches counts non-overlapping occurrences, matching PHP's substr_count. - haystack.matches(needle).count() as i64 -} - -pub fn openssl_x509_parse( - _certificate: &str, - _short_names: bool, -) -> Option<IndexMap<String, PhpMixed>> { - todo!() -} - -pub fn openssl_get_publickey(_certificate: &str) -> Option<PhpMixed> { - todo!() -} - -pub fn openssl_pkey_get_details(_key: PhpMixed) -> Option<IndexMap<String, PhpMixed>> { - todo!() -} - -pub fn fileperms(_path: &str) -> i64 { - todo!() -} - -pub const FILTER_VALIDATE_BOOLEAN: i64 = 258; -pub const FILTER_VALIDATE_URL: i64 = 273; -pub const FILTER_VALIDATE_IP: i64 = 275; -pub const FILTER_VALIDATE_INT: i64 = 257; - -pub fn filter_var(value: &str, filter: i64) -> bool { - match filter { - // Without FILTER_NULL_ON_FAILURE, php_filter_boolean trims surrounding - // whitespace, lowercases, and yields true only for "1"/"true"/"on"/"yes"; - // every other input (including the "0"/"false"/"off"/"no"/"" set) yields - // false. - FILTER_VALIDATE_BOOLEAN => { - let trimmed = value.trim_matches([' ', '\t', '\n', '\r', '\0', '\x0B']); - matches!( - trimmed.to_ascii_lowercase().as_str(), - "1" | "true" | "on" | "yes" - ) - } - // TODO(phase-c): PHP's FILTER_VALIDATE_URL parses with php_url_parse_ex and - // additionally validates the host as a domain/IPv6 literal. reqwest::Url - // (WHATWG/RFC 3986) is stricter on some inputs and more lenient on others, - // so this is not a byte-for-byte compatible validator. - FILTER_VALIDATE_URL => reqwest::Url::parse(value).is_ok(), - _ => todo!(), - } -} - -// Models the configuration of a standard PHP CLI environment. Settings belonging -// to extensions that are not loaded (apcu, uopz, xdebug) are not registered, so -// PHP's ini_get returns false (None) for them. -pub fn ini_get(option: &str) -> Option<String> { - match option { - "allow_url_fopen" => Some("1".to_string()), - "default_socket_timeout" => Some("60".to_string()), - "disable_functions" => Some(String::new()), - "mbstring.func_overload" => Some("0".to_string()), - "memory_limit" => Some("-1".to_string()), - "open_basedir" => Some(String::new()), - _ => None, - } -} - -pub fn apcu_add(key: &str, var: PhpMixed) -> bool { - let _ = (key, var); - todo!() -} - -pub fn apcu_fetch(key: &str, success: &mut bool) -> PhpMixed { - let _ = (key, success); - todo!() -} - -pub fn spl_autoload_register( - callback: Box<dyn Fn(&str) -> PhpMixed + Send + Sync>, - throw: bool, - prepend: bool, -) -> bool { - let _ = (callback, throw, prepend); - todo!() -} - -pub fn spl_autoload_unregister(callback: Box<dyn Fn(&str) -> PhpMixed + Send + Sync>) -> bool { - let _ = callback; - todo!() -} - -pub fn stream_resolve_include_path(filename: &str) -> Option<String> { - let _ = filename; - todo!() -} - -/// Equivalent to PHP `include $file;` -pub fn include_file(file: &str) -> PhpMixed { - let _ = file; - todo!() -} - -// TODO(php-runtime): the callback should be registered in PHP runtime. -pub fn set_error_handler(_callback: fn(i64, &str, &str, i64) -> bool) {} - -pub fn debug_backtrace() -> Vec<IndexMap<String, PhpMixed>> { - todo!() -} - -pub const PHP_VERSION: &str = "8.1.0"; - -pub const STDERR: i64 = 2; - -pub fn is_resource(_value: &PhpMixed) -> bool { - // PhpMixed has no resource variant, so a PhpMixed is never a resource. - false -} - -#[derive(Debug)] -pub struct RarEntry; - -impl RarEntry { - pub fn extract(&self, _path: &str) -> bool { - todo!() - } -} - -pub fn var_export(_value: &PhpMixed, _return: bool) -> String { - todo!() -} - -pub fn var_export_str(_value: &str, _return: bool) -> String { - todo!() -} - -#[derive(Debug)] -pub struct RarArchive; - -impl RarArchive { - pub fn open(_file: &str) -> Option<Self> { - todo!() - } - - pub fn get_entries(&self) -> Option<Vec<RarEntry>> { - todo!() - } - - pub fn close(&self) { - todo!() - } -} - -/// Map a PHP array key (always stored as a `String` here) back to its PHP value -/// type: an integer-like key becomes an int, anything else stays a string. -fn php_key_to_mixed(key: &str) -> PhpMixed { - if let Ok(n) = key.parse::<i64>() { - if n.to_string() == key { - return PhpMixed::Int(n); - } - } - PhpMixed::String(key.to_string()) -} - -pub fn array_fill_keys(keys: PhpMixed, value: PhpMixed) -> PhpMixed { - let entries: Vec<&PhpMixed> = match &keys { - PhpMixed::List(items) => items.iter().collect(), - PhpMixed::Array(map) => map.values().collect(), - _ => panic!("array_fill_keys(): Argument #1 ($keys) must be of type array"), - }; - let mut result: IndexMap<String, PhpMixed> = IndexMap::new(); - for key in entries { - result.insert(php_to_string(key), value.clone()); - } - PhpMixed::Array(result) -} - -/// PHP `array_merge`. -/// -/// Must reproduce PHP's mixed integer/string key semantics: -/// - string keys: a later array's value overwrites an earlier one, keeping the -/// earlier key's position; -/// - integer-like keys ("0","1",...): values are appended and renumbered -/// sequentially across all inputs (they are NOT overwritten by key). -/// -/// A naive per-entry `IndexMap::insert` is INCORRECT for inputs that mix string -/// and integer keys (e.g. an AliasPackage's provides/replaces, where -/// self.version expansion appends links under "0","1",... keys). See the typed -/// [`array_merge_map`] variant used by such call sites. -pub fn array_merge(array1: PhpMixed, array2: PhpMixed) -> PhpMixed { - let mut result: IndexMap<String, PhpMixed> = IndexMap::new(); - let mut next_int: i64 = 0; - for array in [array1, array2] { - match array { - PhpMixed::List(items) => { - for value in items { - result.insert(next_int.to_string(), value); - next_int += 1; - } - } - PhpMixed::Array(map) => { - for (key, value) in map { - if let Ok(n) = key.parse::<i64>() { - if n.to_string() == key { - result.insert(next_int.to_string(), value); - next_int += 1; - continue; - } - } - result.insert(key, value); - } - } - _ => panic!("array_merge(): Argument must be of type array"), - } - } - let is_list = result.keys().enumerate().all(|(i, k)| *k == i.to_string()); - if is_list { - PhpMixed::List(result.into_values().collect()) - } else { - PhpMixed::Array(result) - } -} - -/// PHP `array_merge` for a string-keyed map that MAY also contain integer-like -/// keys. Typed counterpart of [`array_merge`] for `IndexMap<String, V>` values -/// (e.g. `Link` maps from `getProvides`/`getReplaces`). -/// -/// Must reproduce the same mixed-key semantics as [`array_merge`]: string keys -/// overwrite in place (later wins), integer-like keys ("0","1",...) are appended -/// and renumbered sequentially across both inputs. A naive `IndexMap::insert` -/// per entry is INCORRECT because it would collide on shared integer keys. -pub fn array_merge_map<V>( - array1: IndexMap<String, V>, - array2: IndexMap<String, V>, -) -> IndexMap<String, V> { - let mut result: IndexMap<String, V> = IndexMap::new(); - let mut next_int: i64 = 0; - for array in [array1, array2] { - for (key, value) in array { - if let Ok(n) = key.parse::<i64>() { - if n.to_string() == key { - result.insert(next_int.to_string(), value); - next_int += 1; - continue; - } - } - result.insert(key, value); - } - } - result -} - -pub fn substr_replace(_string: &str, _replace: &str, _start: usize, _length: usize) -> String { - todo!() -} - -pub fn constant(_name: &str) -> PhpMixed { - todo!() -} - -pub fn get_loaded_extensions() -> Vec<String> { - todo!() -} - -pub fn phpversion(_extension: &str) -> Option<String> { - todo!() -} - -pub fn ob_start() -> bool { - todo!() -} - -pub fn ob_get_clean() -> Option<String> { - todo!() -} - -pub fn html_entity_decode(_s: &str) -> String { - todo!() -} - -pub fn hash_file(_algo: &str, _filename: &str) -> Option<String> { - todo!() -} - -pub fn filesize(path: impl AsRef<std::path::Path>) -> Option<i64> { - std::fs::metadata(path).ok().map(|m| m.len() as i64) -} - -pub fn json_encode_ex<T: serde::Serialize + ?Sized>(value: &T, flags: i64) -> Option<String> { - // serde_json's compact output already matches PHP's `json_encode` with both - // JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE set: forward slashes and non-ASCII - // characters are emitted verbatim. The two flags below re-apply PHP's default escaping when - // they are absent. - // TODO(phase-c): other flags (e.g. JSON_PRETTY_PRINT, JSON_HEX_*, JSON_THROW_ON_ERROR) are not - // handled yet; add them when a call site needs them. - let mut s = serde_json::to_string(value).ok()?; - - if flags & JSON_UNESCAPED_SLASHES == 0 { - s = s.replace('/', "\\/"); - } - - if flags & JSON_UNESCAPED_UNICODE == 0 { - let mut out = String::with_capacity(s.len()); - for c in s.chars() { - if (c as u32) <= 0x7F { - out.push(c); - } else { - let mut buf = [0u16; 2]; - for unit in c.encode_utf16(&mut buf) { - out.push_str(&format!("\\u{:04x}", unit)); - } - } - } - s = out; - } - - Some(s) -} - -pub const JSON_INVALID_UTF8_IGNORE: i64 = 1048576; - -pub fn is_array(_value: &PhpMixed) -> bool { - matches!(_value, PhpMixed::List(_) | PhpMixed::Array(_)) -} - -pub fn strnatcasecmp(_s1: &str, _s2: &str) -> i64 { - todo!() -} - -pub fn file_exists(path: impl AsRef<std::path::Path>) -> bool { - path.as_ref().exists() -} - -// TODO(phase-c): PHP's is_writable() resolves to access(2) with W_OK, honoring the effective -// user/group and ACLs. This std-only approximation only inspects the permission bits, so it can -// diverge for files the current user does not own. Refine with a syscall (libc/rustix) crate later. -pub fn is_writable(_path: &str) -> bool { - match std::fs::metadata(_path) { - Ok(meta) => !meta.permissions().readonly(), - Err(_) => false, - } -} - -pub fn unlink(path: impl AsRef<std::path::Path>) -> bool { - std::fs::remove_file(path).is_ok() -} - -pub fn file_put_contents(_path: &str, _data: &[u8]) -> Option<i64> { - std::fs::write(_path, _data) - .ok() - .map(|_| _data.len() as i64) -} - -pub fn str_repeat(_s: &str, _count: usize) -> String { - _s.repeat(_count) -} - -pub fn strrpos(_haystack: &str, _needle: &str) -> Option<usize> { - _haystack.rfind(_needle) -} - -pub fn gzcompress(_data: &[u8]) -> Option<Vec<u8>> { - todo!() -} - -pub fn bzcompress(_data: &[u8]) -> Option<Vec<u8>> { - todo!() -} - -pub fn getcwd() -> Option<String> { - std::env::current_dir() - .ok() - .map(|p| p.to_string_lossy().into_owned()) -} - -pub fn chdir(_path: &str) -> anyhow::Result<()> { - Ok(std::env::set_current_dir(_path)?) -} - -pub fn glob(_pattern: &str) -> Vec<String> { - todo!() -} - -pub fn basename(path: &str) -> String { - // PHP basename(): the trailing name component, after stripping trailing directory separators. - let trimmed = path.trim_end_matches(['/', '\\']); - match trimmed.rfind(['/', '\\']) { - Some(index) => trimmed[index + 1..].to_string(), - None => trimmed.to_string(), - } -} - -pub fn explode(delimiter: &str, string: &str) -> Vec<String> { - string.split(delimiter).map(|s| s.to_string()).collect() -} - -fn explode_limit_impl(delimiter: &str, string: &str, limit: i64) -> Vec<String> { - if limit > 0 { - string - .splitn(limit as usize, delimiter) - .map(|s| s.to_string()) - .collect() - } else if limit == 0 { - // PHP treats a zero limit as 1: the whole string is returned as one element. - vec![string.to_string()] - } else { - let parts: Vec<String> = string.split(delimiter).map(|s| s.to_string()).collect(); - let keep = parts.len() as i64 + limit; - if keep <= 0 { - Vec::new() - } else { - parts[..keep as usize].to_vec() - } - } -} - -pub fn explode_with_limit(delimiter: &str, string: &str, limit: i64) -> Vec<String> { - explode_limit_impl(delimiter, string, limit) -} - -pub struct FilesystemIterator; - -impl FilesystemIterator { - pub const KEY_AS_PATHNAME: i64 = 256; - pub const CURRENT_AS_FILEINFO: i64 = 0; -} - -pub const CURLOPT_PROXY: i64 = 10004; -pub const CURLOPT_NOPROXY: i64 = 10177; -pub const CURLOPT_PROXYAUTH: i64 = 111; -pub const CURLOPT_PROXYUSERPWD: i64 = 10006; -pub const CURLAUTH_BASIC: i64 = 1; -pub const CURLOPT_PROXY_CAINFO: i64 = 246; -pub const CURLOPT_PROXY_CAPATH: i64 = 247; -pub const CURL_VERSION_HTTPS_PROXY: i64 = 2097152; - -pub const CURLM_OK: i64 = 0; -pub const CURLM_BAD_HANDLE: i64 = 1; -pub const CURLM_BAD_EASY_HANDLE: i64 = 2; -pub const CURLM_OUT_OF_MEMORY: i64 = 3; -pub const CURLM_INTERNAL_ERROR: i64 = 4; -pub const CURLM_CALL_MULTI_PERFORM: i64 = -1; - -pub const CURLMOPT_PIPELINING: i64 = 3; -pub const CURLMOPT_MAX_HOST_CONNECTIONS: i64 = 7; - -pub const CURLSHOPT_SHARE: i64 = 1; -pub const CURL_LOCK_DATA_COOKIE: i64 = 2; -pub const CURL_LOCK_DATA_DNS: i64 = 3; -pub const CURL_LOCK_DATA_SSL_SESSION: i64 = 4; - -pub const CURLOPT_URL: i64 = 10002; -pub const CURLOPT_FOLLOWLOCATION: i64 = 52; -pub const CURLOPT_CONNECTTIMEOUT: i64 = 78; -pub const CURLOPT_TIMEOUT: i64 = 13; -pub const CURLOPT_WRITEHEADER: i64 = 10029; -pub const CURLOPT_FILE: i64 = 10001; -pub const CURLOPT_ENCODING: i64 = 10102; -pub const CURLOPT_PROTOCOLS: i64 = 181; -pub const CURLOPT_CUSTOMREQUEST: i64 = 10036; -pub const CURLOPT_POSTFIELDS: i64 = 10015; -pub const CURLOPT_HTTPHEADER: i64 = 10023; -pub const CURLOPT_CAINFO: i64 = 10065; -pub const CURLOPT_CAPATH: i64 = 10097; -pub const CURLOPT_SSL_VERIFYPEER: i64 = 64; -pub const CURLOPT_SSL_VERIFYHOST: i64 = 81; -pub const CURLOPT_SSLCERT: i64 = 10025; -pub const CURLOPT_SSLKEY: i64 = 10087; -pub const CURLOPT_SSLKEYPASSWD: i64 = 10026; -pub const CURLOPT_IPRESOLVE: i64 = 113; -pub const CURLOPT_SHARE: i64 = 10100; -pub const CURLOPT_HTTP_VERSION: i64 = 84; - -pub const CURLPROTO_HTTP: i64 = 1; -pub const CURLPROTO_HTTPS: i64 = 2; - -pub const CURL_IPRESOLVE_V4: i64 = 1; -pub const CURL_IPRESOLVE_V6: i64 = 2; - -pub const CURL_HTTP_VERSION_2_0: i64 = 3; -pub const CURL_HTTP_VERSION_3: i64 = 30; - -pub const CURL_VERSION_HTTP2: i64 = 65536; -pub const CURL_VERSION_HTTP3: i64 = 33554432; -pub const CURL_VERSION_LIBZ: i64 = 8; - -pub const CURLE_OK: i64 = 0; -pub const CURLE_OPERATION_TIMEDOUT: i64 = 28; - -#[derive(Debug)] -pub struct CurlHandle; - -#[derive(Debug)] -pub struct CurlMultiHandle; - -#[derive(Debug)] -pub struct CurlShareHandle; - -pub fn curl_version() -> Option<IndexMap<String, PhpMixed>> { - todo!() -} - -pub fn curl_init() -> CurlHandle { - todo!() -} - -pub fn curl_close(_handle: CurlHandle) { - todo!() -} - -pub fn curl_setopt(_handle: &CurlHandle, _option: i64, _value: PhpMixed) -> bool { - todo!() -} - -pub fn curl_setopt_array(_handle: &CurlHandle, _options: &IndexMap<i64, PhpMixed>) -> bool { - todo!() -} - -pub fn curl_getinfo(_handle: &CurlHandle) -> PhpMixed { - todo!() -} - -pub fn curl_error(_handle: &CurlHandle) -> String { - todo!() -} - -pub fn curl_errno(_handle: &CurlHandle) -> i64 { - todo!() -} - -pub fn curl_strerror(_errornum: i64) -> Option<String> { - todo!() -} - -pub fn curl_multi_init() -> CurlMultiHandle { - todo!() -} - -pub fn curl_multi_setopt(_mh: &CurlMultiHandle, _option: i64, _value: PhpMixed) -> bool { - todo!() -} - -pub fn curl_multi_add_handle(_mh: &CurlMultiHandle, _handle: &CurlHandle) -> i64 { - todo!() -} - -pub fn curl_multi_remove_handle(_mh: &CurlMultiHandle, _handle: &CurlHandle) -> i64 { - todo!() -} - -pub fn curl_multi_exec(_mh: &CurlMultiHandle, _still_running: &mut bool) -> i64 { - todo!() -} - -pub fn curl_multi_select(_mh: &CurlMultiHandle, _timeout: f64) -> i64 { - todo!() -} - -pub fn curl_multi_info_read(_mh: &CurlMultiHandle) -> PhpMixed { - todo!() -} - -pub fn curl_share_init() -> CurlShareHandle { - todo!() -} - -pub fn curl_share_setopt(_sh: &CurlShareHandle, _option: i64, _value: PhpMixed) -> bool { - todo!() -} - -/// Cast a `\CurlHandle` to int (its spl_object_id) as `(int) $curlHandle` in PHP. -pub fn curl_handle_id(_handle: &CurlHandle) -> i64 { - todo!() -} - -// TODO(php-runtime): the previous handler should be restored in the PHP runtime. -// Paired with set_error_handler, which is a no-op in this shim. -pub fn restore_error_handler() {} - -pub fn stream_get_contents_with_max(stream: PhpMixed, max_length: Option<i64>) -> Option<String> { - let _ = (stream, max_length); - todo!() -} - -pub fn bin2hex(_data: &[u8]) -> String { - _data.iter().map(|b| format!("{:02x}", b)).collect() -} - -pub fn is_dir(path: impl AsRef<std::path::Path>) -> bool { - path.as_ref().is_dir() -} - -pub fn file_get_contents(_path: &str) -> Option<String> { - std::fs::read(_path) - .ok() - .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) -} - -pub fn file_get_contents5( - _path: &str, - _use_include_path: bool, - _context: PhpMixed, - _offset: i64, - _length: Option<i64>, -) -> Option<String> { - todo!() -} - -pub fn strtolower(_s: &str) -> String { - _s.to_ascii_lowercase() -} - -pub fn ctype_alnum(_s: &str) -> bool { - !_s.is_empty() && _s.bytes().all(|b| b.is_ascii_alphanumeric()) -} - -pub fn ord(_c: &str) -> i64 { - _c.as_bytes().first().copied().unwrap_or(0) as i64 -} - -pub fn gethostname() -> String { - todo!() -} - -pub fn feof(_stream: PhpMixed) -> bool { - todo!() -} - -pub fn str_replace_array(search: &[String], replace: &[String], subject: &str) -> String { - // PHP's array form of str_replace replaces each search element in order with the replace - // element at the same index, falling back to an empty string when replace is shorter. - let mut result = subject.to_string(); - for (i, s) in search.iter().enumerate() { - let r = replace.get(i).map(String::as_str).unwrap_or(""); - result = str_replace(s, r, &result); - } - result -} - -pub fn file(_filename: &str, _flags: i64) -> Option<Vec<String>> { - todo!() -} - -pub fn ucwords(s: &str) -> String { - // PHP's default word delimiters: space, tab, CR, LF, FF and VT. - let delimiters = [' ', '\t', '\r', '\n', '\x0C', '\x0B']; - let mut out = String::with_capacity(s.len()); - let mut capitalize_next = true; - for c in s.chars() { - if capitalize_next { - out.push(c.to_ascii_uppercase()); - } else { - out.push(c); - } - capitalize_next = delimiters.contains(&c); - } - out -} - -pub fn get_current_user() -> String { - todo!() -} - -pub const FILE_IGNORE_NEW_LINES: i64 = 2; - -pub fn array_diff(_array1: &[String], _array2: &[String]) -> Vec<String> { - _array1 - .iter() - .filter(|&x| !_array2.contains(x)) - .cloned() - .collect() -} - -pub fn copy(_source: &str, _dest: &str) -> bool { - std::fs::copy(_source, _dest).is_ok() -} - -pub fn exec( - _command: &str, - _output: Option<&mut Vec<String>>, - _exit_code: Option<&mut i64>, -) -> Option<String> { - todo!() -} - -pub fn tempnam(_dir: &str, _prefix: &str) -> Option<String> { - todo!() -} - -pub fn openssl_verify( - _data: &str, - _signature: &[u8], - _pub_key_id: PhpMixed, - _algorithm: PhpMixed, -) -> i64 { - todo!() -} - -pub fn openssl_pkey_get_public(_public_key: &str) -> PhpMixed { - todo!() -} - -pub fn openssl_get_md_methods() -> Vec<String> { - todo!() -} - -pub fn openssl_free_key(_key: PhpMixed) { - todo!() -} - -pub fn iterator_to_array<I>(iter: I) -> Vec<I::Item> -where - I: IntoIterator, -{ - iter.into_iter().collect() -} - -pub fn end_arr<V: Clone>(_array: &IndexMap<String, V>) -> Option<V> { - _array.values().last().cloned() -} - -pub fn fileowner(_filename: &str) -> Option<i64> { - todo!() -} - -pub fn unlink_silent(_path: &str) -> bool { - todo!() -} - -pub fn array_unique<T: Clone>(_array: &[T]) -> Vec<T> { - todo!() -} - -pub fn current(_value: PhpMixed) -> PhpMixed { - todo!() -} - -pub fn key(_value: PhpMixed) -> Option<String> { - todo!() -} - -pub fn reset<T: Clone>(_array: &[T]) -> Option<T> { - _array.first().cloned() -} - -pub const OPENSSL_ALGO_SHA384: i64 = 9; - -pub fn array_intersect_key( - _array1: &IndexMap<String, PhpMixed>, - _array2: &IndexMap<String, PhpMixed>, -) -> IndexMap<String, PhpMixed> { - _array1 - .iter() - .filter(|(k, _)| _array2.contains_key(k.as_str())) - .map(|(k, v)| (k.clone(), v.clone())) - .collect() -} - -pub fn is_file(path: impl AsRef<std::path::Path>) -> bool { - path.as_ref().is_file() -} - -pub fn spl_object_hash<T: ?Sized>(_object: &T) -> String { - todo!() -} - -pub fn serialize(_value: &PhpMixed) -> String { - todo!() -} - -pub fn stream_context_create( - _options: &IndexMap<String, PhpMixed>, - _params: Option<&IndexMap<String, PhpMixed>>, -) -> PhpMixed { - todo!() -} - -pub fn stripos(_haystack: &str, _needle: &str) -> Option<usize> { - _haystack - .to_ascii_lowercase() - .find(_needle.to_ascii_lowercase().as_str()) -} - -pub fn php_uname(mode: &str) -> String { - match mode { - // sysname, as reported by uname(2). On Windows PHP returns "Windows NT", - // which differs from PHP_OS. - "s" => match std::env::consts::OS { - "linux" => "Linux", - "macos" => "Darwin", - "windows" => "Windows NT", - "freebsd" => "FreeBSD", - "netbsd" => "NetBSD", - "openbsd" => "OpenBSD", - "dragonfly" => "DragonFly", - "solaris" => "SunOS", - other => other, - } - .to_string(), - // TODO(phase-c): use libc? - // release, as reported by uname(2). On Linux this matches the contents - // of /proc/sys/kernel/osrelease. - "r" => std::fs::read_to_string("/proc/sys/kernel/osrelease") - .map(|s| s.trim_end().to_string()) - .unwrap_or_default(), - _ => todo!(), - } -} - -pub fn uasort<T, F>(array: &mut Vec<T>, compare: F) -where - F: FnMut(&T, &T) -> i64, -{ - let mut compare = compare; - array.sort_by(|a, b| compare(a, b).cmp(&0)); -} - -pub fn uasort_map<K, V, F>(array: &mut IndexMap<K, V>, compare: F) -where - F: FnMut(&V, &V) -> i64, -{ - let mut compare = compare; - array.sort_by(|_, v1, _, v2| compare(v1, v2).cmp(&0)); -} - -pub fn array_replace_recursive( - mut base: IndexMap<String, PhpMixed>, - replacement: IndexMap<String, PhpMixed>, -) -> IndexMap<String, PhpMixed> { - for (key, replacement_value) in replacement { - let merged = match base.get(&key) { - Some(base_value) => { - array_replace_recursive_value(base_value.clone(), replacement_value) - } - None => replacement_value, - }; - base.insert(key, merged); - } - base -} - -// PHP recurses only when both the existing and the replacing value are arrays; -// otherwise the replacing value wins outright. -fn array_replace_recursive_value(base: PhpMixed, replacement: PhpMixed) -> PhpMixed { - match (base, replacement) { - (PhpMixed::Array(base), PhpMixed::Array(replacement)) => { - PhpMixed::Array(array_replace_recursive_assoc(base, replacement)) - } - (PhpMixed::List(base), PhpMixed::List(replacement)) => { - PhpMixed::List(array_replace_recursive_list(base, replacement)) - } - (_, replacement) => replacement, - } -} - -fn array_replace_recursive_assoc( - mut base: IndexMap<String, PhpMixed>, - replacement: IndexMap<String, PhpMixed>, -) -> IndexMap<String, PhpMixed> { - for (key, replacement_value) in replacement { - let merged = match base.get(&key) { - Some(base_value) => { - array_replace_recursive_value(base_value.clone(), replacement_value) - } - None => replacement_value, - }; - base.insert(key, merged); - } - base -} - -fn array_replace_recursive_list( - mut base: Vec<PhpMixed>, - replacement: Vec<PhpMixed>, -) -> Vec<PhpMixed> { - for (index, replacement_value) in replacement.into_iter().enumerate() { - if index < base.len() { - base[index] = array_replace_recursive_value(base[index].clone(), replacement_value); - } else { - base.push(replacement_value); - } - } - base -} - -pub const PHP_MAJOR_VERSION: i64 = 8; -pub const PHP_MINOR_VERSION: i64 = 1; -pub const PHP_RELEASE_VERSION: i64 = 0; - -pub const PHP_WINDOWS_VERSION_MAJOR: i64 = 0; -pub const PHP_WINDOWS_VERSION_MINOR: i64 = 0; - -pub const GLOB_MARK: i64 = 8; -pub const GLOB_ONLYDIR: i64 = 1024; -pub const GLOB_BRACE: i64 = 4096; - -pub fn glob_with_flags(_pattern: &str, _flags: i64) -> Vec<String> { - todo!() -} - -pub fn time() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64 -} - -pub fn date(_format: &str, _timestamp: Option<i64>) -> String { - todo!() -} - -pub fn trigger_error(_message: &str, _error_level: i64) { - todo!() -} - -pub fn sys_get_temp_dir() -> String { - std::env::temp_dir().to_string_lossy().into_owned() -} - -// PHP's two-argument `json_decode`: without JSON_THROW_ON_ERROR it never throws, -// returning null on malformed input. With `assoc` false, JSON objects decode to -// stdClass-equivalent ArrayObject values; with `assoc` true, to associative arrays. -pub fn json_decode(s: &str, assoc: bool) -> anyhow::Result<PhpMixed> { - match serde_json::from_str::<serde_json::Value>(s) { - Ok(value) => Ok(json_value_to_php_mixed(value, assoc)), - Err(_) => Ok(PhpMixed::Null), - } -} - -fn json_value_to_php_mixed(value: serde_json::Value, assoc: bool) -> PhpMixed { - match value { - serde_json::Value::Null => PhpMixed::Null, - serde_json::Value::Bool(b) => PhpMixed::Bool(b), - serde_json::Value::Number(n) => match n.as_i64() { - Some(i) => PhpMixed::Int(i), - // Integers beyond i64 and any fractional/exponent number decode to float, - // matching PHP's default (non-bigint) behaviour. - None => PhpMixed::Float(n.as_f64().unwrap_or(0.0)), - }, - serde_json::Value::String(s) => PhpMixed::String(s), - serde_json::Value::Array(items) => PhpMixed::List( - items - .into_iter() - .map(|item| json_value_to_php_mixed(item, assoc)) - .collect(), - ), - serde_json::Value::Object(entries) => { - let data: IndexMap<String, PhpMixed> = entries - .into_iter() - .map(|(k, v)| (k, json_value_to_php_mixed(v, assoc))) - .collect(); - if assoc { - PhpMixed::Array(data) - } else { - PhpMixed::Object(ArrayObject { - data: data.into_iter().collect(), - }) - } - } - } -} - -pub fn http_build_query_mixed( - data: &IndexMap<String, PhpMixed>, - numeric_prefix: &str, - arg_separator: &str, -) -> String { - let _ = (data, numeric_prefix, arg_separator); - todo!() -} - -pub fn http_build_query(_data: &[(&str, &str)], _sep_str: &str, _sep: &str) -> String { - todo!() -} - -pub fn dirname_levels(path: &str, levels: i64) -> String { - let mut result = path.to_string(); - for _ in 0..levels { - result = dirname(&result); - } - result -} - -// Byte-based, matching PHP's array form of strtr: at each position the longest -// matching key wins (insertion order breaks ties), and replacements are not -// re-scanned. Empty keys are ignored. -pub fn strtr_array(s: &str, pairs: &IndexMap<String, String>) -> String { - let mut keys: Vec<&String> = pairs.keys().filter(|k| !k.is_empty()).collect(); - keys.sort_by_key(|k| std::cmp::Reverse(k.len())); - - let bytes = s.as_bytes(); - let mut result: Vec<u8> = Vec::with_capacity(bytes.len()); - let mut i = 0; - while i < bytes.len() { - let mut matched = false; - for key in &keys { - let kb = key.as_bytes(); - if bytes[i..].starts_with(kb) { - result.extend_from_slice(pairs[*key].as_bytes()); - i += kb.len(); - matched = true; - break; - } - } - if !matched { - result.push(bytes[i]); - i += 1; - } - } - String::from_utf8_lossy(&result).into_owned() -} - -pub fn array_search_mixed( - needle: &PhpMixed, - haystack: &PhpMixed, - strict: bool, -) -> Option<PhpMixed> { - if !strict { - // TODO(phase-c): non-strict array_search needs PHP's loose `==` comparison - // semantics. Only the strict path is implemented; loose comparison is - // deferred rather than approximated. - todo!("non-strict array_search (PHP loose comparison)"); - } - match haystack { - PhpMixed::List(items) => items - .iter() - .position(|value| value == needle) - .map(|i| PhpMixed::Int(i as i64)), - PhpMixed::Array(map) => map - .iter() - .find(|(_, value)| *value == needle) - .map(|(key, _)| php_key_to_mixed(key)), - _ => None, - } -} - -pub fn array_search(needle: &str, haystack: &IndexMap<String, String>) -> Option<String> { - haystack - .iter() - .find(|(_, value)| value.as_str() == needle) - .map(|(key, _)| key.clone()) -} - -pub fn strcmp(_s1: &str, _s2: &str) -> i64 { - _s1.cmp(_s2) as i64 -} - -/// PHP's default trim character mask: " \t\n\r\0\x0B". -const PHP_TRIM_DEFAULT_CHARS: &[u8] = b" \t\n\r\0\x0B"; - -/// Build the set of bytes to strip from a PHP trim `$characters` argument, -/// expanding `a..b` range syntax as PHP does. -fn php_trim_mask(chars: &[u8]) -> [bool; 256] { - let mut mask = [false; 256]; - let mut i = 0; - while i < chars.len() { - if i + 3 < chars.len() && chars[i + 1] == b'.' && chars[i + 2] == b'.' { - let start = chars[i]; - let end = chars[i + 3]; - if start <= end { - for b in start..=end { - mask[b as usize] = true; - } - i += 4; - continue; - } - } - mask[chars[i] as usize] = true; - i += 1; - } - mask -} - -pub fn rtrim(s: &str, chars: Option<&str>) -> String { - let mask = php_trim_mask( - chars - .map(|c| c.as_bytes()) - .unwrap_or(PHP_TRIM_DEFAULT_CHARS), - ); - let bytes = s.as_bytes(); - let mut end = bytes.len(); - while end > 0 && mask[bytes[end - 1] as usize] { - end -= 1; - } - String::from_utf8_lossy(&bytes[..end]).into_owned() -} - -pub fn rmdir(dir: impl AsRef<std::path::Path>) -> bool { - std::fs::remove_dir(dir).is_ok() -} - -pub fn is_link(path: impl AsRef<std::path::Path>) -> bool { - std::fs::symlink_metadata(path) - .map(|m| m.file_type().is_symlink()) - .unwrap_or(false) -} - -pub fn str_pad(_input: &str, _length: usize, _pad_string: &str, _pad_type: i64) -> String { - // PHP str_pad() works on bytes: it pads up to `length` bytes by repeating `pad_string`. - let input_len = _input.len(); - if _length <= input_len || _pad_string.is_empty() { - return _input.to_string(); - } - let pad = _pad_string.as_bytes(); - let make = |n: usize| -> Vec<u8> { (0..n).map(|i| pad[i % pad.len()]).collect() }; - let total = _length - input_len; - let mut out: Vec<u8> = Vec::with_capacity(_length); - match _pad_type { - STR_PAD_LEFT => { - out.extend(make(total)); - out.extend_from_slice(_input.as_bytes()); - } - STR_PAD_BOTH => { - let left = total / 2; - out.extend(make(left)); - out.extend_from_slice(_input.as_bytes()); - out.extend(make(total - left)); - } - _ => { - out.extend_from_slice(_input.as_bytes()); - out.extend(make(total)); - } - } - String::from_utf8_lossy(&out).into_owned() -} - -pub const STR_PAD_LEFT: i64 = 0; -pub const STR_PAD_RIGHT: i64 = 1; -pub const STR_PAD_BOTH: i64 = 2; - -pub fn abs(_value: i64) -> i64 { - _value.abs() -} - -pub fn ucfirst(s: &str) -> String { - let mut chars = s.chars(); - match chars.next() { - None => String::new(), - Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()), - } -} - -pub fn strval(value: &PhpMixed) -> String { - php_to_string(value) -} - -pub fn usleep(_microseconds: u64) { - std::thread::sleep(std::time::Duration::from_micros(_microseconds)); -} - -pub fn mb_strlen(s: &str, _encoding: &str) -> i64 { - // `s` is valid UTF-8, so the character count is its number of code points. - s.chars().count() as i64 -} - -pub fn stream_isatty(stream: PhpResource) -> bool { - stream_isatty_resource(&stream) -} - -pub fn posix_getuid() -> i64 { - todo!() -} - -pub fn posix_geteuid() -> i64 { - todo!() -} - -pub fn posix_getpwuid(_uid: i64) -> PhpMixed { - todo!() -} - -pub fn posix_isatty(_stream: PhpResource) -> bool { - todo!() -} - -pub fn fstat(_stream: PhpResource) -> PhpMixed { - todo!() -} - -pub fn getenv(_name: &str) -> Option<String> { - std::env::var(_name).ok() -} - -// TODO(phase-c): only the simple `^(\w+)(=(.+))?$` form is supported. -pub fn putenv(setting: &str) -> bool { - let is_word = - |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_'); - // A setting without `=` deletes the variable, mirroring PHP's putenv('NAME'). - match setting.split_once('=') { - Some((name, value)) => { - if !is_word(name) { - panic!("putenv: unsupported setting format: {setting:?}"); - } - unsafe { - std::env::set_var(name, value); - } - } - None => { - if !is_word(setting) { - panic!("putenv: unsupported setting format: {setting:?}"); - } - unsafe { - std::env::remove_var(setting); - } - } - } - true -} - -/// PHP superglobal $_SERVER access. In the CLI SAPI $_SERVER is populated from -/// the environment, which is the only source modeled here. -pub fn server_get(name: &str) -> Option<String> { - // TODO: is var_os() better? - std::env::var(name).ok() -} - -// TODO(php-runtime): modify the real PHP's $_SERVER. -pub fn server_set(_name: &str, _value: String) {} - -// TODO(php-runtime): modify the real PHP's $_SERVER. -pub fn server_unset(_name: &str) {} - -pub fn server_contains_key(name: &str) -> bool { - std::env::var_os(name).is_some() -} - -/// PHP superglobal $_ENV access. -pub fn env_get(name: &str) -> Option<String> { - // TODO: is var_os() better? - std::env::var(name).ok() -} - -// TODO(php-runtime): modify the real PHP's $_ENV. -pub fn env_set(_name: &str, _value: String) {} - -// TODO(php-runtime): modify the real PHP's $_ENV. -pub fn env_unset(_name: &str) {} - -pub fn env_contains_key(name: &str) -> bool { - std::env::var_os(name).is_some() -} - -pub fn trim(s: &str, chars: Option<&str>) -> String { - let mask: Vec<char> = match chars { - Some(c) => c.chars().collect(), - None => vec![' ', '\t', '\n', '\r', '\0', '\x0B'], - }; - s.trim_matches(|c| mask.contains(&c)).to_string() -} - -pub fn count(value: &PhpMixed) -> usize { - match value { - PhpMixed::List(items) => items.len(), - PhpMixed::Array(entries) => entries.len(), - PhpMixed::Object(object) => object.count(), - // PHP 8 throws a `TypeError` for non-countable arguments. - PhpMixed::Null - | PhpMixed::Bool(_) - | PhpMixed::Int(_) - | PhpMixed::Float(_) - | PhpMixed::String(_) => { - panic!("count(): Argument #1 ($value) must be of type Countable|array") - } - } -} - -pub fn array_shift<T>(_array: &mut Vec<T>) -> Option<T> { - if _array.is_empty() { - None - } else { - Some(_array.remove(0)) - } -} - -pub fn array_pop<T>(_array: &mut Vec<T>) -> Option<T> { - _array.pop() -} - -pub fn array_unshift<T>(_array: &mut Vec<T>, _value: T) { - _array.insert(0, _value); -} - -pub fn array_reverse<T: Clone>(_array: &[T], _preserve_keys: bool) -> Vec<T> { - _array.iter().rev().cloned().collect() -} - -pub fn array_filter<T: Clone, F>(_array: &[T], _callback: F) -> Vec<T> -where - F: Fn(&T) -> bool, -{ - _array.iter().filter(|&x| _callback(x)).cloned().collect() -} - -pub fn array_filter_map<F>( - _array: &IndexMap<String, PhpMixed>, - _callback: F, -) -> IndexMap<String, PhpMixed> -where - F: Fn(&PhpMixed) -> bool, -{ - _array - .iter() - .filter(|&(_, v)| _callback(v)) - .map(|(k, v)| (k.clone(), v.clone())) - .collect() -} - -pub fn array_all<T, F>(_array: &[T], _callback: F) -> bool -where - F: Fn(&T) -> bool, -{ - _array.iter().all(_callback) -} - -pub fn array_any<T, F>(_array: &[T], _callback: F) -> bool -where - F: Fn(&T) -> bool, -{ - _array.iter().any(_callback) -} - -pub fn array_reduce<T, U, F>(_array: &[T], _callback: F, _initial: U) -> U -where - F: Fn(U, &T) -> U, -{ - _array.iter().fold(_initial, _callback) -} - -pub fn array_intersect<T: Clone + PartialEq>(_array1: &[T], _array2: &[T]) -> Vec<T> { - _array1 - .iter() - .filter(|&x| _array2.contains(x)) - .cloned() - .collect() -} - -pub fn mkdir(_pathname: &str, _mode: u32, _recursive: bool) -> bool { - todo!() -} - -pub fn rename( - old_name: impl AsRef<std::path::Path>, - new_name: impl AsRef<std::path::Path>, -) -> bool { - std::fs::rename(old_name, new_name).is_ok() -} - -pub fn clearstatcache() { - // Rust performs a fresh syscall for every metadata query; there is no stat - // cache to invalidate. -} - -pub fn clearstatcache2(_clear_realpath_cache: bool, _filename: &str) { - // Rust performs a fresh syscall for every metadata query; there is no stat - // cache to invalidate. -} - -pub fn disk_free_space(_directory: &str) -> Option<f64> { - todo!() -} - -pub fn filemtime(_filename: &str) -> Option<i64> { - std::fs::metadata(_filename) - .ok() - .and_then(|m| m.modified().ok()) - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|d| d.as_secs() as i64) -} - -/// Equivalent to PHP's __DIR__ magic constant -pub fn php_dir() -> String { - todo!() -} - -/// Equivalent to PHP's `require <file>` returning the file's return value -pub fn require_php_file(_filename: &str) -> PhpMixed { - todo!() -} - -pub fn array_flip(array: &PhpMixed) -> PhpMixed { - let mut result: IndexMap<String, PhpMixed> = IndexMap::new(); - match array { - PhpMixed::List(items) => { - for (i, value) in items.iter().enumerate() { - match value { - PhpMixed::Int(n) => { - result.insert(n.to_string(), PhpMixed::Int(i as i64)); - } - PhpMixed::String(s) => { - result.insert(s.clone(), PhpMixed::Int(i as i64)); - } - // Non int/string values cannot be array keys and are skipped. - _ => {} - } - } - } - PhpMixed::Array(map) => { - for (key, value) in map { - match value { - PhpMixed::Int(n) => { - result.insert(n.to_string(), php_key_to_mixed(key)); - } - PhpMixed::String(s) => { - result.insert(s.clone(), php_key_to_mixed(key)); - } - _ => {} - } - } - } - _ => panic!("array_flip(): Argument #1 ($array) must be of type array"), - } - PhpMixed::Array(result) -} - -pub fn array_flip_strings(_array: &[String]) -> IndexMap<String, PhpMixed> { - _array - .iter() - .enumerate() - .map(|(i, s)| (s.clone(), PhpMixed::Int(i as i64))) - .collect() -} - -pub fn max(_a: i64, _b: i64) -> i64 { - _a.max(_b) -} - -pub fn array_key_exists<V>(_key: &str, _array: &IndexMap<String, V>) -> bool { - _array.contains_key(_key) -} - -pub fn fgets(_handle: PhpMixed) -> Option<String> { - todo!() -} - -pub fn umask() -> u32 { - todo!() -} - -pub fn basename_with_suffix(path: &str, suffix: &str) -> String { - let base = basename(path); - // PHP strips the suffix only when it is a proper trailing part of the name, - // never when it equals the whole basename. - if base != suffix && base.ends_with(suffix) { - base[..base.len() - suffix.len()].to_string() - } else { - base - } -} - -pub fn inet_pton(_host: &str) -> Option<Vec<u8>> { - todo!() -} - -pub fn ltrim(s: &str, chars: Option<&str>) -> String { - let mask: Vec<char> = match chars { - Some(c) => c.chars().collect(), - None => vec![' ', '\t', '\n', '\r', '\0', '\x0B'], - }; - s.trim_start_matches(|c| mask.contains(&c)).to_string() -} - -pub fn floor(_value: f64) -> f64 { - _value.floor() -} - -pub fn chr(_value: u8) -> String { - todo!() -} - -pub fn filter_var_with_options( - _value: &str, - _filter: i64, - _options: &IndexMap<String, PhpMixed>, -) -> PhpMixed { - todo!() -} - -pub fn memory_get_usage() -> i64 { - todo!() -} - -pub fn mb_check_encoding(_value: &str, _encoding: &str) -> bool { - match _encoding.to_ascii_uppercase().replace('-', "").as_str() { - // A Rust &str is, by construction, valid UTF-8. - "UTF8" => true, - _ => todo!(), - } -} - -pub fn iconv(_in_charset: &str, _out_charset: &str, _string: &str) -> Option<String> { - todo!() -} - -pub const JSON_ERROR_NONE: i64 = 0; -pub const JSON_ERROR_DEPTH: i64 = 1; -pub const JSON_ERROR_STATE_MISMATCH: i64 = 2; -pub const JSON_ERROR_CTRL_CHAR: i64 = 3; -pub const JSON_ERROR_UTF8: i64 = 5; - -pub fn json_last_error() -> i64 { - todo!() -} - -pub fn sort<T: Ord>(_array: &mut Vec<T>) { - _array.sort(); -} - -pub fn sort_with_flags<T: Ord>(_array: &mut Vec<T>, _flags: i64) { - todo!() -} - -pub const SORT_REGULAR: i64 = 0; -pub const SORT_NUMERIC: i64 = 1; -pub const SORT_STRING: i64 = 2; -pub const SORT_NATURAL: i64 = 6; -pub const SORT_FLAG_CASE: i64 = 8; - -pub fn usort<T, F>(_array: &mut Vec<T>, _compare: F) -where - F: FnMut(&T, &T) -> i64, -{ - let mut compare = _compare; - _array.sort_by(|a, b| compare(a, b).cmp(&0)); -} - -pub fn ksort<V>(_array: &mut IndexMap<String, V>) { - todo!() -} - -pub fn is_null(_value: &PhpMixed) -> bool { - matches!(_value, PhpMixed::Null) -} - -pub fn r#eval(_code: &str) -> PhpMixed { - todo!() -} - -pub fn array_is_list(array: &PhpMixed) -> bool { - match array { - PhpMixed::List(_) => true, - PhpMixed::Array(map) => map.keys().enumerate().all(|(i, k)| *k == i.to_string()), - _ => panic!("array_is_list(): Argument #1 ($array) must be of type array"), - } -} - -pub fn array_splice<T>( - _array: &mut Vec<T>, - _offset: i64, - _length: Option<i64>, - _replacement: Vec<T>, -) -> Vec<T> { - todo!() -} - -pub fn array_pop_first<T>(array: &mut Vec<T>) -> Option<T> { - if array.is_empty() { - None - } else { - Some(array.remove(0)) - } -} - -pub fn reset_first<T: Clone>(_array: &[T]) -> Option<T> { - _array.first().cloned() -} - -pub fn call_user_func<T>(_callback: &str, _args: &[PhpMixed]) -> T -where - T: From<PhpMixed>, -{ - todo!() -} - -pub fn array_merge_recursive(_arrays: Vec<PhpMixed>) -> PhpMixed { - todo!() -} - -pub fn levenshtein(string1: &str, string2: &str) -> i64 { - // PHP's levenshtein() is byte-based with unit insertion/deletion/replacement costs. - let a = string1.as_bytes(); - let b = string2.as_bytes(); - let n = b.len(); - let mut prev: Vec<usize> = (0..=n).collect(); - let mut curr = vec![0usize; n + 1]; - for (i, &ca) in a.iter().enumerate() { - curr[0] = i + 1; - for (j, &cb) in b.iter().enumerate() { - let cost = if ca == cb { 0 } else { 1 }; - curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost); - } - std::mem::swap(&mut prev, &mut curr); - } - prev[n] as i64 -} - -pub fn array_slice<V: Clone>( - array: &IndexMap<String, V>, - offset: i64, - length: Option<i64>, -) -> IndexMap<String, V> { - let (start, end) = php_slice_bounds(array.len() as i64, offset, length); - array - .iter() - .skip(start) - .take(end - start) - .map(|(k, v)| (k.clone(), v.clone())) - .collect() -} - -pub fn asort<V: Ord>(_array: &mut IndexMap<String, V>) { - todo!() -} - -pub const PHP_INT_MAX: i64 = i64::MAX; -pub const PHP_INT_MIN: i64 = i64::MIN; -pub const PHP_INT_SIZE: i64 = 8; - -pub fn call_user_func_array(_callback: &str, _args: &PhpMixed) -> PhpMixed { - todo!() -} - -pub fn array_map<T, U, F>(_callback: F, _array: &[T]) -> Vec<U> -where - F: Fn(&T) -> U, -{ - _array.iter().map(_callback).collect() -} - -impl Phar { - pub const SHA512: i64 = 16; - - pub fn new_phar(_filename: String, _flags: i64, _alias: &str) -> Self { - todo!() - } - - pub fn set_signature_algorithm(&mut self, _algo: i64) { - todo!() - } - - pub fn start_buffering(&mut self) { - todo!() - } - - pub fn stop_buffering(&mut self) { - todo!() - } - - pub fn add_from_string(&mut self, _path: &str, _content: &str) { - todo!() - } - - pub fn set_stub(&mut self, _stub: &str) { - todo!() - } -} - -pub fn php_strip_whitespace(_path: &str) -> String { - todo!() -} - -// The shim does not raise PHP-level errors, so there is never a last error. -pub fn error_get_last() -> Option<IndexMap<String, PhpMixed>> { - None -} - -pub fn is_readable(_path: &str) -> bool { - let path = std::path::Path::new(_path); - match std::fs::metadata(path) { - Ok(meta) => { - if meta.is_dir() { - std::fs::read_dir(path).is_ok() - } else { - std::fs::File::open(path).is_ok() - } - } - Err(_) => false, - } -} - -pub fn stream_get_wrappers() -> Vec<String> { - todo!() -} - -pub fn php_require(_file: &str) -> PhpMixed { - todo!() -} - -pub fn intval(_value: &PhpMixed) -> i64 { - // Single-argument PHP intval(), i.e. base 10. - match _value { - PhpMixed::Null => 0, - PhpMixed::Bool(b) => *b as i64, - PhpMixed::Int(i) => *i, - PhpMixed::Float(f) => { - if f.is_finite() { - *f as i64 - } else { - 0 - } - } - PhpMixed::String(s) => { - // Skip leading whitespace, read an optional sign and the leading run of digits, - // stopping at the first non-digit; no leading digits yields 0. Overflow saturates. - let bytes = s.as_bytes(); - let mut i = 0; - while i < bytes.len() && bytes[i].is_ascii_whitespace() { - i += 1; - } - let mut negative = false; - if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') { - negative = bytes[i] == b'-'; - i += 1; - } - let start = i; - let mut acc: i64 = 0; - let mut overflow = false; - while i < bytes.len() && bytes[i].is_ascii_digit() { - let digit = (bytes[i] - b'0') as i64; - acc = acc - .checked_mul(10) - .and_then(|v| v.checked_add(digit)) - .unwrap_or_else(|| { - overflow = true; - 0 - }); - i += 1; - } - if i == start { - return 0; - } - if overflow { - return if negative { i64::MIN } else { i64::MAX }; - } - if negative { -acc } else { acc } - } - PhpMixed::List(items) => (!items.is_empty()) as i64, - PhpMixed::Array(array) => (!array.is_empty()) as i64, - PhpMixed::Object(_) => 1, - } -} - -#[derive(Debug)] -pub struct RecursiveDirectoryIterator; - -impl RecursiveDirectoryIterator { - pub const SKIP_DOTS: i64 = 4096; - pub const FOLLOW_SYMLINKS: i64 = 512; -} - -#[derive(Debug)] -pub struct RecursiveIteratorIterator; - -impl RecursiveIteratorIterator { - pub const SELF_FIRST: i64 = 0; - pub const CHILD_FIRST: i64 = 16; - - pub fn get_sub_pathname(&self) -> String { - todo!() - } -} - -impl IntoIterator for &RecursiveIteratorIterator { - type Item = RecursiveIteratorFileInfo; - type IntoIter = std::vec::IntoIter<RecursiveIteratorFileInfo>; - - fn into_iter(self) -> Self::IntoIter { - todo!() - } -} - -#[derive(Debug)] -pub struct RecursiveIteratorFileInfo; - -impl RecursiveIteratorFileInfo { - pub fn is_dir(&self) -> bool { - todo!() - } - - pub fn is_file(&self) -> bool { - todo!() - } - - pub fn is_link(&self) -> bool { - todo!() - } - - pub fn get_pathname(&self) -> String { - todo!() - } - - pub fn get_size(&self) -> i64 { - todo!() - } -} - -pub fn recursive_directory_iterator( - _path: impl AsRef<std::path::Path>, - _flags: i64, -) -> Result<RecursiveDirectoryIterator, UnexpectedValueException> { - todo!() -} - -pub fn recursive_iterator_iterator( - _iter: RecursiveDirectoryIterator, - _mode: i64, -) -> RecursiveIteratorIterator { - todo!() -} - -pub fn globals_get(_name: &str) -> PhpMixed { - todo!() -} - -pub fn globals_set(_name: &str, _value: PhpMixed) { - todo!() -} - -pub fn clone<T: Clone>(_value: T) -> T { - todo!() -} - -static DEFAULT_TIMEZONE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None); - -// PHP defaults to "UTC" when no default timezone has been configured. -pub fn date_default_timezone_get() -> String { - DEFAULT_TIMEZONE - .lock() - .unwrap() - .clone() - .unwrap_or_else(|| "UTC".to_string()) -} - -pub fn date_default_timezone_set(tz: &str) -> bool { - *DEFAULT_TIMEZONE.lock().unwrap() = Some(tz.to_string()); - true -} - -pub fn getmypid() -> i64 { - std::process::id() as i64 -} - -pub fn ini_set(_varname: &str, _value: &str) -> Option<String> { - todo!() -} - -pub fn is_subclass_of(_object_or_class: &PhpMixed, _class_name: &str, _allow_string: bool) -> bool { - todo!() -} - -pub fn memory_get_peak_usage(_real_usage: bool) -> i64 { - todo!() -} - -thread_local! { - static SHUTDOWN_FUNCTIONS: std::cell::RefCell<Vec<Box<dyn Fn()>>> = - const { std::cell::RefCell::new(Vec::new()) }; -} - -pub fn register_shutdown_function(callback: Box<dyn Fn()>) { - SHUTDOWN_FUNCTIONS.with(|f| f.borrow_mut().push(callback)); -} - -// Runs the registered shutdown functions in registration order, mirroring PHP -// executing them at the end of the request. Must be invoked at every process exit. -pub fn run_shutdown_functions() { - let functions = SHUTDOWN_FUNCTIONS.with(|f| std::mem::take(&mut *f.borrow_mut())); - for callback in &functions { - callback(); - } -} - -pub fn round(_value: f64, _precision: i64) -> f64 { - todo!() -} - -pub fn composer_dev_warning_time() -> i64 { - todo!() -} - -pub fn instantiate_class(_class: &str, _args: Vec<PhpMixed>) -> PhpMixed { - todo!() -} - -pub fn array_filter_use_key( - _array: &IndexMap<String, PhpMixed>, - _callback: Box<dyn Fn(&str) -> bool>, -) -> IndexMap<String, PhpMixed> { - _array - .iter() - .filter(|(k, _)| _callback(k.as_str())) - .map(|(k, v)| (k.clone(), v.clone())) - .collect() -} - -pub fn escapeshellcmd(_command: &str) -> String { - todo!() -} - -pub fn system(_command: &str, _result_code: Option<&mut i64>) -> Option<String> { - todo!() -} - -pub fn array_chunk<T: Clone>(_array: &[T], _size: i64, _preserve_keys: bool) -> Vec<Vec<T>> { - _array.chunks(_size as usize).map(|c| c.to_vec()).collect() -} - -pub fn number_format( - _number: f64, - _decimals: i64, - _decimal_separator: &str, - _thousands_separator: &str, -) -> String { - todo!() -} - -pub fn is_executable(_path: &str) -> bool { - todo!() -} - -pub fn gc_collect_cycles() -> i64 { - // Rust has no cycle collector; nothing is collected. - 0 -} - -pub fn gc_disable() { - // Rust has no cycle collector to disable. -} - -pub fn gc_enable() { - // Rust has no cycle collector to enable. -} - -pub fn addcslashes(_string: &str, _charlist: &str) -> String { - todo!() -} - -pub fn strnatcmp(_s1: &str, _s2: &str) -> i64 { - todo!() -} - -pub fn uksort<V, F>(array: &mut IndexMap<String, V>, callback: F) -where - F: FnMut(&str, &str) -> i64, -{ - let mut callback = callback; - array.sort_by(|k1, _, k2, _| callback(k1, k2).cmp(&0)); -} - -pub fn end<V: Clone>(_array: &[V]) -> Option<V> { - _array.last().cloned() -} - -pub fn fileatime(_filename: &str) -> Option<i64> { - std::fs::metadata(_filename) - .ok() - .and_then(|m| m.accessed().ok()) - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|d| d.as_secs() as i64) -} - -pub fn fread(_handle: PhpMixed, _length: i64) -> Option<String> { - todo!() -} - -pub fn lstat(_filename: &str) -> Option<IndexMap<String, PhpMixed>> { - todo!() -} - -pub fn react_promise_resolve(_value: PhpMixed) -> PhpMixed { - todo!() -} - -pub fn symlink(_target: &str, _link: &str) -> bool { - todo!() -} - -pub fn array_diff_key( - _array1: IndexMap<String, PhpMixed>, - _array2: &IndexMap<String, PhpMixed>, -) -> IndexMap<String, PhpMixed> { - _array1 - .into_iter() - .filter(|(k, _)| !_array2.contains_key(k.as_str())) - .collect() -} - -pub fn min(_a: i64, _b: i64) -> i64 { - _a.min(_b) -} - -pub fn escapeshellarg(_arg: &str) -> String { - todo!() -} - -pub fn strcspn(string: &str, characters: &str) -> usize { - let set = characters.as_bytes(); - let mut count = 0; - for &b in string.as_bytes() { - if set.contains(&b) { - break; - } - count += 1; - } - count -} - -pub fn strstr(haystack: &str, needle: &str) -> Option<String> { - haystack.find(needle).map(|i| haystack[i..].to_string()) -} - -pub fn ioncube_loader_iversion() -> i64 { - todo!() -} - -pub fn ioncube_loader_version() -> String { - todo!() -} - -pub fn phpinfo(_what: i64) { - todo!() -} - -pub fn opendir(_path: &str) -> Option<PhpMixed> { - todo!() -} - -pub fn stream_copy_to_stream(_source: PhpMixed, _dest: PhpMixed) -> Option<i64> { - todo!() -} - -pub const SKIP_DOTS: i64 = 4096; -pub const CHILD_FIRST: i64 = 16; -pub const SELF_FIRST: i64 = 0; -pub const CURL_VERSION_ZSTD: i64 = 8388608; -pub const INFO_GENERAL: i64 = 1; -pub const OPENSSL_VERSION_NUMBER: i64 = 0; -pub const OPENSSL_VERSION_TEXT: &str = ""; -pub const PHP_BINARY: &str = ""; -pub const PHP_WINDOWS_VERSION_BUILD: i64 = 0; - #[derive(Debug, Clone)] pub struct ArrayObject { data: IndexMap<String, PhpMixed>, @@ -3414,11 +383,6 @@ impl ArrayObject { } #[derive(Debug)] -pub struct JsonObject { - data: IndexMap<String, PhpMixed>, -} - -#[derive(Debug)] pub struct StdClass { pub data: IndexMap<String, PhpMixed>, } @@ -3430,551 +394,3 @@ pub enum PhpResource { Stderr, File(std::rc::Rc<std::cell::RefCell<std::fs::File>>), } - -pub fn gethostbyname(_hostname: &str) -> String { - todo!() -} - -pub fn http_get_last_response_headers() -> Option<Vec<String>> { - todo!() -} - -pub fn http_clear_last_response_headers() { - todo!() -} - -pub fn zlib_decode(_data: &str) -> Option<String> { - todo!() -} - -pub const STREAM_NOTIFY_FAILURE: i64 = 9; -pub const STREAM_NOTIFY_FILE_SIZE_IS: i64 = 5; -pub const STREAM_NOTIFY_PROGRESS: i64 = 7; - -pub fn date_create<Tz: chrono::TimeZone>(s: &str) -> chrono::ParseResult<chrono::DateTime<Tz>> { - todo!() -} - -/// PHP: \DATE_RFC3339 ("Y-m-d\TH:i:sP"). -pub const DATE_RFC3339: &str = "%Y-%m-%dT%H:%M:%S%:z"; - -/// PHP: \DATE_ATOM (equivalent to \DATE_RFC3339). -pub const DATE_ATOM: &str = DATE_RFC3339; - -/// Convert PHP-compatible date time format to strftime-compatible format. -/// Only the patterns Composer actually passes are supported; anything else panics. -pub fn date_format_to_strftime(format: &str) -> &'static str { - match format { - "Y-m-d H:i:s" => "%Y-%m-%d %H:%M:%S", - "Y-m-d" => "%Y-%m-%d", - "Ymd" => "%Y%m%d", - other => panic!("Unsupported PHP date format: {other:?}"), - } -} - -// NOTE: &str matching in const expression does not compile for now. -pub const PHP_OS: &str = match std::env::consts::OS.as_bytes() { - b"linux" => "Linux", - b"macos" => "Darwin", - b"windows" => "WINNT", - b"freebsd" => "FreeBSD", - b"openbsd" => "OpenBSD", - b"netbsd" => "NetBSD", - b"dragonfly" => "DragonFly", - b"solaris" | b"illumos" => "SunOS", - _ => std::env::consts::OS, -}; - -// ===== Symfony Console Phase B shim additions ===== - -pub fn instance_of<T>(_value: &PhpMixed) -> bool { - todo!() -} -pub fn to_array(_value: PhpMixed) -> IndexMap<String, PhpMixed> { - todo!() -} -pub fn to_string(value: &PhpMixed) -> String { - php_to_string(value) -} -pub fn to_bool(value: &PhpMixed) -> bool { - php_truthy(value) -} -pub fn php_truthy(value: &PhpMixed) -> bool { - match value { - PhpMixed::Null => false, - PhpMixed::Bool(b) => *b, - PhpMixed::Int(i) => *i != 0, - PhpMixed::Float(f) => *f != 0.0, - // PHP treats only "" and "0" as falsy strings. - PhpMixed::String(s) => !s.is_empty() && s != "0", - PhpMixed::List(items) => !items.is_empty(), - PhpMixed::Array(entries) => !entries.is_empty(), - // Objects are always truthy. - PhpMixed::Object(_) => true, - } -} -pub fn boolval(value: &PhpMixed) -> bool { - php_truthy(value) -} -pub fn is_iterable(_value: &PhpMixed) -> bool { - todo!() -} - -pub fn shell_exec(_command: &str) -> Option<String> { - todo!() -} - -pub fn mb_detect_encoding( - _s: &str, - _encodings: Option<Vec<String>>, - _strict: bool, -) -> Option<String> { - // PHP's default detection order is ASCII then UTF-8. `_s` is already valid UTF-8, so detection - // reduces to: pure-ASCII content matches "ASCII", anything else matches "UTF-8". - let order = _encodings.unwrap_or_else(|| vec!["ASCII".to_string(), "UTF-8".to_string()]); - for enc in order { - match canonical_encoding(&enc).as_str() { - "ASCII" if _s.is_ascii() => return Some(enc), - "UTF-8" => return Some(enc), - _ => {} - } - } - None -} -pub fn mb_strwidth(s: &str, _encoding: Option<&str>) -> i64 { - // TODO(phase-c): calculate actual width - s.len() as i64 -} -pub fn mb_substr(s: &str, start: i64, length: Option<i64>, _encoding: Option<&str>) -> String { - // Code-point based, mirroring substr's byte-based offset/length handling. - let chars: Vec<char> = s.chars().collect(); - let (start, end) = php_slice_bounds(chars.len() as i64, start, length); - chars[start..end].iter().collect() -} -pub fn mb_str_split(s: &str, length: i64) -> Vec<String> { - let length = length.max(1) as usize; - let chars: Vec<char> = s.chars().collect(); - chars - .chunks(length) - .map(|chunk| chunk.iter().collect()) - .collect() -} -pub fn mb_convert_variables(_to: &str, _from: &str, _vars: &mut Vec<String>) -> Option<String> { - // Converts each variable in place from `_from` to `_to`, returning the source encoding (PHP - // returns the detected source encoding; here `_from` is a single named encoding). - for v in _vars.iter_mut() { - *v = mb_convert_encoding(std::mem::take(v).into_bytes(), _to, _from); - } - Some(_from.to_string()) -} - -pub fn ceil(v: f64) -> f64 { - v.ceil() -} -pub fn intdiv(a: i64, b: i64) -> i64 { - // PHP intdiv() throws DivisionByZeroError on a zero divisor and ArithmeticError - // for PHP_INT_MIN / -1; Rust's `/` likewise panics in both cases. - a / b -} -pub fn hexdec(_s: &str) -> i64 { - todo!() -} -pub fn byte_at(s: &str, i: usize) -> u8 { - s.as_bytes().get(i).copied().unwrap_or(0) -} -pub fn str_split(_s: &str, _length: i64) -> Vec<String> { - // PHP str_split() chunks the string by bytes into pieces of `length` bytes. - let length = _length.max(1) as usize; - let bytes = _s.as_bytes(); - if bytes.is_empty() { - return vec![String::new()]; - } - bytes - .chunks(length) - .map(|c| String::from_utf8_lossy(c).into_owned()) - .collect() -} -pub fn stripcslashes(_s: &str) -> String { - todo!() -} -pub fn str_bitand(_a: &str, _b: &str) -> String { - todo!() -} -pub fn wordwrap(_s: &str, _width: i64, _break_str: &str, _cut: bool) -> String { - todo!() -} -pub fn ctype_digit(s: &str) -> bool { - !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) -} -pub fn is_numeric_string(s: &str) -> bool { - // PHP is_numeric() on a string: optional leading whitespace, an integer or float literal - // (decimal/scientific). PHP does not treat "inf"/"nan" as numeric. - let trimmed = s.trim(); - if trimmed.is_empty() { - return false; - } - let lower = trimmed.to_ascii_lowercase(); - if lower.contains("inf") || lower.contains("nan") { - return false; - } - trimmed.parse::<i64>().is_ok() || trimmed.parse::<f64>().is_ok() -} -pub fn is_numeric_to_int(value: &PhpMixed) -> i64 { - // PHP: is_numeric($value) ? (int) $value : 0. - match value { - PhpMixed::Int(n) => *n, - PhpMixed::Float(f) => *f as i64, - PhpMixed::String(s) if is_numeric_string(s) => { - let trimmed = s.trim(); - trimmed - .parse::<i64>() - .unwrap_or_else(|_| trimmed.parse::<f64>().map(|f| f as i64).unwrap_or(0)) - } - _ => 0, - } -} -pub fn explode_limit(delimiter: &str, string: &str, limit: i64) -> Vec<String> { - explode_limit_impl(delimiter, string, limit) -} -pub fn sort_natural_flag_case(_values: &mut Vec<String>) { - todo!() -} -pub fn get_debug_type_obj<T>(_value: &T) -> String { - // PHP get_debug_type() returns the class name for an object. Rust has no runtime class names; - // the static type name is the closest faithful diagnostic available here. - std::any::type_name::<T>().to_string() -} -pub fn dir() -> String { - todo!() -} -pub fn exit(status: i64) -> ! { - // PHP runs registered shutdown functions before terminating. - run_shutdown_functions(); - std::process::exit(status as i32); -} - -/// Models PHP's `exit`/`die` language construct propagated as a recoverable error so the actual -/// process termination happens at a single top-level site instead of deep in the call stack. -/// -/// Like PHP's `exit`, this must NOT be caught by ported `try`/`catch` blocks: any broad catch on -/// the propagation path has to re-raise it untouched, and only the outermost handler converts it -/// into the process exit code. -#[derive(Debug)] -pub struct ExitException { - pub code: i64, -} - -impl std::fmt::Display for ExitException { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "exit({})", self.code) - } -} - -impl std::error::Error for ExitException {} - -pub fn is_resource_value(_resource: &PhpResource) -> bool { - true -} -pub fn get_resource_type(_resource: &PhpResource) -> String { - "stream".to_string() -} -pub fn stream_isatty_resource(resource: &PhpResource) -> bool { - use std::io::IsTerminal; - match resource { - PhpResource::Stdin => std::io::stdin().is_terminal(), - PhpResource::Stdout => std::io::stdout().is_terminal(), - PhpResource::Stderr => std::io::stderr().is_terminal(), - PhpResource::File(_) => false, - } -} -pub fn fwrite_resource(resource: &PhpResource, data: &str) { - use std::io::Write; - let bytes = data.as_bytes(); - match resource { - PhpResource::Stdin => {} - PhpResource::Stdout => { - let _ = std::io::stdout().write_all(bytes); - } - PhpResource::Stderr => { - let _ = std::io::stderr().write_all(bytes); - } - PhpResource::File(file) => { - let _ = file.borrow_mut().write_all(bytes); - } - } -} -pub fn fflush_resource(resource: &PhpResource) { - use std::io::Write; - match resource { - PhpResource::Stdin => {} - PhpResource::Stdout => { - let _ = std::io::stdout().flush(); - } - PhpResource::Stderr => { - let _ = std::io::stderr().flush(); - } - PhpResource::File(file) => { - let _ = file.borrow_mut().flush(); - } - } -} -pub fn fgetc(_resource: &PhpResource) -> Option<String> { - todo!() -} -pub fn ftell(_resource: &PhpResource) -> i64 { - todo!() -} -pub fn stream_get_meta_data(_resource: &PhpResource) -> IndexMap<String, PhpMixed> { - todo!() -} -pub fn stream_set_blocking(_resource: &PhpResource, _enable: bool) -> bool { - todo!() -} -pub fn stream_select( - _read: &mut Vec<PhpResource>, - _write: &mut Vec<PhpResource>, - _except: &mut Vec<PhpResource>, - _seconds: i64, - _microseconds: Option<i64>, -) -> i64 { - todo!() -} -pub fn php_fopen_resource(path: &str, mode: &str) -> PhpResource { - match path { - "php://output" | "php://stdout" => return PhpResource::Stdout, - "php://stderr" => return PhpResource::Stderr, - "php://stdin" | "php://input" => return PhpResource::Stdin, - _ => {} - } - // Strip the binary/text flags PHP accepts as part of the mode. - let base_mode: String = mode.chars().filter(|c| *c != 'b' && *c != 't').collect(); - let mut options = std::fs::OpenOptions::new(); - match base_mode.as_str() { - "r" => options.read(true), - "r+" => options.read(true).write(true), - "w" => options.write(true).create(true).truncate(true), - "w+" => options.read(true).write(true).create(true).truncate(true), - "a" => options.append(true).create(true), - "a+" => options.read(true).append(true).create(true), - "x" => options.write(true).create_new(true), - "x+" => options.read(true).write(true).create_new(true), - _ => options.read(true), - }; - let file = options - .open(path) - .unwrap_or_else(|e| panic!("php_fopen_resource failed to open {path:?}: {e}")); - PhpResource::File(std::rc::Rc::new(std::cell::RefCell::new(file))) -} -pub fn php_stdout_resource() -> PhpResource { - PhpResource::Stdout -} -pub fn php_stderr_resource() -> PhpResource { - PhpResource::Stderr -} -pub fn stdin() -> PhpResource { - PhpResource::Stdin -} - -// TODO(phase-c): reports proc_open as unavailable (returns false), so callers fall back to -// their defaults. A real implementation requires holding the child process and its pipes; defer -// it to the broader process-subsystem work (ProcessExecutor). -pub fn proc_open( - _command: &str, - _descriptorspec: &[PhpMixed], - _pipes: &mut PhpMixed, - _cwd: Option<&str>, - _env: Option<&[String]>, - _options: Option<&IndexMap<String, PhpMixed>>, -) -> PhpMixed { - PhpMixed::Bool(false) -} -pub fn proc_close(_process: PhpMixed) -> i64 { - -1 -} -pub fn proc_get_status(_process: &PhpMixed) -> IndexMap<String, PhpMixed> { - todo!() -} -pub fn proc_terminate(_process: &PhpMixed, _signal: i64) -> bool { - todo!() -} -pub fn posix_kill(_pid: i64, _signal: i64) -> bool { - todo!() -} -pub fn uniqid(_prefix: &str, _more_entropy: bool) -> String { - todo!() -} -pub fn ftruncate(_stream: &PhpMixed, _size: i64) -> bool { - todo!() -} -/// PHP `ftell()` over a PhpMixed stream resource. (`ftell` itself is already defined for the -/// `PhpResource`-typed stream API used elsewhere.) -pub fn ftell_stream(_stream: &PhpMixed) -> i64 { - todo!() -} -pub const SEEK_SET: i64 = 0; -pub const SEEK_CUR: i64 = 1; -pub const SEEK_END: i64 = 2; -pub fn fseek3(_stream: PhpMixed, _offset: i64, _whence: i64) -> i64 { - todo!() -} -pub fn stream_get_contents3(_stream: PhpMixed, _max_length: i64, _offset: i64) -> Option<String> { - todo!() -} -/// PHP `getenv()` with no argument: all environment variables. -pub fn getenv_all() -> IndexMap<String, String> { - todo!() -} -/// PHP superglobal `$_ENV`. -pub fn php_env() -> IndexMap<String, PhpMixed> { - todo!() -} -/// PHP superglobal `$_SERVER`. -pub fn php_server() -> IndexMap<String, PhpMixed> { - todo!() -} - -pub fn sapi_windows_vt100_support(_resource: &PhpResource) -> bool { - todo!() -} -pub fn sapi_windows_cp_get(_kind: Option<&str>) -> i64 { - todo!() -} -pub fn sapi_windows_cp_set(_codepage: i64) -> bool { - todo!() -} -pub fn sapi_windows_cp_conv(_in_codepage: i64, _out_codepage: i64, _subject: &str) -> String { - todo!() -} - -pub const SIGINT: i64 = 2; -pub const SIGTERM: i64 = 15; -pub const SIGUSR1: i64 = 10; -pub const SIGUSR2: i64 = 12; -// No-op until real signal handling is wired up; signal registration itself is -// deferred (see the TODO(plugin) notes in SignalRegistry::register). -pub fn pcntl_async_signals(_enable: bool) {} -pub fn pcntl_signal(_signal: i64, _handler: PhpMixed) -> bool { - todo!() -} -pub fn pcntl_signal_get_handler(_signal: i64) -> PhpMixed { - todo!() -} -pub fn call_php_callable(_callback: &PhpMixed, _args: &[PhpMixed]) -> PhpMixed { - todo!() -} - -pub fn cli_set_process_title(_title: &str) -> bool { - todo!() -} -pub fn setproctitle(_title: &str) { - todo!() -} -pub fn spl_object_hash_process<T>(_object: &T) -> String { - todo!() -} - -pub fn server(key: &str) -> String { - if key == "PHP_SELF" { - return server_php_self(); - } - server_get(key).unwrap_or_default() -} -pub fn server_argv() -> Vec<String> { - std::env::args().collect() -} -pub fn server_php_self() -> String { - // CLI SAPI: $_SERVER['PHP_SELF'] is the path of the executed script, i.e. argv[0]. - std::env::args().next().unwrap_or_default() -} -pub fn server_shell() -> Option<String> { - todo!() -} - -pub fn file_put_contents3(_filename: &str, _data: &str, _flags: i64) -> Option<i64> { - todo!() -} - -#[derive(Debug)] -pub struct DirectoryIteratorEntry; -impl DirectoryIteratorEntry { - pub fn get_basename(&self) -> String { - todo!() - } - pub fn is_file(&self) -> bool { - todo!() - } - pub fn get_extension(&self) -> String { - todo!() - } -} -pub fn directory_iterator(_path: &str) -> Vec<DirectoryIteratorEntry> { - todo!() -} - -pub fn array_key_last(_array: &IndexMap<String, PhpMixed>) -> usize { - todo!() -} -pub fn array_splice_mixed( - _array: &mut Vec<PhpMixed>, - _offset: i64, - _length: i64, - _replacement: Vec<PhpMixed>, -) { - todo!() -} - -pub fn str_replace_arrays(search: &[String], replace: &[String], subject: &str) -> String { - str_replace_array(search, replace, subject) -} -pub fn str_replace_arr(search: &[&str], replace: &str, subject: &str) -> String { - // PHP str_replace(array, string, subject): every search element is replaced with - // the same replacement string, applied in order. - let mut result = subject.to_string(); - for s in search { - result = str_replace(s, replace, &result); - } - result -} - -pub fn php_exception_get_code(_error: &anyhow::Error) -> i32 { - // PHP's Throwable::getCode(). anyhow::Error carries the concrete exception type, so enumerate - // the flat standard exception structs and read their `code` field; everything else defaults to - // 0, matching PHP's default exception code. - if let Some(e) = _error.downcast_ref::<Exception>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<RuntimeException>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<UnexpectedValueException>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<InvalidArgumentException>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<TypeError>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<LogicException>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<BadMethodCallException>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<OutOfBoundsException>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<ErrorException>() { - return e.code as i32; - } - if let Some(e) = _error.downcast_ref::<PharException>() { - return e.code as i32; - } - 0 -} -pub fn sscanf(_subject: &str, _format: &str, _a: &mut i64, _b: &mut i64) -> i64 { - todo!() -} -pub fn trigger_deprecation(_package: &str, _version: &str, _message: &str, _arg: &str) { - todo!() -} |
