From fcef25f6ef36287a4984ffdaab39df84f5aceeba Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 21 Jun 2026 15:49:24 +0900 Subject: refactor(php-shim): split lib.rs --- crates/shirabe-php-shim/src/array.rs | 612 +++++++++++++++++++++++++++++++++++ 1 file changed, 612 insertions(+) create mode 100644 crates/shirabe-php-shim/src/array.rs (limited to 'crates/shirabe-php-shim/src/array.rs') 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(_array: &IndexMap) -> Vec { + _array.values().cloned().collect() +} + +pub fn array_keys(_array: &IndexMap) -> Vec { + _array.keys().cloned().collect() +} + +pub fn array_push(_array: &mut Vec, _value: String) -> i64 { + _array.push(_value); + _array.len() as i64 +} + +pub fn array_search_in_vec(_needle: &str, _haystack: &[String]) -> Option { + _haystack.iter().position(|s| s.as_str() == _needle) +} + +pub fn array_map_str_fn String>(_callback: F, _array: &[String]) -> Vec { + _array.iter().map(|s| _callback(s)).collect() +} + +pub fn array_slice_mixed(value: &PhpMixed, offset: i64, length: Option) -> 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) -> Vec { + 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 = 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 = 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::() { + 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` 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( + array1: IndexMap, + array2: IndexMap, +) -> IndexMap { + let mut result: IndexMap = IndexMap::new(); + let mut next_int: i64 = 0; + for array in [array1, array2] { + for (key, value) in array { + if let Ok(n) = key.parse::() { + 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 { + _array1 + .iter() + .filter(|&x| !_array2.contains(x)) + .cloned() + .collect() +} + +pub fn array_unique(_array: &[T]) -> Vec { + todo!() +} + +pub fn array_intersect_key( + _array1: &IndexMap, + _array2: &IndexMap, +) -> IndexMap { + _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, + replacement: IndexMap, +) -> IndexMap { + 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, + replacement: IndexMap, +) -> IndexMap { + 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, + replacement: Vec, +) -> Vec { + 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 { + 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) -> Option { + haystack + .iter() + .find(|(_, value)| value.as_str() == needle) + .map(|(key, _)| key.clone()) +} + +pub fn array_shift(_array: &mut Vec) -> Option { + if _array.is_empty() { + None + } else { + Some(_array.remove(0)) + } +} + +pub fn array_pop(_array: &mut Vec) -> Option { + _array.pop() +} + +pub fn array_unshift(_array: &mut Vec, _value: T) { + _array.insert(0, _value); +} + +pub fn array_reverse(_array: &[T], _preserve_keys: bool) -> Vec { + _array.iter().rev().cloned().collect() +} + +pub fn array_filter(_array: &[T], _callback: F) -> Vec +where + F: Fn(&T) -> bool, +{ + _array.iter().filter(|&x| _callback(x)).cloned().collect() +} + +pub fn array_filter_map( + _array: &IndexMap, + _callback: F, +) -> IndexMap +where + F: Fn(&PhpMixed) -> bool, +{ + _array + .iter() + .filter(|&(_, v)| _callback(v)) + .map(|(k, v)| (k.clone(), v.clone())) + .collect() +} + +pub fn array_all(_array: &[T], _callback: F) -> bool +where + F: Fn(&T) -> bool, +{ + _array.iter().all(_callback) +} + +pub fn array_any(_array: &[T], _callback: F) -> bool +where + F: Fn(&T) -> bool, +{ + _array.iter().any(_callback) +} + +pub fn array_reduce(_array: &[T], _callback: F, _initial: U) -> U +where + F: Fn(U, &T) -> U, +{ + _array.iter().fold(_initial, _callback) +} + +pub fn array_intersect(_array1: &[T], _array2: &[T]) -> Vec { + _array1 + .iter() + .filter(|&x| _array2.contains(x)) + .cloned() + .collect() +} + +pub fn array_flip(array: &PhpMixed) -> PhpMixed { + let mut result: IndexMap = 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 { + _array + .iter() + .enumerate() + .map(|(i, s)| (s.clone(), PhpMixed::Int(i as i64))) + .collect() +} + +pub fn array_key_exists(_key: &str, _array: &IndexMap) -> 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( + _array: &mut Vec, + _offset: i64, + _length: Option, + _replacement: Vec, +) -> Vec { + todo!() +} + +pub fn array_pop_first(array: &mut Vec) -> Option { + if array.is_empty() { + None + } else { + Some(array.remove(0)) + } +} + +pub fn array_merge_recursive(_arrays: Vec) -> PhpMixed { + todo!() +} + +pub fn array_slice( + array: &IndexMap, + offset: i64, + length: Option, +) -> IndexMap { + 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(_callback: F, _array: &[T]) -> Vec +where + F: Fn(&T) -> U, +{ + _array.iter().map(_callback).collect() +} + +pub fn array_filter_use_key( + _array: &IndexMap, + _callback: Box bool>, +) -> IndexMap { + _array + .iter() + .filter(|(k, _)| _callback(k.as_str())) + .map(|(k, v)| (k.clone(), v.clone())) + .collect() +} + +pub fn array_chunk(_array: &[T], _size: i64, _preserve_keys: bool) -> Vec> { + _array.chunks(_size as usize).map(|c| c.to_vec()).collect() +} + +pub fn array_diff_key( + _array1: IndexMap, + _array2: &IndexMap, +) -> IndexMap { + _array1 + .into_iter() + .filter(|(k, _)| !_array2.contains_key(k.as_str())) + .collect() +} + +pub fn array_key_last(_array: &IndexMap) -> usize { + todo!() +} + +pub fn array_splice_mixed( + _array: &mut Vec, + _offset: i64, + _length: i64, + _replacement: Vec, +) { + 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::() { + 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) -> (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(_array: &mut IndexMap) { + todo!() +} + +pub fn uasort(array: &mut Vec, 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(array: &mut IndexMap, 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(_array: &mut Vec) { + _array.sort(); +} + +pub fn sort_with_flags(_array: &mut Vec, _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(_array: &mut Vec, _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(_array: &mut IndexMap) { + todo!() +} + +pub fn asort(_array: &mut IndexMap) { + todo!() +} + +pub fn uksort(array: &mut IndexMap, 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) { + 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 { + todo!() +} + +pub fn reset(_array: &[T]) -> Option { + _array.first().cloned() +} + +pub fn reset_first(_array: &[T]) -> Option { + _array.first().cloned() +} + +pub fn end_arr(_array: &IndexMap) -> Option { + _array.values().last().cloned() +} + +pub fn iterator_to_array(iter: I) -> Vec +where + I: IntoIterator, +{ + iter.into_iter().collect() +} + +pub fn end(_array: &[V]) -> Option { + _array.last().cloned() +} -- cgit v1.3.1