diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-21 15:49:24 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-21 15:49:37 +0900 |
| commit | fcef25f6ef36287a4984ffdaab39df84f5aceeba (patch) | |
| tree | 97ceb2098fbb5642e858929da9578bb41e277ada /crates/shirabe-php-shim/src | |
| parent | f0f5f084c883dc4f5b6e61603e82cd1c2092fd9d (diff) | |
| download | php-shirabe-fcef25f6ef36287a4984ffdaab39df84f5aceeba.tar.gz php-shirabe-fcef25f6ef36287a4984ffdaab39df84f5aceeba.tar.zst php-shirabe-fcef25f6ef36287a4984ffdaab39df84f5aceeba.zip | |
refactor(php-shim): split lib.rs
Diffstat (limited to 'crates/shirabe-php-shim/src')
25 files changed, 3797 insertions, 3632 deletions
diff --git a/crates/shirabe-php-shim/src/apcu.rs b/crates/shirabe-php-shim/src/apcu.rs new file mode 100644 index 0000000..699f66e --- /dev/null +++ b/crates/shirabe-php-shim/src/apcu.rs @@ -0,0 +1,11 @@ +use crate::PhpMixed; + +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!() +} diff --git a/crates/shirabe-php-shim/src/array.rs b/crates/shirabe-php-shim/src/array.rs new file mode 100644 index 0000000..4b3e160 --- /dev/null +++ b/crates/shirabe-php-shim/src/array.rs @@ -0,0 +1,612 @@ +use crate::PhpMixed; +use crate::php_to_string; +use indexmap::IndexMap; + +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 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 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 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 array_diff(_array1: &[String], _array2: &[String]) -> Vec<String> { + _array1 + .iter() + .filter(|&x| !_array2.contains(x)) + .cloned() + .collect() +} + +pub fn array_unique<T: Clone>(_array: &[T]) -> Vec<T> { + todo!() +} + +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 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 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 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 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 array_key_exists<V>(_key: &str, _array: &IndexMap<String, V>) -> bool { + _array.contains_key(_key) +} + +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 array_merge_recursive(_arrays: Vec<PhpMixed>) -> PhpMixed { + todo!() +} + +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 array_map<T, U, F>(_callback: F, _array: &[T]) -> Vec<U> +where + F: Fn(&T) -> U, +{ + _array.iter().map(_callback).collect() +} + +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 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 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 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!() +} + +/// 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()) +} + +/// 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 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) +} + +pub fn krsort<V>(_array: &mut IndexMap<i64, V>) { + 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 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 asort<V: Ord>(_array: &mut IndexMap<String, V>) { + 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 sort_natural_flag_case(_values: &mut Vec<String>) { + todo!() +} + +pub fn count_mixed(value: &PhpMixed) -> i64 { + count(value) as i64 +} + +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 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 fn reset_first<T: Clone>(_array: &[T]) -> Option<T> { + _array.first().cloned() +} + +pub fn end_arr<V: Clone>(_array: &IndexMap<String, V>) -> Option<V> { + _array.values().last().cloned() +} + +pub fn iterator_to_array<I>(iter: I) -> Vec<I::Item> +where + I: IntoIterator, +{ + iter.into_iter().collect() +} + +pub fn end<V: Clone>(_array: &[V]) -> Option<V> { + _array.last().cloned() +} diff --git a/crates/shirabe-php-shim/src/compress.rs b/crates/shirabe-php-shim/src/compress.rs new file mode 100644 index 0000000..6823000 --- /dev/null +++ b/crates/shirabe-php-shim/src/compress.rs @@ -0,0 +1,25 @@ +use crate::PhpMixed; + +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 gzcompress(_data: &[u8]) -> Option<Vec<u8>> { + todo!() +} + +pub fn bzcompress(_data: &[u8]) -> Option<Vec<u8>> { + todo!() +} + +pub fn zlib_decode(_data: &str) -> Option<String> { + todo!() +} diff --git a/crates/shirabe-php-shim/src/curl.rs b/crates/shirabe-php-shim/src/curl.rs new file mode 100644 index 0000000..316e8fd --- /dev/null +++ b/crates/shirabe-php-shim/src/curl.rs @@ -0,0 +1,152 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +pub const CURL_VERSION_ZSTD: i64 = 8388608; + +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!() +} diff --git a/crates/shirabe-php-shim/src/datetime.rs b/crates/shirabe-php-shim/src/datetime.rs new file mode 100644 index 0000000..79c5ea2 --- /dev/null +++ b/crates/shirabe-php-shim/src/datetime.rs @@ -0,0 +1,55 @@ +static DEFAULT_TIMEZONE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None); + +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:?}"), + } +} + +pub fn strtotime(_time: &str) -> Option<i64> { + todo!() +} + +pub fn time() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64 +} + +pub fn microtime(_get_as_float: bool) -> f64 { + todo!() +} + +// 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 date(_format: &str, _timestamp: Option<i64>) -> String { + todo!() +} diff --git a/crates/shirabe-php-shim/src/env.rs b/crates/shirabe-php-shim/src/env.rs new file mode 100644 index 0000000..62dc666 --- /dev/null +++ b/crates/shirabe-php-shim/src/env.rs @@ -0,0 +1,100 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +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() +} + +/// 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 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!() +} diff --git a/crates/shirabe-php-shim/src/exception.rs b/crates/shirabe-php-shim/src/exception.rs new file mode 100644 index 0000000..c57a345 --- /dev/null +++ b/crates/shirabe-php-shim/src/exception.rs @@ -0,0 +1,186 @@ +use crate::PharException; + +#[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 {} + +/// 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 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 +} diff --git a/crates/shirabe-php-shim/src/filter.rs b/crates/shirabe-php-shim/src/filter.rs new file mode 100644 index 0000000..a23752b --- /dev/null +++ b/crates/shirabe-php-shim/src/filter.rs @@ -0,0 +1,39 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +pub const FILTER_VALIDATE_EMAIL: i64 = 274; + +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!(), + } +} + +pub fn filter_var_with_options( + _value: &str, + _filter: i64, + _options: &IndexMap<String, PhpMixed>, +) -> PhpMixed { + todo!() +} diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs new file mode 100644 index 0000000..df7d3b3 --- /dev/null +++ b/crates/shirabe-php-shim/src/fs.rs @@ -0,0 +1,492 @@ +use crate::PhpMixed; +use crate::PhpResource; +use crate::UnexpectedValueException; +use indexmap::IndexMap; + +pub const PHP_EOL: &str = "\n"; + +pub const FILE_APPEND: i64 = 8; + +pub const STDIN: PhpResource = PhpResource::Stdin; + +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 PATH_SEPARATOR: &str = ":"; +pub const DIRECTORY_SEPARATOR: &str = "/"; + +pub const FILE_IGNORE_NEW_LINES: i64 = 2; + +pub const SEEK_SET: i64 = 0; +pub const SEEK_CUR: i64 = 1; +pub const SEEK_END: i64 = 2; + +pub const SKIP_DOTS: i64 = 4096; +pub const CHILD_FIRST: i64 = 16; +pub const SELF_FIRST: i64 = 0; + +pub struct FilesystemIterator; + +impl FilesystemIterator { + pub const KEY_AS_PATHNAME: i64 = 256; + pub const CURRENT_AS_FILEINFO: i64 = 0; +} + +#[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!() + } +} + +#[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 directory_iterator(_path: &str) -> Vec<DirectoryIteratorEntry> { + todo!() +} + +pub fn fopen(_file: &str, _mode: &str) -> PhpMixed { + todo!() +} + +pub fn fwrite(_file: PhpMixed, _data: &str, _length: i64) -> Option<i64> { + todo!() +} + +pub fn fread(_handle: PhpMixed, _length: i64) -> Option<String> { + todo!() +} + +pub fn feof(_stream: PhpMixed) -> bool { + todo!() +} + +pub fn fclose(_file: PhpMixed) { + todo!() +} + +pub fn fgets(_handle: PhpMixed) -> Option<String> { + todo!() +} + +pub fn fgetc(_resource: &PhpResource) -> Option<String> { + todo!() +} + +pub fn ftell(_resource: &PhpResource) -> i64 { + todo!() +} + +pub fn fseek(_stream: PhpMixed, _offset: i64) -> i64 { + todo!() +} + +pub fn rewind(_stream: PhpMixed) -> bool { + todo!() +} + +pub fn fstat(_stream: PhpResource) -> PhpMixed { + todo!() +} + +pub fn lstat(_filename: &str) -> Option<IndexMap<String, PhpMixed>> { + 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 fn fseek3(_stream: PhpMixed, _offset: i64, _whence: i64) -> i64 { + todo!() +} + +pub fn touch(_path: &str) -> bool { + todo!() +} + +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 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 touch2(_path: &str, _mtime: i64) -> bool { + todo!() +} + +pub fn touch3(_path: &str, _mtime: i64, _atime: i64) -> bool { + todo!() +} + +pub fn chmod(_path: &str, _mode: u32) -> bool { + todo!() +} + +pub fn fileperms(_path: &str) -> i64 { + 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 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 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 is_executable(_path: &str) -> bool { + todo!() +} + +pub fn is_file(path: impl AsRef<std::path::Path>) -> bool { + path.as_ref().is_file() +} + +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 is_dir(path: impl AsRef<std::path::Path>) -> bool { + path.as_ref().is_dir() +} + +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 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) +} + +pub fn fileowner(_filename: &str) -> Option<i64> { + todo!() +} + +pub fn unlink(path: impl AsRef<std::path::Path>) -> bool { + std::fs::remove_file(path).is_ok() +} + +pub fn unlink_silent(_path: &str) -> bool { + todo!() +} + +pub fn file_put_contents(_path: &str, _data: &[u8]) -> Option<i64> { + std::fs::write(_path, _data) + .ok() + .map(|_| _data.len() as i64) +} + +pub fn file_put_contents3(_filename: &str, _data: &str, _flags: i64) -> Option<i64> { + todo!() +} + +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 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 file(_filename: &str, _flags: i64) -> Option<Vec<String>> { + todo!() +} + +pub fn umask() -> u32 { + todo!() +} + +pub fn mkdir(_pathname: &str, _mode: u32, _recursive: bool) -> bool { + todo!() +} + +pub fn rmdir(dir: impl AsRef<std::path::Path>) -> bool { + std::fs::remove_dir(dir).is_ok() +} + +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 copy(_source: &str, _dest: &str) -> bool { + std::fs::copy(_source, _dest).is_ok() +} + +pub fn ftruncate(_stream: &PhpMixed, _size: i64) -> bool { + todo!() +} + +pub fn symlink(_target: &str, _link: &str) -> bool { + todo!() +} + +pub fn sys_get_temp_dir() -> String { + std::env::temp_dir().to_string_lossy().into_owned() +} + +pub fn tempnam(_dir: &str, _prefix: &str) -> Option<String> { + todo!() +} + +pub fn opendir(_path: &str) -> Option<PhpMixed> { + todo!() +} + +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) +} + +// 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 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 dirname_levels(path: &str, levels: i64) -> String { + let mut result = path.to_string(); + for _ in 0..levels { + result = dirname(&result); + } + result +} + +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 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 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 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!() +} diff --git a/crates/shirabe-php-shim/src/hash.rs b/crates/shirabe-php-shim/src/hash.rs new file mode 100644 index 0000000..45d53c0 --- /dev/null +++ b/crates/shirabe-php-shim/src/hash.rs @@ -0,0 +1,11 @@ +pub fn hash(_algo: &str, _data: &str) -> String { + todo!() +} + +pub fn hash_raw(_algo: &str, _data: &str) -> Vec<u8> { + todo!() +} + +pub fn hash_file(_algo: &str, _filename: &str) -> Option<String> { + todo!() +} diff --git a/crates/shirabe-php-shim/src/json.rs b/crates/shirabe-php-shim/src/json.rs new file mode 100644 index 0000000..ed704f9 --- /dev/null +++ b/crates/shirabe-php-shim/src/json.rs @@ -0,0 +1,107 @@ +use crate::ArrayObject; +use crate::PhpMixed; +use indexmap::IndexMap; + +pub trait JsonSerializable { + fn json_serialize(&self) -> PhpMixed; +} + +#[derive(Debug)] +pub struct JsonObject { + data: IndexMap<String, PhpMixed>, +} + +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 const JSON_INVALID_UTF8_IGNORE: i64 = 1048576; + +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 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 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) +} + +// 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(), + }) + } + } + } +} 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!() -} diff --git a/crates/shirabe-php-shim/src/math.rs b/crates/shirabe-php-shim/src/math.rs new file mode 100644 index 0000000..fffe2d6 --- /dev/null +++ b/crates/shirabe-php-shim/src/math.rs @@ -0,0 +1,33 @@ +pub fn max_i64(_a: i64, _b: i64) -> i64 { + _a.max(_b) +} + +pub fn max(_a: i64, _b: i64) -> i64 { + _a.max(_b) +} + +pub fn min(_a: i64, _b: i64) -> i64 { + _a.min(_b) +} + +pub fn abs(_value: i64) -> i64 { + _value.abs() +} + +pub fn floor(_value: f64) -> f64 { + _value.floor() +} + +pub fn ceil(v: f64) -> f64 { + v.ceil() +} + +pub fn round(_value: f64, _precision: i64) -> f64 { + todo!() +} + +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 +} diff --git a/crates/shirabe-php-shim/src/net.rs b/crates/shirabe-php-shim/src/net.rs new file mode 100644 index 0000000..e7ecb18 --- /dev/null +++ b/crates/shirabe-php-shim/src/net.rs @@ -0,0 +1,19 @@ +pub fn gethostname() -> String { + todo!() +} + +pub fn gethostbyname(_hostname: &str) -> String { + todo!() +} + +pub fn inet_pton(_host: &str) -> Option<Vec<u8>> { + todo!() +} + +pub fn http_get_last_response_headers() -> Option<Vec<String>> { + todo!() +} + +pub fn http_clear_last_response_headers() { + todo!() +} diff --git a/crates/shirabe-php-shim/src/openssl.rs b/crates/shirabe-php-shim/src/openssl.rs new file mode 100644 index 0000000..41dda0e --- /dev/null +++ b/crates/shirabe-php-shim/src/openssl.rs @@ -0,0 +1,42 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +pub const OPENSSL_ALGO_SHA384: i64 = 9; +pub const OPENSSL_VERSION_NUMBER: i64 = 0; +pub const OPENSSL_VERSION_TEXT: &str = ""; + +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 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!() +} diff --git a/crates/shirabe-php-shim/src/output.rs b/crates/shirabe-php-shim/src/output.rs new file mode 100644 index 0000000..17f7ab9 --- /dev/null +++ b/crates/shirabe-php-shim/src/output.rs @@ -0,0 +1,7 @@ +pub fn ob_start() -> bool { + todo!() +} + +pub fn ob_get_clean() -> Option<String> { + todo!() +} diff --git a/crates/shirabe-php-shim/src/phar.rs b/crates/shirabe-php-shim/src/phar.rs new file mode 100644 index 0000000..8217808 --- /dev/null +++ b/crates/shirabe-php-shim/src/phar.rs @@ -0,0 +1,137 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +#[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!() + } +} + +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!() + } +} + +#[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!() + } +} diff --git a/crates/shirabe-php-shim/src/process.rs b/crates/shirabe-php-shim/src/process.rs new file mode 100644 index 0000000..cb5aa50 --- /dev/null +++ b/crates/shirabe-php-shim/src/process.rs @@ -0,0 +1,105 @@ +use crate::{PhpMixed, PhpResource}; +use indexmap::IndexMap; + +pub const SIGINT: i64 = 2; +pub const SIGTERM: i64 = 15; +pub const SIGUSR1: i64 = 10; +pub const SIGUSR2: i64 = 12; + +pub fn exec( + _command: &str, + _output: Option<&mut Vec<String>>, + _exit_code: Option<&mut i64>, +) -> Option<String> { + todo!() +} + +pub fn shell_exec(_command: &str) -> Option<String> { + todo!() +} + +pub fn system(_command: &str, _result_code: Option<&mut i64>) -> Option<String> { + todo!() +} + +pub fn escapeshellcmd(_command: &str) -> String { + todo!() +} + +pub fn escapeshellarg(_arg: &str) -> String { + todo!() +} + +// 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 getmypid() -> i64 { + std::process::id() as i64 +} + +pub fn cli_set_process_title(_title: &str) -> bool { + todo!() +} + +pub fn setproctitle(_title: &str) { + todo!() +} + +// 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 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 posix_kill(_pid: i64, _signal: i64) -> bool { + todo!() +} + +pub fn get_current_user() -> String { + todo!() +} diff --git a/crates/shirabe-php-shim/src/rar.rs b/crates/shirabe-php-shim/src/rar.rs new file mode 100644 index 0000000..8efce90 --- /dev/null +++ b/crates/shirabe-php-shim/src/rar.rs @@ -0,0 +1,28 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +#[derive(Debug)] +pub struct RarEntry; + +impl RarEntry { + pub fn extract(&self, _path: &str) -> bool { + 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!() + } +} diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs new file mode 100644 index 0000000..cc63af6 --- /dev/null +++ b/crates/shirabe-php-shim/src/runtime.rs @@ -0,0 +1,416 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +pub const PHP_VERSION_ID: i64 = 80100; +pub const PHP_VERSION: &str = "8.1.0"; + +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 PHP_WINDOWS_VERSION_BUILD: i64 = 0; + +pub const PHP_INT_MAX: i64 = i64::MAX; +pub const PHP_INT_MIN: i64 = i64::MIN; +pub const PHP_INT_SIZE: i64 = 8; + +pub const HHVM_VERSION: Option<&str> = None; + +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 INFO_GENERAL: i64 = 1; +pub const PHP_BINARY: &str = ""; + +// 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, +}; + +pub fn constant(_name: &str) -> PhpMixed { + 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 method_exists(_object: &PhpMixed, _method_name: &str) -> bool { + 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" + ) +} + +/// 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", + } +} + +// 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" + ) +} + +// 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 get_loaded_extensions() -> Vec<String> { + todo!() +} + +pub fn phpversion(_extension: &str) -> Option<String> { + 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!() +} + +/// Equivalent to PHP `include $file;` +pub fn include_file(file: &str) -> PhpMixed { + let _ = file; + 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!() +} + +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 fn spl_autoload_functions() -> Vec<PhpMixed> { + todo!() +} + +pub fn version_compare(_v1: &str, _v2: &str, _op: &str) -> bool { + todo!() +} + +pub fn version_compare_2(_v1: &str, _v2: &str) -> 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 spl_object_hash<T: ?Sized>(_object: &T) -> String { + todo!() +} + +pub fn spl_object_hash_process<T>(_object: &T) -> String { + todo!() +} + +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 trigger_error(_message: &str, _error_level: i64) { + todo!() +} + +pub fn trigger_deprecation(_package: &str, _version: &str, _message: &str, _arg: &str) { + todo!() +} + +pub fn usleep(_microseconds: u64) { + std::thread::sleep(std::time::Duration::from_micros(_microseconds)); +} + +/// Equivalent to PHP's __DIR__ magic constant +pub fn php_dir() -> String { + todo!() +} + +pub fn 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 php_require(_file: &str) -> PhpMixed { + todo!() +} + +pub fn r#eval(_code: &str) -> PhpMixed { + todo!() +} + +pub fn memory_get_usage() -> i64 { + todo!() +} + +pub fn memory_get_peak_usage(_real_usage: bool) -> i64 { + todo!() +} + +pub fn call_user_func<T>(_callback: &str, _args: &[PhpMixed]) -> T +where + T: From<PhpMixed>, +{ + todo!() +} + +pub fn call_user_func_array(_callback: &str, _args: &PhpMixed) -> PhpMixed { + todo!() +} + +pub fn call_php_callable(_callback: &PhpMixed, _args: &[PhpMixed]) -> PhpMixed { + 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 globals_get(_name: &str) -> PhpMixed { + todo!() +} + +pub fn globals_set(_name: &str, _value: PhpMixed) { + todo!() +} + +pub fn clone<T: Clone>(_value: T) -> T { + todo!() +} + +pub fn ini_set(_varname: &str, _value: &str) -> Option<String> { + 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 composer_dev_warning_time() -> i64 { + 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 react_promise_resolve(_value: PhpMixed) -> PhpMixed { + todo!() +} + +pub fn ioncube_loader_iversion() -> i64 { + todo!() +} + +pub fn ioncube_loader_version() -> String { + todo!() +} + +pub fn phpinfo(_what: i64) { + todo!() +} + +pub fn exit(status: i64) -> ! { + // PHP runs registered shutdown functions before terminating. + run_shutdown_functions(); + std::process::exit(status as i32); +} + +pub fn sapi_windows_vt100_support(_resource: &crate::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!() +} diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs new file mode 100644 index 0000000..0747cc1 --- /dev/null +++ b/crates/shirabe-php-shim/src/stream.rs @@ -0,0 +1,120 @@ +use crate::{PhpMixed, PhpResource}; +use indexmap::IndexMap; + +pub const STREAM_NOTIFY_FAILURE: i64 = 9; +pub const STREAM_NOTIFY_FILE_SIZE_IS: i64 = 5; +pub const STREAM_NOTIFY_PROGRESS: i64 = 7; + +pub const STDERR: i64 = 2; + +pub fn stream_get_contents(_stream: PhpMixed) -> Option<String> { + todo!() +} + +pub fn stream_resolve_include_path(filename: &str) -> Option<String> { + let _ = filename; + todo!() +} + +pub fn stream_get_contents_with_max(stream: PhpMixed, max_length: Option<i64>) -> Option<String> { + let _ = (stream, max_length); + todo!() +} + +pub fn stream_context_create( + _options: &IndexMap<String, PhpMixed>, + _params: Option<&IndexMap<String, PhpMixed>>, +) -> PhpMixed { + todo!() +} + +pub fn stream_isatty(stream: PhpResource) -> bool { + stream_isatty_resource(&stream) +} + +pub fn stream_get_wrappers() -> Vec<String> { + todo!() +} + +pub fn stream_copy_to_stream(_source: PhpMixed, _dest: PhpMixed) -> Option<i64> { + todo!() +} + +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 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 stream_get_contents3(_stream: PhpMixed, _max_length: i64, _offset: i64) -> Option<String> { + todo!() +} + +pub fn is_resource_value(_resource: &PhpResource) -> bool { + true +} + +pub fn get_resource_type(_resource: &PhpResource) -> String { + "stream".to_string() +} + +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 +} diff --git a/crates/shirabe-php-shim/src/string.rs b/crates/shirabe-php-shim/src/string.rs new file mode 100644 index 0000000..8b6f361 --- /dev/null +++ b/crates/shirabe-php-shim/src/string.rs @@ -0,0 +1,623 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +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 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 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 substr_replace(_string: &str, _replace: &str, _start: usize, _length: usize) -> String { + todo!() +} + +pub fn str_repeat(_s: &str, _count: usize) -> String { + _s.repeat(_count) +} + +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 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 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 str_bitand(_a: &str, _b: &str) -> String { + 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 strcasecmp(_s1: &str, _s2: &str) -> i64 { + _s1.to_ascii_lowercase().cmp(&_s2.to_ascii_lowercase()) as i64 +} + +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 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 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 +} + +pub fn strnatcasecmp(_s1: &str, _s2: &str) -> i64 { + todo!() +} + +pub fn strrpos(_haystack: &str, _needle: &str) -> Option<usize> { + _haystack.rfind(_needle) +} + +pub fn strtolower(_s: &str) -> String { + _s.to_ascii_lowercase() +} + +pub fn stripos(_haystack: &str, _needle: &str) -> Option<usize> { + _haystack + .to_ascii_lowercase() + .find(_needle.to_ascii_lowercase().as_str()) +} + +// 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 strcmp(_s1: &str, _s2: &str) -> i64 { + _s1.cmp(_s2) as i64 +} + +pub fn strnatcmp(_s1: &str, _s2: &str) -> i64 { + 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()) +} + +/// 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 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 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() +} + +// 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 fn implode(_glue: &str, _pieces: &[String]) -> String { + _pieces.join(_glue) +} + +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 fn explode_limit(delimiter: &str, string: &str, limit: i64) -> Vec<String> { + explode_limit_impl(delimiter, string, limit) +} + +/// 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 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 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 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 iconv(_in_charset: &str, _out_charset: &str, _string: &str) -> Option<String> { + todo!() +} + +/// 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 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 ctype_alnum(_s: &str) -> bool { + !_s.is_empty() && _s.bytes().all(|b| b.is_ascii_alphanumeric()) +} + +pub fn ctype_digit(s: &str) -> bool { + !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) +} + +pub fn ord(_c: &str) -> i64 { + _c.as_bytes().first().copied().unwrap_or(0) as i64 +} + +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 +} + +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 pack(_format: &str, _values: &[PhpMixed]) -> Vec<u8> { + todo!() +} + +pub fn unpack(_format: &str, _data: &[u8]) -> Option<IndexMap<String, PhpMixed>> { + todo!() +} + +pub fn sscanf(_subject: &str, _format: &str, _a: &mut i64, _b: &mut i64) -> i64 { + todo!() +} + +pub fn sprintf(_format: &str, _args: &[PhpMixed]) -> String { + todo!() +} + +pub fn strip_tags(_str: &str) -> String { + todo!() +} + +pub fn html_entity_decode(_s: &str) -> String { + todo!() +} + +pub fn bin2hex(_data: &[u8]) -> String { + _data.iter().map(|b| format!("{:02x}", b)).collect() +} + +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 chr(_value: u8) -> String { + todo!() +} + +pub fn addcslashes(_string: &str, _charlist: &str) -> String { + todo!() +} + +pub fn php_strip_whitespace(_path: &str) -> String { + todo!() +} + +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 stripcslashes(_s: &str) -> String { + todo!() +} + +pub fn wordwrap(_s: &str, _width: i64, _break_str: &str, _cut: bool) -> String { + 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 number_format( + _number: f64, + _decimals: i64, + _decimal_separator: &str, + _thousands_separator: &str, +) -> String { + todo!() +} + +pub fn uniqid(_prefix: &str, _more_entropy: bool) -> String { + todo!() +} diff --git a/crates/shirabe-php-shim/src/url.rs b/crates/shirabe-php-shim/src/url.rs new file mode 100644 index 0000000..ddb46b6 --- /dev/null +++ b/crates/shirabe-php-shim/src/url.rs @@ -0,0 +1,91 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +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 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 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!() +} diff --git a/crates/shirabe-php-shim/src/var.rs b/crates/shirabe-php-shim/src/var.rs new file mode 100644 index 0000000..b4dcf21 --- /dev/null +++ b/crates/shirabe-php-shim/src/var.rs @@ -0,0 +1,253 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +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 serialize(_value: &PhpMixed) -> String { + todo!() +} + +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 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 is_resource(_value: &PhpMixed) -> bool { + // PhpMixed has no resource variant, so a PhpMixed is never a resource. + false +} + +pub fn is_array(_value: &PhpMixed) -> bool { + matches!(_value, PhpMixed::List(_) | PhpMixed::Array(_)) +} + +pub fn is_null(_value: &PhpMixed) -> bool { + matches!(_value, PhpMixed::Null) +} + +pub fn is_iterable(_value: &PhpMixed) -> bool { + todo!() +} + +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 instance_of<T>(_value: &PhpMixed) -> bool { + todo!() +} + +pub fn is_subclass_of(_object_or_class: &PhpMixed, _class_name: &str, _allow_string: bool) -> 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!() +} + +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 instantiate_class(_class: &str, _args: Vec<PhpMixed>) -> PhpMixed { + todo!() +} + +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!(), + } +} + +pub fn strval(value: &PhpMixed) -> String { + php_to_string(value) +} + +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, + } +} + +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 var_export(_value: &PhpMixed, _return: bool) -> String { + todo!() +} + +pub fn var_export_str(_value: &str, _return: bool) -> String { + todo!() +} diff --git a/crates/shirabe-php-shim/src/zip.rs b/crates/shirabe-php-shim/src/zip.rs new file mode 100644 index 0000000..1258c3d --- /dev/null +++ b/crates/shirabe-php-shim/src/zip.rs @@ -0,0 +1,85 @@ +use crate::PhpMixed; +use indexmap::IndexMap; + +#[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; +} |
