aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim/src/array.rs
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/src/array.rs
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/src/array.rs')
-rw-r--r--crates/shirabe-php-shim/src/array.rs612
1 files changed, 612 insertions, 0 deletions
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()
+}