claviron-php-embed 0.2.0-alpha.17

Managed in-process PHP execution, application hosting, and native extensions for Claviron
use std::collections::HashSet;

use claviron_app::{EventArrayKey, EventValue};
use ext_php_rs::types::{ArrayKey, ZendHashTable, Zval};

const MAX_DEPTH: usize = 64;

pub(crate) fn from_zval(value: &Zval) -> Result<EventValue, String> {
    from_zval_inner(value, 0, &mut HashSet::new())
}

fn from_zval_inner(
    value: &Zval,
    depth: usize,
    ancestors: &mut HashSet<usize>,
) -> Result<EventValue, String> {
    if depth > MAX_DEPTH {
        return Err(format!(
            "PHP event payload exceeds maximum nesting depth {MAX_DEPTH}"
        ));
    }

    let value = value.dereference();
    if value.is_null() {
        return Ok(EventValue::Null);
    }
    if let Some(value) = value.bool() {
        return Ok(EventValue::Bool(value));
    }
    if let Some(value) = value.long() {
        return Ok(EventValue::Int(value));
    }
    if let Some(value) = value.double() {
        return Ok(EventValue::Float(value));
    }
    if let Some(value) = value.binary_slice::<u8>() {
        return Ok(EventValue::String(value.to_vec()));
    }
    if let Some(array) = value.array() {
        let identity = std::ptr::from_ref(array) as usize;
        if !ancestors.insert(identity) {
            return Err(
                "PHP event payload contains a recursive array".to_owned()
            );
        }

        let mut entries = Vec::with_capacity(array.len());
        for (key, value) in array {
            let key = match key {
                ArrayKey::Long(value) => EventArrayKey::Int(value),
                ArrayKey::String(value) => EventArrayKey::String(value),
                ArrayKey::Str(value) => EventArrayKey::String(value.to_owned()),
                ArrayKey::ZendString(value) => EventArrayKey::String(
                    value
                        .as_str()
                        .map_err(|_| {
                            "PHP event array keys must be valid UTF-8"
                                .to_owned()
                        })?
                        .to_owned(),
                ),
            };
            entries.push((key, from_zval_inner(value, depth + 1, ancestors)?));
        }
        ancestors.remove(&identity);
        return Ok(EventValue::Array(entries));
    }

    Err("unsupported PHP event payload; expected null, bool, int, float, string, or array"
        .to_owned())
}

pub(crate) fn set_zval(
    source: &EventValue,
    value: &mut Zval,
) -> Result<(), String> {
    match source {
        EventValue::Null => value.set_null(),
        EventValue::Bool(inner) => value.set_bool(*inner),
        EventValue::Int(inner) => value.set_long(*inner),
        EventValue::Float(inner) => value.set_double(*inner),
        EventValue::String(inner) => value.set_binary(inner.clone()),
        EventValue::Array(entries) => {
            let mut array = ZendHashTable::with_capacity(
                entries.len().try_into().unwrap_or(u32::MAX),
            );
            for (key, entry) in entries {
                let mut zval = Zval::new();
                set_zval(entry, &mut zval)?;
                let key = match key {
                    EventArrayKey::Int(value) => ArrayKey::Long(*value),
                    EventArrayKey::String(value) => {
                        ArrayKey::String(value.clone())
                    }
                };
                array.insert(key, zval).map_err(|error| error.to_string())?;
            }
            value.set_hashtable(array);
        }
    }
    Ok(())
}