aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-21 15:49:24 +0900
committernsfisis <nsfisis@gmail.com>2026-06-21 15:49:37 +0900
commitfcef25f6ef36287a4984ffdaab39df84f5aceeba (patch)
tree97ceb2098fbb5642e858929da9578bb41e277ada /crates/shirabe-php-shim
parentf0f5f084c883dc4f5b6e61603e82cd1c2092fd9d (diff)
downloadphp-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')
-rw-r--r--crates/shirabe-php-shim/src/apcu.rs11
-rw-r--r--crates/shirabe-php-shim/src/array.rs612
-rw-r--r--crates/shirabe-php-shim/src/compress.rs25
-rw-r--r--crates/shirabe-php-shim/src/curl.rs152
-rw-r--r--crates/shirabe-php-shim/src/datetime.rs55
-rw-r--r--crates/shirabe-php-shim/src/env.rs100
-rw-r--r--crates/shirabe-php-shim/src/exception.rs186
-rw-r--r--crates/shirabe-php-shim/src/filter.rs39
-rw-r--r--crates/shirabe-php-shim/src/fs.rs492
-rw-r--r--crates/shirabe-php-shim/src/hash.rs11
-rw-r--r--crates/shirabe-php-shim/src/json.rs107
-rw-r--r--crates/shirabe-php-shim/src/lib.rs3680
-rw-r--r--crates/shirabe-php-shim/src/math.rs33
-rw-r--r--crates/shirabe-php-shim/src/net.rs19
-rw-r--r--crates/shirabe-php-shim/src/openssl.rs42
-rw-r--r--crates/shirabe-php-shim/src/output.rs7
-rw-r--r--crates/shirabe-php-shim/src/phar.rs137
-rw-r--r--crates/shirabe-php-shim/src/process.rs105
-rw-r--r--crates/shirabe-php-shim/src/rar.rs28
-rw-r--r--crates/shirabe-php-shim/src/runtime.rs416
-rw-r--r--crates/shirabe-php-shim/src/stream.rs120
-rw-r--r--crates/shirabe-php-shim/src/string.rs623
-rw-r--r--crates/shirabe-php-shim/src/url.rs91
-rw-r--r--crates/shirabe-php-shim/src/var.rs253
-rw-r--r--crates/shirabe-php-shim/src/zip.rs85
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;
+}