From 32d93e0977a5b011bfd29352269f51b1632afe28 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Mon, 13 Jul 2026 06:31:18 +0000 Subject: [PATCH 01/12] [RFC] ext-user_cache: initial implementation --- ext/date/php_date.c | 368 + ext/spl/spl_array.c | 193 + ext/spl/spl_dllist.c | 146 + ext/spl/spl_fixedarray.c | 129 + ext/spl/spl_heap.c | 265 + ext/user_cache/config.m4 | 18 + ext/user_cache/config.w32 | 16 + ext/user_cache/php_user_cache.h | 125 + ext/user_cache/tests/fpm/skipif.inc | 13 + ext/user_cache/tests/fpm/tester.inc | 4 + ...r_cache_fpm_complex_value_persistence.phpt | 140 + .../user_cache_fpm_decode_failure_delete.phpt | 119 + .../fpm/user_cache_fpm_lookup_cache.phpt | 96 + .../tests/fpm/user_cache_fpm_persistence.phpt | 108 + .../fpm/user_cache_fpm_pool_separate.phpt | 113 + ..._cache_fpm_remember_callable_lifetime.phpt | 122 + .../user_cache_fpm_request_local_slot.phpt | 95 + .../tests/fpm/user_cache_fpm_reset.phpt | 106 + .../tests/fpm/user_cache_fpm_safe_direct.phpt | 143 + ext/user_cache/tests/user_cache_add.phpt | 38 + ...ser_cache_apache2handler_boundary_001.phpt | 218 + .../tests/user_cache_array_int_keys.phpt | 94 + ext/user_cache/tests/user_cache_atomic.phpt | 82 + ext/user_cache/tests/user_cache_basic.phpt | 92 + ext/user_cache/tests/user_cache_cgi.phpt | 31 + .../tests/user_cache_cgi_boundary.phpt | 305 + .../tests/user_cache_cgi_no_boundary.phpt | 32 + .../tests/user_cache_cgi_value_matrix.phpt | 424 ++ .../tests/user_cache_clear_pool_scope.phpt | 41 + .../user_cache_cross_container_reference.phpt | 91 + .../tests/user_cache_dateperiod.phpt | 80 + .../user_cache_datetime_safe_direct.phpt | 353 + ...r_cache_default_valued_property_alias.phpt | 31 + ext/user_cache/tests/user_cache_disabled.phpt | 61 + .../user_cache_empty_array_roundtrip.phpt | 98 + .../user_cache_expired_read_reclaim.phpt | 44 + .../user_cache_fetch_multiple_int_keys.phpt | 39 + .../tests/user_cache_float_precision.phpt | 83 + .../tests/user_cache_fork_entry_lock.phpt | 88 + .../tests/user_cache_fuzz_roundtrip.phpt | 438 ++ .../tests/user_cache_info_counters.phpt | 36 + .../tests/user_cache_info_reset.phpt | 73 + .../tests/user_cache_invalid_keys.phpt | 44 + .../tests/user_cache_litespeed_boundary.phpt | 372 + .../tests/user_cache_lock_key_isolation.phpt | 61 + .../tests/user_cache_lock_lease.phpt | 41 + .../tests/user_cache_lock_reentrancy.phpt | 33 + .../user_cache_magic_serialize_contract.phpt | 285 + .../user_cache_magic_state_unstorable.phpt | 69 + .../tests/user_cache_negative_ttl.phpt | 40 + .../tests/user_cache_overwrite_reuse.phpt | 42 + .../user_cache_packed_dynamic_array.phpt | 58 + .../tests/user_cache_pool_factory.phpt | 68 + .../tests/user_cache_pool_status_memory.phpt | 99 + .../tests/user_cache_pressure_clear.phpt | 30 + ..._cache_private_property_missing_scope.phpt | 30 + .../tests/user_cache_pure_enum.phpt | 35 + .../tests/user_cache_readonly_properties.phpt | 65 + .../tests/user_cache_reference_cycle_gc.phpt | 57 + .../tests/user_cache_reference_graphs.phpt | 272 + .../tests/user_cache_remember_failure.phpt | 49 + .../user_cache_remember_key_argument.phpt | 36 + ...request_local_slot_deferred_promotion.phpt | 99 + ...ache_request_local_slot_magic_objects.phpt | 118 + ...e_request_local_slot_store_invalidate.phpt | 62 + ...ache_request_local_slot_strict_parity.phpt | 145 + ...uest_local_slot_unserialize_exception.phpt | 66 + ext/user_cache/tests/user_cache_reset.phpt | 40 + .../tests/user_cache_serdes_identity.phpt | 75 + .../tests/user_cache_serdes_mixed_keys.phpt | 51 + .../tests/user_cache_serdes_sleep_wakeup.phpt | 110 + .../user_cache_serializable_interface.phpt | 118 + .../user_cache_serializable_precedence.phpt | 164 + ...ser_cache_serializable_value_coverage.phpt | 230 + .../tests/user_cache_serialize_only.phpt | 169 + .../tests/user_cache_shaped_array_state.phpt | 127 + .../tests/user_cache_shared_graph.phpt | 40 + .../user_cache_shared_graph_dag_dedup.phpt | 51 + .../user_cache_shm_size_clamp_warning.phpt | 17 + ...user_cache_shm_size_runtime_immutable.phpt | 19 + .../tests/user_cache_shm_size_units.phpt | 15 + .../tests/user_cache_sleep_shared_graph.phpt | 138 + .../tests/user_cache_spl_object_storage.phpt | 107 + .../tests/user_cache_spl_safe_direct.phpt | 397 ++ .../user_cache_state_schema_prototype.phpt | 202 + .../tests/user_cache_status_objects.phpt | 83 + .../tests/user_cache_storable_values.phpt | 150 + .../user_cache_store_multiple_rollback.phpt | 46 + .../user_cache_store_multiple_typeerror.phpt | 39 + .../tests/user_cache_tombstone_rehash.phpt | 47 + .../tests/user_cache_ttl_remember.phpt | 37 + .../tests/user_cache_ttl_semantics.phpt | 38 + .../user_cache_uninitialized_typed_prop.phpt | 94 + .../user_cache_unserialize_only_contract.phpt | 84 + .../tests/user_cache_unstorable_extended.phpt | 51 + .../user_cache_wakeup_only_identity.phpt | 63 + ext/user_cache/user_cache.c | 3348 +++++++++ ext/user_cache/user_cache.stub.php | 127 + ext/user_cache/user_cache_alloc_posix.c | 128 + ext/user_cache/user_cache_alloc_shm.c | 134 + ext/user_cache/user_cache_arginfo.h | 267 + ext/user_cache/user_cache_decl.h | 19 + ext/user_cache/user_cache_entries.c | 4218 +++++++++++ ext/user_cache/user_cache_internal.h | 1163 +++ ext/user_cache/user_cache_serdes.c | 1661 +++++ ext/user_cache/user_cache_shared_graph.c | 6281 +++++++++++++++++ ext/user_cache/user_cache_shm.h | 112 + ext/user_cache/user_cache_storage.c | 3947 +++++++++++ .../user_cache_storage_portability.h | 96 + main/main.c | 5 + sapi/apache2handler/sapi_apache2.c | 78 +- sapi/cgi/cgi_main.c | 52 +- sapi/cli/php_cli.c | 12 +- sapi/cli/php_cli_server.c | 15 +- sapi/fpm/config.m4 | 1 + sapi/fpm/fpm/fpm.c | 2 + sapi/fpm/fpm/fpm_children.c | 3 + sapi/fpm/fpm/fpm_user_cache.c | 43 + sapi/fpm/fpm/fpm_user_cache.h | 20 + sapi/fpm/fpm/fpm_worker_pool.h | 2 + sapi/fpm/tests/tester.inc | 2 +- sapi/litespeed/lsapi_main.c | 33 + sapi/phpdbg/phpdbg.c | 4 + 123 files changed, 32037 insertions(+), 5 deletions(-) create mode 100644 ext/user_cache/config.m4 create mode 100644 ext/user_cache/config.w32 create mode 100644 ext/user_cache/php_user_cache.h create mode 100644 ext/user_cache/tests/fpm/skipif.inc create mode 100644 ext/user_cache/tests/fpm/tester.inc create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_complex_value_persistence.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_lookup_cache.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_persistence.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_pool_separate.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_remember_callable_lifetime.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_request_local_slot.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_reset.phpt create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_safe_direct.phpt create mode 100644 ext/user_cache/tests/user_cache_add.phpt create mode 100644 ext/user_cache/tests/user_cache_apache2handler_boundary_001.phpt create mode 100644 ext/user_cache/tests/user_cache_array_int_keys.phpt create mode 100644 ext/user_cache/tests/user_cache_atomic.phpt create mode 100644 ext/user_cache/tests/user_cache_basic.phpt create mode 100644 ext/user_cache/tests/user_cache_cgi.phpt create mode 100644 ext/user_cache/tests/user_cache_cgi_boundary.phpt create mode 100644 ext/user_cache/tests/user_cache_cgi_no_boundary.phpt create mode 100644 ext/user_cache/tests/user_cache_cgi_value_matrix.phpt create mode 100644 ext/user_cache/tests/user_cache_clear_pool_scope.phpt create mode 100644 ext/user_cache/tests/user_cache_cross_container_reference.phpt create mode 100644 ext/user_cache/tests/user_cache_dateperiod.phpt create mode 100644 ext/user_cache/tests/user_cache_datetime_safe_direct.phpt create mode 100644 ext/user_cache/tests/user_cache_default_valued_property_alias.phpt create mode 100644 ext/user_cache/tests/user_cache_disabled.phpt create mode 100644 ext/user_cache/tests/user_cache_empty_array_roundtrip.phpt create mode 100644 ext/user_cache/tests/user_cache_expired_read_reclaim.phpt create mode 100644 ext/user_cache/tests/user_cache_fetch_multiple_int_keys.phpt create mode 100644 ext/user_cache/tests/user_cache_float_precision.phpt create mode 100644 ext/user_cache/tests/user_cache_fork_entry_lock.phpt create mode 100644 ext/user_cache/tests/user_cache_fuzz_roundtrip.phpt create mode 100644 ext/user_cache/tests/user_cache_info_counters.phpt create mode 100644 ext/user_cache/tests/user_cache_info_reset.phpt create mode 100644 ext/user_cache/tests/user_cache_invalid_keys.phpt create mode 100644 ext/user_cache/tests/user_cache_litespeed_boundary.phpt create mode 100644 ext/user_cache/tests/user_cache_lock_key_isolation.phpt create mode 100644 ext/user_cache/tests/user_cache_lock_lease.phpt create mode 100644 ext/user_cache/tests/user_cache_lock_reentrancy.phpt create mode 100644 ext/user_cache/tests/user_cache_magic_serialize_contract.phpt create mode 100644 ext/user_cache/tests/user_cache_magic_state_unstorable.phpt create mode 100644 ext/user_cache/tests/user_cache_negative_ttl.phpt create mode 100644 ext/user_cache/tests/user_cache_overwrite_reuse.phpt create mode 100644 ext/user_cache/tests/user_cache_packed_dynamic_array.phpt create mode 100644 ext/user_cache/tests/user_cache_pool_factory.phpt create mode 100644 ext/user_cache/tests/user_cache_pool_status_memory.phpt create mode 100644 ext/user_cache/tests/user_cache_pressure_clear.phpt create mode 100644 ext/user_cache/tests/user_cache_private_property_missing_scope.phpt create mode 100644 ext/user_cache/tests/user_cache_pure_enum.phpt create mode 100644 ext/user_cache/tests/user_cache_readonly_properties.phpt create mode 100644 ext/user_cache/tests/user_cache_reference_cycle_gc.phpt create mode 100644 ext/user_cache/tests/user_cache_reference_graphs.phpt create mode 100644 ext/user_cache/tests/user_cache_remember_failure.phpt create mode 100644 ext/user_cache/tests/user_cache_remember_key_argument.phpt create mode 100644 ext/user_cache/tests/user_cache_request_local_slot_deferred_promotion.phpt create mode 100644 ext/user_cache/tests/user_cache_request_local_slot_magic_objects.phpt create mode 100644 ext/user_cache/tests/user_cache_request_local_slot_store_invalidate.phpt create mode 100644 ext/user_cache/tests/user_cache_request_local_slot_strict_parity.phpt create mode 100644 ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt create mode 100644 ext/user_cache/tests/user_cache_reset.phpt create mode 100644 ext/user_cache/tests/user_cache_serdes_identity.phpt create mode 100644 ext/user_cache/tests/user_cache_serdes_mixed_keys.phpt create mode 100644 ext/user_cache/tests/user_cache_serdes_sleep_wakeup.phpt create mode 100644 ext/user_cache/tests/user_cache_serializable_interface.phpt create mode 100644 ext/user_cache/tests/user_cache_serializable_precedence.phpt create mode 100644 ext/user_cache/tests/user_cache_serializable_value_coverage.phpt create mode 100644 ext/user_cache/tests/user_cache_serialize_only.phpt create mode 100644 ext/user_cache/tests/user_cache_shaped_array_state.phpt create mode 100644 ext/user_cache/tests/user_cache_shared_graph.phpt create mode 100644 ext/user_cache/tests/user_cache_shared_graph_dag_dedup.phpt create mode 100644 ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt create mode 100644 ext/user_cache/tests/user_cache_shm_size_runtime_immutable.phpt create mode 100644 ext/user_cache/tests/user_cache_shm_size_units.phpt create mode 100644 ext/user_cache/tests/user_cache_sleep_shared_graph.phpt create mode 100644 ext/user_cache/tests/user_cache_spl_object_storage.phpt create mode 100644 ext/user_cache/tests/user_cache_spl_safe_direct.phpt create mode 100644 ext/user_cache/tests/user_cache_state_schema_prototype.phpt create mode 100644 ext/user_cache/tests/user_cache_status_objects.phpt create mode 100644 ext/user_cache/tests/user_cache_storable_values.phpt create mode 100644 ext/user_cache/tests/user_cache_store_multiple_rollback.phpt create mode 100644 ext/user_cache/tests/user_cache_store_multiple_typeerror.phpt create mode 100644 ext/user_cache/tests/user_cache_tombstone_rehash.phpt create mode 100644 ext/user_cache/tests/user_cache_ttl_remember.phpt create mode 100644 ext/user_cache/tests/user_cache_ttl_semantics.phpt create mode 100644 ext/user_cache/tests/user_cache_uninitialized_typed_prop.phpt create mode 100644 ext/user_cache/tests/user_cache_unserialize_only_contract.phpt create mode 100644 ext/user_cache/tests/user_cache_unstorable_extended.phpt create mode 100644 ext/user_cache/tests/user_cache_wakeup_only_identity.phpt create mode 100644 ext/user_cache/user_cache.c create mode 100644 ext/user_cache/user_cache.stub.php create mode 100644 ext/user_cache/user_cache_alloc_posix.c create mode 100644 ext/user_cache/user_cache_alloc_shm.c create mode 100644 ext/user_cache/user_cache_arginfo.h create mode 100644 ext/user_cache/user_cache_decl.h create mode 100644 ext/user_cache/user_cache_entries.c create mode 100644 ext/user_cache/user_cache_internal.h create mode 100644 ext/user_cache/user_cache_serdes.c create mode 100644 ext/user_cache/user_cache_shared_graph.c create mode 100644 ext/user_cache/user_cache_shm.h create mode 100644 ext/user_cache/user_cache_storage.c create mode 100644 ext/user_cache/user_cache_storage_portability.h create mode 100644 sapi/fpm/fpm/fpm_user_cache.c create mode 100644 sapi/fpm/fpm/fpm_user_cache.h diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 2b0696305d2b..1e4112db9cc6 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -17,6 +17,7 @@ #include "php_ini.h" #include "ext/standard/info.h" #include "ext/standard/php_versioning.h" +#include "ext/user_cache/php_user_cache.h" #include "php_date.h" #include "zend_attributes.h" #include "zend_interfaces.h" @@ -441,6 +442,8 @@ ZEND_MODULE_POST_ZEND_DEACTIVATE_D(date) #define DATE_TIMEZONEDB php_date_global_timezone_db ? php_date_global_timezone_db : timelib_builtin_db() +static void php_date_register_user_cache_handlers(void); + /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(date) { @@ -451,6 +454,9 @@ PHP_MINIT_FUNCTION(date) php_date_global_timezone_db = NULL; php_date_global_timezone_db_enabled = 0; DATEG(last_errors) = NULL; + + php_date_register_user_cache_handlers(); + return SUCCESS; } /* }}} */ @@ -4758,6 +4764,368 @@ static void php_date_interval_initialize_from_hash(php_interval_obj *intobj, con intobj->initialized = true; } /* }}} */ +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE "timezone" +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_TIMEZONE) - 1) +#define PHP_DATE_USER_CACHE_STATE_CTIME "ctime" +#define PHP_DATE_USER_CACHE_STATE_CTIME_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_CTIME) - 1) + +static void php_date_user_cache_add_assoc_int64(zval *arr, const char *name, int64_t value) +{ +#if PHP_DATE_SIZEOF_LONG == 8 + add_assoc_long(arr, name, (zend_long) value); +#else + add_assoc_str(arr, name, zend_strpprintf(0, "%lld", (long long) value)); +#endif +} + +static void php_date_user_cache_copy_time_snapshot(timelib_time *dst, const timelib_time *src) +{ + memset(dst, 0, sizeof(*dst)); + + dst->y = src->y; + dst->m = src->m; + dst->d = src->d; + dst->h = src->h; + dst->i = src->i; + dst->s = src->s; + dst->us = src->us; + dst->z = src->z; + dst->dst = src->dst; + dst->relative = src->relative; + dst->sse = src->sse; + dst->have_time = src->have_time; + dst->have_date = src->have_date; + dst->have_zone = src->have_zone; + dst->have_relative = src->have_relative; + dst->have_weeknr_day = src->have_weeknr_day; + dst->sse_uptodate = src->sse_uptodate; + dst->tim_uptodate = src->tim_uptodate; + dst->is_localtime = src->is_localtime; + dst->zone_type = src->zone_type; +} + +static bool php_date_copy_user_cache_state( + void *ctx, + zend_object *new_object, + zend_object *old_object, + php_user_cache_safe_direct_clone_value_func_t clone_value) +{ + php_date_obj *new_obj, *old_obj; + php_timezone_obj *new_tzobj, *old_tzobj; + php_interval_obj *new_intervalobj, *old_intervalobj; + + (void) ctx; + + if (clone_value == NULL) { + return false; + } + + if (instanceof_function(old_object->ce, date_ce_date) || + instanceof_function(old_object->ce, date_ce_immutable) + ) { + old_obj = php_date_obj_from_obj(old_object); + new_obj = php_date_obj_from_obj(new_object); + + if (old_obj->time == NULL) { + return true; + } + + new_obj->time = timelib_time_clone(old_obj->time); + + return new_obj->time != NULL; + } + + if (instanceof_function(old_object->ce, date_ce_timezone)) { + old_tzobj = php_timezone_obj_from_obj(old_object); + new_tzobj = php_timezone_obj_from_obj(new_object); + + if (!old_tzobj->initialized) { + return true; + } + + new_tzobj->type = old_tzobj->type; + new_tzobj->initialized = true; + switch (new_tzobj->type) { + case TIMELIB_ZONETYPE_ID: + new_tzobj->tzi.tz = old_tzobj->tzi.tz; + + return true; + case TIMELIB_ZONETYPE_OFFSET: + new_tzobj->tzi.utc_offset = old_tzobj->tzi.utc_offset; + + return true; + case TIMELIB_ZONETYPE_ABBR: + new_tzobj->tzi.z.utc_offset = old_tzobj->tzi.z.utc_offset; + new_tzobj->tzi.z.dst = old_tzobj->tzi.z.dst; + new_tzobj->tzi.z.abbr = old_tzobj->tzi.z.abbr != NULL + ? timelib_strdup(old_tzobj->tzi.z.abbr) + : NULL + ; + + return old_tzobj->tzi.z.abbr == NULL || new_tzobj->tzi.z.abbr != NULL; + default: + return false; + } + } + + if (instanceof_function(old_object->ce, date_ce_interval)) { + old_intervalobj = php_interval_obj_from_obj(old_object); + new_intervalobj = php_interval_obj_from_obj(new_object); + + new_intervalobj->civil_or_wall = old_intervalobj->civil_or_wall; + new_intervalobj->from_string = old_intervalobj->from_string; + + if (old_intervalobj->date_string != NULL) { + new_intervalobj->date_string = zend_string_copy(old_intervalobj->date_string); + } + + new_intervalobj->initialized = old_intervalobj->initialized; + + if (old_intervalobj->diff != NULL) { + new_intervalobj->diff = timelib_rel_time_clone(old_intervalobj->diff); + + return new_intervalobj->diff != NULL; + } + + return true; + } + + return false; +} + +static bool php_date_user_cache_timezone_pair_to_hash(php_date_obj *dateobj, HashTable *props) +{ + zval zv; + + ZVAL_LONG(&zv, dateobj->time->zone_type); + zend_hash_str_update(props, "timezone_type", sizeof("timezone_type")-1, &zv); + + switch (dateobj->time->zone_type) { + case TIMELIB_ZONETYPE_ID: + ZVAL_STRING(&zv, dateobj->time->tz_info->name); + break; + case TIMELIB_ZONETYPE_OFFSET: + ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z)); + break; + case TIMELIB_ZONETYPE_ABBR: + ZVAL_STRING(&zv, dateobj->time->tz_abbr); + break; + default: + return false; + } + zend_hash_str_update(props, "timezone", sizeof("timezone")-1, &zv); + + return true; +} + +static bool php_date_serialize_datetime_user_cache_state(php_date_obj *dateobj, zval *state) +{ + timelib_time snapshot; + zval zv; + + if (dateobj->time == NULL || !dateobj->time->is_localtime) { + return false; + } + + /* ctime (binary timelib_time snapshot) plus timezone_type/timezone. The + * "date" string of the regular __serialize is omitted; unserialize rebuilds + * the time fields from the snapshot but still reads the timezone pair. */ + array_init_size(state, 3); + + php_date_user_cache_copy_time_snapshot(&snapshot, dateobj->time); + + if (!php_date_user_cache_timezone_pair_to_hash(dateobj, Z_ARRVAL_P(state))) { + zval_ptr_dtor(state); + ZVAL_UNDEF(state); + + return false; + } + + ZVAL_STRINGL(&zv, (const char *) &snapshot, sizeof(snapshot)); + zend_hash_str_update(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN, &zv); + + return true; +} + +static bool php_date_serialize_user_cache_state(zval *state, const zval *object) +{ + php_timezone_obj *tzobj; + php_interval_obj *intervalobj; + + ZVAL_UNDEF(state); + + if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || + instanceof_function(Z_OBJCE_P(object), date_ce_immutable) + ) { + return php_date_serialize_datetime_user_cache_state(Z_PHPDATE_P((zval *) object), state); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + tzobj = Z_PHPTIMEZONE_P((zval *) object); + + if (!tzobj->initialized) { + return false; + } + + array_init_size(state, 2); + date_timezone_object_to_hash(tzobj, Z_ARRVAL_P(state)); + + return true; + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { + intervalobj = Z_PHPINTERVAL_P((zval *) object); + + if (!intervalobj->initialized || intervalobj->diff == NULL) { + return false; + } + + if (intervalobj->from_string && intervalobj->date_string == NULL) { + return false; + } + + array_init_size(state, intervalobj->from_string ? 2 : 18); + date_interval_object_to_hash(intervalobj, Z_ARRVAL_P(state)); + + if (!intervalobj->from_string) { + add_assoc_long(state, "weekday", intervalobj->diff->weekday); + add_assoc_long(state, "weekday_behavior", intervalobj->diff->weekday_behavior); + add_assoc_long(state, "first_last_day_of", intervalobj->diff->first_last_day_of); + add_assoc_long(state, "special_type", intervalobj->diff->special.type); + php_date_user_cache_add_assoc_int64(state, "special_amount", intervalobj->diff->special.amount); + add_assoc_long(state, "have_weekday_relative", intervalobj->diff->have_weekday_relative); + add_assoc_long(state, "have_special_relative", intervalobj->diff->have_special_relative); + add_assoc_long(state, "civil_or_wall", intervalobj->civil_or_wall); + } + + return true; + } + + return false; +} + +static bool php_date_unserialize_datetime_user_cache_state(zval *object, zval *state) +{ + php_date_obj *dateobj; + timelib_time *time; + timelib_tzinfo *tzi; + zval *z_ctime, *z_timezone; + + z_ctime = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN); + if (z_ctime == NULL) { + return false; + } + + if (Z_TYPE_P(z_ctime) != IS_STRING || Z_STRLEN_P(z_ctime) != sizeof(timelib_time)) { + return false; + } + + time = timelib_time_ctor(); + if (time == NULL) { + return false; + } + + memcpy(time, Z_STRVAL_P(z_ctime), sizeof(timelib_time)); + time->tz_abbr = NULL; + time->tz_info = NULL; + + switch (time->zone_type) { + case TIMELIB_ZONETYPE_ID: + z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); + if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || + UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) + ) { + timelib_time_dtor(time); + + return false; + } + + tzi = php_date_parse_tzfile(Z_STRVAL_P(z_timezone), DATE_TIMEZONEDB); + if (tzi == NULL) { + timelib_time_dtor(time); + + return false; + } + time->tz_info = tzi; + break; + case TIMELIB_ZONETYPE_OFFSET: + break; + case TIMELIB_ZONETYPE_ABBR: + z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); + if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || + UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) + ) { + timelib_time_dtor(time); + + return false; + } + + time->tz_abbr = timelib_strdup(Z_STRVAL_P(z_timezone)); + if (time->tz_abbr == NULL) { + timelib_time_dtor(time); + + return false; + } + break; + default: + timelib_time_dtor(time); + + return false; + } + + dateobj = Z_PHPDATE_P(object); + if (dateobj->time != NULL) { + timelib_time_dtor(dateobj->time); + } + dateobj->time = time; + + return true; +} + +static bool php_date_unserialize_user_cache_state(zval *object, zval *state) +{ + php_timezone_obj *tzobj; + + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || + instanceof_function(Z_OBJCE_P(object), date_ce_immutable) + ) { + return php_date_unserialize_datetime_user_cache_state(object, state); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + tzobj = Z_PHPTIMEZONE_P(object); + + return php_date_timezone_initialize_from_hash(&tzobj, Z_ARRVAL_P(state)); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { + php_date_interval_initialize_from_hash(Z_PHPINTERVAL_P(object), Z_ARRVAL_P(state)); + + return !EG(exception); + } + + return false; +} + +static const php_user_cache_safe_direct_handlers php_date_user_cache_handlers = { + .prefer_request_local_prototype = true, + .copy = php_date_copy_user_cache_state, + .state_serialize = php_date_serialize_user_cache_state, + .state_unserialize = php_date_unserialize_user_cache_state, +}; + +static void php_date_register_user_cache_handlers(void) +{ + php_user_cache_safe_direct_register_class(date_ce_date, &php_date_user_cache_handlers); + php_user_cache_safe_direct_register_class(date_ce_immutable, &php_date_user_cache_handlers); + php_user_cache_safe_direct_register_class(date_ce_timezone, &php_date_user_cache_handlers); + php_user_cache_safe_direct_register_class(date_ce_interval, &php_date_user_cache_handlers); +} + /* {{{ */ PHP_METHOD(DateInterval, __set_state) { diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index 1976192e7b06..a223bdd1964f 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -18,6 +18,7 @@ #include "php.h" #include "ext/standard/php_var.h" +#include "ext/user_cache/php_user_cache.h" #include "zend_smart_str.h" #include "zend_interfaces.h" #include "zend_exceptions.h" @@ -1506,6 +1507,194 @@ PHP_METHOD(ArrayObject, __unserialize) } /* }}} */ +/* Serializes ArrayObject state without dynamic members; used by the user-cache + * safe-direct path only (the regular __serialize includes members). */ +static void spl_array_object_user_cache_serialize_state(zval *object, zval *return_value) +{ + spl_array_object *intern = Z_SPLARRAY_P(object); + zval tmp; + + array_init(return_value); + + /* flags */ + ZVAL_LONG(&tmp, (intern->ar_flags & SPL_ARRAY_CLONE_MASK)); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); + + /* storage */ + if (intern->ar_flags & SPL_ARRAY_IS_SELF) { + ZVAL_NULL(&tmp); + } else { + ZVAL_COPY(&tmp, &intern->array); + } + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); + + /* iterator class */ + if (intern->ce_get_iterator == spl_ce_ArrayIterator) { + ZVAL_NULL(&tmp); + } else { + ZVAL_STR_COPY(&tmp, intern->ce_get_iterator->name); + } + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); +} + +static void spl_array_object_user_cache_unserialize_state(zval *object, HashTable *data) +{ + spl_array_object *intern = Z_SPLARRAY_P(object); + zval *flags_zv, *storage_zv, *iterator_class_zv; + zend_long flags; + + flags_zv = zend_hash_index_find(data, 0); + storage_zv = zend_hash_index_find(data, 1); + iterator_class_zv = zend_hash_index_find(data, 2); + + if (!flags_zv || !storage_zv || + Z_TYPE_P(flags_zv) != IS_LONG || + (iterator_class_zv && (Z_TYPE_P(iterator_class_zv) != IS_NULL && + Z_TYPE_P(iterator_class_zv) != IS_STRING))) { + zend_throw_exception(spl_ce_UnexpectedValueException, + "Incomplete or ill-typed serialization data", 0); + return; + } + + flags = Z_LVAL_P(flags_zv); + intern->ar_flags &= ~SPL_ARRAY_CLONE_MASK; + intern->ar_flags |= flags & SPL_ARRAY_CLONE_MASK; + + if (flags & SPL_ARRAY_IS_SELF) { + zval_ptr_dtor(&intern->array); + ZVAL_UNDEF(&intern->array); + } else { + if (Z_TYPE_P(storage_zv) != IS_OBJECT && Z_TYPE_P(storage_zv) != IS_ARRAY) { + zend_throw_exception(spl_ce_InvalidArgumentException, "Passed variable is not an array or object", 0); + return; + } + spl_array_set_array(object, intern, storage_zv, 0L, true); + } + + if (iterator_class_zv && Z_TYPE_P(iterator_class_zv) == IS_STRING) { + zend_class_entry *ce = zend_lookup_class(Z_STR_P(iterator_class_zv)); + + if (!ce) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, + "Cannot deserialize ArrayObject with iterator class '%s'; no such class exists", + ZSTR_VAL(Z_STR_P(iterator_class_zv))); + return; + } + + if (!instanceof_function(ce, spl_ce_ArrayIterator)) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, + "Cannot deserialize ArrayObject with iterator class '%s'; this class is not derived from ArrayIterator", + ZSTR_VAL(Z_STR_P(iterator_class_zv))); + return; + } + + intern->ce_get_iterator = ce; + } +} + +static bool spl_array_object_copy_user_cache_state( + void *ctx, + zend_object *new_obj, + zend_object *old_obj, + php_user_cache_safe_direct_clone_value_func_t clone_value) +{ + spl_array_object *old_intern, *new_intern; + zval new_zv, cloned_storage_zv; + bool result; + + result = false; + old_intern = spl_array_from_obj(old_obj); + new_intern = spl_array_from_obj(new_obj); + + if (clone_value == NULL) { + return false; + } + + ZVAL_OBJ(&new_zv, new_obj); + ZVAL_UNDEF(&cloned_storage_zv); + + new_intern->ar_flags &= ~SPL_ARRAY_CLONE_MASK; + new_intern->ar_flags |= old_intern->ar_flags & SPL_ARRAY_CLONE_MASK; + new_intern->ce_get_iterator = old_intern->ce_get_iterator; + + if (old_intern->ar_flags & SPL_ARRAY_IS_SELF) { + zval_ptr_dtor(&new_intern->array); + ZVAL_UNDEF(&new_intern->array); + result = true; + + goto cleanup; + } + + if (!clone_value(ctx, &cloned_storage_zv, &old_intern->array) || + (Z_TYPE(cloned_storage_zv) != IS_OBJECT && Z_TYPE(cloned_storage_zv) != IS_ARRAY) + ) { + goto cleanup; + } + + spl_array_set_array(&new_zv, new_intern, &cloned_storage_zv, old_intern->ar_flags & SPL_ARRAY_CLONE_MASK, true); + result = !EG(exception); + +cleanup: + if (Z_TYPE(cloned_storage_zv) != IS_UNDEF) { + zval_ptr_dtor(&cloned_storage_zv); + } + + return result; +} + +static bool spl_array_object_user_cache_state_has_unstorable( + void *ctx, + const zval *object, + php_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + spl_array_object *intern; + + if (value_has_unstorable == NULL) { + return false; + } + + intern = Z_SPLARRAY_P(object); + if (intern->ar_flags & SPL_ARRAY_IS_SELF) { + return false; + } + + return value_has_unstorable(ctx, &intern->array); +} + +static bool spl_array_object_serialize_user_cache_state(zval *state, const zval *object) +{ + ZVAL_UNDEF(state); + spl_array_object_user_cache_serialize_state((zval *) object, state); + if (EG(exception) || Z_TYPE_P(state) != IS_ARRAY) { + if (Z_TYPE_P(state) != IS_UNDEF) { + zval_ptr_dtor(state); + } + ZVAL_UNDEF(state); + + return false; + } + + return true; +} + +static bool spl_array_object_unserialize_user_cache_state(zval *object, zval *state) +{ + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + spl_array_object_user_cache_unserialize_state(object, Z_ARRVAL_P(state)); + + return !EG(exception); +} + +static const php_user_cache_safe_direct_handlers spl_array_user_cache_handlers = { + .copy = spl_array_object_copy_user_cache_state, + .state_has_unstorable = spl_array_object_user_cache_state_has_unstorable, + .state_serialize = spl_array_object_serialize_user_cache_state, + .state_unserialize = spl_array_object_unserialize_user_cache_state, +}; + /* {{{ */ PHP_METHOD(ArrayObject, __debugInfo) { @@ -1888,6 +2077,10 @@ PHP_MINIT_FUNCTION(spl_array) spl_ce_RecursiveArrayIterator->create_object = spl_array_object_new; spl_ce_RecursiveArrayIterator->get_iterator = spl_array_get_iterator; + php_user_cache_safe_direct_register_class(spl_ce_ArrayObject, &spl_array_user_cache_handlers); + php_user_cache_safe_direct_register_class(spl_ce_ArrayIterator, &spl_array_user_cache_handlers); + php_user_cache_safe_direct_register_class(spl_ce_RecursiveArrayIterator, &spl_array_user_cache_handlers); + return SUCCESS; } /* }}} */ diff --git a/ext/spl/spl_dllist.c b/ext/spl/spl_dllist.c index 77e8b0d4e7da..f43f14f34b1c 100644 --- a/ext/spl/spl_dllist.c +++ b/ext/spl/spl_dllist.c @@ -17,6 +17,7 @@ #endif #include "php.h" +#include "ext/user_cache/php_user_cache.h" #include "zend_interfaces.h" #include "zend_exceptions.h" #include "zend_hash.h" @@ -1101,6 +1102,147 @@ PHP_METHOD(SplDoublyLinkedList, __unserialize) { object_properties_load(&intern->std, Z_ARRVAL_P(members_zv)); } /* }}} */ +/* Serializes list state without dynamic members; used by the user-cache + * safe-direct path only. */ +static void spl_dllist_object_user_cache_serialize_state(zval *object, zval *return_value) +{ + spl_dllist_object *intern = Z_SPLDLLIST_P(object); + spl_ptr_llist_element *current = intern->llist->head; + zval tmp; + + array_init(return_value); + + /* flags */ + ZVAL_LONG(&tmp, intern->flags); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); + + /* elements */ + array_init_size(&tmp, intern->llist->count); + while (current) { + zend_hash_next_index_insert(Z_ARRVAL(tmp), ¤t->data); + Z_TRY_ADDREF(current->data); + current = current->next; + } + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); +} + +/* Strict restore for the machine-written user-cache state: validates the flags + * value and requires an empty list. Returns false on malformed data. */ +static bool spl_dllist_object_user_cache_unserialize_state(zval *object, HashTable *data) +{ + spl_dllist_object *intern = Z_SPLDLLIST_P(object); + zval *flags_zv, *storage_zv, *elem; + zend_long flags; + + flags_zv = zend_hash_index_find(data, 0); + storage_zv = zend_hash_index_find(data, 1); + if (!flags_zv || !storage_zv || + Z_TYPE_P(flags_zv) != IS_LONG || Z_TYPE_P(storage_zv) != IS_ARRAY + ) { + return false; + } + + flags = Z_LVAL_P(flags_zv); + if ((flags & ~(SPL_DLLIST_IT_MASK | SPL_DLLIST_IT_FIX)) != 0) { + return false; + } + + if (intern->llist->count != 0) { + return false; + } + + intern->flags = (int) flags; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(storage_zv), elem) { + spl_ptr_llist_push(intern->llist, elem); + } ZEND_HASH_FOREACH_END(); + + return !EG(exception); +} + +static bool spl_dllist_object_copy_user_cache_state( + void *ctx, + zend_object *new_obj, + zend_object *old_obj, + php_user_cache_safe_direct_clone_value_func_t clone_value) +{ + spl_dllist_object *old_intern, *new_intern; + spl_ptr_llist_element *current; + zval cloned_elem; + + if (clone_value == NULL) { + return false; + } + + old_intern = spl_dllist_from_obj(old_obj); + new_intern = spl_dllist_from_obj(new_obj); + if (new_intern->llist->count != 0) { + return false; + } + + new_intern->flags = old_intern->flags; + current = old_intern->llist->head; + while (current) { + ZVAL_UNDEF(&cloned_elem); + if (!clone_value(ctx, &cloned_elem, ¤t->data)) { + return false; + } + + spl_ptr_llist_push(new_intern->llist, &cloned_elem); + zval_ptr_dtor(&cloned_elem); + current = current->next; + } + + return !EG(exception); +} + +static bool spl_dllist_object_user_cache_state_has_unstorable( + void *ctx, + const zval *object, + php_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + spl_dllist_object *intern; + spl_ptr_llist_element *current; + + if (value_has_unstorable == NULL) { + return false; + } + + intern = Z_SPLDLLIST_P((zval *) object); + current = intern->llist->head; + while (current) { + if (value_has_unstorable(ctx, ¤t->data)) { + return true; + } + + current = current->next; + } + + return false; +} + +static bool spl_dllist_object_serialize_user_cache_state(zval *state, const zval *object) +{ + spl_dllist_object_user_cache_serialize_state((zval *) object, state); + + return true; +} + +static bool spl_dllist_object_unserialize_user_cache_state(zval *object, zval *state) +{ + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + return spl_dllist_object_user_cache_unserialize_state(object, Z_ARRVAL_P(state)); +} + +static const php_user_cache_safe_direct_handlers spl_dllist_user_cache_handlers = { + .copy = spl_dllist_object_copy_user_cache_state, + .state_has_unstorable = spl_dllist_object_user_cache_state_has_unstorable, + .state_serialize = spl_dllist_object_serialize_user_cache_state, + .state_unserialize = spl_dllist_object_unserialize_user_cache_state, +}; + /* {{{ Inserts a new entry before the specified $index consisting of $newval. */ PHP_METHOD(SplDoublyLinkedList, add) { @@ -1219,6 +1361,10 @@ PHP_MINIT_FUNCTION(spl_dllist) /* {{{ */ spl_ce_SplStack->create_object = spl_dllist_object_new; spl_ce_SplStack->get_iterator = spl_dllist_get_iterator; + php_user_cache_safe_direct_register_class(spl_ce_SplDoublyLinkedList, &spl_dllist_user_cache_handlers); + php_user_cache_safe_direct_register_class(spl_ce_SplQueue, &spl_dllist_user_cache_handlers); + php_user_cache_safe_direct_register_class(spl_ce_SplStack, &spl_dllist_user_cache_handlers); + return SUCCESS; } /* }}} */ diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c index d2eae52e3c0c..aa1884e66e9e 100644 --- a/ext/spl/spl_fixedarray.c +++ b/ext/spl/spl_fixedarray.c @@ -26,6 +26,7 @@ #include "spl_fixedarray.h" #include "spl_exceptions.h" #include "ext/json/php_json.h" /* For php_json_serializable_ce */ +#include "ext/user_cache/php_user_cache.h" static zend_object_handlers spl_handler_SplFixedArray; PHPAPI zend_class_entry *spl_ce_SplFixedArray; @@ -652,6 +653,132 @@ PHP_METHOD(SplFixedArray, __unserialize) } } +/* Builds an array containing only the fixed-size elements, without the object + * properties (those are handled separately by the user cache). */ +static void spl_fixedarray_object_build_user_cache_elements(spl_fixedarray_object *intern, zval *return_value) +{ + zval *current; + zend_long i; + + array_init_size(return_value, intern->array.size); + + for (i = 0; i < intern->array.size; i++) { + current = &intern->array.elements[i]; + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), current); + Z_TRY_ADDREF_P(current); + } +} + +static bool spl_fixedarray_object_copy_user_cache_state( + void *ctx, + zend_object *new_obj, + zend_object *old_obj, + php_user_cache_safe_direct_clone_value_func_t clone_value) +{ + spl_fixedarray_object *old_intern, *new_intern; + zend_long size, i; + zval cloned_elem; + + if (clone_value == NULL) { + return false; + } + + old_intern = spl_fixed_array_from_obj(old_obj); + new_intern = spl_fixed_array_from_obj(new_obj); + if (new_intern->array.size != 0) { + return false; + } + size = old_intern->array.size; + + /* spl_fixedarray_init() NULL-fills the newly allocated elements before we + * populate them one at a time below, so a clone failure partway through + * the loop leaves the not-yet-cloned tail as NULL for the destructor to + * walk, instead of the uninitialized memory init_non_empty_struct() alone + * would leave behind. */ + spl_fixedarray_init(&new_intern->array, size); + + for (i = 0; i < size; i++) { + ZVAL_UNDEF(&cloned_elem); + if (!clone_value(ctx, &cloned_elem, &old_intern->array.elements[i])) { + return false; + } + + ZVAL_COPY_DEREF(&new_intern->array.elements[i], &cloned_elem); + zval_ptr_dtor(&cloned_elem); + } + + return !EG(exception); +} + +static bool spl_fixedarray_object_user_cache_state_has_unstorable( + void *ctx, + const zval *object, + php_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + zval state_zv; + bool result; + + if (value_has_unstorable == NULL) { + return false; + } + + spl_fixedarray_object_build_user_cache_elements(Z_SPLFIXEDARRAY_P((zval *) object), &state_zv); + result = value_has_unstorable(ctx, &state_zv); + zval_ptr_dtor(&state_zv); + + return result; +} + +static bool spl_fixedarray_object_serialize_user_cache_state(zval *state, const zval *object) +{ + spl_fixedarray_object_build_user_cache_elements(Z_SPLFIXEDARRAY_P((zval *) object), state); + + return true; +} + +/* Restores the fixed-size elements from the user-cache state (elements only, no + * object properties). */ +static void spl_fixedarray_object_user_cache_unserialize_state(zval *object, HashTable *data) +{ + spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(object); + zval *elem; + zend_long size; + + if (intern->array.size != 0) { + return; + } + + size = zend_hash_num_elements(data); + spl_fixedarray_init_non_empty_struct(&intern->array, size); + if (!size) { + return; + } + + intern->array.size = 0; + ZEND_HASH_FOREACH_VAL(data, elem) { + ZVAL_COPY_DEREF(&intern->array.elements[intern->array.size], elem); + intern->array.size++; + } ZEND_HASH_FOREACH_END(); +} + +static bool spl_fixedarray_object_unserialize_user_cache_state(zval *object, zval *state) +{ + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + spl_fixedarray_object_user_cache_unserialize_state(object, Z_ARRVAL_P(state)); + + return !EG(exception); +} + +static const php_user_cache_safe_direct_handlers spl_fixedarray_user_cache_handlers = { + .copy = spl_fixedarray_object_copy_user_cache_state, + .state_has_unstorable = spl_fixedarray_object_user_cache_state_has_unstorable, + .state_serialize = spl_fixedarray_object_serialize_user_cache_state, + .state_unserialize = spl_fixedarray_object_unserialize_user_cache_state, +}; + PHP_METHOD(SplFixedArray, count) { zval *object = ZEND_THIS; @@ -960,5 +1087,7 @@ PHP_MINIT_FUNCTION(spl_fixedarray) spl_handler_SplFixedArray.get_gc = spl_fixedarray_object_get_gc; spl_handler_SplFixedArray.free_obj = spl_fixedarray_object_free_storage; + php_user_cache_safe_direct_register_class(spl_ce_SplFixedArray, &spl_fixedarray_user_cache_handlers); + return SUCCESS; } diff --git a/ext/spl/spl_heap.c b/ext/spl/spl_heap.c index 1073836aa53b..e58b4d6eb8ac 100644 --- a/ext/spl/spl_heap.c +++ b/ext/spl/spl_heap.c @@ -24,6 +24,7 @@ #include "spl_heap_arginfo.h" #include "spl_exceptions.h" #include "spl_functions.h" /* For spl_set_private_debug_info_property() */ +#include "ext/user_cache/php_user_cache.h" #define PTR_HEAP_BLOCK_SIZE 64 @@ -1259,6 +1260,265 @@ PHP_METHOD(SplHeap, __unserialize) } } +static bool spl_heap_object_is_pqueue(zend_class_entry *ce) +{ + return instanceof_function(ce, spl_ce_SplPriorityQueue); +} + +/* Grows the heap backing store to hold at least `count` elements; used by the + * user-cache safe-direct bulk restore path. */ +static void spl_heap_user_cache_ensure_capacity(spl_ptr_heap *heap, size_t count) +{ + while (count > heap->max_size) { + size_t alloc_size = heap->max_size * heap->elem_size; + /* we need to allocate more memory */ + heap->elements = safe_erealloc(heap->elements, 2, alloc_size, 0); + memset((char *) heap->elements + alloc_size, 0, alloc_size); + heap->max_size *= 2; + } +} + +static void spl_heap_object_user_cache_append_zval(spl_heap_object *intern, zval *value) +{ + zval *target; + + spl_heap_user_cache_ensure_capacity(intern->heap, intern->heap->count + 1); + target = spl_heap_elem(intern->heap, intern->heap->count); + + intern->heap->count++; + + ZVAL_COPY(target, value); +} + +static void spl_heap_object_user_cache_append_pqueue_elem(spl_heap_object *intern, zval *data, zval *priority) +{ + spl_pqueue_elem *target; + + spl_heap_user_cache_ensure_capacity(intern->heap, intern->heap->count + 1); + target = spl_heap_elem(intern->heap, intern->heap->count); + + intern->heap->count++; + + ZVAL_COPY(&target->data, data); + ZVAL_COPY(&target->priority, priority); +} + +static bool spl_heap_object_copy_user_cache_state( + void *ctx, + zend_object *new_obj, + zend_object *old_obj, + php_user_cache_safe_direct_clone_value_func_t clone_value) +{ + spl_heap_object *old_intern, *new_intern; + spl_pqueue_elem *old_elem; + zval cloned_data, cloned_priority, *zv_old_elem, cloned_elem; + size_t i; + bool is_pqueue; + + if (clone_value == NULL) { + return false; + } + + old_intern = spl_heap_from_obj(old_obj); + new_intern = spl_heap_from_obj(new_obj); + if (new_intern->heap->count != 0 || + (old_intern->heap->flags & (SPL_HEAP_CORRUPTED | SPL_HEAP_WRITE_LOCKED)) != 0 + ) { + return false; + } + + new_intern->flags = old_intern->flags; + is_pqueue = spl_heap_object_is_pqueue(old_obj->ce); + for (i = 0; i < old_intern->heap->count; i++) { + if (is_pqueue) { + old_elem = spl_heap_elem(old_intern->heap, i); + + ZVAL_UNDEF(&cloned_data); + ZVAL_UNDEF(&cloned_priority); + if (!clone_value(ctx, &cloned_data, &old_elem->data)) { + return false; + } + if (!clone_value(ctx, &cloned_priority, &old_elem->priority)) { + zval_ptr_dtor(&cloned_data); + + return false; + } + + spl_heap_object_user_cache_append_pqueue_elem(new_intern, &cloned_data, &cloned_priority); + + zval_ptr_dtor(&cloned_data); + zval_ptr_dtor(&cloned_priority); + } else { + zv_old_elem = spl_heap_elem(old_intern->heap, i); + ZVAL_UNDEF(&cloned_elem); + if (!clone_value(ctx, &cloned_elem, zv_old_elem)) { + return false; + } + + spl_heap_object_user_cache_append_zval(new_intern, &cloned_elem); + + zval_ptr_dtor(&cloned_elem); + } + } + + return !EG(exception); +} + +static bool spl_heap_object_user_cache_state_has_unstorable( + void *ctx, + const zval *object, + php_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + spl_heap_object *intern; + spl_pqueue_elem *elem; + zval *zv_elem; + size_t i; + bool is_pqueue; + + if (value_has_unstorable == NULL) { + return false; + } + + intern = Z_SPLHEAP_P((zval *) object); + if ((intern->heap->flags & (SPL_HEAP_CORRUPTED | SPL_HEAP_WRITE_LOCKED)) != 0) { + return true; + } + + is_pqueue = spl_heap_object_is_pqueue(Z_OBJCE_P(object)); + for (i = 0; i < intern->heap->count; i++) { + if (is_pqueue) { + elem = spl_heap_elem(intern->heap, i); + + if (value_has_unstorable(ctx, &elem->data) || + value_has_unstorable(ctx, &elem->priority)) { + return true; + } + } else { + zv_elem = spl_heap_elem(intern->heap, i); + + if (value_has_unstorable(ctx, zv_elem)) { + return true; + } + } + } + + return false; +} + +static bool spl_heap_object_serialize_user_cache_state(zval *state, const zval *object) +{ + spl_heap_object *intern; + spl_pqueue_elem *pqueue_elem; + zval *heap_elem; + zval elems, entry, tmp; + size_t i; + bool is_pqueue; + + intern = Z_SPLHEAP_P((zval *) object); + if ((intern->heap->flags & (SPL_HEAP_CORRUPTED | SPL_HEAP_WRITE_LOCKED)) != 0) { + return false; + } + + is_pqueue = spl_heap_object_is_pqueue(Z_OBJCE_P(object)); + + array_init(state); + + ZVAL_LONG(&tmp, intern->flags); + zend_hash_next_index_insert(Z_ARRVAL_P(state), &tmp); + + array_init_size(&elems, intern->heap->count); + for (i = 0; i < intern->heap->count; i++) { + if (is_pqueue) { + pqueue_elem = spl_heap_elem(intern->heap, i); + + array_init_size(&entry, 2); + Z_TRY_ADDREF(pqueue_elem->data); + zend_hash_index_add_new(Z_ARRVAL(entry), 0, &pqueue_elem->data); + Z_TRY_ADDREF(pqueue_elem->priority); + zend_hash_index_add_new(Z_ARRVAL(entry), 1, &pqueue_elem->priority); + zend_hash_next_index_insert(Z_ARRVAL(elems), &entry); + } else { + heap_elem = spl_heap_elem(intern->heap, i); + + zend_hash_next_index_insert(Z_ARRVAL(elems), heap_elem); + Z_TRY_ADDREF_P(heap_elem); + } + } + + zend_hash_next_index_insert(Z_ARRVAL_P(state), &elems); + + return true; +} + +static bool spl_heap_object_unserialize_user_cache_state(zval *object, zval *state) +{ + spl_heap_object *intern; + zend_long flags; + zval *flags_zv, *elements_zv, *elem, *data_zv, *priority_zv; + HashTable *data; + bool is_pqueue; + + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + data = Z_ARRVAL_P(state); + flags_zv = zend_hash_index_find(data, 0); + elements_zv = zend_hash_index_find(data, 1); + if (!flags_zv || !elements_zv || + Z_TYPE_P(flags_zv) != IS_LONG || Z_TYPE_P(elements_zv) != IS_ARRAY + ) { + return false; + } + + is_pqueue = spl_heap_object_is_pqueue(Z_OBJCE_P(object)); + flags = Z_LVAL_P(flags_zv); + if (is_pqueue) { + flags &= SPL_PQUEUE_EXTR_MASK; + if (!flags) { + return false; + } + } else if (flags != 0) { + return false; + } + + intern = Z_SPLHEAP_P(object); + if (intern->heap->count != 0 || + (intern->heap->flags & SPL_HEAP_WRITE_LOCKED) != 0 + ) { + return false; + } + + intern->flags = (int) flags; + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(elements_zv), elem) { + if (is_pqueue) { + if (Z_TYPE_P(elem) != IS_ARRAY) { + return false; + } + + data_zv = zend_hash_index_find(Z_ARRVAL_P(elem), 0); + priority_zv = zend_hash_index_find(Z_ARRVAL_P(elem), 1); + if (!data_zv || !priority_zv) { + return false; + } + + spl_heap_object_user_cache_append_pqueue_elem(intern, data_zv, priority_zv); + } else { + spl_heap_object_user_cache_append_zval(intern, elem); + } + } ZEND_HASH_FOREACH_END(); + + return !EG(exception); +} + +static const php_user_cache_safe_direct_handlers spl_heap_user_cache_handlers = { + .copy = spl_heap_object_copy_user_cache_state, + .state_has_unstorable = spl_heap_object_user_cache_state_has_unstorable, + .state_serialize = spl_heap_object_serialize_user_cache_state, + .state_unserialize = spl_heap_object_unserialize_user_cache_state, +}; + /* iterator handler table */ static const zend_object_iterator_funcs spl_heap_it_funcs = { spl_heap_it_dtor, @@ -1356,6 +1616,11 @@ PHP_MINIT_FUNCTION(spl_heap) /* {{{ */ spl_handler_SplPriorityQueue.get_gc = spl_pqueue_object_get_gc; spl_handler_SplPriorityQueue.free_obj = spl_heap_object_free_storage; + php_user_cache_safe_direct_register_class(spl_ce_SplHeap, &spl_heap_user_cache_handlers); + php_user_cache_safe_direct_register_class(spl_ce_SplMinHeap, &spl_heap_user_cache_handlers); + php_user_cache_safe_direct_register_class(spl_ce_SplMaxHeap, &spl_heap_user_cache_handlers); + php_user_cache_safe_direct_register_class(spl_ce_SplPriorityQueue, &spl_heap_user_cache_handlers); + return SUCCESS; } /* }}} */ diff --git a/ext/user_cache/config.m4 b/ext/user_cache/config.m4 new file mode 100644 index 000000000000..a7c45f7bc826 --- /dev/null +++ b/ext/user_cache/config.m4 @@ -0,0 +1,18 @@ +dnl config.m4 for extension user_cache + +PHP_NEW_EXTENSION([user_cache], m4_normalize([ + user_cache.c + user_cache_storage.c + user_cache_shared_graph.c + user_cache_entries.c + user_cache_serdes.c + user_cache_alloc_shm.c + user_cache_alloc_posix.c + ]), + [no]) + +PHP_ADD_EXTENSION_DEP(user_cache, date) +PHP_ADD_EXTENSION_DEP(user_cache, spl) +PHP_ADD_EXTENSION_DEP(user_cache, hash) + +PHP_INSTALL_HEADERS([ext/user_cache], [php_user_cache.h]) diff --git a/ext/user_cache/config.w32 b/ext/user_cache/config.w32 new file mode 100644 index 000000000000..8ed2457d1d75 --- /dev/null +++ b/ext/user_cache/config.w32 @@ -0,0 +1,16 @@ +EXTENSION("user_cache", "\ + user_cache.c \ + user_cache_storage.c \ + user_cache_shared_graph.c \ + user_cache_entries.c \ + user_cache_serdes.c \ + user_cache_alloc_shm.c \ + user_cache_alloc_posix.c", false); + +ADD_EXTENSION_DEP('user_cache', 'date'); +ADD_EXTENSION_DEP('user_cache', 'spl'); +ADD_EXTENSION_DEP('user_cache', 'hash'); + +ADD_FLAG('CFLAGS_USER_CACHE', "/I " + configure_module_dirname); + +PHP_INSTALL_HEADERS("ext/user_cache", "php_user_cache.h"); diff --git a/ext/user_cache/php_user_cache.h b/ext/user_cache/php_user_cache.h new file mode 100644 index 000000000000..e770c7a244e5 --- /dev/null +++ b/ext/user_cache/php_user_cache.h @@ -0,0 +1,125 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_USER_CACHE_H +#define PHP_USER_CACHE_H + +#include "php.h" + +/* Public API for extensions, SAPIs and embedders. */ + +/* Keep in sync with availability_enum_case(). */ +typedef enum { + PHP_USER_CACHE_REASON_NONE = 0, + PHP_USER_CACHE_REASON_DISABLED_BY_INI, + PHP_USER_CACHE_REASON_SHM_INIT_FAILED, + PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED, + PHP_USER_CACHE_REASON_BACKEND_NOT_INITIALIZED_BEFORE_WORKER, + PHP_USER_CACHE_REASON_BACKEND_INITIALIZED_AFTER_WORKER, + PHP_USER_CACHE_REASON_CGI_BOUNDARY_UNAVAILABLE, + PHP_USER_CACHE_REASON_LSAPI_BOUNDARY_UNAVAILABLE +} php_user_cache_reason; + +/* Handlers for copying native object state without invoking user code. */ +typedef bool (*php_user_cache_safe_direct_clone_value_func_t)( + void *context, + zval *dst, + zval *src +); + +typedef bool (*php_user_cache_safe_direct_value_has_unstorable_func_t)( + void *context, + const zval *value +); + +typedef bool (*php_user_cache_safe_direct_state_copy_func_t)( + void *context, + zend_object *new_object, + zend_object *old_object, + php_user_cache_safe_direct_clone_value_func_t clone_value +); + +typedef bool (*php_user_cache_safe_direct_state_has_unstorable_func_t)( + void *context, + const zval *value, + php_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable +); + +typedef bool (*php_user_cache_safe_direct_state_serialize_func_t)( + zval *state, + const zval *object +); + +typedef bool (*php_user_cache_safe_direct_state_unserialize_func_t)( + zval *object, + zval *state +); + +typedef struct { + bool prefer_request_local_prototype; + php_user_cache_safe_direct_state_copy_func_t copy; + php_user_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable; + php_user_cache_safe_direct_state_serialize_func_t state_serialize; + php_user_cache_safe_direct_state_unserialize_func_t state_unserialize; +} php_user_cache_safe_direct_handlers; + +typedef struct _php_user_cache_partition php_user_cache_partition; + +BEGIN_EXTERN_C() + +/* The handler structure is copied and may be temporary. */ +ZEND_API void php_user_cache_safe_direct_register_class( + zend_class_entry *ce, + const php_user_cache_safe_direct_handlers *handlers +); +/* SAPI and embedder integration. */ +ZEND_API void php_user_cache_opt_in(void); +ZEND_API bool php_user_cache_startup_default_context_storage(void); +ZEND_API php_user_cache_partition *php_user_cache_partition_create(const char *name); +ZEND_API bool php_user_cache_partition_startup_storage(php_user_cache_partition *partition); +ZEND_API void php_user_cache_activate_request_unavailable(php_user_cache_reason reason); +ZEND_API void php_user_cache_partition_activate(php_user_cache_partition *partition); +ZEND_API php_user_cache_partition *php_user_cache_boundary_partition_get( + const char *sapi_prefix, + const char *boundary, + void (*log_message)(const char *message) +); +/* Activate a request partition using DOCUMENT_ROOT or SERVER_NAME. */ +ZEND_API void php_user_cache_activate_boundary_partition( + const char *sapi_prefix, + const char *(*get_env)(const char *name), + void (*log_message)(const char *message), + php_user_cache_reason failure_reason +); +ZEND_API void php_user_cache_boundary_partitions_shutdown(void); + +#ifdef ZTS +/* Called by php_tsrm_startup_ex() before module startup. */ +size_t php_user_cache_globals_size(void); +void php_user_cache_globals_startup(void); +#endif + +void php_user_cache_minit(void); +void php_user_cache_mshutdown(void); +zend_result php_user_cache_rshutdown(void); +zend_result php_user_cache_post_deactivate(void); +void php_user_cache_invalidate_active(void); + +extern zend_module_entry user_cache_module_entry; + +#define phpext_user_cache_ptr &user_cache_module_entry + +END_EXTERN_C() + +#endif /* PHP_USER_CACHE_H */ diff --git a/ext/user_cache/tests/fpm/skipif.inc b/ext/user_cache/tests/fpm/skipif.inc new file mode 100644 index 000000000000..bc6813f63979 --- /dev/null +++ b/ext/user_cache/tests/fpm/skipif.inc @@ -0,0 +1,13 @@ + +--FILE-- +id = $id; + $this->name = $name; + } + + public function __serialize(): array + { + return ['id' => $this->id, 'name' => $this->name]; + } + + public function __unserialize(array $data): void + { + $this->id = $data['id']; + $this->name = $data['name']; + } + + public function info(): string + { + return $this->id . ':' . $this->name; + } +} + +$cache = UserCache\Cache::getPool('default'); +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + $gap = []; + $gap[4] = 'seed'; + unset($gap[4]); + + $payload = [ + 'props' => new SimpleUser('Alice', 30), + 'serialize' => new SerUser(7, 'Bob'), + 'internal' => new DateTimeImmutable('2026-06-15 09:30:00', new DateTimeZone('UTC')), + 'gap' => $gap, + ]; + + $shared = new stdClass(); + $shared->value = 42; + + $refs = ['value' => 1]; + $refs['alias'] =& $refs['value']; + + $cache->clear(); + var_dump($cache->store('complex', $payload)); + var_dump($cache->store('shared_pair', [$shared, $shared])); + var_dump($cache->store('refs', $refs)); + echo "seed\n"; + return; +} + +$complex = $cache->fetch('complex'); +$complex['gap'][] = 'tail'; +echo $complex['props']->name, ',', $complex['props']->age, ',', $complex['serialize']->info(), ',', $complex['internal']->format('Y-m-d H:i:s'), ',', array_key_last($complex['gap']), "\n"; + +$pair = $cache->fetch('shared_pair'); +var_dump(spl_object_id($pair[0]) === spl_object_id($pair[1])); + +$refs = $cache->fetch('refs'); +$refs['alias'] = 7; +var_dump($refs['value']); + +echo "fetch\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "Alice,30,7:Bob,2026-06-15 09:30:00,5\n" . + "bool(true)\n" . + "int(7)\n" . + "fetch" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt new file mode 100644 index 000000000000..d14f9be26237 --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt @@ -0,0 +1,119 @@ +--TEST-- +FPM: UserCache\Cache deletes entries that fail to decode +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +clear(); + var_dump($cache->store('single', new UserCacheGone())); + var_dump($cache->store('multiple', new UserCacheGone())); + var_dump($cache->store('autoload-single', new UserCacheAutoloadThrows())); + var_dump($cache->store('autoload-multiple', new UserCacheAutoloadThrows())); + var_dump($cache->has('single')); + var_dump($cache->has('multiple')); + var_dump($cache->has('autoload-single')); + var_dump($cache->has('autoload-multiple')); + return; +} + +if ($action === 'fetch') { + spl_autoload_register(function (string $class): void { + if ($class === 'UserCacheAutoloadThrows') { + throw new Exception('autoload failed'); + } + }); + + var_dump($cache->fetch('single', 'DEFAULT')); + var_dump($cache->has('single')); + var_dump($cache->fetch('autoload-single', 'DEFAULT')); + var_dump($cache->has('autoload-single')); + var_dump($cache->fetchMultiple(['multiple', 'missing', 'autoload-multiple'], 'DEFAULT')); + var_dump($cache->has('multiple')); + var_dump($cache->has('autoload-multiple')); + return; +} +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)" +); + +$tester->request(query: 'action=fetch')->expectBody( + "string(7) \"DEFAULT\"\n" . + "bool(false)\n" . + "string(7) \"DEFAULT\"\n" . + "bool(false)\n" . + "array(3) {\n" . + " [\"multiple\"]=>\n" . + " string(7) \"DEFAULT\"\n" . + " [\"missing\"]=>\n" . + " string(7) \"DEFAULT\"\n" . + " [\"autoload-multiple\"]=>\n" . + " string(7) \"DEFAULT\"\n" . + "}\n" . + "bool(false)\n" . + "bool(false)" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_lookup_cache.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_lookup_cache.phpt new file mode 100644 index 000000000000..c8acc43ad3d1 --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_lookup_cache.phpt @@ -0,0 +1,96 @@ +--TEST-- +FPM: UserCache\Cache request-local lookup cache sees same-request updates +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +fetch($key, 'MISS'); +} + +$cache = UserCache\Cache::getPool('default'); +$scenario = $_GET['scenario'] ?? 'hit_store'; +$key = 'fpm_lookup_cache_' . $scenario . '_key'; +$cache->clear(); + +if ($scenario === 'hit_store') { + $cache->store($key, 'old'); + $first = fetch_or_miss($cache, $key); + $cache->store($key, 'new'); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +if ($scenario === 'miss_store') { + $first = fetch_or_miss($cache, $key); + $cache->store($key, 'created'); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +if ($scenario === 'hit_delete') { + $cache->store($key, 'old'); + $first = fetch_or_miss($cache, $key); + $cache->delete($key); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +if ($scenario === 'hit_clear') { + $cache->store($key, 'old'); + $first = fetch_or_miss($cache, $key); + $cache->clear(); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +throw new RuntimeException('unknown scenario ' . $scenario); +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'scenario=hit_store')->expectBody("old\nnew"); +$tester->request(query: 'scenario=miss_store')->expectBody("MISS\ncreated"); +$tester->request(query: 'scenario=hit_delete')->expectBody("old\nMISS"); +$tester->request(query: 'scenario=hit_clear')->expectBody("old\nMISS"); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_persistence.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_persistence.phpt new file mode 100644 index 000000000000..9744eff7018a --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_persistence.phpt @@ -0,0 +1,108 @@ +--TEST-- +FPM: UserCache\Cache persists across requests +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +clear(); + var_dump($cache->store('counter', 41)); + var_dump($cache->storeMultiple([ + 'message' => 'hello from fpm', + 'payload' => ['a' => 1, 'b' => [2, 3]], + ])); + var_dump($cache->store('persistent_key', 'long-lived')); + echo "seed\n"; + return; +} + +if ($action === 'fetch') { + var_dump($cache->fetch('counter')); + var_dump($cache->fetch('message')); + var_dump($cache->fetch('payload')); + $cache->deleteMultiple(['message']); + echo $cache->fetch('message', 'MISS'), "\n"; + echo "fetch\n"; + return; +} + +echo $cache->fetch('persistent_key', 'MISS'), "\n"; +echo "persist\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "int(41)\n" . + "string(14) \"hello from fpm\"\n" . + "array(2) {\n" . + " [\"a\"]=>\n" . + " int(1)\n" . + " [\"b\"]=>\n" . + " array(2) {\n" . + " [0]=>\n" . + " int(2)\n" . + " [1]=>\n" . + " int(3)\n" . + " }\n" . + "}\n" . + "MISS\n" . + "fetch" +); + +sleep(1); + +$tester->request(query: 'action=persist')->expectBody( + "long-lived\n" . + "persist" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_pool_separate.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_pool_separate.phpt new file mode 100644 index 000000000000..126aa3e97cf5 --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_pool_separate.phpt @@ -0,0 +1,113 @@ +--TEST-- +FPM: UserCache\Cache is separated between pools +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +store($key, $pool . '-value'); +} + +printf( + "%s:%s:%s\n", + $pool, + $cache->fetch($key, 'MISS'), + $cache->getPoolStatus()->getPoolName() +); +PHP; + +function expectPoolState(FPM\Tester $tester, string $pool, string $expected): void +{ + $response = $tester->request( + query: 'action=fetch&pool=' . $pool, + address: '{{ADDR[' . $pool . ']}}', + ); + $body = trim((string) $response->getBody()); + if ($body !== $expected) { + throw new RuntimeException(sprintf( + 'Unexpected state for pool %s: expected %s, got %s', + $pool, + $expected, + $body + )); + } +} + +function seedPool(FPM\Tester $tester, string $pool, string $expected): void +{ + $response = $tester->request( + query: 'action=seed&pool=' . $pool, + address: '{{ADDR[' . $pool . ']}}', + ); + $body = trim((string) $response->getBody()); + if ($body !== $expected) { + throw new RuntimeException(sprintf( + 'Unexpected seed state for pool %s: expected %s, got %s', + $pool, + $expected, + $body + )); + } +} + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', + 'opcache.file_update_protection' => '0', +]); +$tester->expectLogStartNotices(); + +seedPool($tester, 'alpha', 'alpha:alpha-value:default'); +expectPoolState($tester, 'alpha', 'alpha:alpha-value:default'); +expectPoolState($tester, 'beta', 'beta:MISS:default'); + +seedPool($tester, 'beta', 'beta:beta-value:default'); +expectPoolState($tester, 'alpha', 'alpha:alpha-value:default'); +expectPoolState($tester, 'beta', 'beta:beta-value:default'); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_remember_callable_lifetime.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_remember_callable_lifetime.phpt new file mode 100644 index 000000000000..488b83439efb --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_remember_callable_lifetime.phpt @@ -0,0 +1,122 @@ +--TEST-- +FPM: UserCache\Cache remember does not retain callables across requests +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +clear(); + + $token = 'seed-token'; + $carrier = new RememberCarrier('captured-object'); + $resource = tmpfile(); + fwrite($resource, 'request-local-resource'); + + $value = $cache->remember('captured', function () use (&$token, $carrier, $resource) { + return [ + 'token' => $token, + 'carrier' => $carrier->name, + 'position' => ftell($resource), + ]; + }); + + var_dump($value); + echo "seed\n"; + return; +} + +if ($action === 'hit') { + $poison = 'second-request'; + $value = $cache->remember('captured', function () use (&$poison) { + echo "CALLBACK_RAN:$poison\n"; + throw new RuntimeException('remember callback must not run on cache hit'); + }); + + var_dump($value); + echo "hit\n"; + return; +} + +$cache->delete('captured'); +$value = $cache->remember('captured', function () { + return 'fresh-after-delete'; +}); + +var_dump($value); +echo "miss\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$expectedValue = + "array(3) {\n" . + " [\"token\"]=>\n" . + " string(10) \"seed-token\"\n" . + " [\"carrier\"]=>\n" . + " string(15) \"captured-object\"\n" . + " [\"position\"]=>\n" . + " int(22)\n" . + "}"; + +$tester->request(query: 'action=seed')->expectBody( + $expectedValue . "\n" . + "seed" +); + +$tester->request(query: 'action=hit')->expectBody( + $expectedValue . "\n" . + "hit" +); + +$tester->request(query: 'action=miss')->expectBody( + "string(18) \"fresh-after-delete\"\n" . + "miss" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_request_local_slot.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_request_local_slot.phpt new file mode 100644 index 000000000000..3fdca8abd087 --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_request_local_slot.phpt @@ -0,0 +1,95 @@ +--TEST-- +FPM: UserCache\Cache request-local slots amortize __unserialize per request only +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- + $this->value]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + $this->value = $data['value']; + } +} + +$cache = UserCache\Cache::getPool('default'); +$scenario = $_GET['scenario'] ?? 'seed'; +$key = 'fpm_request_local_slot_key'; + +if ($scenario === 'seed') { + $cache->clear(); + echo $cache->store($key, new FpmSlotMagicValue(41)) ? 'stored' : 'store-failed'; + return; +} + +if ($scenario === 'measure') { + $last = null; + for ($i = 0; $i < 5; $i++) { + $last = $cache->fetch($key); + } + echo FpmSlotMagicValue::$unserializeCalls, ':', $last->value; + return; +} + +throw new RuntimeException('unknown scenario ' . $scenario); +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'scenario=seed')->expectBody('stored'); + +/* Each fetch must run __unserialize(), including repeated fetches in one request. */ +$tester->request(query: 'scenario=measure')->expectBody('5:41'); +$tester->request(query: 'scenario=measure')->expectBody('5:41'); + +$tester->terminate(); + +$tester->close(); + +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_reset.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_reset.phpt new file mode 100644 index 000000000000..509d04745cc7 --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_reset.phpt @@ -0,0 +1,106 @@ +--TEST-- +FPM: user_cache_reset clears only the active pool partition +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +store($key, $pool . '-value'); +} elseif ($action === 'reset') { + var_dump(user_cache_reset()); +} elseif ($action === 'user-reset') { + var_dump(user_cache_reset()); +} + +printf("%s:%s\n", $pool, $cache->fetch($key, 'MISS')); +PHP; + +function requestBody(FPM\Tester $tester, string $pool, string $action): string +{ + return trim((string) $tester->request( + query: 'action=' . $action . '&pool=' . $pool, + address: '{{ADDR[' . $pool . ']}}', + )->getBody()); +} + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', + 'opcache.file_update_protection' => '0', +]); +$tester->expectLogStartNotices(); + +var_dump(requestBody($tester, 'alpha', 'seed')); +var_dump(requestBody($tester, 'beta', 'seed')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); +var_dump(requestBody($tester, 'alpha', 'reset')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); +var_dump(requestBody($tester, 'alpha', 'seed')); +var_dump(requestBody($tester, 'beta', 'seed')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); +var_dump(requestBody($tester, 'beta', 'user-reset')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +?> +--EXPECT-- +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(21) "bool(true) +alpha:MISS" +string(10) "alpha:MISS" +string(15) "beta:beta-value" +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(20) "bool(true) +beta:MISS" +string(17) "alpha:alpha-value" +string(9) "beta:MISS" +--CLEAN-- + diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_safe_direct.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_safe_direct.phpt new file mode 100644 index 000000000000..babf5dade31d --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_safe_direct.phpt @@ -0,0 +1,143 @@ +--TEST-- +FPM: UserCache\Cache safe-direct DateTime and SPL state survives requests +--EXTENSIONS-- +user_cache +spl +--SKIPIF-- + +--FILE-- +label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +class LabelIterator extends ArrayIterator +{ +} + +class TaggedCollection extends ArrayObject +{ + private string $type; + + public function __construct(array $data, string $type, string $iteratorClass) + { + parent::__construct($data, 0, $iteratorClass); + $this->type = $type; + } + + public function type(): string + { + return $this->type; + } +} + +$cache = UserCache\Cache::getPool('default'); +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + $cache->clear(); + + $event = new EventDateTime('2026-06-15 09:30:00.123456', new DateTimeZone('Europe/Paris'), 'launch', 7); + $collection = new TaggedCollection(['alpha' => 10, 'beta' => 20], 'metric', LabelIterator::class); + $fixed = SplFixedArray::fromArray(['zero', 'one', ['two']], false); + $queue = new SplQueue(); + $queue->enqueue('q1'); + $queue->enqueue('q2'); + $priorityQueue = new SplPriorityQueue(); + $priorityQueue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); + $priorityQueue->insert('low', 1); + $priorityQueue->insert('high', 10); + + var_dump($cache->store('safe_direct_payload', [ + 'event' => $event, + 'collection' => $collection, + 'fixed' => $fixed, + 'queue' => $queue, + 'priorityQueue' => $priorityQueue, + ])); + echo "seed\n"; + return; +} + +$payload = $cache->fetch('safe_direct_payload'); +$event = $payload['event']; +$collection = $payload['collection']; +$iterator = $collection->getIterator(); +$fixed = $payload['fixed']; +$queue = $payload['queue']; +$priorityQueue = $payload['priorityQueue']; + +echo $event->format('Y-m-d H:i:s.u e'), ',', $event->describe(), "\n"; +echo $collection->type(), ',', ($iterator instanceof LabelIterator ? 'LabelIterator' : get_debug_type($iterator)), ',', $collection['alpha'], ',', $collection['beta'], "\n"; +echo $fixed->getSize(), ',', $fixed[2][0], "\n"; +echo $queue->dequeue(), ',', $queue->dequeue(), "\n"; +$top = $priorityQueue->extract(); +echo $top['data'], ',', $top['priority'], "\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "2026-06-15 09:30:00.123456 Europe/Paris,launch:7\n" . + "metric,LabelIterator,10,20\n" . + "3,two\n" . + "q1,q2\n" . + "high,10" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/user_cache_add.phpt b/ext/user_cache/tests/user_cache_add.phpt new file mode 100644 index 000000000000..376c7e05bae7 --- /dev/null +++ b/ext/user_cache/tests/user_cache_add.phpt @@ -0,0 +1,38 @@ +--TEST-- +UserCache\Cache: add() stores only when the key is absent +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +add('key', 'first')); +var_dump($cache->add('key', 'second')); +var_dump($cache->fetch('key')); + +var_dump($cache->store('key', 'overwritten')); +var_dump($cache->fetch('key')); + +var_dump($cache->delete('key')); +var_dump($cache->add('key', 'after-delete')); +var_dump($cache->fetch('key')); + +var_dump($cache->add('ttl-key', 'v', 60)); +var_dump($cache->fetch('ttl-key')); +?> +--EXPECT-- +bool(true) +bool(false) +string(5) "first" +bool(true) +string(11) "overwritten" +bool(true) +bool(true) +string(12) "after-delete" +bool(true) +string(1) "v" diff --git a/ext/user_cache/tests/user_cache_apache2handler_boundary_001.phpt b/ext/user_cache/tests/user_cache_apache2handler_boundary_001.phpt new file mode 100644 index 000000000000..783f4b04d672 --- /dev/null +++ b/ext/user_cache/tests/user_cache_apache2handler_boundary_001.phpt @@ -0,0 +1,218 @@ +--TEST-- +UserCache\Cache: apache2handler partitions cache data by virtual host +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +store($key, $host . '-value'); +} + +echo $host, ':', $cache->fetch($key, 'MISS'), ':', $cache->getPoolStatus()->getPoolName(), "\n"; +PHP; + +file_put_contents($alphaRoot . '/index.php', $script); +file_put_contents($betaRoot . '/index.php', $script); +file_put_contents($root . '/php.ini', implode("\n", [ + 'user_cache.enable=1', + 'user_cache.shm_size=32M', + 'opcache.file_update_protection=0', +])); + +$port = user_cache_apache_free_port(); +$httpd = getenv('TEST_PHP_APACHE2HANDLER_HTTPD'); +$module = getenv('TEST_PHP_APACHE2HANDLER_MODULE'); +$moduleName = getenv('TEST_PHP_APACHE2HANDLER_MODULE_NAME') ?: 'php_module'; +$extraConfig = getenv('TEST_PHP_APACHE2HANDLER_EXTRA_CONFIG') ?: ''; +$ldPreload = getenv('TEST_PHP_APACHE2HANDLER_LD_PRELOAD'); +$conf = $root . '/httpd.conf'; + +file_put_contents($conf, << + ServerName alpha.local + DocumentRoot "$alphaRoot" + + Require all granted + AllowOverride None + + + SetHandler application/x-httpd-php + + + + + ServerName beta.local + DocumentRoot "$betaRoot" + + Require all granted + AllowOverride None + + + SetHandler application/x-httpd-php + + +CONF); + +try { + $env = null; + if ($ldPreload !== false && $ldPreload !== '') { + /* Sanitizer builds need the runtime preloaded by httpd, but not by the PHP test runner. */ + $env = getenv(); + $env['LD_PRELOAD'] = $ldPreload; + } + + $process = proc_open( + [$httpd, '-X', '-f', $conf], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + $root, + $env + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start httpd'); + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + user_cache_apache_wait($process, $pipes, $port); + + $checks = [ + ['alpha.local', '/index.php?action=seed', 'alpha.local:alpha.local-value:default'], + ['alpha.local', '/index.php?action=fetch', 'alpha.local:alpha.local-value:default'], + ['beta.local', '/index.php?action=fetch', 'beta.local:MISS:default'], + ['beta.local', '/index.php?action=seed', 'beta.local:beta.local-value:default'], + ['alpha.local', '/index.php?action=fetch', 'alpha.local:alpha.local-value:default'], + ['beta.local', '/index.php?action=fetch', 'beta.local:beta.local-value:default'], + ]; + + foreach ($checks as [$host, $path, $expected]) { + $actual = user_cache_apache_request($port, $host, $path); + if ($actual !== $expected) { + throw new RuntimeException("Expected $expected, got $actual"); + } + } + + echo "Done\n"; +} finally { + if (is_resource($process)) { + proc_terminate($process); + proc_close($process); + } + user_cache_apache_rm_rf($root); +} + +?> +--EXPECT-- +Done diff --git a/ext/user_cache/tests/user_cache_array_int_keys.phpt b/ext/user_cache/tests/user_cache_array_int_keys.phpt new file mode 100644 index 000000000000..c505af5c11c7 --- /dev/null +++ b/ext/user_cache/tests/user_cache_array_int_keys.phpt @@ -0,0 +1,94 @@ +--TEST-- +UserCache\Cache: negative, sparse, and post-unset integer array keys round-trip exactly +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + 'a', -1 => 'b', 0 => 'c', -100 => 'd', 7 => 'e']; +$cache->store('negative', $negative); +$fetched = $cache->fetch('negative'); +ok('negative keys value', $fetched === $negative); +ok('negative keys order', array_keys($fetched) === [-5, -1, 0, -100, 7]); + +$sparse = [0 => 'x', 5 => 'y', 10 => 'z']; +$cache->store('sparse', $sparse); +$fetched = $cache->fetch('sparse'); +ok('sparse keys value', $fetched === $sparse); +ok('sparse not list', !array_is_list($fetched)); + +$packed = [10, 20, 30, 40]; +unset($packed[1]); +$cache->store('unset', $packed); +$fetched = $cache->fetch('unset'); +ok('post-unset keys', array_keys($fetched) === [0, 2, 3]); +ok('post-unset value', $fetched === $packed); +$fetched[] = 99; +ok('append after unset uses key 4', array_key_last($fetched) === 4); + +$list = ['p', 'q', 'r']; +$cache->store('list', $list); +$fetched = $cache->fetch('list'); +ok('list is list', array_is_list($fetched)); +$fetched[] = 's'; +ok('list append index 3', array_key_last($fetched) === 3); + +$numeric = ['123' => 'a', '007' => 'b', '-3' => 'c', 'x' => 'd']; +$cache->store('numeric', $numeric); +$fetched = $cache->fetch('numeric'); +ok('numeric string key value', $fetched === $numeric); +ok('numeric string key coerced', array_key_first($fetched) === 123 && array_key_exists(-3, $fetched)); + +$limits = [PHP_INT_MAX => 'max', PHP_INT_MIN => 'min', 0 => 'zero']; +$cache->store('limits', $limits); +$fetched = $cache->fetch('limits'); +ok('int limit keys', $fetched[PHP_INT_MAX] === 'max' && $fetched[PHP_INT_MIN] === 'min' && $fetched[0] === 'zero'); + +$transition = [0 => 'a', 1 => 'b']; +$transition[100] = 'c'; +$cache->store('transition', $transition); +$fetched = $cache->fetch('transition'); +ok('packed to hashed value', $fetched === $transition); +ok('packed to hashed keys', array_keys($fetched) === [0, 1, 100]); + +$nested = [ + 'neg' => $negative, + 'sparse' => $sparse, + 'list' => ['deep' => [-2 => 'q', 4 => 'r']], + 9 => [PHP_INT_MAX => 'z'], +]; +$cache->store('nested', $nested); +ok('nested mixed keys', $cache->fetch('nested') === $nested); + +$all = [$negative, $sparse, $packed, $list, $numeric, $limits, $transition, $nested]; +$cache->store('all', $all); +ok('serialize parity', serialize($cache->fetch('all')) === serialize($all)); +?> +--EXPECT-- +negative keys value: OK +negative keys order: OK +sparse keys value: OK +sparse not list: OK +post-unset keys: OK +post-unset value: OK +append after unset uses key 4: OK +list is list: OK +list append index 3: OK +numeric string key value: OK +numeric string key coerced: OK +int limit keys: OK +packed to hashed value: OK +packed to hashed keys: OK +nested mixed keys: OK +serialize parity: OK diff --git a/ext/user_cache/tests/user_cache_atomic.phpt b/ext/user_cache/tests/user_cache_atomic.phpt new file mode 100644 index 000000000000..fa5320121497 --- /dev/null +++ b/ext/user_cache/tests/user_cache_atomic.phpt @@ -0,0 +1,82 @@ +--TEST-- +UserCache\Cache: atomic increment and decrement +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +var_dump($cache->increment('counter')); +var_dump($cache->increment('counter', 4)); +var_dump($cache->decrement('counter', 2)); +var_dump($cache->fetch('counter')); + +var_dump($cache->decrement('missing', 3)); +var_dump($cache->fetch('missing')); + +var_dump($cache->store('string', 'x')); +try { + $cache->increment('string'); +} catch (\ValueError $e) { + echo $e->getMessage(), "\n"; +} +try { + $cache->decrement('string'); +} catch (\ValueError $e) { + echo $e->getMessage(), "\n"; +} +var_dump($cache->fetch('string')); + +var_dump($cache->store('max', PHP_INT_MAX)); +try { + $cache->increment('max'); +} catch (\ArithmeticError $e) { + echo $e->getMessage(), "\n"; +} +var_dump($cache->fetch('max') === PHP_INT_MAX); + +var_dump($cache->store('min', PHP_INT_MIN)); +try { + $cache->decrement('min'); +} catch (\ArithmeticError $e) { + echo $e->getMessage(), "\n"; +} +var_dump($cache->fetch('min') === PHP_INT_MIN); + +try { + $cache->increment('counter', -1); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +try { + $cache->decrement('counter', -1); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(1) +int(5) +int(3) +int(3) +int(-3) +int(-3) +bool(true) +Increment of user cache key "string" requires the stored value to be an integer +Decrement of user cache key "string" requires the stored value to be an integer +string(1) "x" +bool(true) +Increment of user cache key "max" would exceed the range of a PHP integer +bool(true) +bool(true) +Decrement of user cache key "min" would exceed the range of a PHP integer +bool(true) +UserCache\Cache::increment(): Argument #2 ($step) must be greater than or equal to 0 +UserCache\Cache::decrement(): Argument #2 ($step) must be greater than or equal to 0 diff --git a/ext/user_cache/tests/user_cache_basic.phpt b/ext/user_cache/tests/user_cache_basic.phpt new file mode 100644 index 000000000000..925e59ae8347 --- /dev/null +++ b/ext/user_cache/tests/user_cache_basic.phpt @@ -0,0 +1,92 @@ +--TEST-- +UserCache\Cache: basic API +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +getPoolStatus(); +var_dump($poolStatus->getPoolName(), $status->getAvailability()->name, $poolStatus->getEntryCount()); + +var_dump($cache->store('array', ['x' => 1])); +var_dump($cache->fetch('array')); +var_dump($cache->has('array')); + +$fetched = $cache->fetch('array'); +$fetched['x'] = 2; +var_dump($cache->fetch('array')); + +var_dump($cache->storeMultiple(['one' => 1, 'two' => 'x'], 60)); +var_dump($cache->fetch('one')); +var_dump($cache->fetchMultiple(['one', 'missing'], 'fallback')); +var_dump($cache->fetch('missing', 'fallback')); + +var_dump($cache->delete('one')); +var_dump($cache->has('one')); + +var_dump($cache->delete('never-stored')); +var_dump($cache->deleteMultiple(['never-stored', 'also-missing'])); + +var_dump($cache->lock('lock')); +var_dump($cache->unlock('lock')); + +var_dump($cache->lock('locked-store')); +var_dump($cache->store('locked-store', 'value')); +var_dump($cache->unlock('locked-store')); + +var_dump($cache->lock('locked-delete')); +var_dump($cache->store('locked-delete', 'value')); +var_dump($cache->delete('locked-delete')); +var_dump($cache->unlock('locked-delete')); + +var_dump($cache->lock('locked-clear')); +var_dump($cache->clear()); +var_dump($cache->unlock('locked-clear')); +var_dump($cache->has('two')); +?> +--EXPECT-- +string(5) "basic" +string(9) "Available" +int(0) +bool(true) +array(1) { + ["x"]=> + int(1) +} +bool(true) +array(1) { + ["x"]=> + int(1) +} +bool(true) +int(1) +array(2) { + ["one"]=> + int(1) + ["missing"]=> + string(8) "fallback" +} +string(8) "fallback" +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) diff --git a/ext/user_cache/tests/user_cache_cgi.phpt b/ext/user_cache/tests/user_cache_cgi.phpt new file mode 100644 index 000000000000..907b756c3209 --- /dev/null +++ b/ext/user_cache/tests/user_cache_cgi.phpt @@ -0,0 +1,31 @@ +--TEST-- +CGI: UserCache\Cache is available +--CGI-- +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--ENV-- +DOCUMENT_ROOT=/tmp/php-user-cache-cgi +SERVER_NAME=php-user-user_cache.local +--FILE-- +getPoolStatus(); + +var_dump(php_sapi_name()); +var_dump($status->getAvailability()->name, $poolStatus->getEntryCount()); +var_dump($cache->store('key', 'value')); +var_dump($cache->fetch('key', 'MISS')); +?> +--EXPECT-- +string(8) "cgi-fcgi" +string(9) "Available" +int(0) +bool(true) +string(5) "value" diff --git a/ext/user_cache/tests/user_cache_cgi_boundary.phpt b/ext/user_cache/tests/user_cache_cgi_boundary.phpt new file mode 100644 index 000000000000..b81791f482d3 --- /dev/null +++ b/ext/user_cache/tests/user_cache_cgi_boundary.phpt @@ -0,0 +1,305 @@ +--TEST-- +CGI/FastCGI: UserCache\Cache partitions cache data by boundary +--EXTENSIONS-- +user_cache +--CONFLICTS-- +all +--SKIPIF-- + +--FILE-- + $value) { + $body .= user_cache_cgi_name_value($name, $value); + } + + return $body; +} + +function user_cache_cgi_read_exact($fp, int $length): string +{ + $buffer = ''; + + while (strlen($buffer) < $length && !feof($fp)) { + $chunk = fread($fp, $length - strlen($buffer)); + if ($chunk === false) { + throw new RuntimeException('Failed to read FastCGI response'); + } + if ($chunk === '') { + if (stream_get_meta_data($fp)['timed_out']) { + throw new RuntimeException('Timed out reading FastCGI response'); + } + usleep(10000); + continue; + } + $buffer .= $chunk; + } + + if (strlen($buffer) !== $length) { + throw new RuntimeException('Truncated FastCGI response'); + } + + return $buffer; +} + +function user_cache_cgi_request(int $port, string $script, string $docRoot, string $serverName, string $query): string +{ + $fp = @stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 2); + if ($fp === false) { + throw new RuntimeException($errstr); + } + stream_set_timeout($fp, 5); + + $params = [ + 'SCRIPT_FILENAME' => $script, + 'SCRIPT_NAME' => '/index.php', + 'QUERY_STRING' => $query, + 'REQUEST_METHOD' => 'GET', + 'SERVER_NAME' => $serverName, + 'DOCUMENT_ROOT' => $docRoot, + 'REQUEST_URI' => '/index.php' . ($query !== '' ? '?' . $query : ''), + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REMOTE_ADDR' => '127.0.0.1', + 'REDIRECT_STATUS' => '1', + ]; + + fwrite($fp, user_cache_cgi_record(FCGI_BEGIN_REQUEST, pack('nC6', FCGI_RESPONDER, 0, 0, 0, 0, 0, 0))); + fwrite($fp, user_cache_cgi_record(FCGI_PARAMS, user_cache_cgi_params($params))); + fwrite($fp, user_cache_cgi_record(FCGI_PARAMS, '')); + fwrite($fp, user_cache_cgi_record(FCGI_STDIN, '')); + + $stdout = ''; + $stderr = ''; + + while (!feof($fp)) { + $header = user_cache_cgi_read_exact($fp, 8); + $type = ord($header[1]); + $contentLength = unpack('n', substr($header, 4, 2))[1]; + $paddingLength = ord($header[6]); + $content = $contentLength > 0 ? user_cache_cgi_read_exact($fp, $contentLength) : ''; + if ($paddingLength > 0) { + user_cache_cgi_read_exact($fp, $paddingLength); + } + + if ($type === FCGI_STDOUT) { + $stdout .= $content; + } elseif ($type === FCGI_STDERR) { + $stderr .= $content; + } elseif ($type === FCGI_END_REQUEST) { + break; + } + } + + fclose($fp); + + if ($stderr !== '') { + throw new RuntimeException($stderr); + } + + $parts = preg_split("/\r?\n\r?\n/", $stdout, 2); + + return trim($parts[1] ?? $stdout); +} + +function user_cache_cgi_wait($process, array $pipes, int $port, string $script, string $docRoot, string $serverName): void +{ + for ($i = 0; $i < 50; $i++) { + $status = proc_get_status($process); + if (!$status['running']) { + throw new RuntimeException(stream_get_contents($pipes[2])); + } + + try { + user_cache_cgi_request($port, $script, $docRoot, $serverName, 'action=fetch'); + return; + } catch (Throwable) { + usleep(100000); + } + } + + throw new RuntimeException(stream_get_contents($pipes[2]) ?: 'php-cgi did not become ready'); +} + +function user_cache_cgi_rm_rf(string $path): void +{ + if (!file_exists($path)) { + return; + } + + if (!is_dir($path) || is_link($path)) { + unlink($path); + return; + } + + foreach (scandir($path) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + user_cache_cgi_rm_rf($path . DIRECTORY_SEPARATOR . $entry); + } + + rmdir($path); +} + +$root = sys_get_temp_dir() . '/php-user-cache-cgi-boundary-' . getmypid(); +$alphaRoot = $root . '/alpha'; +$betaRoot = $root . '/beta'; +$process = null; +$pipes = []; + +user_cache_cgi_rm_rf($root); +mkdir($alphaRoot, 0777, true); +mkdir($betaRoot, 0777, true); + +$script = <<<'PHP' +store($key, $host . '-value'); + $cache->store($complexKey, ['host' => $host, 'nested' => ['value' => 42]]); +} + +$status = UserCache\Cache::getStatus(); +$complex = $cache->fetch($complexKey, ['host' => 'MISS', 'nested' => ['value' => 'MISS']]); +echo $host, ':', $cache->fetch($key, 'MISS'), ':', $complex['host'], ':', $complex['nested']['value'], ':', ($status->getAvailability() === UserCache\CacheAvailability::Available ? 'available' : $status->getAvailability()->name), "\n"; +PHP; + +file_put_contents($alphaRoot . '/index.php', $script); +file_put_contents($betaRoot . '/index.php', $script); +file_put_contents($root . '/php.ini', implode("\n", [ + 'user_cache.enable=1', + 'user_cache.shm_size=32M', + 'opcache.file_update_protection=0', +])); + +try { + $phpCgi = user_cache_cgi_binary(); + $port = user_cache_cgi_free_port(); + $alphaScript = $alphaRoot . '/index.php'; + $betaScript = $betaRoot . '/index.php'; + + $process = proc_open( + [$phpCgi, '-c', $root, '-b', "127.0.0.1:$port"], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + $root + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start php-cgi'); + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + user_cache_cgi_wait($process, $pipes, $port, $alphaScript, $alphaRoot, 'alpha.local'); + + $checks = [ + [$alphaScript, $alphaRoot, 'alpha.local', 'action=seed', 'alpha.local:alpha.local-value:alpha.local:42:available'], + [$alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:alpha.local:42:available'], + [$betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:MISS:MISS:MISS:available'], + [$betaScript, $betaRoot, 'beta.local', 'action=seed', 'beta.local:beta.local-value:beta.local:42:available'], + [$alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:alpha.local:42:available'], + [$betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:beta.local-value:beta.local:42:available'], + ]; + + foreach ($checks as [$file, $docRoot, $serverName, $query, $expected]) { + $actual = user_cache_cgi_request($port, $file, $docRoot, $serverName, $query); + if ($actual !== $expected) { + $stderr = stream_get_contents($pipes[2]); + throw new RuntimeException("Expected $expected, got $actual" . ($stderr !== '' ? "\n$stderr" : '')); + } + } + + echo "Done\n"; +} finally { + if (is_resource($process)) { + proc_terminate($process); + proc_close($process); + } + user_cache_cgi_rm_rf($root); +} + +?> +--EXPECT-- +Done diff --git a/ext/user_cache/tests/user_cache_cgi_no_boundary.phpt b/ext/user_cache/tests/user_cache_cgi_no_boundary.phpt new file mode 100644 index 000000000000..557000863f30 --- /dev/null +++ b/ext/user_cache/tests/user_cache_cgi_no_boundary.phpt @@ -0,0 +1,32 @@ +--TEST-- +CGI: UserCache\Cache is unavailable without a cache boundary +--CGI-- +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--ENV-- +DOCUMENT_ROOT= +SERVER_NAME= +--FILE-- +getPoolStatus(); + +var_dump($status->getAvailability()->name, $poolStatus->getEntryCount(), $poolStatus->getEntryKeys(), $poolStatus->getUsedMemory()); +var_dump($cache->store('key', 'value')); +var_dump($cache->fetch('key', 'MISS')); +var_dump($cache->delete('key')); +?> +--EXPECT-- +string(31) "UnavailableByCgiFastCgiBoundary" +int(0) +array(0) { +} +int(0) +bool(false) +string(4) "MISS" +bool(true) diff --git a/ext/user_cache/tests/user_cache_cgi_value_matrix.phpt b/ext/user_cache/tests/user_cache_cgi_value_matrix.phpt new file mode 100644 index 000000000000..6508be52c907 --- /dev/null +++ b/ext/user_cache/tests/user_cache_cgi_value_matrix.phpt @@ -0,0 +1,424 @@ +--TEST-- +CGI/FastCGI: UserCache\Cache round-trips a rich value matrix across the cross-process named segment +--EXTENSIONS-- +user_cache +--CONFLICTS-- +all +--SKIPIF-- + +--FILE-- + $value) { + $body .= user_cache_cgi_name_value($name, $value); + } + + return $body; +} + +function user_cache_cgi_read_exact($fp, int $length): string +{ + $buffer = ''; + + while (strlen($buffer) < $length && !feof($fp)) { + $chunk = fread($fp, $length - strlen($buffer)); + if ($chunk === false) { + throw new RuntimeException('Failed to read FastCGI response'); + } + if ($chunk === '') { + if (stream_get_meta_data($fp)['timed_out']) { + throw new RuntimeException('Timed out reading FastCGI response'); + } + usleep(10000); + continue; + } + $buffer .= $chunk; + } + + if (strlen($buffer) !== $length) { + throw new RuntimeException('Truncated FastCGI response'); + } + + return $buffer; +} + +function user_cache_cgi_request(int $port, string $script, string $docRoot, string $serverName, string $query): string +{ + $fp = @stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 2); + if ($fp === false) { + throw new RuntimeException($errstr); + } + stream_set_timeout($fp, 5); + + $params = [ + 'SCRIPT_FILENAME' => $script, + 'SCRIPT_NAME' => '/index.php', + 'QUERY_STRING' => $query, + 'REQUEST_METHOD' => 'GET', + 'SERVER_NAME' => $serverName, + 'DOCUMENT_ROOT' => $docRoot, + 'REQUEST_URI' => '/index.php' . ($query !== '' ? '?' . $query : ''), + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REMOTE_ADDR' => '127.0.0.1', + 'REDIRECT_STATUS' => '1', + ]; + + fwrite($fp, user_cache_cgi_record(FCGI_BEGIN_REQUEST, pack('nC6', FCGI_RESPONDER, 0, 0, 0, 0, 0, 0))); + fwrite($fp, user_cache_cgi_record(FCGI_PARAMS, user_cache_cgi_params($params))); + fwrite($fp, user_cache_cgi_record(FCGI_PARAMS, '')); + fwrite($fp, user_cache_cgi_record(FCGI_STDIN, '')); + + $stdout = ''; + $stderr = ''; + + while (!feof($fp)) { + $header = user_cache_cgi_read_exact($fp, 8); + $type = ord($header[1]); + $contentLength = unpack('n', substr($header, 4, 2))[1]; + $paddingLength = ord($header[6]); + $content = $contentLength > 0 ? user_cache_cgi_read_exact($fp, $contentLength) : ''; + if ($paddingLength > 0) { + user_cache_cgi_read_exact($fp, $paddingLength); + } + + if ($type === FCGI_STDOUT) { + $stdout .= $content; + } elseif ($type === FCGI_STDERR) { + $stderr .= $content; + } elseif ($type === FCGI_END_REQUEST) { + break; + } + } + + fclose($fp); + + if ($stderr !== '') { + throw new RuntimeException($stderr); + } + + $parts = preg_split("/\r?\n\r?\n/", $stdout, 2); + + return trim($parts[1] ?? $stdout); +} + +function user_cache_cgi_wait($process, array $pipes, int $port, string $script, string $docRoot, string $serverName): void +{ + for ($i = 0; $i < 50; $i++) { + $status = proc_get_status($process); + if (!$status['running']) { + throw new RuntimeException(stream_get_contents($pipes[2])); + } + + try { + user_cache_cgi_request($port, $script, $docRoot, $serverName, 'action=warmup'); + return; + } catch (Throwable) { + usleep(100000); + } + } + + throw new RuntimeException(stream_get_contents($pipes[2]) ?: 'php-cgi did not become ready'); +} + +function user_cache_cgi_rm_rf(string $path): void +{ + if (!file_exists($path)) { + return; + } + + if (!is_dir($path) || is_link($path)) { + unlink($path); + return; + } + + foreach (scandir($path) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + user_cache_cgi_rm_rf($path . DIRECTORY_SEPARATOR . $entry); + } + + rmdir($path); +} + +$root = sys_get_temp_dir() . '/php-user-cache-cgi-matrix-' . getmypid(); +$docRoot = $root . '/site'; +$process = null; +$pipes = []; + +user_cache_cgi_rm_rf($root); +mkdir($docRoot, 0777, true); + +/* Compare values across processes through native serialization. */ +$script = <<<'PHP' + $this->state]; + } + + public function __unserialize(array $data): void + { + $this->state = $data['state']; + } +} + +class MatrixSleep +{ + public int $kept = 5; + public string $dropped = 'gone'; + + public function __sleep(): array + { + return ['kept']; + } + + public function __wakeup(): void + { + } +} + +class MatrixLegacy implements Serializable +{ + public function __construct(public int $v = 0) + { + } + + public function serialize(): string + { + return (string) $this->v; + } + + public function unserialize(string $data): void + { + $this->v = (int) $data; + } +} + +function matrix_build(): array +{ + $shared = new stdClass(); + $shared->tag = 'shared'; + + $cycleA = new stdClass(); + $cycleB = new stdClass(); + $cycleA->peer = $cycleB; + $cycleB->peer = $cycleA; + + $ref = 1; + $withRef = ['a' => &$ref, 'b' => &$ref]; + + $storage = new SplObjectStorage(); + $storageKey = new stdClass(); + $storageKey->id = 1; + $storage[$storageKey] = ['data' => [1, 2, 3]]; + + $stack = new SplStack(); + $stack->push('a'); + $stack->push('b'); + + return [ + 'scalars' => [null, true, PHP_INT_MIN, PHP_INT_MAX, 1.5, 'str', "bin\0\xff\xfe"], + 'arrays' => ['packed' => [1, 2, 3], 'hashed' => ['a' => 1, -5 => 'neg'], 'nested' => ['x' => ['y' => [1, 2]]]], + 'refs' => $withRef, + 'cycle' => $cycleA, + 'plain' => new MatrixPlain(), + 'enum' => MatrixSuit::Spades, + 'magic' => new MatrixMagic(['id' => 7, 'items' => [1, 2, 3]]), + 'sleep' => new MatrixSleep(), + 'legacy' => new MatrixLegacy(99), + 'date' => new DateTimeImmutable('2026-07-09 12:34:56.123456', new DateTimeZone('Asia/Tokyo')), + 'interval' => new DateInterval('P1Y2DT3H'), + 'period' => new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 2), + 'stack' => $stack, + 'arrayobject' => new ArrayObject(['k' => 'v', 'n' => [1, 2]]), + 'storage' => $storage, + 'sharedid' => [$shared, $shared], + ]; +} + +if ($action === 'seed') { + foreach (matrix_build() as $key => $value) { + if (!$cache->store('m_' . $key, $value)) { + echo 'seed-fail:', $key, "\n"; + exit; + } + } + echo "seeded:ok\n"; + exit; +} + +if ($action === 'warmup') { + echo "warmup:ok\n"; + exit; +} + +$mismatch = []; +foreach (matrix_build() as $key => $expected) { + $missing = new stdClass(); + $fetched = $cache->fetch('m_' . $key, $missing); + if ($fetched === $missing) { + $mismatch[] = $key . ':MISS'; + continue; + } + if (serialize($fetched) !== serialize($expected)) { + $mismatch[] = $key . ':DIFF'; + } +} + +echo $mismatch === [] ? "verify:ok\n" : ('verify:FAIL ' . implode(',', $mismatch) . "\n"); +PHP; + +file_put_contents($docRoot . '/index.php', $script); +file_put_contents($root . '/php.ini', implode("\n", [ + 'user_cache.enable=1', + 'user_cache.shm_size=32M', + 'opcache.file_update_protection=0', + 'date.timezone=UTC', + 'error_reporting=E_ALL & ~E_DEPRECATED', + 'display_errors=0', +])); + +try { + $phpCgi = user_cache_cgi_binary(); + $port = user_cache_cgi_free_port(); + $scriptFile = $docRoot . '/index.php'; + + $process = proc_open( + [$phpCgi, '-c', $root, '-b', "127.0.0.1:$port"], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + $root + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start php-cgi'); + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + user_cache_cgi_wait($process, $pipes, $port, $scriptFile, $docRoot, 'matrix.local'); + + $checks = [ + [$scriptFile, $docRoot, 'matrix.local', 'action=seed', 'seeded:ok'], + [$scriptFile, $docRoot, 'matrix.local', 'action=verify', 'verify:ok'], + ]; + + foreach ($checks as [$file, $root2, $serverName, $query, $expected]) { + $actual = user_cache_cgi_request($port, $file, $root2, $serverName, $query); + if ($actual !== $expected) { + $stderr = stream_get_contents($pipes[2]); + throw new RuntimeException("Expected $expected, got $actual" . ($stderr !== '' ? "\n$stderr" : '')); + } + } + + echo "Done\n"; +} finally { + if (is_resource($process)) { + proc_terminate($process); + proc_close($process); + } + user_cache_cgi_rm_rf($root); +} + +?> +--EXPECT-- +Done diff --git a/ext/user_cache/tests/user_cache_clear_pool_scope.phpt b/ext/user_cache/tests/user_cache_clear_pool_scope.phpt new file mode 100644 index 000000000000..9160b30112d4 --- /dev/null +++ b/ext/user_cache/tests/user_cache_clear_pool_scope.phpt @@ -0,0 +1,41 @@ +--TEST-- +UserCache\Cache: clear() only affects its own pool +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store('shared-key', 'from-a')); +var_dump($a->store('a-only', 1)); +var_dump($b->store('shared-key', 'from-b')); +var_dump($b->store('b-only', 2)); + +var_dump($a->clear()); + +var_dump($a->has('shared-key')); +var_dump($a->has('a-only')); +var_dump($a->getPoolStatus()->getEntryCount()); + +var_dump($b->fetch('shared-key')); +var_dump($b->fetch('b-only')); +var_dump($b->getPoolStatus()->getEntryCount()); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +int(0) +string(6) "from-b" +int(2) +int(2) diff --git a/ext/user_cache/tests/user_cache_cross_container_reference.phpt b/ext/user_cache/tests/user_cache_cross_container_reference.phpt new file mode 100644 index 000000000000..9af1501875ea --- /dev/null +++ b/ext/user_cache/tests/user_cache_cross_container_reference.phpt @@ -0,0 +1,91 @@ +--TEST-- +UserCache\Cache: a single reference shared across array and object containers survives +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +slot = &$value; +$graph = ['inArray' => &$value, 'holder' => $holder]; + +$cache->store('scalar', $graph); +$fetched = $cache->fetch('scalar'); +ok('initial value', $fetched['inArray'] === 5 && $fetched['holder']->slot === 5); +$fetched['inArray'] = 42; +ok('array write reaches property', $fetched['holder']->slot === 42); +$fetched['holder']->slot = 99; +ok('property write reaches array', $fetched['inArray'] === 99); + +$other = $cache->fetch('scalar'); +ok('other fetch independent', $other['inArray'] === 5); + +class RefPair +{ + public $left; + public $right; +} + +$shared = 1; +$pair = new RefPair(); +$pair->left = &$shared; +$pair->right = &$shared; +$wide = ['cell' => &$shared, 'pair' => $pair]; +$cache->store('wide', $wide); +$fetchedWide = $cache->fetch('wide'); +$fetchedWide['cell'] = 7; +ok('wide alias left', $fetchedWide['pair']->left === 7); +ok('wide alias right', $fetchedWide['pair']->right === 7); + +$object = new stdClass(); +$object->n = 0; +$objRef = $object; +$objHolder = new RefHolder(); +$objHolder->slot = &$objRef; +$objGraph = ['ref' => &$objRef, 'holder' => $objHolder]; +$cache->store('object', $objGraph); +$fetchedObj = $cache->fetch('object'); +ok('object alias identity', $fetchedObj['ref'] === $fetchedObj['holder']->slot); +$fetchedObj['ref']->n = 5; +ok('object shared mutation', $fetchedObj['holder']->slot->n === 5); + +class ArrayPropHolder +{ + public array $data = []; +} +$inner = 3; +$aph = new ArrayPropHolder(); +$aph->data['a'] = &$inner; +$aph->data['b'] = &$inner; +$cache->store('array-prop', $aph); +$fetchedAph = $cache->fetch('array-prop'); +$fetchedAph->data['a'] = 88; +ok('reference inside object-array property', $fetchedAph->data['b'] === 88); +?> +--EXPECT-- +initial value: OK +array write reaches property: OK +property write reaches array: OK +other fetch independent: OK +wide alias left: OK +wide alias right: OK +object alias identity: OK +object shared mutation: OK +reference inside object-array property: OK diff --git a/ext/user_cache/tests/user_cache_dateperiod.phpt b/ext/user_cache/tests/user_cache_dateperiod.phpt new file mode 100644 index 000000000000..a10f7ec930a7 --- /dev/null +++ b/ext/user_cache/tests/user_cache_dateperiod.phpt @@ -0,0 +1,80 @@ +--TEST-- +UserCache\Cache: DatePeriod round-trips through its serialization contract +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +--FILE-- +format('Y-m-d'); + } + return $dates; +} + +/* Compare before iteration mutates the DatePeriod cursor. */ +$byRecurrences = new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 3); +$cache->store('recurrences', $byRecurrences); +$fetched = $cache->fetch('recurrences'); +ok('recurrences instanceof', $fetched instanceof DatePeriod); +ok('recurrences parity', serialize($fetched) === serialize($byRecurrences)); +ok('recurrences dates', period_dates($fetched) === ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']); + +$byEnd = new DatePeriod( + new DateTimeImmutable('2026-03-01'), + new DateInterval('P1M'), + new DateTimeImmutable('2026-05-15'), +); +$cache->store('end', $byEnd); +$fetched = $cache->fetch('end'); +ok('end parity', serialize($fetched) === serialize($byEnd)); +ok('end dates', period_dates($fetched) === ['2026-03-01', '2026-04-01', '2026-05-01']); + +$excludeStart = new DatePeriod( + new DateTimeImmutable('2026-01-01'), + new DateInterval('P1D'), + 2, + DatePeriod::EXCLUDE_START_DATE, +); +$cache->store('exclude', $excludeStart); +$fetched = $cache->fetch('exclude'); +ok('exclude-start parity', serialize($fetched) === serialize($excludeStart)); +ok('exclude-start dates', period_dates($fetched) === ['2026-01-02', '2026-01-03']); + +$interval = new DateInterval('P2D'); +$graph = [ + 'period' => new DatePeriod(new DateTimeImmutable('2026-06-01'), $interval, 2), + 'interval' => $interval, + 'zone' => new DateTimeZone('Asia/Tokyo'), +]; +$cache->store('graph', $graph); +$fetched = $cache->fetch('graph'); +ok('graph parity', serialize($fetched) === serialize($graph)); +ok('nested components', $fetched['interval'] instanceof DateInterval && $fetched['zone']->getName() === 'Asia/Tokyo'); +ok('nested period dates', period_dates($fetched['period']) === ['2026-06-01', '2026-06-03', '2026-06-05']); +?> +--EXPECT-- +recurrences instanceof: OK +recurrences parity: OK +recurrences dates: OK +end parity: OK +end dates: OK +exclude-start parity: OK +exclude-start dates: OK +graph parity: OK +nested components: OK +nested period dates: OK diff --git a/ext/user_cache/tests/user_cache_datetime_safe_direct.phpt b/ext/user_cache/tests/user_cache_datetime_safe_direct.phpt new file mode 100644 index 000000000000..bcc2ad0bea07 --- /dev/null +++ b/ext/user_cache/tests/user_cache_datetime_safe_direct.phpt @@ -0,0 +1,353 @@ +--TEST-- +UserCache\Cache: DateTime safe-direct state is restored for subclasses +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +--FILE-- +cachedSelfHash = spl_object_hash($this); + $this->publicSelfHash = spl_object_hash($this); + } + + public function cachedSelfHash(): string + { + return $this->cachedSelfHash; + } +} + +class UserCacheMagicDateTime extends DateTime +{ + public static int $serializeCount = 0; + public static int $unserializeCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __serialize(): array + { + self::$serializeCount++; + + return parent::__serialize() + ['label' => 'serialized-' . $this->label]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->label = $data['label']; + } + + public function label(): string + { + return $this->label; + } +} + +class UserCacheMagicSelfHashDateTime extends DateTime +{ + public static int $serializeCount = 0; + public static int $unserializeCount = 0; + + private string $constructedObjectId; + + public function __construct(string $time, DateTimeZone $timezone) + { + parent::__construct($time, $timezone); + $this->constructedObjectId = spl_object_hash($this); + } + + public function __serialize(): array + { + self::$serializeCount++; + + return parent::__serialize() + ['constructedObjectId' => $this->constructedObjectId]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->constructedObjectId = spl_object_hash($this); + } + + public function constructedObjectId(): string + { + return $this->constructedObjectId; + } +} + +class UserCacheMagicFilteredDateTime extends DateTime +{ + public static int $serializeCount = 0; + public static int $unserializeCount = 0; + + public Closure $hidden; + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->hidden = static fn(): int => 1; + $this->label = $label; + } + + public function __serialize(): array + { + self::$serializeCount++; + $timezone = $this->getTimezone(); + + return [ + 'date' => $this->format('Y-m-d H:i:s.u'), + 'timezone_type' => 3, + 'timezone' => $timezone->getName(), + 'label' => $this->label, + ]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->hidden = static fn(): int => 2; + $this->label = $data['label']; + } + + public function label(): string + { + return $this->label; + } +} + +class UserCacheWakefulDateTime extends DateTime +{ + public static int $sleepCount = 0; + public static int $wakeupCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __sleep(): array + { + self::$sleepCount++; + + return ['label']; + } + + public function __wakeup(): void + { + self::$wakeupCount++; + } + + public function label(): string + { + return $this->label; + } +} + +class UserCacheTaggedTimeZone extends DateTimeZone +{ + private string $label; + + public function __construct(string $timezone, string $label) + { + parent::__construct($timezone); + $this->label = $label; + } + + public function label(): string + { + return $this->label; + } +} + +class UserCacheTaggedInterval extends DateInterval +{ + private string $label; + protected int $revision; + + public function __construct(string $duration, string $label, int $revision) + { + parent::__construct($duration); + $this->label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +$cache = UserCache\Cache::getPool('datetime-safe-direct'); + +$date = new UserCacheCarbonLikeDateTime('2024-01-02 03:04:05.123456', new DateTimeZone('Asia/Tokyo')); +$date->label = 'tokyo'; + +$model = new UserCacheDateModel( + new UserCacheCarbonLikeDateTime('2026-06-29 09:00:00.000001', new DateTimeZone('UTC')), + new UserCacheCarbonLikeDateTime('2026-06-29 09:30:00.000002', new DateTimeZone('Europe/Paris')), + new UserCacheCarbonLikeDateTime('2026-06-29 10:00:00.000003', new DateTimeZone('America/New_York')), +); + +$payload = [ + 'date' => $date, + 'model' => $model, + 'offset' => new DateTimeImmutable('2023-10-27 10:00:00.000001 +05:30'), + 'abbr' => new DateTimeImmutable('2023-10-27 10:00:00.000002 EST'), + 'timezone' => new DateTimeZone('Europe/Paris'), + 'interval' => new DateInterval('P1DT2H'), + 'taggedTimezone' => new UserCacheTaggedTimeZone('Europe/Paris', 'paris'), + 'taggedInterval' => new UserCacheTaggedInterval('P1Y2M3DT4H5M6S', 'window', 9), + 'relativeInterval' => DateInterval::createFromDateString('2 days 4 hours'), + 'selfHash' => new UserCacheSelfHashDateTime('2026-06-15 10:15:00.333333', new DateTimeZone('UTC')), + 'magicDate' => new UserCacheMagicDateTime('2026-06-15 10:45:00.654321', new DateTimeZone('UTC'), 'magic'), + 'magicSelfHash' => new UserCacheMagicSelfHashDateTime('2026-06-15 11:45:00.111111', new DateTimeZone('UTC')), + 'magicFiltered' => new UserCacheMagicFilteredDateTime('2026-06-15 12:00:00.222222', new DateTimeZone('UTC'), 'filtered'), + 'wakefulDate' => new UserCacheWakefulDateTime('2026-06-15 12:15:00.987654', new DateTimeZone('UTC'), 'wakeful'), +]; + +var_dump($cache->store('payload', $payload)); + +$fetched = $cache->fetch('payload'); + +var_dump($fetched['date'] instanceof UserCacheCarbonLikeDateTime); +var_dump($fetched['date']->label); +var_dump($fetched['date']->format('Y-m-d H:i:s.u P e')); + +var_dump($fetched['model'] instanceof UserCacheDateModel); +var_dump($fetched['model']->createdAt->format('Y-m-d H:i:s.u e')); +var_dump($fetched['model']->updatedAt->format('Y-m-d H:i:s.u e')); +var_dump($fetched['model']->deletedAt->format('Y-m-d H:i:s.u e')); + +var_dump($fetched['offset']->format('Y-m-d H:i:s.u P e')); +var_dump($fetched['abbr']->format('Y-m-d H:i:s.u P e')); +var_dump($fetched['timezone']->getName()); +var_dump($fetched['interval']->format('%d %h')); +var_dump($fetched['taggedTimezone'] instanceof UserCacheTaggedTimeZone); +var_dump($fetched['taggedTimezone']->getName()); +var_dump($fetched['taggedTimezone']->label()); +var_dump($fetched['taggedInterval'] instanceof UserCacheTaggedInterval); +var_dump($fetched['taggedInterval']->format('%y-%m-%d %h:%i:%s')); +var_dump($fetched['taggedInterval']->describe()); +var_dump($fetched['relativeInterval'] instanceof DateInterval); +var_dump($fetched['relativeInterval']->format('%d %h')); + +var_dump($fetched['selfHash'] instanceof UserCacheSelfHashDateTime); +var_dump($fetched['selfHash']->cachedSelfHash() === $payload['selfHash']->cachedSelfHash()); +var_dump($fetched['selfHash']->cachedSelfHash() !== spl_object_hash($fetched['selfHash'])); +var_dump($fetched['selfHash']->publicSelfHash === spl_object_hash($payload['selfHash'])); +var_dump($fetched['selfHash']->publicSelfHash === spl_object_hash($fetched['selfHash'])); + +var_dump($fetched['magicDate'] instanceof UserCacheMagicDateTime); +var_dump($fetched['magicDate']->format('Y-m-d H:i:s.u e')); +var_dump($fetched['magicDate']->label()); +var_dump(UserCacheMagicDateTime::$serializeCount); +var_dump(UserCacheMagicDateTime::$unserializeCount); + +var_dump($fetched['magicSelfHash'] instanceof UserCacheMagicSelfHashDateTime); +var_dump($fetched['magicSelfHash']->format('Y-m-d H:i:s.u e')); +var_dump($fetched['magicSelfHash']->constructedObjectId() === spl_object_hash($fetched['magicSelfHash'])); +var_dump($fetched['magicSelfHash']->constructedObjectId() !== spl_object_hash($payload['magicSelfHash'])); +var_dump(UserCacheMagicSelfHashDateTime::$serializeCount); +var_dump(UserCacheMagicSelfHashDateTime::$unserializeCount); + +var_dump($fetched['magicFiltered'] instanceof UserCacheMagicFilteredDateTime); +var_dump($fetched['magicFiltered']->format('Y-m-d H:i:s.u e')); +var_dump($fetched['magicFiltered']->label()); +var_dump(($fetched['magicFiltered']->hidden)()); +var_dump(UserCacheMagicFilteredDateTime::$serializeCount); +var_dump(UserCacheMagicFilteredDateTime::$unserializeCount); + +var_dump($fetched['wakefulDate'] instanceof UserCacheWakefulDateTime); +var_dump($fetched['wakefulDate']->format('Y-m-d H:i:s.u e')); +var_dump($fetched['wakefulDate']->label()); +var_dump(UserCacheWakefulDateTime::$sleepCount); +var_dump(UserCacheWakefulDateTime::$wakeupCount); +?> +--EXPECT-- +bool(true) +bool(true) +string(5) "tokyo" +string(44) "2024-01-02 03:04:05.123456 +09:00 Asia/Tokyo" +bool(true) +string(30) "2026-06-29 09:00:00.000001 UTC" +string(39) "2026-06-29 09:30:00.000002 Europe/Paris" +string(43) "2026-06-29 10:00:00.000003 America/New_York" +string(40) "2023-10-27 10:00:00.000001 +05:30 +05:30" +string(37) "2023-10-27 10:00:00.000002 -05:00 EST" +string(12) "Europe/Paris" +string(3) "1 2" +bool(true) +string(12) "Europe/Paris" +string(5) "paris" +bool(true) +string(11) "1-2-3 4:5:6" +string(8) "window:9" +bool(true) +string(3) "2 4" +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +string(30) "2026-06-15 10:45:00.654321 UTC" +string(16) "serialized-magic" +int(1) +int(1) +bool(true) +string(30) "2026-06-15 11:45:00.111111 UTC" +bool(true) +bool(true) +int(1) +int(1) +bool(true) +string(30) "2026-06-15 12:00:00.222222 UTC" +string(8) "filtered" +int(2) +int(1) +int(1) +bool(true) +string(30) "2026-06-15 12:15:00.987654 UTC" +string(7) "wakeful" +int(0) +int(0) diff --git a/ext/user_cache/tests/user_cache_default_valued_property_alias.phpt b/ext/user_cache/tests/user_cache_default_valued_property_alias.phpt new file mode 100644 index 000000000000..d790c43680e3 --- /dev/null +++ b/ext/user_cache/tests/user_cache_default_valued_property_alias.phpt @@ -0,0 +1,31 @@ +--TEST-- +UserCache\Cache: default-valued properties are stored when they carry aliases +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +left =& $object->right; + +var_dump($cache->store('object', $object)); + +$fetched = $cache->fetch('object'); +$fetched->left = 99; +var_dump($fetched->left, $fetched->right); +?> +--EXPECT-- +bool(true) +int(99) +int(99) diff --git a/ext/user_cache/tests/user_cache_disabled.phpt b/ext/user_cache/tests/user_cache_disabled.phpt new file mode 100644 index 000000000000..6621e4762c6f --- /dev/null +++ b/ext/user_cache/tests/user_cache_disabled.phpt @@ -0,0 +1,61 @@ +--TEST-- +UserCache\Cache: disabled by INI size +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=0 +--FILE-- +getPoolStatus(); +var_dump($poolStatus->getPoolName(), $status->getAvailability()->name, $poolStatus->getEntryCount(), $poolStatus->getEntryKeys(), $poolStatus->getUsedMemory()); +var_dump($status->getConfiguredMemory()); +var_dump($cache->store('key', 1)); +var_dump($cache->add('key', 1)); +var_dump($cache->storeMultiple(['key' => 1])); +var_dump($cache->increment('key')); +var_dump($cache->decrement('key')); +var_dump($cache->has('key')); +var_dump($cache->fetch('key', 'default')); +var_dump($cache->fetchMultiple(['key', 'other'], 'default')); +var_dump($cache->delete('key')); +var_dump($cache->deleteMultiple(['key', 'other'])); +var_dump($cache->clear()); +var_dump($cache->lock('key')); +var_dump($cache->unlock('key')); +var_dump($cache->remember('key', fn() => 42)); + +var_dump($status->getAvailability()); +?> +--EXPECT-- +string(3) "off" +string(13) "DisabledByIni" +int(0) +array(0) { +} +int(0) +int(0) +bool(false) +bool(false) +bool(false) +NULL +NULL +bool(false) +string(7) "default" +array(2) { + ["key"]=> + string(7) "default" + ["other"]=> + string(7) "default" +} +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +int(42) +enum(UserCache\CacheAvailability::DisabledByIni) diff --git a/ext/user_cache/tests/user_cache_empty_array_roundtrip.phpt b/ext/user_cache/tests/user_cache_empty_array_roundtrip.phpt new file mode 100644 index 000000000000..150427e449d0 --- /dev/null +++ b/ext/user_cache/tests/user_cache_empty_array_roundtrip.phpt @@ -0,0 +1,98 @@ +--TEST-- +UserCache\Cache: empty arrays round-trip from the lazy (unallocated) table +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store('root', [])); +var_dump($cache->fetch('root')); + +var_dump($cache->store('nested', ['a' => [], 'b' => [[], []], 'c' => ['x' => 1]])); +var_dump($cache->fetch('nested')); + +$shared = []; +$wrap = ['first' => &$shared, 'second' => &$shared]; +var_dump($cache->store('shared', $wrap)); +$fetched = $cache->fetch('shared'); +$fetched['first'][] = 'linked'; +var_dump($fetched['second']); + +$root = new UserCacheEmptyArrayNode('root'); +$leaf = new UserCacheEmptyArrayNode('leaf'); +$leaf->parent = $root; +$root->children[] = $leaf; +var_dump($cache->store('graph', $root)); +$graph = $cache->fetch('graph'); +var_dump($graph->children[0]->children); +var_dump($graph->children[0]->parent === $graph); + +$empty = $cache->fetch('root'); +$empty[] = 42; +var_dump($empty); + +unset($shared, $wrap, $fetched, $root, $leaf, $graph, $empty); +gc_collect_cycles(); +?> +--EXPECT-- +bool(true) +array(0) { +} +bool(true) +array(3) { + ["a"]=> + array(0) { + } + ["b"]=> + array(2) { + [0]=> + array(0) { + } + [1]=> + array(0) { + } + } + ["c"]=> + array(1) { + ["x"]=> + int(1) + } +} +bool(true) +array(1) { + [0]=> + string(6) "linked" +} +bool(true) +array(0) { +} +bool(true) +array(1) { + [0]=> + int(42) +} diff --git a/ext/user_cache/tests/user_cache_expired_read_reclaim.phpt b/ext/user_cache/tests/user_cache_expired_read_reclaim.phpt new file mode 100644 index 000000000000..568b3c851c55 --- /dev/null +++ b/ext/user_cache/tests/user_cache_expired_read_reclaim.phpt @@ -0,0 +1,44 @@ +--TEST-- +UserCache\Cache: expired entries observed by reads are reclaimed by a later mutation +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +; Capacity fits inside one bounded expunge window, so a single mutation +; reclaims every observed expired entry. +user_cache.shm_size=1M +--FILE-- +store('expiring-' . $i, str_repeat('v', 64), 1); +} +$cache->store('persistent', 'kept'); + +var_dump($cache->getPoolStatus()->getEntryCount()); + +sleep(2); + +/* Cross the observation threshold before triggering expiry cleanup. */ +for ($i = 0; $i < 80; $i++) { + if ($cache->fetch('expiring-' . $i, null) !== null) { + exit('unexpected hit'); + } +} + +$cache->store('trigger', 'mutation'); + +var_dump($cache->getPoolStatus()->getEntryCount()); +var_dump($cache->fetch('persistent')); +var_dump($cache->fetch('trigger')); +?> +--EXPECT-- +int(81) +int(2) +string(4) "kept" +string(8) "mutation" diff --git a/ext/user_cache/tests/user_cache_fetch_multiple_int_keys.phpt b/ext/user_cache/tests/user_cache_fetch_multiple_int_keys.phpt new file mode 100644 index 000000000000..2080f7142723 --- /dev/null +++ b/ext/user_cache/tests/user_cache_fetch_multiple_int_keys.phpt @@ -0,0 +1,39 @@ +--TEST-- +UserCache\Cache: fetchMultiple normalizes integer keys +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +storeMultiple([ + 123 => 'int-key', + '456' => 'numeric-string-key', +])); + +$result = $cache->fetchMultiple([123, '456', 789], 'default'); +var_dump($result); +var_dump(array_key_exists(123, $result)); +var_dump(array_key_exists(456, $result)); +var_dump($result[123], $result[456], $result[789]); +?> +--EXPECT-- +bool(true) +array(3) { + [123]=> + string(7) "int-key" + [456]=> + string(18) "numeric-string-key" + [789]=> + string(7) "default" +} +bool(true) +bool(true) +string(7) "int-key" +string(18) "numeric-string-key" +string(7) "default" diff --git a/ext/user_cache/tests/user_cache_float_precision.phpt b/ext/user_cache/tests/user_cache_float_precision.phpt new file mode 100644 index 000000000000..52ce6ebce393 --- /dev/null +++ b/ext/user_cache/tests/user_cache_float_precision.phpt @@ -0,0 +1,83 @@ +--TEST-- +UserCache\Cache: floats round-trip bit-exactly (negative zero, subnormals, precision) +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + 0.0, + 'negative zero' => -0.0, + 'epsilon' => PHP_FLOAT_EPSILON, + 'float min' => PHP_FLOAT_MIN, + 'float max' => PHP_FLOAT_MAX, + 'smallest subnormal' => 4.9e-324, + 'one third' => 1 / 3, + 'point one plus point two' => 0.1 + 0.2, + 'large' => 1.7e308, + 'small negative' => -2.2250738585072014e-308, +]; + +foreach ($floats as $label => $value) { + $cache->store('f', $value); + $fetched = $cache->fetch('f'); + ok($label, pack('d', $fetched) === pack('d', $value)); +} + +$cache->store('negzero', -0.0); +ok('negative zero sign bit', fdiv(1.0, $cache->fetch('negzero')) === -INF); + +foreach (['inf' => INF, '-inf' => -INF] as $label => $value) { + $cache->store('nf', $value); + ok($label, $cache->fetch('nf') === $value); +} +$cache->store('nan', NAN); +ok('nan', is_nan($cache->fetch('nan'))); + +$graph = [ + 'array' => [-0.0, PHP_FLOAT_MIN, 4.9e-324], + 'object' => (object) ['ratio' => 1 / 3, 'neg' => -0.0], +]; +$cache->store('graph', $graph); +$fetched = $cache->fetch('graph'); +$arrayOk = true; +foreach ($graph['array'] as $index => $value) { + $arrayOk = $arrayOk && pack('d', $fetched['array'][$index]) === pack('d', $value); +} +ok('floats in array', $arrayOk); +ok('floats in object', pack('d', $fetched['object']->ratio) === pack('d', 1 / 3) + && pack('d', $fetched['object']->neg) === pack('d', -0.0)); + +$cache->store('all', $floats); +ok('serialize parity', serialize($cache->fetch('all')) === serialize($floats)); +?> +--EXPECT-- +positive zero: OK +negative zero: OK +epsilon: OK +float min: OK +float max: OK +smallest subnormal: OK +one third: OK +point one plus point two: OK +large: OK +small negative: OK +negative zero sign bit: OK +inf: OK +-inf: OK +nan: OK +floats in array: OK +floats in object: OK +serialize parity: OK diff --git a/ext/user_cache/tests/user_cache_fork_entry_lock.phpt b/ext/user_cache/tests/user_cache_fork_entry_lock.phpt new file mode 100644 index 000000000000..91fbd6d1f405 --- /dev/null +++ b/ext/user_cache/tests/user_cache_fork_entry_lock.phpt @@ -0,0 +1,88 @@ +--TEST-- +UserCache\Cache: forked child does not inherit parent entry lock ownership +--EXTENSIONS-- +user_cache +pcntl +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +$shared = range(1, 4); +$value = [$shared, $shared, ['nested' => $shared]]; + +var_dump($cache->store('graph', $value)); +var_dump($cache->lock('lock')); + +$pid = pcntl_fork(); +if ($pid === 0) { + echo "child unlock\n"; + var_dump($cache->unlock('lock')); + echo "child lock\n"; + var_dump($cache->lock('lock', 1)); + echo "child fetch\n"; + var_dump($cache->fetch('graph')); + exit(0); +} + +if ($pid > 0) { + pcntl_waitpid($pid, $status); + echo "parent unlock\n"; + var_dump($cache->unlock('lock')); +} else { + echo "pcntl_fork() failed\n"; +} +?> +--EXPECT-- +bool(true) +bool(true) +child unlock +bool(false) +child lock +bool(false) +child fetch +array(3) { + [0]=> + array(4) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + [3]=> + int(4) + } + [1]=> + array(4) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + [3]=> + int(4) + } + [2]=> + array(1) { + ["nested"]=> + array(4) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + [3]=> + int(4) + } + } +} +parent unlock +bool(true) diff --git a/ext/user_cache/tests/user_cache_fuzz_roundtrip.phpt b/ext/user_cache/tests/user_cache_fuzz_roundtrip.phpt new file mode 100644 index 000000000000..99f379f9c062 --- /dev/null +++ b/ext/user_cache/tests/user_cache_fuzz_roundtrip.phpt @@ -0,0 +1,438 @@ +--TEST-- +UserCache\Cache: deterministic store/fetch round-trip fuzz cases +--EXTENSIONS-- +user_cache +spl +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- + 'serialized-' . $this->name, 'values' => []]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + $this->name = $data['name']; + $this->values = $data['values']; + } +} + +class UCFuzzDateTime extends DateTimeImmutable +{ + public function __construct( + string $time, + DateTimeZone $timezone, + public string $label, + ) { + parent::__construct($time, $timezone); + } +} + +class UCFuzzDateModel +{ + public function __construct( + public int $id, + public UCFuzzDateTime $createdAt, + public UCFuzzDateTime $updatedAt, + public UCFuzzDateTime $publishedAt, + public array $attributes, + ) { + } +} + +class UCFuzzArrayObject extends ArrayObject +{ + public function __construct( + array $storage, + public string $name, + public int $revision, + ) { + parent::__construct($storage, ArrayObject::ARRAY_AS_PROPS); + } +} + +function uc_fuzz_scalar(int $seed): mixed +{ + return match ($seed % 6) { + 0 => null, + 1 => $seed % 2 === 0, + 2 => $seed * 17 - 500, + 3 => $seed / 10.0, + 4 => 'str-' . $seed . '-' . str_repeat(chr(65 + ($seed % 26)), ($seed % 5) + 1), + default => ['seed' => $seed, 'enabled' => $seed % 3 === 0], + }; +} + +function uc_fuzz_array(int $seed, int $depth): array +{ + $result = []; + $count = ($seed % 4) + 1; + + for ($i = 0; $i < $count; $i++) { + $key = $i % 2 === 0 ? 'k' . $seed . '_' . $i : $i; + if ($depth >= 3) { + $result[$key] = uc_fuzz_scalar($seed + $i); + continue; + } + + $result[$key] = match (($seed + $i) % 5) { + 0 => uc_fuzz_scalar($seed + $i), + 1 => uc_fuzz_array($seed + $i + 1, $depth + 1), + 2 => new UCFuzzLeaf('leaf-' . $seed . '-' . $i, $seed + $i, ['hot' => $i % 2 === 0]), + 3 => new DateTimeImmutable(sprintf('2026-06-%02d 10:%02d:00.%06d', ($seed % 20) + 1, $i, $seed), new DateTimeZone('UTC')), + default => UCFuzzSuit::Hearts, + }; + } + + return $result; +} + +function uc_fuzz_date(int $seed, string $timezone): UCFuzzDateTime +{ + return new UCFuzzDateTime( + sprintf('2026-06-%02d %02d:%02d:%02d.%06d', ($seed % 20) + 1, $seed % 24, $seed % 60, ($seed * 3) % 60, $seed), + new DateTimeZone($timezone), + 'label-' . $seed, + ); +} + +function uc_fuzz_build_payload(int $seed): mixed +{ + switch ($seed % 16) { + case 0: + return uc_fuzz_scalar($seed); + case 1: + return uc_fuzz_array($seed, 0); + case 2: + return (object) [ + 'name' => 'std-' . $seed, + 'items' => uc_fuzz_array($seed + 10, 0), + ]; + case 3: + return new UCFuzzLeaf('leaf-' . $seed, $seed * 3, ['a' => true, 'b' => $seed % 2 === 0]); + case 4: + $shared = new UCFuzzLeaf('shared-' . $seed, $seed, ['shared' => true]); + return ['left' => $shared, 'right' => $shared, 'list' => [$shared]]; + case 5: + return [ + 'created' => uc_fuzz_date($seed, 'Asia/Tokyo'), + 'updated' => new DateTime('2026-06-29 12:34:56.123456', new DateTimeZone('Europe/Paris')), + 'timezone' => new DateTimeZone($seed % 2 === 0 ? 'UTC' : 'America/New_York'), + 'interval' => new DateInterval('P1DT2H'), + ]; + case 6: + return new UCFuzzArrayObject([ + 'rows' => uc_fuzz_array($seed + 20, 0), + 'owner' => new UCFuzzLeaf('owner-' . $seed, $seed, []), + ], 'array-object-' . $seed, $seed); + case 7: + return new ArrayIterator(uc_fuzz_array($seed + 30, 0)); + case 8: + return new RecursiveArrayIterator(['branch' => ['leaf' => uc_fuzz_array($seed + 40, 1)]]); + case 9: + $fixed = new SplFixedArray(3); + $fixed[0] = 'fixed-' . $seed; + $fixed[1] = uc_fuzz_array($seed + 50, 1); + $fixed[2] = new UCFuzzLeaf('fixed-leaf-' . $seed, $seed, []); + return $fixed; + case 10: + $list = new SplDoublyLinkedList(); + $list->setIteratorMode(SplDoublyLinkedList::IT_MODE_FIFO); + $list->push('list-' . $seed); + $list->push(new UCFuzzLeaf('list-leaf-' . $seed, $seed, [])); + $queue = new SplQueue(); + $queue->enqueue('queue-' . $seed); + $queue->enqueue($seed); + $stack = new SplStack(); + $stack->push('stack-' . $seed); + $stack->push($seed); + return ['list' => $list, 'queue' => $queue, 'stack' => $stack]; + case 11: + $min = new SplMinHeap(); + $max = new SplMaxHeap(); + $pq = new SplPriorityQueue(); + $pq->setExtractFlags(SplPriorityQueue::EXTR_BOTH); + for ($i = 0; $i < 4; $i++) { + $min->insert($seed + $i); + $max->insert($seed + $i); + $pq->insert('pq-' . $seed . '-' . $i, $seed + $i); + } + return ['min' => $min, 'max' => $max, 'pq' => $pq]; + case 12: + return [ + 'enum' => $seed % 2 === 0 ? UCFuzzSuit::Hearts : UCFuzzSuit::Spades, + 'magic' => new UCFuzzMagicObject('magic-' . $seed, uc_fuzz_array($seed + 60, 0)), + ]; + case 13: + return new UCFuzzDateModel( + 1000 + $seed, + uc_fuzz_date($seed, 'UTC'), + uc_fuzz_date($seed + 1, 'Europe/Paris'), + uc_fuzz_date($seed + 2, 'America/Los_Angeles'), + ['status' => 'published', 'score' => $seed * 7], + ); + case 14: + $node = new UCFuzzNode('node-' . $seed, $seed); + $node->payload = uc_fuzz_array($seed + 70, 0); + $node->child = new UCFuzzNode('child-' . $seed, $seed + 1); + $node->child->payload = new UCFuzzLeaf('nested-' . $seed, $seed, []); + return $node; + default: + $root = new UCFuzzNode('cycle-root-' . $seed, $seed); + $peer = new UCFuzzNode('cycle-peer-' . $seed, $seed + 1); + $root->child = $peer; + $peer->child = $root; + $root->payload = ['peer' => $peer]; + $peer->payload = ['root' => $root]; + return $root; + } +} + +function uc_fuzz_object_properties(object $value): array +{ + $properties = []; + foreach ((array) $value as $name => $propertyValue) { + $properties[str_replace("\0", '\\0', (string) $name)] = $propertyValue; + } + + ksort($properties); + return $properties; +} + +function uc_fuzz_normalize_iterable(iterable $values, SplObjectStorage $seen, int $depth): array +{ + $result = []; + foreach ($values as $key => $value) { + $result[(string) $key] = uc_fuzz_normalize($value, $seen, $depth + 1); + } + + return $result; +} + +function uc_fuzz_normalize_heap(SplHeap $heap, SplObjectStorage $seen, int $depth): array +{ + $copy = clone $heap; + $values = []; + + while (!$copy->isEmpty()) { + $values[] = uc_fuzz_normalize($copy->extract(), $seen, $depth + 1); + } + + return $values; +} + +function uc_fuzz_normalize(mixed $value, SplObjectStorage $seen, int $depth = 0): mixed +{ + if ($depth > 32) { + return '*depth*'; + } + + if ($value === null || is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + return $value; + } + + if (is_array($value)) { + $result = []; + foreach ($value as $key => $arrayValue) { + $result[(string) $key] = uc_fuzz_normalize($arrayValue, $seen, $depth + 1); + } + + return ['array' => $result]; + } + + if ($value instanceof UnitEnum) { + return [ + 'enum' => get_class($value), + 'name' => $value->name, + 'value' => $value instanceof BackedEnum ? $value->value : null, + ]; + } + + if (!$value instanceof object) { + return ['unknown' => get_debug_type($value)]; + } + + if ($seen->contains($value)) { + return ['ref' => $seen[$value]]; + } + + $seen[$value] = $seen->count(); + + if ($value instanceof DateTimeInterface) { + return [ + 'class' => get_class($value), + 'datetime' => $value->format('Y-m-d H:i:s.u P e'), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof DateTimeZone) { + return [ + 'class' => get_class($value), + 'timezone' => $value->getName(), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof DateInterval) { + return [ + 'class' => get_class($value), + 'interval' => $value->format('%r%y-%m-%d %h:%i:%s.%f'), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof ArrayObject) { + return [ + 'class' => get_class($value), + 'flags' => $value->getFlags(), + 'iterator' => $value->getIteratorClass(), + 'storage' => uc_fuzz_normalize($value->getArrayCopy(), $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof ArrayIterator) { + return [ + 'class' => get_class($value), + 'storage' => uc_fuzz_normalize($value->getArrayCopy(), $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplFixedArray) { + return [ + 'class' => get_class($value), + 'size' => $value->getSize(), + 'storage' => uc_fuzz_normalize($value->toArray(), $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplPriorityQueue) { + return [ + 'class' => get_class($value), + 'flags' => $value->getExtractFlags(), + 'values' => uc_fuzz_normalize_heap($value, $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplHeap) { + return [ + 'class' => get_class($value), + 'values' => uc_fuzz_normalize_heap($value, $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplDoublyLinkedList) { + return [ + 'class' => get_class($value), + 'mode' => $value->getIteratorMode(), + 'values' => uc_fuzz_normalize_iterable($value, $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + return [ + 'class' => get_class($value), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; +} + +function uc_fuzz_digest(mixed $value): mixed +{ + return uc_fuzz_normalize($value, new SplObjectStorage()); +} + +$count = 128; +for ($i = 0; $i < $count; $i++) { + $key = 'case_' . $i; + $payload = uc_fuzz_build_payload($i); + $before = uc_fuzz_digest($payload); + + if (!$cache->store($key, $payload)) { + throw new RuntimeException('store failed for fuzz case ' . $i); + } + + $missing = new stdClass(); + $fetched = $cache->fetch($key, $missing); + if ($fetched === $missing) { + throw new RuntimeException('fetch missed fuzz case ' . $i); + } + + $after = uc_fuzz_digest($fetched); + if ($before !== $after) { + echo "mismatch case ", $i, "\n"; + var_dump($before); + var_dump($after); + exit(1); + } +} + +echo "fuzz cases: ", $count, "\n"; +echo "magic calls: "; +var_dump([UCFuzzMagicObject::$serializeCalls, UCFuzzMagicObject::$unserializeCalls]); + +unset($payload, $before, $missing, $fetched, $after); +gc_collect_cycles(); +?> +--EXPECT-- +fuzz cases: 128 +magic calls: array(2) { + [0]=> + int(8) + [1]=> + int(8) +} diff --git a/ext/user_cache/tests/user_cache_info_counters.phpt b/ext/user_cache/tests/user_cache_info_counters.phpt new file mode 100644 index 000000000000..8a00282620a0 --- /dev/null +++ b/ext/user_cache/tests/user_cache_info_counters.phpt @@ -0,0 +1,36 @@ +--TEST-- +UserCache\Cache: expunge and store-failure counters are exposed +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=2M +--FILE-- +getExpungeCount()); +var_dump($info->getStoreFailureCount()); + +/* Fill until pressure resets the partition. */ +$payload = str_repeat('x', 64 * 1024); +for ($i = 0; $i < 64; $i++) { + $cache->store('big-' . $i, $payload . $i); +} + +$info = UserCache\Cache::getStatus(); +var_dump($info->getExpungeCount() >= 1); + +var_dump($cache->store('too-big', str_repeat('y', 4 * 1024 * 1024))); +$info = UserCache\Cache::getStatus(); +var_dump($info->getStoreFailureCount() >= 1); +?> +--EXPECT-- +int(0) +int(0) +bool(true) +bool(false) +bool(true) diff --git a/ext/user_cache/tests/user_cache_info_reset.phpt b/ext/user_cache/tests/user_cache_info_reset.phpt new file mode 100644 index 000000000000..79729ff5e93f --- /dev/null +++ b/ext/user_cache/tests/user_cache_info_reset.phpt @@ -0,0 +1,73 @@ +--TEST-- +UserCache\Cache: info statistics and user cache reset +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); +$other->clear(); + +$initial = UserCache\Cache::getStatus(); +$initialPool = $cache->getPoolStatus(); +var_dump($initialPool->getPoolName()); +var_dump($initial->getAvailability()->name); +var_dump($initial->getConfiguredMemory() === 16 * 1024 * 1024); +var_dump($initial->getSharedMemorySize() > 0); +var_dump($initial->getEntryCount()); +var_dump($initialPool->getEntryCount()); + +var_dump($cache->store('key', ['value' => 1])); +var_dump($other->store('key', 'other')); + +$afterStore = UserCache\Cache::getStatus(); +$afterStorePool = $cache->getPoolStatus(); +$afterStoreOther = $other->getPoolStatus(); +var_dump($afterStore->getEntryCount() >= 2); +var_dump($afterStorePool->getEntryCount()); +var_dump($afterStoreOther->getEntryCount()); +var_dump($afterStore->getEntryCapacity() > 0); +var_dump($afterStore->getUsedMemory() > 0); +var_dump($afterStore->getFreeMemory() > 0); +var_dump($afterStore->getWastedMemory() >= 0); +var_dump($afterStore->getTombstoneCount() >= 0); +var_dump($afterStorePool->getUsedMemory() > 0); + +var_dump(user_cache_reset()); +var_dump($cache->fetch('key', 'missing')); +var_dump($other->fetch('key', 'missing')); + +$afterReset = UserCache\Cache::getStatus(); +$afterResetPool = $cache->getPoolStatus(); +var_dump($afterReset->getEntryCount()); +var_dump($afterResetPool->getEntryCount()); +?> +--EXPECT-- +string(12) "info-reset-a" +string(9) "Available" +bool(true) +bool(true) +int(0) +int(0) +bool(true) +bool(true) +bool(true) +int(1) +int(1) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(7) "missing" +string(7) "missing" +int(0) +int(0) diff --git a/ext/user_cache/tests/user_cache_invalid_keys.phpt b/ext/user_cache/tests/user_cache_invalid_keys.phpt new file mode 100644 index 000000000000..95b8ea0dac5a --- /dev/null +++ b/ext/user_cache/tests/user_cache_invalid_keys.phpt @@ -0,0 +1,44 @@ +--TEST-- +UserCache\Cache: invalid cache keys are rejected consistently +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +getMessage()); + echo $label, ': ', $e::class, ': ', $message, "\n"; + } +} + +show_error('store empty', fn() => $cache->store('', 1)); +show_error('fetch delimiter', fn() => $cache->fetch("bad\x1fkey")); +show_error('has empty', fn() => $cache->has('')); +show_error('lock delimiter', fn() => $cache->lock("bad\x1fkey")); +show_error('fetchMultiple empty', fn() => $cache->fetchMultiple(['ok', ''])); +show_error('fetchMultiple type', fn() => $cache->fetchMultiple(['ok', 1.5])); +show_error('deleteMultiple type', fn() => $cache->deleteMultiple(['ok', new stdClass()])); +show_error('storeMultiple empty key', fn() => $cache->storeMultiple(['' => 1])); +show_error('storeMultiple delimiter key', fn() => $cache->storeMultiple(["bad\x1fkey" => 1])); +?> +--EXPECT-- +store empty: ValueError: Argument #1 ($key) must be a non-empty string +fetch delimiter: ValueError: Argument #1 ($key) must not contain the user-cache key delimiter 0x1F +has empty: ValueError: Argument #1 ($key) must be a non-empty string +lock delimiter: ValueError: Argument #1 ($key) must not contain the user-cache key delimiter 0x1F +fetchMultiple empty: ValueError: Argument #1 ($keys) must contain only non-empty string or int cache keys that do not contain 0x1F +fetchMultiple type: ValueError: Argument #1 ($keys) must contain only non-empty string or int cache keys that do not contain 0x1F +deleteMultiple type: ValueError: Argument #1 ($keys) must contain only non-empty string or int cache keys that do not contain 0x1F +storeMultiple empty key: ValueError: Argument #1 ($values) must be an array with non-empty string or int keys that do not contain 0x1F +storeMultiple delimiter key: ValueError: Argument #1 ($values) must be an array with non-empty string or int keys that do not contain 0x1F diff --git a/ext/user_cache/tests/user_cache_litespeed_boundary.phpt b/ext/user_cache/tests/user_cache_litespeed_boundary.phpt new file mode 100644 index 000000000000..226ad2d4bdd4 --- /dev/null +++ b/ext/user_cache/tests/user_cache_litespeed_boundary.phpt @@ -0,0 +1,372 @@ +--TEST-- +UserCache\Cache: litespeed partitions cache data by boundary +--EXTENSIONS-- +user_cache +--CONFLICTS-- +all +--SKIPIF-- + +--FILE-- + $value) { + $keyString = $key . "\0"; + $valueString = $value . "\0"; + $offsets[$key] = $baseOffset + strlen($block) + 4 + strlen($keyString); + $block .= pack('n', strlen($keyString)); + $block .= pack('n', strlen($valueString)); + $block .= $keyString; + $block .= $valueString; + } + + return $block . "\0\0\0\0"; +} + +function user_cache_lsapi_request_packet(string $script, string $docRoot, string $serverName, string $query): string +{ + $offsets = []; + $specialBlock = "\0\0\0\0"; + $envBase = LSAPI_REQ_HEADER_LEN + strlen($specialBlock); + $env = [ + 'SCRIPT_FILENAME' => $script, + 'SCRIPT_NAME' => '/index.php', + 'QUERY_STRING' => $query, + 'REQUEST_METHOD' => 'GET', + 'SERVER_NAME' => $serverName, + 'DOCUMENT_ROOT' => $docRoot, + 'REQUEST_URI' => '/index.php' . ($query !== '' ? '?' . $query : ''), + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REMOTE_ADDR' => '127.0.0.1', + ]; + $envBlock = user_cache_lsapi_env_block($env, $envBase, $offsets); + $body = $specialBlock . $envBlock; + $body .= str_repeat("\0", (8 - ((LSAPI_REQ_HEADER_LEN + strlen($body)) % 8)) & 7); + $body .= str_repeat("\0", LSAPI_HTTP_HEADER_INDEX_LEN); + $length = LSAPI_REQ_HEADER_LEN + strlen($body); + + return user_cache_lsapi_packet_header(LSAPI_BEGIN_REQUEST, $length) + . user_cache_lsapi_pack_int(0) + . user_cache_lsapi_pack_int(0) + . user_cache_lsapi_pack_int($offsets['SCRIPT_FILENAME']) + . user_cache_lsapi_pack_int($offsets['SCRIPT_NAME']) + . user_cache_lsapi_pack_int($offsets['QUERY_STRING']) + . user_cache_lsapi_pack_int($offsets['REQUEST_METHOD']) + . user_cache_lsapi_pack_int(0) + . user_cache_lsapi_pack_int(count($env)) + . user_cache_lsapi_pack_int(0) + . $body; +} + +function user_cache_lsapi_read_exact($fp, int $length): string +{ + $buffer = ''; + + while (strlen($buffer) < $length && !feof($fp)) { + $chunk = fread($fp, $length - strlen($buffer)); + if ($chunk === false) { + throw new RuntimeException('Failed to read LSAPI response'); + } + if ($chunk === '') { + if (stream_get_meta_data($fp)['timed_out']) { + throw new RuntimeException('Timed out reading LSAPI response'); + } + usleep(10000); + continue; + } + $buffer .= $chunk; + } + + if (strlen($buffer) !== $length) { + throw new RuntimeException('Truncated LSAPI response'); + } + + return $buffer; +} + +function user_cache_lsapi_request(int $port, string $script, string $docRoot, string $serverName, string $query): string +{ + $fp = @stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 2); + if ($fp === false) { + throw new RuntimeException($errstr); + } + stream_set_timeout($fp, 5); + + fwrite($fp, user_cache_lsapi_request_packet($script, $docRoot, $serverName, $query)); + $body = ''; + + while (!feof($fp)) { + try { + $header = user_cache_lsapi_read_exact($fp, 8); + } catch (RuntimeException $e) { + if ($body !== '' && $e->getMessage() === 'Truncated LSAPI response') { + break; + } + throw $e; + } + if (substr($header, 0, 2) !== 'LS') { + throw new RuntimeException('Invalid LSAPI response header'); + } + + $type = ord($header[2]); + $length = user_cache_lsapi_unpack_int(substr($header, 4, 4)); + $payload = $length > 8 ? user_cache_lsapi_read_exact($fp, $length - 8) : ''; + + if ($type === LSAPI_RESP_STREAM) { + $body .= $payload; + } elseif ($type === LSAPI_RESP_END || $type === LSAPI_CONN_CLOSE) { + break; + } + } + + fclose($fp); + + return trim($body); +} + +function user_cache_lsapi_free_port(): int +{ + $server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr); + if ($server === false) { + throw new RuntimeException($errstr); + } + + $name = stream_socket_get_name($server, false); + fclose($server); + + return (int) substr(strrchr($name, ':'), 1); +} + +function user_cache_lsapi_rm_rf(string $path): void +{ + if (!file_exists($path)) { + return; + } + + if (!is_dir($path) || is_link($path)) { + unlink($path); + return; + } + + foreach (scandir($path) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + user_cache_lsapi_rm_rf($path . DIRECTORY_SEPARATOR . $entry); + } + + rmdir($path); +} + +function user_cache_lsapi_start(string $lsphp, string $iniDir, int $port): array +{ + $env = array_merge($_ENV, [ + 'PHP_LSAPI_CHILDREN' => '1', + 'LSAPI_PPID_NO_CHECK' => '1', + 'LSAPI_MAX_REQS' => '100', + 'LSAPI_CLEAN_SHUTDOWN' => '1', + ]); + $process = proc_open( + [$lsphp, '-c', $iniDir, '-b', "127.0.0.1:$port"], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + $iniDir, + $env + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start lsphp'); + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + return [$process, $pipes]; +} + +function user_cache_lsapi_stop($process, array $pipes): void +{ + if (is_resource($process)) { + proc_terminate($process); + + for ($i = 0; $i < 20; $i++) { + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + usleep(100000); + } + + $status = proc_get_status($process); + if ($status['running']) { + proc_terminate($process, 9); + } + } + + foreach ($pipes as $pipe) { + if (is_resource($pipe)) { + fclose($pipe); + } + } + + if (is_resource($process)) { + proc_close($process); + } +} + +function user_cache_lsapi_wait($process, array $pipes, int $port, string $script, string $docRoot, string $serverName): void +{ + for ($i = 0; $i < 50; $i++) { + $status = proc_get_status($process); + if (!$status['running']) { + throw new RuntimeException(stream_get_contents($pipes[2])); + } + + try { + user_cache_lsapi_request($port, $script, $docRoot, $serverName, 'action=fetch'); + return; + } catch (Throwable) { + usleep(100000); + } + } + + throw new RuntimeException(stream_get_contents($pipes[2]) ?: 'lsphp did not become ready'); +} + +$root = sys_get_temp_dir() . '/php-user-cache-lsapi-' . getmypid(); +$alphaRoot = $root . '/alpha'; +$betaRoot = $root . '/beta'; +$otherRoot = $root . '/other'; +$processes = []; + +user_cache_lsapi_rm_rf($root); +mkdir($alphaRoot, 0777, true); +mkdir($betaRoot, 0777, true); +mkdir($otherRoot, 0777, true); + +$script = <<<'PHP' +store($key, $host . '-value'); +} + +echo $host, ':', $cache->fetch($key, 'MISS'), ':', $cache->getPoolStatus()->getPoolName(), "\n"; +PHP; + +file_put_contents($alphaRoot . '/index.php', $script); +file_put_contents($betaRoot . '/index.php', $script); +file_put_contents($otherRoot . '/index.php', $script); +file_put_contents($root . '/php.ini', implode("\n", [ + 'user_cache.enable=1', + 'user_cache.shm_size=32M', + 'opcache.file_update_protection=0', +])); + +try { + $lsphp = user_cache_lsapi_binary(); + $alphaPort = user_cache_lsapi_free_port(); + $betaPort = user_cache_lsapi_free_port(); + $alphaScript = $alphaRoot . '/index.php'; + $betaScript = $betaRoot . '/index.php'; + $otherScript = $otherRoot . '/index.php'; + + $processes[] = user_cache_lsapi_start($lsphp, $root, $alphaPort); + $processes[] = user_cache_lsapi_start($lsphp, $root, $betaPort); + + user_cache_lsapi_wait($processes[0][0], $processes[0][1], $alphaPort, $alphaScript, $alphaRoot, 'alpha.local'); + user_cache_lsapi_wait($processes[1][0], $processes[1][1], $betaPort, $betaScript, $betaRoot, 'beta.local'); + + $checks = [ + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=seed', 'alpha.local:alpha.local-value:default'], + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:default'], + [$alphaPort, $otherScript, $otherRoot, 'other.local', 'action=fetch', 'other.local:MISS:default'], + [$alphaPort, $otherScript, $otherRoot, 'other.local', 'action=seed', 'other.local:other.local-value:default'], + [$alphaPort, $otherScript, $otherRoot, 'other.local', 'action=fetch', 'other.local:other.local-value:default'], + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:default'], + [$betaPort, $betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:MISS:default'], + [$betaPort, $betaScript, $betaRoot, 'beta.local', 'action=seed', 'beta.local:beta.local-value:default'], + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:default'], + [$betaPort, $betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:beta.local-value:default'], + ]; + + foreach ($checks as [$port, $file, $docRoot, $serverName, $query, $expected]) { + $actual = user_cache_lsapi_request($port, $file, $docRoot, $serverName, $query); + if ($actual !== $expected) { + $stderr = stream_get_contents($port === $alphaPort ? $processes[0][1][2] : $processes[1][1][2]); + throw new RuntimeException("Expected $expected, got $actual" . ($stderr !== '' ? "\n$stderr" : '')); + } + } + + echo "Done\n"; +} finally { + foreach ($processes as [$process, $pipes]) { + user_cache_lsapi_stop($process, $pipes); + } + user_cache_lsapi_rm_rf($root); +} + +?> +--EXPECT-- +Done diff --git a/ext/user_cache/tests/user_cache_lock_key_isolation.phpt b/ext/user_cache/tests/user_cache_lock_key_isolation.phpt new file mode 100644 index 000000000000..300b22cea9d5 --- /dev/null +++ b/ext/user_cache/tests/user_cache_lock_key_isolation.phpt @@ -0,0 +1,61 @@ +--TEST-- +UserCache\Cache: locks are isolated by exact key +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +var_dump($cache->lock('alpha')); +var_dump($cache->store('beta', 'beta-value')); +var_dump($cache->fetch('beta')); +var_dump($cache->delete('beta')); +var_dump($cache->has('beta')); + +var_dump($cache->storeMultiple([ + 'gamma' => 'gamma-value', + 'delta' => 'delta-value', +])); +var_dump($cache->fetchMultiple(['gamma', 'delta'])); +var_dump($cache->deleteMultiple(['gamma', 'delta'])); +var_dump($cache->has('gamma')); +var_dump($cache->has('delta')); + +var_dump($cache->lock('epsilon')); +var_dump($cache->unlock('epsilon')); +var_dump($cache->unlock('alpha')); + +var_dump($cache->lock('prefix')); +var_dump($cache->store('prefix:child', 'child-value')); +var_dump($cache->fetch('prefix:child')); +var_dump($cache->unlock('prefix')); +?> +--EXPECT-- +bool(true) +bool(true) +string(10) "beta-value" +bool(true) +bool(false) +bool(true) +array(2) { + ["gamma"]=> + string(11) "gamma-value" + ["delta"]=> + string(11) "delta-value" +} +bool(true) +bool(false) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(11) "child-value" +bool(true) diff --git a/ext/user_cache/tests/user_cache_lock_lease.phpt b/ext/user_cache/tests/user_cache_lock_lease.phpt new file mode 100644 index 000000000000..004a026382c4 --- /dev/null +++ b/ext/user_cache/tests/user_cache_lock_lease.phpt @@ -0,0 +1,41 @@ +--TEST-- +UserCache\Cache: lock lease expires and can be taken over by another process +--EXTENSIONS-- +user_cache +pcntl +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +var_dump($cache->lock('leased', 1)); + +$pid = pcntl_fork(); +if ($pid === 0) { + echo "child lock before expiry\n"; + var_dump($cache->lock('leased')); + sleep(2); + echo "child lock after expiry\n"; + var_dump($cache->lock('leased')); + var_dump($cache->unlock('leased')); + exit(0); +} + +if ($pid > 0) { + pcntl_waitpid($pid, $status); +} else { + echo "pcntl_fork() failed\n"; +} +?> +--EXPECT-- +bool(true) +child lock before expiry +bool(false) +child lock after expiry +bool(true) +bool(true) diff --git a/ext/user_cache/tests/user_cache_lock_reentrancy.phpt b/ext/user_cache/tests/user_cache_lock_reentrancy.phpt new file mode 100644 index 000000000000..5960c4db296e --- /dev/null +++ b/ext/user_cache/tests/user_cache_lock_reentrancy.phpt @@ -0,0 +1,33 @@ +--TEST-- +UserCache\Cache: lock re-entry within a request and unlock of unheld keys +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +lock('key')); +var_dump($cache->lock('key')); +var_dump($cache->unlock('key')); +var_dump($cache->unlock('key')); + +var_dump($cache->unlock('never-locked')); + +try { + $cache->lock('key', -1); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +UserCache\Cache::lock(): Argument #2 ($lease) must be greater than or equal to 0 diff --git a/ext/user_cache/tests/user_cache_magic_serialize_contract.phpt b/ext/user_cache/tests/user_cache_magic_serialize_contract.phpt new file mode 100644 index 000000000000..369db5a2f2fe --- /dev/null +++ b/ext/user_cache/tests/user_cache_magic_serialize_contract.phpt @@ -0,0 +1,285 @@ +--TEST-- +UserCache\Cache: classes that cannot round-trip without __serialize()/__unserialize() +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +formatter = fn (string $v): string => $this->prefix . ':' . $v; + } + + public function __serialize(): array + { + return ['prefix' => $this->prefix]; + } + + public function __unserialize(array $data): void + { + $this->prefix = $data['prefix']; + $this->formatter = fn (string $v): string => $this->prefix . ':' . $v; + } + + public function format(string $v): string + { + return ($this->formatter)($v); + } +} + +$holder = new ContractClosureHolder('log'); +var_dump($cache->store('closure-holder', $holder)); +var_dump($cache->fetch('closure-holder')->format('entry')); + +class ContractIndexedCollection +{ + /** @var array */ + private array $items; + /** @var array */ + private array $indexByName = []; + + public function __construct(array $items) + { + $this->items = $items; + $this->rebuildIndex(); + } + + public function __serialize(): array + { + return ['items' => $this->items]; + } + + public function __unserialize(array $data): void + { + $this->items = $data['items']; + $this->rebuildIndex(); + } + + private function rebuildIndex(): void + { + $this->indexByName = []; + foreach ($this->items as $position => $item) { + $this->indexByName[$item['name']] = $position; + } + } + + public function findByName(string $name): ?array + { + $position = $this->indexByName[$name] ?? null; + + return $position === null ? null : $this->items[$position]; + } +} + +$collection = new ContractIndexedCollection([ + ['id' => 10, 'name' => 'alpha'], + ['id' => 20, 'name' => 'beta'], +]); +var_dump($cache->store('indexed', $collection)); +var_dump($cache->fetch('indexed')->findByName('beta')['id']); + +class ContractPackedMatrix +{ + /** @var array> */ + private array $rows; + + public function __construct(array $rows) + { + $this->rows = $rows; + } + + public function __serialize(): array + { + return ['packed' => implode(';', array_map(static fn (array $r): string => implode(',', $r), $this->rows))]; + } + + public function __unserialize(array $data): void + { + $this->rows = array_map( + static fn (string $r): array => array_map('intval', explode(',', $r)), + explode(';', $data['packed']) + ); + } + + public function cell(int $row, int $column): int + { + return $this->rows[$row][$column]; + } +} + +var_dump($cache->store('matrix', new ContractPackedMatrix([[1, 2], [3, 4]]))); +var_dump($cache->fetch('matrix')->cell(1, 0)); + +/* Snapshot hooks run once per store; restore hooks run on every fetch. */ +class ContractCounter +{ + public static int $serializeCalls = 0; + public static int $unserializeCalls = 0; + + public function __construct(private int $value = 7) + { + } + + public function __serialize(): array + { + self::$serializeCalls++; + return ['value' => $this->value]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + $this->value = $data['value']; + } +} + +var_dump($cache->store('counter', new ContractCounter())); +var_dump(ContractCounter::$serializeCalls); +$cache->fetch('counter'); +$cache->fetch('counter'); +var_dump(ContractCounter::$unserializeCalls); + +class ContractParent +{ + private array $secret = []; + + public function __serialize(): array + { + return ['secret' => $this->secret]; + } + + public function __unserialize(array $data): void + { + $this->secret = $data['secret']; + } + + public function addSecret(string $v): void + { + $this->secret[] = $v; + } + + public function secretCount(): int + { + return count($this->secret); + } +} + +class ContractChild extends ContractParent +{ + public string $label = 'child'; +} + +$child = new ContractChild(); +$child->addSecret('a'); +$child->addSecret('b'); +var_dump($cache->store('child', $child)); +$fetchedChild = $cache->fetch('child'); +var_dump($fetchedChild instanceof ContractChild, $fetchedChild->secretCount(), $fetchedChild->label); + +class ContractReadonly +{ + public function __construct(public readonly string $token) + { + } + + public function __serialize(): array + { + return ['token' => $this->token]; + } + + public function __unserialize(array $data): void + { + $this->token = $data['token']; + } +} + +var_dump($cache->store('ro', new ContractReadonly('tok-1'))); +var_dump($cache->fetch('ro')->token); + +class ContractSelf +{ + public ?ContractSelf $self = null; + + public function __construct(public int $v = 1) + { + } + + public function __serialize(): array + { + return ['self' => $this, 'v' => $this->v]; + } + + public function __unserialize(array $data): void + { + $this->self = $data['self']; + $this->v = $data['v']; + } +} + +var_dump($cache->store('self', new ContractSelf())); +$fetchedSelf = $cache->fetch('self'); +var_dump($fetchedSelf->self === $fetchedSelf, $fetchedSelf->v); + +$nested = [ + 'holders' => [new ContractClosureHolder('a'), new ContractClosureHolder('b')], + 'plain' => ['x' => 1], +]; +var_dump($cache->store('nested', $nested)); +$fetchedNested = $cache->fetch('nested'); +var_dump($fetchedNested['holders'][1]->format('n'), $fetchedNested['plain']['x']); + +class ContractBadReturn +{ + public function __serialize() + { + return 'not-an-array'; + } + + public function __unserialize(array $data): void + { + } +} + +try { + $cache->store('bad', new ContractBadReturn()); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} + +unset($holder, $collection, $child, $fetchedChild, $fetchedSelf, $nested, $fetchedNested); +gc_collect_cycles(); +?> +--EXPECT-- +bool(true) +string(9) "log:entry" +bool(true) +int(20) +bool(true) +int(3) +bool(true) +int(1) +int(2) +bool(true) +bool(true) +int(2) +string(5) "child" +bool(true) +string(5) "tok-1" +bool(true) +bool(true) +int(1) +bool(true) +string(3) "b:n" +int(1) +ContractBadReturn::__serialize() must return an array diff --git a/ext/user_cache/tests/user_cache_magic_state_unstorable.phpt b/ext/user_cache/tests/user_cache_magic_state_unstorable.phpt new file mode 100644 index 000000000000..094a9a0a68f0 --- /dev/null +++ b/ext/user_cache/tests/user_cache_magic_state_unstorable.phpt @@ -0,0 +1,69 @@ +--TEST-- +UserCache\Cache: magic serialization state rejects unstorable values +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + $this->value]; + } + + public function __unserialize(array $data): void + { + $this->value = $data['value'] ?? null; + } +} + +class UserCacheSleepResource +{ + public $value; + + public function __sleep(): array + { + return ['value']; + } + + public function __wakeup(): void + { + } +} + +function show(string $label, callable $callback): void +{ + try { + var_dump($callback()); + } catch (TypeError $e) { + echo $label, ": TypeError\n"; + } +} + +$cache = UserCache\Cache::getPool('magic-state-unstorable'); + +$serializeResource = new UserCacheSerializeResource(); +$serializeResource->value = fopen(__FILE__, 'r'); +show('serialize-resource', fn() => $cache->store('serialize-resource', $serializeResource)); +fclose($serializeResource->value); + +$serializeClosure = new UserCacheSerializeResource(); +$serializeClosure->value = static fn() => null; +show('serialize-closure', fn() => $cache->store('serialize-closure', $serializeClosure)); + +$sleepResource = new UserCacheSleepResource(); +$sleepResource->value = fopen(__FILE__, 'r'); +show('sleep-resource', fn() => $cache->store('sleep-resource', $sleepResource)); +fclose($sleepResource->value); +?> +--EXPECT-- +serialize-resource: TypeError +serialize-closure: TypeError +sleep-resource: TypeError diff --git a/ext/user_cache/tests/user_cache_negative_ttl.phpt b/ext/user_cache/tests/user_cache_negative_ttl.phpt new file mode 100644 index 000000000000..323f5342a2d9 --- /dev/null +++ b/ext/user_cache/tests/user_cache_negative_ttl.phpt @@ -0,0 +1,40 @@ +--TEST-- +UserCache\Cache: negative TTL throws ValueError on every write API +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + fn() => $cache->store('key', 1, -1), + 'add' => fn() => $cache->add('key', 1, -1), + 'storeMultiple' => fn() => $cache->storeMultiple(['key' => 1], -1), + 'increment' => fn() => $cache->increment('key', 1, -1), + 'decrement' => fn() => $cache->decrement('key', 1, -1), + 'remember' => fn() => $cache->remember('key', fn() => 1, -1), +]; + +foreach ($calls as $fn) { + try { + $fn(); + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } +} + +var_dump($cache->has('key')); +?> +--EXPECT-- +UserCache\Cache::store(): Argument #3 ($ttl) must be greater than or equal to 0 +UserCache\Cache::add(): Argument #3 ($ttl) must be greater than or equal to 0 +UserCache\Cache::storeMultiple(): Argument #2 ($ttl) must be greater than or equal to 0 +UserCache\Cache::increment(): Argument #3 ($ttl) must be greater than or equal to 0 +UserCache\Cache::decrement(): Argument #3 ($ttl) must be greater than or equal to 0 +UserCache\Cache::remember(): Argument #3 ($ttl) must be greater than or equal to 0 +bool(false) diff --git a/ext/user_cache/tests/user_cache_overwrite_reuse.phpt b/ext/user_cache/tests/user_cache_overwrite_reuse.phpt new file mode 100644 index 000000000000..b77b87f02277 --- /dev/null +++ b/ext/user_cache/tests/user_cache_overwrite_reuse.phpt @@ -0,0 +1,42 @@ +--TEST-- +UserCache\Cache: repeated stores reuse existing entries +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store('same', ['i' => $i, 'payload' => str_repeat('x', 1024)])) { + echo "same failed at $i\n"; + exit; + } +} + +$same = $cache->fetch('same'); +var_dump($same['i']); +var_dump(strlen($same['payload'])); + +for ($i = 0; $i < 5000; $i++) { + if (!$cache->delete('same')) { + echo "delete failed at $i\n"; + exit; + } + if (!$cache->store('same', ['i' => $i])) { + echo "store failed at $i\n"; + exit; + } +} + +$churned = $cache->fetch('same'); +var_dump($churned['i']); +?> +--EXPECT-- +int(19999) +int(1024) +int(4999) diff --git a/ext/user_cache/tests/user_cache_packed_dynamic_array.phpt b/ext/user_cache/tests/user_cache_packed_dynamic_array.phpt new file mode 100644 index 000000000000..cf045fd80390 --- /dev/null +++ b/ext/user_cache/tests/user_cache_packed_dynamic_array.phpt @@ -0,0 +1,58 @@ +--TEST-- +UserCache\Cache: packed dynamic arrays preserve append semantics +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + [ + new PackedDynamicArrayNode(3), + new PackedDynamicArrayNode(4), + ], + ], +]; + +var_dump($cache->store('payload', $payload)); + +$fetched = $cache->fetch('payload'); +var_dump(array_is_list($fetched)); +echo implode(',', array_keys($fetched)), "\n"; + +$fetched[] = new PackedDynamicArrayNode(5); +echo implode(',', array_keys($fetched)), "\n"; +echo $fetched[3]->id, "\n"; + +var_dump(array_is_list($fetched[2]['nested'])); +$fetched[2]['nested'][] = new PackedDynamicArrayNode(6); +echo implode(',', array_keys($fetched[2]['nested'])), "\n"; +echo $fetched[2]['nested'][2]->id, "\n"; + +$again = $cache->fetch('payload'); +echo count($again), "\n"; +echo count($again[2]['nested']), "\n"; +?> +--EXPECT-- +bool(true) +bool(true) +0,1,2 +0,1,2,3 +5 +bool(true) +0,1,2 +6 +3 +2 diff --git a/ext/user_cache/tests/user_cache_pool_factory.phpt b/ext/user_cache/tests/user_cache_pool_factory.phpt new file mode 100644 index 000000000000..e1d350309613 --- /dev/null +++ b/ext/user_cache/tests/user_cache_pool_factory.phpt @@ -0,0 +1,68 @@ +--TEST-- +UserCache\Cache: pool factory returns per-name singletons +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +getMessage(), "\n"; +} + +try { + UserCache\Cache::getPool("bad\x1fpool"); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($a->store('shared-key', 'from-factory')); +var_dump($c->fetch('shared-key', 'MISS')); +var_dump($a->fetch('shared-key')); +?> +--EXPECT-- +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +array(2) { + [0]=> + string(7) "factory" + [1]=> + string(13) "factory-other" +} +bool(true) +Error +UserCache\Cache::getPool(): Argument #1 ($pool) must not be empty +UserCache\Cache::getPool(): Argument #1 ($pool) must not contain the user-cache key delimiter 0x1F +bool(true) +string(4) "MISS" +string(12) "from-factory" diff --git a/ext/user_cache/tests/user_cache_pool_status_memory.phpt b/ext/user_cache/tests/user_cache_pool_status_memory.phpt new file mode 100644 index 000000000000..68559b5907bf --- /dev/null +++ b/ext/user_cache/tests/user_cache_pool_status_memory.phpt @@ -0,0 +1,99 @@ +--TEST-- +UserCache\Cache: pool status reports per-pool memory usage +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +getPoolStatus(); +var_dump($empty->getEntryCount()); +var_dump($empty->getEntryKeys()); +var_dump($empty->getUsedMemory()); + +var_dump($a->store('int', 123)); +$afterInt = $a->getPoolStatus(); +var_dump($afterInt->getEntryCount()); +var_dump($afterInt->getEntryKeys()); +var_dump($afterInt->getUsedMemory() > 0); + +$intMemory = $afterInt->getUsedMemory(); +var_dump($a->store('str', str_repeat('x', 100))); +$afterString = $a->getPoolStatus(); +$keys = $afterString->getEntryKeys(); +sort($keys); +var_dump($afterString->getEntryCount()); +var_dump($keys); +var_dump($afterString->getUsedMemory() > $intMemory); +$stringMemory = $afterString->getUsedMemory(); + +var_dump($b->store('int', 123)); +var_dump($b->store('str', str_repeat('x', 100))); +$bStatus = $b->getPoolStatus(); +var_dump($bStatus->getUsedMemory() === $afterString->getUsedMemory()); + +var_dump($a->store('large', str_repeat('y', 4096))); +$afterLarge = $a->getPoolStatus(); +var_dump($afterLarge->getEntryCount()); +var_dump($afterLarge->getUsedMemory() > $stringMemory); +/* Status objects are snapshots, not live views. */ +var_dump($afterString->getUsedMemory() === $afterLarge->getUsedMemory()); +var_dump($b->getPoolStatus()->getUsedMemory() === $bStatus->getUsedMemory()); + +var_dump($a->delete('large')); +$afterDelete = $a->getPoolStatus(); +var_dump($afterDelete->getEntryCount()); +var_dump($afterDelete->getUsedMemory() === $stringMemory); + +var_dump($a->clear()); +$afterClear = $a->getPoolStatus(); +var_dump($afterClear->getEntryCount()); +var_dump($afterClear->getEntryKeys()); +var_dump($afterClear->getUsedMemory()); +?> +--EXPECT-- +int(0) +array(0) { +} +int(0) +bool(true) +int(1) +array(1) { + [0]=> + string(3) "int" +} +bool(true) +bool(true) +int(2) +array(2) { + [0]=> + string(3) "int" + [1]=> + string(3) "str" +} +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(3) +bool(true) +bool(false) +bool(true) +bool(true) +int(2) +bool(true) +bool(true) +int(0) +array(0) { +} +int(0) diff --git a/ext/user_cache/tests/user_cache_pressure_clear.phpt b/ext/user_cache/tests/user_cache_pressure_clear.phpt new file mode 100644 index 000000000000..564f8860f9ad --- /dev/null +++ b/ext/user_cache/tests/user_cache_pressure_clear.phpt @@ -0,0 +1,30 @@ +--TEST-- +UserCache\Cache: store retries can clear on memory pressure +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=1M +--FILE-- +clear(); + +var_dump($cache->store('marker', 'kept-until-pressure')); + +for ($i = 0; $i < 220; $i++) { + $cache->store('fill-' . $i, str_repeat(chr(65 + ($i % 26)), 4096)); +} + +$large = str_repeat('L', 256 * 1024); +var_dump($cache->store('large', $large)); +var_dump($cache->fetch('large', '') === $large); +var_dump($cache->fetch('marker', 'missing')); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +string(7) "missing" diff --git a/ext/user_cache/tests/user_cache_private_property_missing_scope.phpt b/ext/user_cache/tests/user_cache_private_property_missing_scope.phpt new file mode 100644 index 000000000000..9cda62d090e1 --- /dev/null +++ b/ext/user_cache/tests/user_cache_private_property_missing_scope.phpt @@ -0,0 +1,30 @@ +--TEST-- +UserCache\Cache: missing private property declaring class fails decode +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +var_dump($cache->store('value', $value)); +var_dump($cache->fetch('value', 'default')); +var_dump($cache->has('value')); +?> +--EXPECT-- +bool(true) +string(7) "default" +bool(false) diff --git a/ext/user_cache/tests/user_cache_pure_enum.phpt b/ext/user_cache/tests/user_cache_pure_enum.phpt new file mode 100644 index 000000000000..7c5df08c01ec --- /dev/null +++ b/ext/user_cache/tests/user_cache_pure_enum.phpt @@ -0,0 +1,35 @@ +--TEST-- +UserCache\Cache: pure (non-backed) enum cases round-trip as singletons +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store('single', PureSuit::Hearts)); +var_dump($cache->fetch('single') === PureSuit::Hearts); + +var_dump($cache->store('list', [PureSuit::Hearts, PureSuit::Spades, PureSuit::Hearts])); +$fetched = $cache->fetch('list'); +var_dump($fetched[0] === PureSuit::Hearts); +var_dump($fetched[1] === PureSuit::Spades); +var_dump($fetched[2] === PureSuit::Hearts); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/user_cache/tests/user_cache_readonly_properties.phpt b/ext/user_cache/tests/user_cache_readonly_properties.phpt new file mode 100644 index 000000000000..2e5a65a0366b --- /dev/null +++ b/ext/user_cache/tests/user_cache_readonly_properties.phpt @@ -0,0 +1,65 @@ +--TEST-- +UserCache\Cache: readonly and inherited readonly properties survive the round trip +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +baseName . ':' . $this->baseCount; + } +} + +class UserCacheReadonlyChild extends UserCacheReadonlyBase +{ + public function __construct( + string $baseName, + int $baseCount, + public readonly array $items, + ) { + parent::__construct($baseName, $baseCount); + } +} + +$child = new UserCacheReadonlyChild('alpha', 7, ['a', 'b']); + +var_dump($cache->store('child', $child)); + +$fetched = $cache->fetch('child'); +var_dump($fetched instanceof UserCacheReadonlyChild); +var_dump($fetched->describe()); +var_dump($fetched->baseName); +var_dump($fetched->items); + +try { + $fetched->baseName = 'changed'; +} catch (Error $e) { + echo get_class($e), "\n"; +} +?> +--EXPECT-- +bool(true) +bool(true) +string(7) "alpha:7" +string(5) "alpha" +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" +} +Error diff --git a/ext/user_cache/tests/user_cache_reference_cycle_gc.phpt b/ext/user_cache/tests/user_cache_reference_cycle_gc.phpt new file mode 100644 index 000000000000..797bd6a0d38d --- /dev/null +++ b/ext/user_cache/tests/user_cache_reference_cycle_gc.phpt @@ -0,0 +1,57 @@ +--TEST-- +UserCache\Cache: plain cyclic object graph round-trips and remains collectable +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +parent = $root; +$root->children[] = $leaf; + +var_dump($cache->store('graph', $root)); + +$graph = $cache->fetch('graph'); +echo "decoded names: ", $graph->name, " ", $graph->children[0]->name, "\n"; +echo "back edge identity: "; +var_dump($graph->children[0]->parent === $graph); +echo "decoded graph is a copy: "; +var_dump($graph !== $root); +echo "leaf children: "; +var_dump($graph->children[0]->children); + +$again = $cache->fetch('graph'); +echo "fetches independent: "; +var_dump($again !== $graph); +echo "fetched back edge identity: "; +var_dump($again->children[0]->parent === $again); + +unset($root, $leaf, $graph, $again); +gc_collect_cycles(); +?> +--EXPECT-- +bool(true) +decoded names: root leaf +back edge identity: bool(true) +decoded graph is a copy: bool(true) +leaf children: array(0) { +} +fetches independent: bool(true) +fetched back edge identity: bool(true) diff --git a/ext/user_cache/tests/user_cache_reference_graphs.phpt b/ext/user_cache/tests/user_cache_reference_graphs.phpt new file mode 100644 index 000000000000..aebd82b29a97 --- /dev/null +++ b/ext/user_cache/tests/user_cache_reference_graphs.phpt @@ -0,0 +1,272 @@ +--TEST-- +UserCache\Cache: references and cyclic graphs survive fetch and access +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +name . ':' . $this->revision . ':' . $this->peer?->name; + } +} + +class UCReferencedPayload +{ + public function __construct( + public string $label, + public int $revision, + ) { + } +} + +class UCReferenceAssignmentPayload +{ + public UCReferencedPayload $alias; + + public function __construct( + public string $name, + public UCReferencedPayload $child, + ) { + $this->alias = $child; + } +} + +function uc_build_cycle_payload(string $rootName, string $peerName, int $revision): UCCyclePayload +{ + $root = new UCCyclePayload($rootName, $revision); + $peer = new UCCyclePayload($peerName, $revision + 1); + $root->peer = $peer; + $peer->peer = $root; + + return $root; +} + +$a = ['value' => 1, 'tag' => 't']; +$a['alias'] = &$a['value']; +var_dump($cache->store('k_scalar', $a)); + +$g = $cache->fetch('k_scalar'); +$g['value'] = 99; +echo "scalar alias follows write: "; +var_dump($g['alias'] === 99); +echo "scalar other fetch is independent: "; +var_dump($cache->fetch('k_scalar')['value'] === 1); + +$x = 10; +$b = ['r' => &$x, 'tag' => 'u']; +var_dump($cache->store('k_single', $b)); +echo "single value: "; +var_dump($cache->fetch('k_single')['r'] === 10); + +$o = new UCReferenceHolder(); +$o->n = 7; +$c = []; +$c['p'] = &$o; +$c['q'] = &$o; +var_dump($cache->store('k_objref', $c)); + +$gc = $cache->fetch('k_objref'); +echo "objref identity: "; +var_dump($gc['p'] === $gc['q']); +$gc['p']->n = 123; +echo "objref shared mutation: "; +var_dump($gc['q']->n === 123); + +$pair = new UCReferencePair(); +$v = 1; +$pair->a = &$v; +$pair->b = &$v; +var_dump($cache->store('k_proptref', $pair)); +echo "proptref round-trips to object: "; +var_dump($cache->fetch('k_proptref') instanceof UCReferencePair); + +$gp = $cache->fetch('k_proptref'); +$gp->a = 55; +echo "proptref alias preserved: "; +var_dump($gp->b === 55); + +$typed = new UCTypedReferencePair(); +$typedValue = 11; +$typed->a = &$typedValue; +$typed->b = &$typedValue; +var_dump($cache->store('k_typed_proptref', $typed)); +$gt = $cache->fetch('k_typed_proptref'); +$gt->a = 77; +echo "typed proptref alias preserved: "; +var_dump($gt->b === 77); +try { + $gt->a = 'not an int'; +} catch (TypeError $e) { + echo "typed proptref type check: TypeError\n"; +} + +$dynamic = new stdClass(); +$dynamicValue = 1; +$dynamic->a = &$dynamicValue; +$dynamic->b = &$dynamicValue; +var_dump($cache->store('dpr_scalar', $dynamic)); +$gd = $cache->fetch('dpr_scalar'); +$gd->a = 99; +echo "dynamic scalar alias: "; +var_dump($gd->b === 99); +echo "dynamic independent fetch: "; +var_dump($cache->fetch('dpr_scalar')->a === 1); + +$shared = (object) ['n' => 0]; +$dynamicObject = new stdClass(); +$dynamicObject->x = &$shared; +$dynamicObject->y = &$shared; +var_dump($cache->store('dpr_obj', $dynamicObject)); +$gdo = $cache->fetch('dpr_obj'); +echo "dynamic object identity: "; +var_dump($gdo->x === $gdo->y); +$gdo->x->n = 7; +echo "dynamic object shared mutation: "; +var_dump($gdo->y->n === 7); + +$self = ['n' => 5, 'list' => [1, 2, 3]]; +$self['self'] = &$self; +var_dump($cache->store('k_self', $self)); +$gs = $cache->fetch('k_self'); +echo "self cycle resolves: "; +var_dump($gs['self'] === $gs); +echo "value through cycle: ", $gs['self']['n'], " ", $gs['self']['list'][1], "\n"; + +$p = ['name' => 'p']; +$q = ['name' => 'q']; +$p['other'] = &$q; +$q['other'] = &$p; +var_dump($cache->store('k_mutual', $p)); +$gm = $cache->fetch('k_mutual'); +echo "mutual cycle resolves: "; +var_dump($gm['other']['other'] === $gm); +echo "names: ", $gm['name'], " ", $gm['other']['name'], "\n"; + +$root = ['a' => ['b' => ['c' => 7]]]; +$root['a']['b']['back'] = &$root; +var_dump($cache->store('k_deep', $root)); +$deep = $cache->fetch('k_deep'); +echo "deep cycle resolves: "; +var_dump($deep['a']['b']['back'] === $deep); +echo "deep value: ", $deep['a']['b']['back']['a']['b']['c'], "\n"; + +$g1 = $cache->fetch('k_self'); +$g2 = $cache->fetch('k_self'); +$g1['n'] = 99; +echo "fetches independent: "; +var_dump($g2['n'] === 5); + +$serializedCycle = uc_build_cycle_payload('serialized-root', 'serialized-peer', 1); +var_dump($cache->store('serialized_cycle_object', $serializedCycle)); +$fetchedSerializedCycle = $cache->fetch('serialized_cycle_object'); +echo "serialized cycle access: ", $fetchedSerializedCycle->describe(), "\n"; +echo "serialized cycle back edge: "; +var_dump($fetchedSerializedCycle->peer->peer === $fetchedSerializedCycle); + +$referenceAssignment = new UCReferenceAssignmentPayload( + 'reference-root', + new UCReferencedPayload('reference-child', 1), +); +var_dump($cache->store('reference_assignment_object', $referenceAssignment)); +$fetchedReferenceAssignment = $cache->fetch('reference_assignment_object'); +echo "reference assignment access: "; +var_dump($fetchedReferenceAssignment->name . ':' . $fetchedReferenceAssignment->child->label . ':' . $fetchedReferenceAssignment->child->revision); +echo "reference assignment identity: "; +var_dump($fetchedReferenceAssignment->child === $fetchedReferenceAssignment->alias); +$fetchedReferenceAssignment->child->revision = 9; +echo "reference assignment shared mutation: "; +var_dump($fetchedReferenceAssignment->alias->revision === 9); + +$cycleAssignment = uc_build_cycle_payload('cycle-root', 'cycle-peer', 1); +var_dump($cache->store('cycle_assignment_object', $cycleAssignment)); +$fetchedCycleAssignment = $cache->fetch('cycle_assignment_object'); +echo "cycle assignment access: ", $fetchedCycleAssignment->describe(), "\n"; +echo "cycle assignment back edge: "; +var_dump($fetchedCycleAssignment->peer->peer === $fetchedCycleAssignment); + +unset( + $a, $g, $x, $b, $o, $c, $gc, $pair, $v, $gp, + $typed, $typedValue, $gt, $dynamic, $dynamicValue, $gd, + $shared, $dynamicObject, $gdo, $self, $gs, $p, $q, $gm, + $root, $deep, $g1, $g2, $serializedCycle, $fetchedSerializedCycle, + $referenceAssignment, $fetchedReferenceAssignment, $cycleAssignment, + $fetchedCycleAssignment +); +gc_collect_cycles(); +?> +--EXPECT-- +bool(true) +scalar alias follows write: bool(true) +scalar other fetch is independent: bool(true) +bool(true) +single value: bool(true) +bool(true) +objref identity: bool(true) +objref shared mutation: bool(true) +bool(true) +proptref round-trips to object: bool(true) +proptref alias preserved: bool(true) +bool(true) +typed proptref alias preserved: bool(true) +typed proptref type check: TypeError +bool(true) +dynamic scalar alias: bool(true) +dynamic independent fetch: bool(true) +bool(true) +dynamic object identity: bool(true) +dynamic object shared mutation: bool(true) +bool(true) +self cycle resolves: bool(true) +value through cycle: 5 2 +bool(true) +mutual cycle resolves: bool(true) +names: p q +bool(true) +deep cycle resolves: bool(true) +deep value: 7 +fetches independent: bool(true) +bool(true) +serialized cycle access: serialized-root:1:serialized-peer +serialized cycle back edge: bool(true) +bool(true) +reference assignment access: string(32) "reference-root:reference-child:1" +reference assignment identity: bool(true) +reference assignment shared mutation: bool(true) +bool(true) +cycle assignment access: cycle-root:1:cycle-peer +cycle assignment back edge: bool(true) diff --git a/ext/user_cache/tests/user_cache_remember_failure.phpt b/ext/user_cache/tests/user_cache_remember_failure.phpt new file mode 100644 index 000000000000..f83978d3d990 --- /dev/null +++ b/ext/user_cache/tests/user_cache_remember_failure.phpt @@ -0,0 +1,49 @@ +--TEST-- +UserCache\Cache: remember() failure paths (callback throw, unstorable result) +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +remember('boom', function () { + throw new RuntimeException('from callback'); + }); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($cache->has('boom')); +var_dump($cache->lock('boom')); +var_dump($cache->unlock('boom')); + +foreach ([ + 'res' => fn() => fopen('php://memory', 'r'), + 'closure' => fn() => fn() => 1, + 'nested-res' => fn() => ['inner' => fopen('php://memory', 'r')], +] as $key => $callback) { + try { + $cache->remember($key, $callback); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } + var_dump($cache->has($key)); +} +?> +--EXPECT-- +from callback +bool(false) +bool(true) +bool(true) +resources cannot be stored in the user cache +bool(false) +Closure objects cannot be stored in the user cache +bool(false) +resources cannot be stored in the user cache +bool(false) diff --git a/ext/user_cache/tests/user_cache_remember_key_argument.phpt b/ext/user_cache/tests/user_cache_remember_key_argument.phpt new file mode 100644 index 000000000000..5c4cde68973d --- /dev/null +++ b/ext/user_cache/tests/user_cache_remember_key_argument.phpt @@ -0,0 +1,36 @@ +--TEST-- +UserCache\Cache: remember() passes the cache key to the callback +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +remember('first', $loader)); +var_dump($cache->remember('second', $loader)); + +$calls = 0; +$counting = static function (string $key) use (&$calls): string { + $calls++; + return 'value:' . $key; +}; +var_dump($cache->remember('first', $counting)); +var_dump($calls); + +var_dump($cache->remember('third', static fn (): int => 42)); +?> +--EXPECT-- +string(12) "loaded:first" +string(13) "loaded:second" +string(12) "loaded:first" +int(0) +int(42) diff --git a/ext/user_cache/tests/user_cache_request_local_slot_deferred_promotion.phpt b/ext/user_cache/tests/user_cache_request_local_slot_deferred_promotion.phpt new file mode 100644 index 000000000000..fdc1528a6e53 --- /dev/null +++ b/ext/user_cache/tests/user_cache_request_local_slot_deferred_promotion.phpt @@ -0,0 +1,99 @@ +--TEST-- +UserCache\Cache: request-local slots are promoted after the first shared-graph fetch +--EXTENSIONS-- +user_cache +pcntl +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +--FILE-- +clear(); + +class UserCacheDeferredPromotionDate extends DateTime +{ + private string $cachedSelfHash; + + public function __construct(string $time, DateTimeZone $timezone) + { + parent::__construct($time, $timezone); + $this->cachedSelfHash = spl_object_hash($this); + } + + public function cachedSelfHash(): string + { + return $this->cachedSelfHash; + } +} + +$pid = pcntl_fork(); +if ($pid === 0) { + $date = new UserCacheDeferredPromotionDate('2026-07-03 12:34:56.123456', new DateTimeZone('Asia/Tokyo')); + $payload = [ + 'first' => $date, + 'second' => $date, + 'nested' => ['label' => 'original'], + 'storedHash' => $date->cachedSelfHash(), + ]; + + var_dump($cache->store('payload', $payload)); + exit(0); +} + +if ($pid < 0) { + echo "pcntl_fork() failed\n"; + exit(1); +} + +pcntl_waitpid($pid, $status); + +$first = $cache->fetch('payload'); +$first['first']->modify('+1 day'); +$first['nested']['label'] = 'mutated-first'; + +$second = $cache->fetch('payload'); +$secondDateUnchanged = $second['first']->format('Y-m-d H:i:s.u e') === '2026-07-03 12:34:56.123456 Asia/Tokyo'; +$secondNestedUnchanged = $second['nested']['label'] === 'original'; +$second['first']->modify('+2 days'); +$second['nested']['label'] = 'mutated-second'; + +$third = $cache->fetch('payload'); + +echo "status: ", pcntl_wexitstatus($status), "\n"; +echo "first alias: "; +var_dump($first['first'] === $first['second']); +echo "second alias: "; +var_dump($second['first'] === $second['second']); +echo "third alias: "; +var_dump($third['first'] === $third['second']); +echo "fetches independent: "; +var_dump($first['first'] !== $second['first'] && $second['first'] !== $third['first']); +echo "second unchanged by first: "; +var_dump($secondDateUnchanged); +var_dump($secondNestedUnchanged); +echo "third unchanged by second: "; +var_dump($third['first']->format('Y-m-d H:i:s.u e') === '2026-07-03 12:34:56.123456 Asia/Tokyo'); +var_dump($third['nested']['label'] === 'original'); +echo "object hashes preserved as stored: "; +var_dump($first['first']->cachedSelfHash() === $first['storedHash']); +var_dump($second['first']->cachedSelfHash() === $second['storedHash']); +var_dump($third['first']->cachedSelfHash() === $third['storedHash']); +?> +--EXPECTF-- +bool(true) +status: 0 +first alias: bool(true) +second alias: bool(true) +third alias: bool(true) +fetches independent: bool(true) +second unchanged by first: bool(true) +bool(true) +third unchanged by second: bool(true) +bool(true) +object hashes preserved as stored: bool(true) +bool(true) +bool(true) diff --git a/ext/user_cache/tests/user_cache_request_local_slot_magic_objects.phpt b/ext/user_cache/tests/user_cache_request_local_slot_magic_objects.phpt new file mode 100644 index 000000000000..2643aea7d9f7 --- /dev/null +++ b/ext/user_cache/tests/user_cache_request_local_slot_magic_objects.phpt @@ -0,0 +1,118 @@ +--TEST-- +UserCache\Cache: graphs with magic restore methods re-run them on every fetch +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +/* Restore hooks must run on every fetch. */ +class SlotMagicPair +{ + public static int $serializeCalls = 0; + public static int $unserializeCalls = 0; + + public function __construct(public int $value = 0) + { + } + + public function __serialize(): array + { + self::$serializeCalls++; + return ['value' => $this->value]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + $this->value = $data['value']; + } +} + +class SlotUnserializeOnly +{ + public static int $unserializeCalls = 0; + + public function __construct(public int $value = 0) + { + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + $this->value = $data['value']; + } +} + +class SlotSleepWakeup +{ + public static int $sleepCalls = 0; + public static int $wakeupCalls = 0; + + public function __construct(public int $value = 0) + { + } + + public function __sleep(): array + { + self::$sleepCalls++; + return ['value']; + } + + public function __wakeup(): void + { + self::$wakeupCalls++; + } +} + +var_dump($cache->store('pair', new SlotMagicPair(11))); +$first = $cache->fetch('pair'); +$second = $cache->fetch('pair'); +$third = $cache->fetch('pair'); +$fourth = $cache->fetch('pair'); +echo "pair serialize calls: ", SlotMagicPair::$serializeCalls, "\n"; +echo "pair unserialize calls: ", SlotMagicPair::$unserializeCalls, "\n"; +echo "pair fetches independent: "; +var_dump($third !== $fourth && $third->value === 11 && $fourth->value === 11); +$third->value = 99; +echo "pair mutation isolated: "; +var_dump($cache->fetch('pair')->value === 11); + +var_dump($cache->store('unserialize-only', new SlotUnserializeOnly(22))); +$cache->fetch('unserialize-only'); +$cache->fetch('unserialize-only'); +$clone1 = $cache->fetch('unserialize-only'); +$clone2 = $cache->fetch('unserialize-only'); +echo "unserialize-only calls: ", SlotUnserializeOnly::$unserializeCalls, "\n"; +echo "unserialize-only fetches independent: "; +var_dump($clone1 !== $clone2 && $clone1->value === 22 && $clone2->value === 22); + +var_dump($cache->store('sleeper', new SlotSleepWakeup(33))); +$cache->fetch('sleeper'); +$cache->fetch('sleeper'); +$clone1 = $cache->fetch('sleeper'); +$clone2 = $cache->fetch('sleeper'); +echo "sleep calls: ", SlotSleepWakeup::$sleepCalls, "\n"; +echo "wakeup calls: ", SlotSleepWakeup::$wakeupCalls, "\n"; +echo "sleeper fetches independent: "; +var_dump($clone1 !== $clone2 && $clone1->value === 33 && $clone2->value === 33); +?> +--EXPECT-- +bool(true) +pair serialize calls: 1 +pair unserialize calls: 4 +pair fetches independent: bool(true) +pair mutation isolated: bool(true) +bool(true) +unserialize-only calls: 4 +unserialize-only fetches independent: bool(true) +bool(true) +sleep calls: 1 +wakeup calls: 4 +sleeper fetches independent: bool(true) diff --git a/ext/user_cache/tests/user_cache_request_local_slot_store_invalidate.phpt b/ext/user_cache/tests/user_cache_request_local_slot_store_invalidate.phpt new file mode 100644 index 000000000000..a8259fc87b3a --- /dev/null +++ b/ext/user_cache/tests/user_cache_request_local_slot_store_invalidate.phpt @@ -0,0 +1,62 @@ +--TEST-- +UserCache\Cache: same-request store()/delete() invalidate promoted request-local slots +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +class SlotInvalidateValue +{ + public static int $unserializeCalls = 0; + + public function __construct(public int $value = 0) + { + } + + public function __serialize(): array + { + return ['value' => $this->value]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + $this->value = $data['value']; + } +} + +var_dump($cache->store('key', new SlotInvalidateValue(1))); +$cache->fetch('key'); +$cache->fetch('key'); +$promoted = $cache->fetch('key'); +echo "calls after promotion: ", SlotInvalidateValue::$unserializeCalls, "\n"; +echo "promoted value: ", $promoted->value, "\n"; + +var_dump($cache->store('key', new SlotInvalidateValue(2))); +$fresh = $cache->fetch('key'); +echo "value after overwrite: ", $fresh->value, "\n"; +echo "calls after overwrite fetch: ", SlotInvalidateValue::$unserializeCalls, "\n"; + +var_dump($cache->delete('key')); +var_dump($cache->fetch('key')); +var_dump($cache->store('key', new SlotInvalidateValue(3))); +echo "value after delete and re-store: ", $cache->fetch('key')->value, "\n"; +?> +--EXPECT-- +bool(true) +calls after promotion: 3 +promoted value: 1 +bool(true) +value after overwrite: 2 +calls after overwrite fetch: 4 +bool(true) +NULL +bool(true) +value after delete and re-store: 3 diff --git a/ext/user_cache/tests/user_cache_request_local_slot_strict_parity.phpt b/ext/user_cache/tests/user_cache_request_local_slot_strict_parity.phpt new file mode 100644 index 000000000000..47bc73403536 --- /dev/null +++ b/ext/user_cache/tests/user_cache_request_local_slot_strict_parity.phpt @@ -0,0 +1,145 @@ +--TEST-- +UserCache\Cache: request-local slots are strictly serialize-parity-safe +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +--FILE-- +clear(); + +/* Restore hooks must run on every fetch; structural cloning must not run __clone(). */ +class StrictParityMagicDate extends DateTime +{ + public static int $unserializeCalls = 0; + public static int $cloneCalls = 0; + + public function __serialize(): array + { + return parent::__serialize(); + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + parent::__unserialize($data); + } + + public function __clone(): void + { + self::$cloneCalls++; + } +} + +/* Structural slot cloning must not invoke __clone(). */ +class StrictParityPlainClone +{ + public static int $cloneCalls = 0; + + public function __construct(public int $value = 0, public array $tags = []) + { + } + + public function __clone(): void + { + self::$cloneCalls++; + } +} + +class StrictParitySleeper +{ + public static int $wakeupCalls = 0; + + public int $value = 0; + + public function __construct(int $value = 0) + { + $this->value = $value; + } + + public function __sleep(): array + { + return ['value']; + } + + public function __wakeup(): void + { + self::$wakeupCalls++; + } +} + +class StrictParityWakeupOnly +{ + public static int $wakeupCalls = 0; + + public int $value = 0; + + public function __construct(int $value = 0) + { + $this->value = $value; + } + + public function __wakeup(): void + { + self::$wakeupCalls++; + } +} + +var_dump($cache->store('magic', new StrictParityMagicDate('2026-07-08 01:02:03.456789', new DateTimeZone('UTC')))); +$magic = null; +for ($i = 0; $i < 5; $i++) { + $magic = $cache->fetch('magic'); +} +echo "magic __unserialize calls: ", StrictParityMagicDate::$unserializeCalls, "\n"; +echo "magic __clone calls: ", StrictParityMagicDate::$cloneCalls, "\n"; +echo "magic round-trips: "; +var_dump($magic instanceof StrictParityMagicDate && + $magic->format('Y-m-d H:i:s.u') === '2026-07-08 01:02:03.456789'); + +var_dump($cache->store('plain', new StrictParityPlainClone(42, ['a', 'b']))); +$plainOk = true; +for ($i = 0; $i < 5; $i++) { + $plain = $cache->fetch('plain'); + $plainOk = $plainOk && $plain->value === 42 && $plain->tags === ['a', 'b']; +} +echo "plain __clone calls: ", StrictParityPlainClone::$cloneCalls, "\n"; +echo "plain round-trips: "; +var_dump($plainOk); + +var_dump($cache->store('sleeper', new StrictParitySleeper(7))); +$sleeperOk = true; +for ($i = 0; $i < 5; $i++) { + $sleeper = $cache->fetch('sleeper'); + $sleeperOk = $sleeperOk && $sleeper->value === 7; +} +echo "sleeper __wakeup calls: ", StrictParitySleeper::$wakeupCalls, "\n"; +echo "sleeper round-trips: "; +var_dump($sleeperOk); + +var_dump($cache->store('wakeup-only', new StrictParityWakeupOnly(9))); +$wakeupOnlyOk = true; +for ($i = 0; $i < 5; $i++) { + $wakeupOnly = $cache->fetch('wakeup-only'); + $wakeupOnlyOk = $wakeupOnlyOk && $wakeupOnly->value === 9; +} +echo "wakeup-only __wakeup calls: ", StrictParityWakeupOnly::$wakeupCalls, "\n"; +echo "wakeup-only round-trips: "; +var_dump($wakeupOnlyOk); +?> +--EXPECT-- +bool(true) +magic __unserialize calls: 5 +magic __clone calls: 0 +magic round-trips: bool(true) +bool(true) +plain __clone calls: 0 +plain round-trips: bool(true) +bool(true) +sleeper __wakeup calls: 5 +sleeper round-trips: bool(true) +bool(true) +wakeup-only __wakeup calls: 5 +wakeup-only round-trips: bool(true) diff --git a/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt b/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt new file mode 100644 index 000000000000..46eab8d5415d --- /dev/null +++ b/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt @@ -0,0 +1,66 @@ +--TEST-- +UserCache\Cache: throwing __unserialize drops the entry and never seeds a request-local slot +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +class SlotThrowingUnserialize +{ + public static bool $failNext = false; + public static int $unserializeCalls = 0; + + public function __construct(public int $value = 0) + { + } + + public function __serialize(): array + { + return ['value' => $this->value]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + if (self::$failNext) { + self::$failNext = false; + throw new RuntimeException('unserialize failure'); + } + + $this->value = $data['value']; + } +} + +var_dump($cache->store('key', new SlotThrowingUnserialize(7))); + +/* A failed restore drops the entry without seeding the local slot. */ +SlotThrowingUnserialize::$failNext = true; +var_dump($cache->fetch('key')); +echo "calls after throwing fetch: ", SlotThrowingUnserialize::$unserializeCalls, "\n"; +echo "entry dropped: "; +var_dump($cache->has('key') === false); +var_dump($cache->fetch('key')); + +var_dump($cache->store('key', new SlotThrowingUnserialize(8))); +$cache->fetch('key'); +$cache->fetch('key'); +$cloned = $cache->fetch('key'); +echo "calls after re-store and three fetches: ", SlotThrowingUnserialize::$unserializeCalls, "\n"; +echo "restored value: ", $cloned->value, "\n"; +?> +--EXPECT-- +bool(true) +NULL +calls after throwing fetch: 1 +entry dropped: bool(true) +NULL +bool(true) +calls after re-store and three fetches: 4 +restored value: 8 diff --git a/ext/user_cache/tests/user_cache_reset.phpt b/ext/user_cache/tests/user_cache_reset.phpt new file mode 100644 index 000000000000..2233061f3aa6 --- /dev/null +++ b/ext/user_cache/tests/user_cache_reset.phpt @@ -0,0 +1,40 @@ +--TEST-- +UserCache\Cache: user_cache_reset() clears cached values in the active partition +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store('key', ['value' => 1])); +var_dump($other->store('key', 'other')); + +var_dump($cache->fetch('key')); +var_dump($other->fetch('key')); + +var_dump(user_cache_reset()); + +var_dump($cache->fetch('key', 'missing')); +var_dump($other->fetch('key', 'missing')); +var_dump($cache->has('key')); +var_dump($other->has('key')); +?> +--EXPECT-- +bool(true) +bool(true) +array(1) { + ["value"]=> + int(1) +} +string(5) "other" +bool(true) +string(7) "missing" +string(7) "missing" +bool(false) +bool(false) diff --git a/ext/user_cache/tests/user_cache_serdes_identity.phpt b/ext/user_cache/tests/user_cache_serdes_identity.phpt new file mode 100644 index 000000000000..9d29e95c4162 --- /dev/null +++ b/ext/user_cache/tests/user_cache_serdes_identity.phpt @@ -0,0 +1,75 @@ +--TEST-- +UserCache\Cache: identity, cycles, and graph nesting of __sleep() objects +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +link = $self; +var_dump($cache->store('self-cycle', $self)); +$fetched = $cache->fetch('self-cycle'); +var_dump($fetched->link === $fetched); + +$shared = ['payload' => range(1, 4)]; +$a = new SerdesNode(); +$b = new SerdesNode(); +$a->items = $shared; +$b->items = $shared; +$a->link = $b; +var_dump($cache->store('pair', $a)); +$pair = $cache->fetch('pair'); +var_dump($pair->link instanceof SerdesNode); +var_dump($pair->items === $pair->link->items); + +$node = new SerdesNode(); +$node->items = ['x' => 1]; +var_dump($cache->store('nested', ['first' => $node, 'second' => $node, 'plain' => [1, 2, 3]])); +$nested = $cache->fetch('nested'); +var_dump($nested['first'] instanceof SerdesNode); +var_dump($nested['first'] === $nested['second']); +var_dump($nested['first']->items['x']); +var_dump($nested['plain']); + +unset($self, $fetched, $shared, $a, $b, $pair, $node, $nested); +gc_collect_cycles(); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(1) +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} diff --git a/ext/user_cache/tests/user_cache_serdes_mixed_keys.phpt b/ext/user_cache/tests/user_cache_serdes_mixed_keys.phpt new file mode 100644 index 000000000000..6980b62dbf52 --- /dev/null +++ b/ext/user_cache/tests/user_cache_serdes_mixed_keys.phpt @@ -0,0 +1,51 @@ +--TEST-- +UserCache\Cache: serdes arrays keep mixed string/integer keys intact +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + ['name' => 'x', 5 => 'y'], + 'strings then int' => ['a' => 1, 'b' => 2, 10 => 3], + 'int then string' => [5 => 'y', 'name' => 'x'], + 'interleaved' => ['a' => 1, 3 => 'x', 'b' => 2, 7 => 'y'], + 'packed' => ['a', 'b'], + 'nested mixed' => ['outer' => [1 => 'one', 'two' => 2], 9 => ['k' => 'v']], +]; + +foreach ($cases as $label => $data) { + $holder = new MixedKeyHolder(); + $holder->data = $data; + + var_dump($cache->store($label, $holder)); + $fetched = $cache->fetch($label); + var_dump($fetched->data === $data); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/user_cache/tests/user_cache_serdes_sleep_wakeup.phpt b/ext/user_cache/tests/user_cache_serdes_sleep_wakeup.phpt new file mode 100644 index 000000000000..091034656a26 --- /dev/null +++ b/ext/user_cache/tests/user_cache_serdes_sleep_wakeup.phpt @@ -0,0 +1,110 @@ +--TEST-- +UserCache\Cache: __sleep()/__wakeup() classes round-trip through the serialization contract +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +kept . '/' . $this->dropped . '/' . $this->protectedKept . '/' . count($this->privateKept); + } +} + +$object = new SerdesSleeper(); +$object->kept = 'stored-kept'; +$object->dropped = 'stored-dropped'; +(function () { + $this->protectedKept = 7; + $this->privateKept = ['x', 'y']; +})->call($object); + +var_dump($cache->store('sleeper', $object)); +var_dump(SerdesSleeper::$sleepCalls); + +$fetched = $cache->fetch('sleeper'); +var_dump($fetched instanceof SerdesSleeper); +var_dump(SerdesSleeper::$wakeupCalls); +var_dump($fetched->describe()); + +/* Restore hooks run on every fetch. */ +$second = $cache->fetch('sleeper'); +var_dump(SerdesSleeper::$wakeupCalls); +var_dump($second !== $fetched); + +class SerdesBadSleeper +{ + public int $real = 1; + + public function __sleep(): array + { + return ['real', 'missing']; + } + + public function __wakeup(): void + { + } +} + +var_dump($cache->store('bad-sleeper', new SerdesBadSleeper())); +var_dump($cache->fetch('bad-sleeper')->real); + +class SerdesWakerOnly +{ + public static int $wakeupCalls = 0; + public int $value = 5; + + public function __wakeup(): void + { + self::$wakeupCalls++; + } +} + +$waker = new SerdesWakerOnly(); +$waker->value = 11; +var_dump($cache->store('waker', $waker)); +var_dump($cache->fetch('waker')->value); +var_dump(SerdesWakerOnly::$wakeupCalls); +?> +--EXPECTF-- +bool(true) +int(1) +bool(true) +int(1) +string(31) "stored-kept/default-dropped/7/2" +int(2) +bool(true) + +Warning: UserCache\Cache::store(): "missing" returned as member variable from __sleep() but does not exist in %s on line %d +bool(true) +int(1) +bool(true) +int(11) +int(1) diff --git a/ext/user_cache/tests/user_cache_serializable_interface.phpt b/ext/user_cache/tests/user_cache_serializable_interface.phpt new file mode 100644 index 000000000000..91dc4d677b22 --- /dev/null +++ b/ext/user_cache/tests/user_cache_serializable_interface.phpt @@ -0,0 +1,118 @@ +--TEST-- +UserCache\Cache: legacy Serializable interface honours its serialize()/unserialize() contract +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +value; + } + + public function unserialize(string $data): void + { + self::$unserializeCalls++; + $this->value = (int) $data; + } +} + +$scalar = new LegacyScalar(42); +$cache->store('scalar', $scalar); +$fetched = $cache->fetch('scalar'); +ok('scalar instanceof', $fetched instanceof LegacyScalar); +ok('scalar value', $fetched->value === 42); +ok('scalar unserialize ran once per fetch', LegacyScalar::$unserializeCalls === 1); +ok('scalar parity', serialize($fetched) === serialize($scalar)); + +class Partial implements Serializable +{ + public int $kept = 0; + public int $notRestored = 0; + + public function serialize(): string + { + return (string) $this->kept; + } + + public function unserialize(string $data): void + { + $this->kept = (int) $data; + } +} +$partial = new Partial(); +$partial->kept = 5; +$partial->notRestored = 99; +$cache->store('partial', $partial); +$fetchedPartial = $cache->fetch('partial'); +$nativePartial = unserialize(serialize($partial)); +ok('partial matches native', $fetchedPartial->kept === $nativePartial->kept + && $fetchedPartial->notRestored === $nativePartial->notRestored); + +class LegacyBag implements Serializable +{ + public array $items = []; + + public function serialize(): string + { + return serialize($this->items); + } + + public function unserialize(string $data): void + { + $this->items = unserialize($data); + } +} + +$bag = new LegacyBag(); +$bag->items = ['a' => 1, 'b' => [2, 3], 'when' => new DateTimeImmutable('2026-01-01', new DateTimeZone('UTC'))]; +$cache->store('bag', $bag); +$fetched = $cache->fetch('bag'); +ok('bag instanceof', $fetched instanceof LegacyBag); +ok('bag array', $fetched->items['a'] === 1 && $fetched->items['b'] === [2, 3]); +ok('bag nested object', $fetched->items['when'] instanceof DateTimeImmutable + && $fetched->items['when']->format('Y-m-d') === '2026-01-01'); +ok('bag parity', serialize($fetched) === serialize($bag)); + +$graph = ['first' => new LegacyScalar(7), 'list' => [new LegacyScalar(8), new LegacyScalar(9)]]; +$cache->store('graph', $graph); +$fetched = $cache->fetch('graph'); +ok('graph values', $fetched['first']->value === 7 + && $fetched['list'][0]->value === 8 + && $fetched['list'][1]->value === 9); +ok('graph parity', serialize($fetched) === serialize($graph)); +?> +--EXPECT-- +scalar instanceof: OK +scalar value: OK +scalar unserialize ran once per fetch: OK +scalar parity: OK +partial matches native: OK +bag instanceof: OK +bag array: OK +bag nested object: OK +bag parity: OK +graph values: OK +graph parity: OK diff --git a/ext/user_cache/tests/user_cache_serializable_precedence.phpt b/ext/user_cache/tests/user_cache_serializable_precedence.phpt new file mode 100644 index 000000000000..2dcf07bd963c --- /dev/null +++ b/ext/user_cache/tests/user_cache_serializable_precedence.phpt @@ -0,0 +1,164 @@ +--TEST-- +UserCache\Cache: Serializable interface outranks __sleep/__wakeup/__unserialize (native precedence) +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +kept; + } + + public function unserialize(string $data): void + { + $this->kept = (int) $data; + $this->via = 'iface'; + } + + public function __sleep(): array + { + return ['kept']; + } + + public function __wakeup(): void + { + $this->via = 'sleep'; + } +} + +class PrecedenceSerVsWakeup implements Serializable +{ + public int $kept = 0; + public string $via = 'none'; + + public function serialize(): string + { + return (string) $this->kept; + } + + public function unserialize(string $data): void + { + $this->kept = (int) $data; + $this->via = 'iface'; + } + + public function __wakeup(): void + { + $this->via = 'wakeup'; + } +} + +class PrecedenceSerVsMagicUnserialize implements Serializable +{ + public int $kept = 0; + public string $via = 'none'; + + public function serialize(): string + { + return (string) $this->kept; + } + + public function unserialize(string $data): void + { + $this->kept = (int) $data; + $this->via = 'iface'; + } + + public function __unserialize(array $data): void + { + $this->via = 'magic'; + } +} + +function check_precedence(UserCache\Cache $cache, string $label, object $value): void +{ + $native = unserialize(serialize($value)); + $cache->store($label, $value); + $fetched = $cache->fetch($label); + + ok($label . ' instanceof', $fetched instanceof $value); + ok($label . ' interface won (native parity)', + $fetched->via === $native->via && $fetched->via === 'iface'); + ok($label . ' value', $fetched->kept === $native->kept); +} + +$a = new PrecedenceSerVsSleep(); +$a->kept = 5; +check_precedence($cache, 'PrecedenceSerVsSleep', $a); + +$b = new PrecedenceSerVsWakeup(); +$b->kept = 6; +check_precedence($cache, 'PrecedenceSerVsWakeup', $b); + +$c = new PrecedenceSerVsMagicUnserialize(); +$c->kept = 7; +check_precedence($cache, 'PrecedenceSerVsMagicUnserialize', $c); + +/* __serialize()/__unserialize() take precedence over Serializable. */ +class PrecedenceMagicOverIface implements Serializable +{ + public int $kept = 0; + public string $via = 'none'; + + public function serialize(): string + { + return 'IFACE'; + } + + public function unserialize(string $data): void + { + $this->via = 'iface'; + } + + public function __serialize(): array + { + return ['kept' => $this->kept]; + } + + public function __unserialize(array $data): void + { + $this->kept = $data['kept']; + $this->via = 'magic'; + } +} + +$d = new PrecedenceMagicOverIface(); +$d->kept = 8; +$native = unserialize(serialize($d)); +$cache->store('magic-over-iface', $d); +$fetched = $cache->fetch('magic-over-iface'); +ok('magic outranks interface', $fetched->via === $native->via && $fetched->via === 'magic'); +ok('magic value', $fetched->kept === $native->kept && $fetched->kept === 8); +?> +--EXPECT-- +PrecedenceSerVsSleep instanceof: OK +PrecedenceSerVsSleep interface won (native parity): OK +PrecedenceSerVsSleep value: OK +PrecedenceSerVsWakeup instanceof: OK +PrecedenceSerVsWakeup interface won (native parity): OK +PrecedenceSerVsWakeup value: OK +PrecedenceSerVsMagicUnserialize instanceof: OK +PrecedenceSerVsMagicUnserialize interface won (native parity): OK +PrecedenceSerVsMagicUnserialize value: OK +magic outranks interface: OK +magic value: OK diff --git a/ext/user_cache/tests/user_cache_serializable_value_coverage.phpt b/ext/user_cache/tests/user_cache_serializable_value_coverage.phpt new file mode 100644 index 000000000000..aff5cb651f9c --- /dev/null +++ b/ext/user_cache/tests/user_cache_serializable_value_coverage.phpt @@ -0,0 +1,230 @@ +--TEST-- +UserCache\Cache: every serializable value class round-trips through store/fetch +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=32M +--FILE-- +store($key, $value)) { + echo $label, ": STORE FAILED\n"; + return; + } + + $fetched = $cache->fetch($key); + if ($verify !== null) { + $ok = $verify($value, $fetched); + echo $label, ': ', $ok ? 'OK' : 'MISMATCH', "\n"; + unset($fetched); + gc_collect_cycles(); + return; + } + + $ok = serialize($fetched) === serialize($value); + echo $label, ': ', $ok ? 'OK' : 'MISMATCH', "\n"; + unset($fetched); + gc_collect_cycles(); +} + +check($cache, 'null', null); +check($cache, 'bool', true); +check($cache, 'int', PHP_INT_MAX); +check($cache, 'negative int', PHP_INT_MIN); +check($cache, 'float', 1.5); +check($cache, 'float INF', INF); +check($cache, 'float NAN', NAN); +check($cache, 'empty string', ''); +check($cache, 'binary string', "a\0b\xff\xfe"); +check($cache, 'unicode string', 'こんにちは🌏'); + +check($cache, 'empty array', []); +check($cache, 'packed array', range(1, 64)); +check($cache, 'hashed array', ['a' => 1, 'b' => ['c' => 2.5, 'd' => null], 10 => 'x']); +check($cache, 'binary keys', ["k\0ey" => 1, '0' => 2]); +$deep = 'leaf'; +for ($d = 0; $d < 32; $d++) { + $deep = ['level' => $d, 'inner' => $deep]; +} +check($cache, 'deep array', $deep); +$aliased = ['v' => 1]; +$aliased['alias'] = &$aliased['v']; +check($cache, 'array with references', $aliased); +$selfref = ['x' => 1]; +$selfref['self'] = &$selfref; +check($cache, 'self-referencing array', $selfref, static function ($orig, $fetched) { + /* Argument copying detaches the outer reference; inspect the cycle directly. */ + return $fetched['self']['self']['self']['x'] === 1 && is_array($fetched['self']['self']); +}); + +check($cache, 'stdClass', (object) ['a' => 1, 'nested' => (object) ['b' => 2]]); + +class CoveragePlain +{ + public int $pub = 1; + protected string $prot = 'p'; + private array $priv = ['x']; +} +check($cache, 'plain class with visibility', new CoveragePlain()); + +#[\AllowDynamicProperties] +class CoverageDynamic +{ + public int $declared = 1; +} +$dynamic = new CoverageDynamic(); +$dynamic->added = 'dynamic'; +check($cache, 'dynamic properties', $dynamic); + +class CoverageReadonly +{ + public function __construct(public readonly string $name) {} +} +check($cache, 'readonly property', new CoverageReadonly('ro')); + +class CoverageMagicSerialize +{ + public function __construct(private array $state) {} + public function __serialize(): array { return ['state' => $this->state]; } + public function __unserialize(array $data): void { $this->state = $data['state']; } +} +check($cache, '__serialize/__unserialize', new CoverageMagicSerialize(['k' => [1, 2]])); + +class CoverageSleep +{ + public int $kept = 5; + public string $dropped = 'gone'; + public function __sleep(): array { return ['kept']; } + public function __wakeup(): void {} +} +check($cache, '__sleep/__wakeup', new CoverageSleep()); + +$shared = new stdClass(); +$shared->tag = 'shared'; +check($cache, 'shared object identity', [$shared, $shared], static function ($orig, $fetched) { + return $fetched[0] === $fetched[1] && $fetched[0]->tag === 'shared'; +}); + +class CoverageNode +{ + public ?CoverageNode $parent = null; + /** @var CoverageNode[] */ + public array $children = []; + public function __construct(public string $id) {} +} +$root = new CoverageNode('root'); +$child = new CoverageNode('child'); +$child->parent = $root; +$root->children[] = $child; +check($cache, 'cyclic object graph', $root, static function ($orig, $fetched) { + return $fetched->children[0]->parent === $fetched && $fetched->children[0]->id === 'child'; +}); + +$selfobj = new CoverageNode('self'); +$selfobj->parent = $selfobj; +check($cache, 'self-referencing object', $selfobj, static function ($orig, $fetched) { + return $fetched->parent === $fetched; +}); + +enum CoverageSuit: string +{ + case Hearts = 'h'; + case Spades = 's'; +} +check($cache, 'backed enum case', CoverageSuit::Spades, static function ($orig, $fetched) { + return $fetched === CoverageSuit::Spades; +}); +check($cache, 'enum inside array', ['suit' => CoverageSuit::Hearts], static function ($orig, $fetched) { + return $fetched['suit'] === CoverageSuit::Hearts; +}); + +check($cache, 'DateTimeImmutable', new DateTimeImmutable('2026-07-02 12:00:00', new DateTimeZone('Asia/Tokyo'))); +check($cache, 'DateInterval', new DateInterval('P1DT2H')); +check($cache, 'ArrayObject', new ArrayObject(['a' => 1, 'b' => 2])); +check($cache, 'SplStack', (static function () { + $s = new SplStack(); + $s->push('one'); + $s->push('two'); + return $s; +})()); +check($cache, 'SplFixedArray', SplFixedArray::fromArray([1, 'two', 3.0])); +check($cache, 'SplObjectStorage', (static function () { + $s = new SplObjectStorage(); + $o = new stdClass(); + $o->v = 1; + $s[$o] = 'data'; + return $s; +})(), static function ($orig, $fetched) { + if (!$fetched instanceof SplObjectStorage || count($fetched) !== 1) { + return false; + } + $fetched->rewind(); + return $fetched->current()->v === 1 && $fetched->getInfo() === 'data'; +}); +check($cache, 'Randomizer engine object', new Random\Randomizer(new Random\Engine\Xoshiro256StarStar(1234)), static function ($orig, $fetched) { + return $fetched->getInt(0, PHP_INT_MAX) === $orig->getInt(0, PHP_INT_MAX); +}); + +check($cache, 'mixed payload', [ + 'config' => ['ttl' => 300, 'flags' => [true, false]], + 'entity' => new CoverageMagicSerialize(['id' => 7]), + 'sleeper' => new CoverageSleep(), + 'when' => new DateTimeImmutable('2026-01-01', new DateTimeZone('UTC')), + 'suit' => CoverageSuit::Hearts, +], static function ($orig, $fetched) { + return $fetched['config']['ttl'] === 300 + && $fetched['entity'] instanceof CoverageMagicSerialize + && $fetched['sleeper'] instanceof CoverageSleep + && $fetched['when'] instanceof DateTimeImmutable + && $fetched['suit'] === CoverageSuit::Hearts; +}); + +unset($deep, $aliased, $selfref, $dynamic, $shared, $root, $child, $selfobj); +gc_collect_cycles(); +?> +--EXPECT-- +null: OK +bool: OK +int: OK +negative int: OK +float: OK +float INF: OK +float NAN: OK +empty string: OK +binary string: OK +unicode string: OK +empty array: OK +packed array: OK +hashed array: OK +binary keys: OK +deep array: OK +array with references: OK +self-referencing array: OK +stdClass: OK +plain class with visibility: OK +dynamic properties: OK +readonly property: OK +__serialize/__unserialize: OK +__sleep/__wakeup: OK +shared object identity: OK +cyclic object graph: OK +self-referencing object: OK +backed enum case: OK +enum inside array: OK +DateTimeImmutable: OK +DateInterval: OK +ArrayObject: OK +SplStack: OK +SplFixedArray: OK +SplObjectStorage: OK +Randomizer engine object: OK +mixed payload: OK diff --git a/ext/user_cache/tests/user_cache_serialize_only.phpt b/ext/user_cache/tests/user_cache_serialize_only.phpt new file mode 100644 index 000000000000..f680cccfbeaf --- /dev/null +++ b/ext/user_cache/tests/user_cache_serialize_only.phpt @@ -0,0 +1,169 @@ +--TEST-- +UserCache\Cache: __serialize() without __unserialize() restores by property assignment +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +kept = $kept; + $this->dropped = 'set'; + } + + public function __serialize(): array + { + return ['kept' => $this->kept]; + } +} + +$value = new SerializeOnly(5); +$cache->store('basic', $value); +$fetched = $cache->fetch('basic'); +ok('basic instanceof', $fetched instanceof SerializeOnly); +ok('basic kept restored', $fetched->kept === 5); +ok('basic omitted reverts to default', $fetched->dropped === 'default'); +ok('basic parity', serialize($fetched) === serialize($value)); + +class SerializeOnlyWakeful +{ + public static int $wakeupCalls = 0; + public int $v = 0; + + public function __construct(int $v) + { + $this->v = $v; + } + + public function __serialize(): array + { + return ['v' => $this->v]; + } + + public function __wakeup(): void + { + self::$wakeupCalls++; + } +} + +$cache->store('wakeful', new SerializeOnlyWakeful(9)); +$first = $cache->fetch('wakeful'); +$second = $cache->fetch('wakeful'); +ok('wakeful value', $first->v === 9 && $second->v === 9); +ok('wakeful __wakeup per fetch', SerializeOnlyWakeful::$wakeupCalls === 2); +ok('wakeful distinct instances', $first !== $second); + +class SerializeOnlyRich +{ + public int $num; + private string $secret; + public ?object $child; + + public function __construct() + { + $this->num = 1; + $this->secret = 'hidden'; + $this->child = null; + } + + public function __serialize(): array + { + return [ + 'num' => 42, + "\0" . self::class . "\0secret" => 'restored', + 'child' => (object) ['deep' => [1, 2, 3]], + ]; + } + + public function reveal(): string + { + return $this->secret; + } +} + +$rich = new SerializeOnlyRich(); +$cache->store('rich', $rich); +$fetchedRich = $cache->fetch('rich'); +ok('rich typed', $fetchedRich->num === 42); +ok('rich private', $fetchedRich->reveal() === 'restored'); +ok('rich nested object', $fetchedRich->child instanceof stdClass && $fetchedRich->child->deep === [1, 2, 3]); +ok('rich parity', serialize($fetchedRich) === serialize($rich)); + +$graph = ['a' => new SerializeOnly(1), 'list' => [new SerializeOnly(2), new SerializeOnly(3)]]; +$cache->store('graph', $graph); +$fetchedGraph = $cache->fetch('graph'); +ok('graph values', $fetchedGraph['a']->kept === 1 + && $fetchedGraph['list'][0]->kept === 2 + && $fetchedGraph['list'][1]->kept === 3); +ok('graph parity', serialize($fetchedGraph) === serialize($graph)); + +#[\AllowDynamicProperties] +class SerializeOnlyIntKeys +{ + public int $declared = 1; + + public function __serialize(): array + { + return ['declared' => 7, 42 => 'answer', 'label' => 'x']; + } +} +$intKeys = new SerializeOnlyIntKeys(); +$cache->store('intkeys', $intKeys); +$fetchedIntKeys = $cache->fetch('intkeys'); +ok('integer key parity', serialize($fetchedIntKeys) === serialize(unserialize(serialize($intKeys)))); +ok('integer key property', $fetchedIntKeys->declared === 7 && $fetchedIntKeys->{42} === 'answer'); + +class SerializeOnlyResource +{ + public $handle; + + public function __serialize(): array + { + return ['handle' => $this->handle]; + } +} + +$withResource = new SerializeOnlyResource(); +$withResource->handle = fopen(__FILE__, 'r'); +try { + $cache->store('resource', $withResource); + ok('resource rejected', false); +} catch (TypeError $e) { + ok('resource rejected', true); +} +fclose($withResource->handle); +?> +--EXPECT-- +basic instanceof: OK +basic kept restored: OK +basic omitted reverts to default: OK +basic parity: OK +wakeful value: OK +wakeful __wakeup per fetch: OK +wakeful distinct instances: OK +rich typed: OK +rich private: OK +rich nested object: OK +rich parity: OK +graph values: OK +graph parity: OK +integer key parity: OK +integer key property: OK +resource rejected: OK diff --git a/ext/user_cache/tests/user_cache_shaped_array_state.phpt b/ext/user_cache/tests/user_cache_shaped_array_state.phpt new file mode 100644 index 000000000000..296f5025fce1 --- /dev/null +++ b/ext/user_cache/tests/user_cache_shaped_array_state.phpt @@ -0,0 +1,127 @@ +--TEST-- +UserCache\Cache: shaped associative state arrays preserve unserialize semantics +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +id = $id; + $this->label = $label; + } + + public function __serialize(): array + { + return [ + 'id' => $this->id, + 'label' => $this->label, + 'nested' => [ + 'id' => $this->id, + 'label' => $this->label, + 'checksum' => $this->id + strlen($this->label), + ], + ]; + } + + public function __unserialize(array $data): void + { + $GLOBALS['shaped_array_state_unserialize_count']++; + self::$events[] = array_keys($data); + $this->id = $data['id']; + $this->label = $data['label']; + $this->checksum = $data['nested']['checksum']; + } +} + +$GLOBALS['shaped_array_state_unserialize_count'] = 0; + +$cache = UserCache\Cache::getPool('shaped-array-state'); +$payload = [ + new ShapedArrayStatePayload(10, 'first'), + new ShapedArrayStatePayload(20, 'second'), + new ShapedArrayStatePayload(30, 'third'), +]; + +var_dump($cache->store('payload', $payload)); + +$first = $cache->fetch('payload'); +$second = $cache->fetch('payload'); + +echo $GLOBALS['shaped_array_state_unserialize_count'], "\n"; +echo $first[0]->checksum, ",", $first[1]->checksum, ",", $first[2]->checksum, "\n"; +echo $second[0]->checksum, ",", $second[1]->checksum, ",", $second[2]->checksum, "\n"; +var_dump(ShapedArrayStatePayload::$events); +?> +--EXPECT-- +bool(true) +6 +15,26,35 +15,26,35 +array(6) { + [0]=> + array(3) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + } + [1]=> + array(3) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + } + [2]=> + array(3) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + } + [3]=> + array(3) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + } + [4]=> + array(3) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + } + [5]=> + array(3) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + } +} diff --git a/ext/user_cache/tests/user_cache_shared_graph.phpt b/ext/user_cache/tests/user_cache_shared_graph.phpt new file mode 100644 index 000000000000..43a77b8a5607 --- /dev/null +++ b/ext/user_cache/tests/user_cache_shared_graph.phpt @@ -0,0 +1,40 @@ +--TEST-- +UserCache\Cache: classes declaring both magic methods use their serialization contract +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + 99]; + } + + public function __unserialize(array $data): void { + self::$calls += 10; + $this->value = $data['value']; + } +} + +$object = new MagicUserCacheObject(); +var_dump($cache->store('object', $object)); +$fetched = $cache->fetch('object'); +var_dump($fetched instanceof MagicUserCacheObject); +var_dump($fetched->value); +var_dump(MagicUserCacheObject::$calls); +?> +--EXPECT-- +bool(true) +bool(true) +int(99) +int(11) diff --git a/ext/user_cache/tests/user_cache_shared_graph_dag_dedup.phpt b/ext/user_cache/tests/user_cache_shared_graph_dag_dedup.phpt new file mode 100644 index 000000000000..e5320a6ee41d --- /dev/null +++ b/ext/user_cache/tests/user_cache_shared_graph_dag_dedup.phpt @@ -0,0 +1,51 @@ +--TEST-- +UserCache\Cache: repeated arrays in a DAG round-trip with copy-on-write isolation +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + str_repeat('q', 64), + 'list' => range(1, 32), + 'nested' => ['deep' => 'value'], +]; + +$dag = [ + 'first' => $shared, + 'second' => $shared, + 'third' => $shared, + 'wrapped' => [$shared, $shared], +]; + +var_dump($cache->store('dag', $dag)); +$fetched = $cache->fetch('dag'); +var_dump($fetched === $dag); + +$fetched['first']['nested']['deep'] = 'mutated'; +var_dump($fetched['second']['nested']['deep']); +var_dump($fetched['wrapped'][1] === $shared); + +$pair = [['k' => 'v'], ['k' => 'v']]; +var_dump($cache->store('pair', $pair)); +var_dump($cache->fetch('pair') === $pair); + +$dag['third']['extra'] = 'tail'; +var_dump($cache->store('dag', $dag)); +var_dump($cache->fetch('dag') === $dag); +?> +--EXPECT-- +bool(true) +bool(true) +string(5) "value" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt b/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt new file mode 100644 index 000000000000..43bb0800d00e --- /dev/null +++ b/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt @@ -0,0 +1,17 @@ +--TEST-- +UserCache\Cache: shm size clamp emits a PHP warning +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +opcache.log_verbosity_level=1 +user_cache.shm_size=8192M +--FILE-- +getConfiguredMemory() > 0); +?> +--EXPECTF-- +Warning: user_cache.shm_size is limited to slightly under 4096M; clamping in Unknown on line 0 +bool(true) diff --git a/ext/user_cache/tests/user_cache_shm_size_runtime_immutable.phpt b/ext/user_cache/tests/user_cache_shm_size_runtime_immutable.phpt new file mode 100644 index 000000000000..7d7f54619d05 --- /dev/null +++ b/ext/user_cache/tests/user_cache_shm_size_runtime_immutable.phpt @@ -0,0 +1,19 @@ +--TEST-- +UserCache\Cache: user_cache.shm_size cannot be changed at runtime +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +getConfiguredMemory()); +?> +--EXPECT-- +bool(false) +string(3) "16M" +int(16777216) diff --git a/ext/user_cache/tests/user_cache_shm_size_units.phpt b/ext/user_cache/tests/user_cache_shm_size_units.phpt new file mode 100644 index 000000000000..d81d219124a2 --- /dev/null +++ b/ext/user_cache/tests/user_cache_shm_size_units.phpt @@ -0,0 +1,15 @@ +--TEST-- +UserCache\Cache: shm size directive accepts PHP quantity syntax +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16 +--FILE-- +getConfiguredMemory()); +?> +--EXPECT-- +int(16) diff --git a/ext/user_cache/tests/user_cache_sleep_shared_graph.phpt b/ext/user_cache/tests/user_cache_sleep_shared_graph.phpt new file mode 100644 index 000000000000..df00f6f419cc --- /dev/null +++ b/ext/user_cache/tests/user_cache_sleep_shared_graph.phpt @@ -0,0 +1,138 @@ +--TEST-- +UserCache\Cache: __sleep() objects restore from shared graph state +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +protectedKept = $protectedKept; + $this->privateKept = $privateKept; + } + + public function __sleep(): array + { + self::$sleepCalls++; + return ['kept', 'protectedKept', 'privateKept', 'self']; + } + + public function __wakeup(): void + { + self::$wakeupCalls++; + $this->kept .= ':awake'; + } + + public function describe(): string + { + return $this->kept . '/' . + $this->dropped . '/' . + $this->protectedKept . '/' . + count($this->privateKept) . '/' . + ($this->self === $this ? 'self' : 'other'); + } +} + +$box = new UCSleepSharedGraphBox(); +$box->kept = 'stored-kept'; +$box->dropped = 'stored-dropped'; +$box->setHidden(7, ['x', 'y']); +$box->self = $box; + +var_dump($cache->store('box', $box)); +var_dump(UCSleepSharedGraphBox::$sleepCalls); + +$first = $cache->fetch('box'); +var_dump($first instanceof UCSleepSharedGraphBox); +var_dump(UCSleepSharedGraphBox::$wakeupCalls); +var_dump($first->describe()); + +$second = $cache->fetch('box'); +var_dump(UCSleepSharedGraphBox::$wakeupCalls); +var_dump($second !== $first); +var_dump($second->self === $second); + +class UCSleepSharedGraphReadonly +{ + public function __construct(public readonly string $token) + { + } + + public function __sleep(): array + { + return ['token']; + } + + public function __wakeup(): void + { + } +} + +var_dump($cache->store('readonly', new UCSleepSharedGraphReadonly('tok-1'))); +var_dump($cache->fetch('readonly')->token); + +class UCSleepSharedGraphWakeupMutates +{ + public array $items = [1]; + + public function __sleep(): array + { + return ['items']; + } + + public function __wakeup(): void + { + $this->items[] = 2; + } +} + +var_dump($cache->store('wakeup-mutates', new UCSleepSharedGraphWakeupMutates())); +$mutated = $cache->fetch('wakeup-mutates'); +var_dump($mutated->items); +$mutated->items[] = 3; +var_dump($cache->fetch('wakeup-mutates')->items); + +unset($box, $first, $second, $mutated); +gc_collect_cycles(); +?> +--EXPECT-- +bool(true) +int(1) +bool(true) +int(1) +string(42) "stored-kept:awake/default-dropped/7/2/self" +int(2) +bool(true) +bool(true) +bool(true) +string(5) "tok-1" +bool(true) +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} diff --git a/ext/user_cache/tests/user_cache_spl_object_storage.phpt b/ext/user_cache/tests/user_cache_spl_object_storage.phpt new file mode 100644 index 000000000000..3ee7305d2f09 --- /dev/null +++ b/ext/user_cache/tests/user_cache_spl_object_storage.phpt @@ -0,0 +1,107 @@ +--TEST-- +UserCache\Cache: SplObjectStorage round-trips entries, data, and shared identity +--EXTENSIONS-- +user_cache +spl +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +id = 1; +$b = new stdClass(); +$b->id = 2; +$c = new stdClass(); +$c->id = 3; + +$storage = new SplObjectStorage(); +$storage[$a] = 'scalar-data'; +$storage[$b] = ['nested' => [1, 2, 3], 'flag' => true]; +$storage[$c] = (object) ['tag' => 'object-data']; + +$cache->store('storage', $storage); +$fetched = $cache->fetch('storage'); + +ok('instanceof', $fetched instanceof SplObjectStorage); +ok('count', count($fetched) === 3); + +$byId = []; +foreach ($fetched as $object) { + $byId[$object->id] = $fetched->getInfo(); +} +ok('data preserved', $byId[1] === 'scalar-data' + && $byId[2] === ['nested' => [1, 2, 3], 'flag' => true] + && $byId[3] instanceof stdClass && $byId[3]->tag === 'object-data'); +ok('parity', serialize($fetched) === serialize($storage)); + +$shared = new stdClass(); +$shared->id = 100; +$sharedStorage = new SplObjectStorage(); +$sharedStorage[$shared] = 'info'; +$graph = ['storage' => $sharedStorage, 'also' => $shared]; + +$cache->store('graph', $graph); +$fetchedGraph = $cache->fetch('graph'); +$fetchedGraph['storage']->rewind(); +$keyObject = $fetchedGraph['storage']->current(); +ok('shared key identity', $keyObject === $fetchedGraph['also']); +$fetchedGraph['also']->id = 200; +ok('shared mutation follows', $keyObject->id === 200); + +$empty = new SplObjectStorage(); +$cache->store('empty', $empty); +ok('empty storage', count($cache->fetch('empty')) === 0); + +class TaggedStorage extends SplObjectStorage +{ + public string $label = 'default'; +} +$tagged = new TaggedStorage(); +$tagged->label = 'tagged'; +$element = new stdClass(); +$element->id = 5; +$tagged[$element] = 'x'; +$cache->store('tagged', $tagged); +$fetchedTagged = $cache->fetch('tagged'); +ok('subclass instanceof', $fetchedTagged instanceof TaggedStorage); +ok('subclass property', $fetchedTagged->label === 'tagged' && count($fetchedTagged) === 1); +ok('subclass parity', serialize($fetchedTagged) === serialize($tagged)); + +$inner = new SplObjectStorage(); +$innerKey = new stdClass(); +$innerKey->id = 7; +$inner[$innerKey] = 'inner'; +$outer = new SplObjectStorage(); +$outerKey = new stdClass(); +$outerKey->id = 8; +$outer[$outerKey] = $inner; +$cache->store('nested', $outer); +$fetchedNested = $cache->fetch('nested'); +$fetchedNested->rewind(); +$nestedInfo = $fetchedNested->getInfo(); +ok('nested storage', $nestedInfo instanceof SplObjectStorage && count($nestedInfo) === 1); +ok('nested parity', serialize($fetchedNested) === serialize($outer)); +?> +--EXPECT-- +instanceof: OK +count: OK +data preserved: OK +parity: OK +shared key identity: OK +shared mutation follows: OK +empty storage: OK +subclass instanceof: OK +subclass property: OK +subclass parity: OK +nested storage: OK +nested parity: OK diff --git a/ext/user_cache/tests/user_cache_spl_safe_direct.phpt b/ext/user_cache/tests/user_cache_spl_safe_direct.phpt new file mode 100644 index 000000000000..3f9511aca537 --- /dev/null +++ b/ext/user_cache/tests/user_cache_spl_safe_direct.phpt @@ -0,0 +1,397 @@ +--TEST-- +UserCache\Cache: SPL safe-direct state is restored +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + parent::__unserialize($data['payload']); + } +} + +class UserCacheSerializedStack extends SplStack +{ + public static int $serializeCalls = 0; + public static int $unserializeCalls = 0; + + public function __serialize(): array + { + self::$serializeCalls++; + + return parent::__serialize(); + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + parent::__unserialize($data); + } +} + +class UserCacheTaggedFixedArray extends SplFixedArray +{ + private string $tag; + protected int $version; + + public function __construct(int $size, string $tag, int $version) + { + parent::__construct($size); + $this->tag = $tag; + $this->version = $version; + } + + public function describe(): string + { + return $this->tag . ':' . $this->version; + } +} + +class UserCacheLabelIterator extends ArrayIterator +{ +} + +class UserCacheTaggedCollection extends ArrayObject +{ + private string $type; + + public function __construct(array $data, string $type, string $iteratorClass) + { + parent::__construct($data, 0, $iteratorClass); + $this->type = $type; + } + + public function type(): string + { + return $this->type; + } +} + +class UserCacheTaggedIterator extends ArrayIterator +{ + private string $label; + + public function __construct(array $data, string $label) + { + parent::__construct($data); + $this->label = $label; + } + + public function label(): string + { + return $this->label; + } +} + +class UserCacheTaggedRecursiveIterator extends RecursiveArrayIterator +{ + private string $name; + + public function __construct(array $data, string $name) + { + parent::__construct($data); + $this->name = $name; + } + + public function name(): string + { + return $this->name; + } +} + +class UserCacheCountingMaxHeap extends SplMaxHeap +{ + public static int $compareCalls = 0; + + protected function compare(mixed $a, mixed $b): int + { + self::$compareCalls++; + + return $a['priority'] <=> $b['priority']; + } +} + +$arrayObject = new ArrayObject(['a' => 1, 'b' => ['c' => 2]], ArrayObject::ARRAY_AS_PROPS); +$arrayObject->extra = 'prop'; + +$arrayIterator = new ArrayIterator(['x' => 10, 'y' => 20]); +$recursiveArrayIterator = new RecursiveArrayIterator(['nested' => ['leaf' => 30]]); + +$fixed = SplFixedArray::fromArray(['zero', 'one', ['two']], false); + +$taggedFixed = new UserCacheTaggedFixedArray(3, 'vec', 7); +$taggedFixed[0] = 'a'; +$taggedFixed[1] = ['nested' => 1]; +$taggedFixed[2] = 42; + +$taggedCollection = new UserCacheTaggedCollection(['alpha' => 10, 'beta' => 20], 'metric', UserCacheLabelIterator::class); +$taggedIterator = new UserCacheTaggedIterator([3, 5, 8], 'fib'); +$taggedRecursiveIterator = new UserCacheTaggedRecursiveIterator(['leaf' => ['value' => 99]], 'tree'); + +$dll = new SplDoublyLinkedList(); +$dll->setIteratorMode(SplDoublyLinkedList::IT_MODE_FIFO); +$dll->push('first'); +$dll->push('second'); + +$queue = new SplQueue(); +$queue->enqueue('q1'); +$queue->enqueue('q2'); + +$stack = new SplStack(); +$stack->push('s1'); +$stack->push('s2'); + +$min = new SplMinHeap(); +$min->insert(3); +$min->insert(1); +$min->insert(2); + +$max = new SplMaxHeap(); +$max->insert(3); +$max->insert(1); +$max->insert(2); + +$pq = new SplPriorityQueue(); +$pq->setExtractFlags(SplPriorityQueue::EXTR_BOTH); +$pq->insert('low', 1); +$pq->insert('high', 10); + +$countingMaxHeap = new UserCacheCountingMaxHeap(); +$countingMaxHeap->insert(['priority' => 1, 'node' => (object) ['score' => 61]]); +$countingMaxHeap->insert(['priority' => 2, 'node' => (object) ['score' => 67]]); +UserCacheCountingMaxHeap::$compareCalls = 0; + +$serializedArrayObject = new UserCacheSerializedArrayObject(['x' => 1]); + +$serializedStack = new UserCacheSerializedStack(); +$serializedStack->push('fallback'); + +$payload = compact( + 'arrayObject', + 'arrayIterator', + 'recursiveArrayIterator', + 'fixed', + 'taggedFixed', + 'taggedCollection', + 'taggedIterator', + 'taggedRecursiveIterator', + 'dll', + 'queue', + 'stack', + 'min', + 'max', + 'pq', + 'countingMaxHeap', + 'serializedArrayObject', + 'serializedStack' +); + +var_dump($cache->store('spl', $payload)); + +$dirty = $cache->fetch('spl'); +$dirty['arrayObject']['a'] = 999; +$clean = $cache->fetch('spl'); +var_dump($clean['arrayObject']['a']); + +$fetched = $cache->fetch('spl'); + +var_dump($fetched['arrayObject'] instanceof ArrayObject); +var_dump($fetched['arrayObject']['b']['c']); +var_dump($fetched['arrayObject']->extra); + +var_dump($fetched['arrayIterator'] instanceof ArrayIterator); +var_dump(iterator_to_array($fetched['arrayIterator'])); + +var_dump($fetched['recursiveArrayIterator'] instanceof RecursiveArrayIterator); +var_dump($fetched['recursiveArrayIterator']->hasChildren()); + +var_dump($fetched['fixed'] instanceof SplFixedArray); +var_dump($fetched['fixed']->getSize()); +var_dump($fetched['fixed'][2][0]); + +var_dump($fetched['taggedFixed'] instanceof UserCacheTaggedFixedArray); +var_dump($fetched['taggedFixed']->getSize()); +var_dump($fetched['taggedFixed'][0]); +var_dump($fetched['taggedFixed'][1]['nested']); +var_dump($fetched['taggedFixed'][2]); +var_dump($fetched['taggedFixed']->describe()); + +$taggedCollectionIterator = $fetched['taggedCollection']->getIterator(); +var_dump($fetched['taggedCollection'] instanceof UserCacheTaggedCollection); +var_dump($taggedCollectionIterator instanceof UserCacheLabelIterator); +var_dump($fetched['taggedCollection']['alpha']); +var_dump($fetched['taggedCollection']['beta']); +var_dump($fetched['taggedCollection']->type()); + +$fetched['taggedIterator']->rewind(); +var_dump($fetched['taggedIterator'] instanceof UserCacheTaggedIterator); +var_dump($fetched['taggedIterator']->count()); +var_dump($fetched['taggedIterator']->current()); +var_dump($fetched['taggedIterator']->label()); + +$fetched['taggedRecursiveIterator']->rewind(); +var_dump($fetched['taggedRecursiveIterator'] instanceof UserCacheTaggedRecursiveIterator); +var_dump($fetched['taggedRecursiveIterator']->count()); +var_dump($fetched['taggedRecursiveIterator']->hasChildren()); +var_dump($fetched['taggedRecursiveIterator']->name()); + +var_dump($fetched['dll'] instanceof SplDoublyLinkedList); +var_dump(iterator_to_array($fetched['dll'], false)); + +var_dump($fetched['queue'] instanceof SplQueue); +var_dump($fetched['queue']->dequeue()); +var_dump($fetched['queue']->dequeue()); + +var_dump($fetched['stack'] instanceof SplStack); +var_dump($fetched['stack']->pop()); +var_dump($fetched['stack']->pop()); + +var_dump($fetched['min'] instanceof SplMinHeap); +$minOut = []; +while (!$fetched['min']->isEmpty()) { + $minOut[] = $fetched['min']->extract(); +} +var_dump($minOut); + +var_dump($fetched['max'] instanceof SplMaxHeap); +$maxOut = []; +while (!$fetched['max']->isEmpty()) { + $maxOut[] = $fetched['max']->extract(); +} +var_dump($maxOut); + +var_dump($fetched['pq'] instanceof SplPriorityQueue); +var_dump($fetched['pq']->extract()); +var_dump($fetched['pq']->extract()); + +var_dump($fetched['countingMaxHeap'] instanceof UserCacheCountingMaxHeap); +var_dump($fetched['countingMaxHeap']->top()['node']->score); +var_dump(UserCacheCountingMaxHeap::$compareCalls); + +var_dump($fetched['serializedArrayObject'] instanceof UserCacheSerializedArrayObject); +var_dump($fetched['serializedArrayObject']['x']); +var_dump(UserCacheSerializedArrayObject::$serializeCalls); +var_dump(UserCacheSerializedArrayObject::$unserializeCalls); + +var_dump($fetched['serializedStack'] instanceof UserCacheSerializedStack); +var_dump($fetched['serializedStack'][0]); +var_dump(UserCacheSerializedStack::$serializeCalls); +var_dump(UserCacheSerializedStack::$unserializeCalls); +?> +--EXPECT-- +bool(true) +int(1) +bool(true) +int(2) +string(4) "prop" +bool(true) +array(2) { + ["x"]=> + int(10) + ["y"]=> + int(20) +} +bool(true) +bool(true) +bool(true) +int(3) +string(3) "two" +bool(true) +int(3) +string(1) "a" +int(1) +int(42) +string(5) "vec:7" +bool(true) +bool(true) +int(10) +int(20) +string(6) "metric" +bool(true) +int(3) +int(3) +string(3) "fib" +bool(true) +int(1) +bool(true) +string(4) "tree" +bool(true) +array(2) { + [0]=> + string(5) "first" + [1]=> + string(6) "second" +} +bool(true) +string(2) "q1" +string(2) "q2" +bool(true) +string(2) "s2" +string(2) "s1" +bool(true) +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +bool(true) +array(3) { + [0]=> + int(3) + [1]=> + int(2) + [2]=> + int(1) +} +bool(true) +array(2) { + ["data"]=> + string(4) "high" + ["priority"]=> + int(10) +} +array(2) { + ["data"]=> + string(3) "low" + ["priority"]=> + int(1) +} +bool(true) +int(67) +int(0) +bool(true) +int(1) +int(1) +int(3) +bool(true) +string(8) "fallback" +int(1) +int(3) diff --git a/ext/user_cache/tests/user_cache_state_schema_prototype.phpt b/ext/user_cache/tests/user_cache_state_schema_prototype.phpt new file mode 100644 index 000000000000..3a1f914118dd --- /dev/null +++ b/ext/user_cache/tests/user_cache_state_schema_prototype.phpt @@ -0,0 +1,202 @@ +--TEST-- +UserCache\Cache: class-scoped state schema and shape prototypes preserve magic semantics +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + $this->id, + 'label' => $this->label, + 'nested' => [ + 'label' => $this->label, + 'checksum' => $this->id + strlen($this->label), + ], + ]; + } + + public function __unserialize(array $data): void + { + $id = $data['id']; + $label = $data['label']; + + self::$unserializeCalls++; + $data['id'] = -1; + $data[] = 'local-only'; + self::$events[] = [array_keys($data), array_key_last($data)]; + + $this->id = $id; + $this->label = $label . ':' . $data['nested']['checksum']; + } + + public function label(): string + { + return $this->id . ':' . $this->label; + } +} + +class UserCacheSchemaA +{ + public function __construct(private int $value) + { + } + + public function __serialize(): array + { + return ['value' => $this->value, 'tag' => 'a']; + } + + public function __unserialize(array $data): void + { + $this->value = $data['value'] + 10; + } + + public function value(): int + { + return $this->value; + } +} + +class UserCacheSchemaB +{ + public function __construct(private int $value) + { + } + + public function __serialize(): array + { + return ['value' => $this->value, 'tag' => 'b']; + } + + public function __unserialize(array $data): void + { + $this->value = $data['value'] + 20; + } + + public function value(): int + { + return $this->value; + } +} + +class UserCacheSchemaSelfState +{ + public static int $selfMatches = 0; + + public function __construct(private string $name) + { + } + + public function __serialize(): array + { + return ['self' => $this, 'name' => $this->name]; + } + + public function __unserialize(array $data): void + { + if ($data['self'] === $this) { + self::$selfMatches++; + } + + $this->name = $data['name']; + } + + public function name(): string + { + return $this->name; + } +} + +$cache = UserCache\Cache::getPool('state-schema-prototype'); + +var_dump($cache->store('payload', [ + new UserCacheSchemaPayload(1, 'one'), + new UserCacheSchemaPayload(2, 'two'), + new UserCacheSchemaPayload(3, 'three'), +])); + +$first = $cache->fetch('payload'); +$second = $cache->fetch('payload'); + +echo UserCacheSchemaPayload::$serializeCalls, ',', UserCacheSchemaPayload::$unserializeCalls, "\n"; +echo $first[0]->label(), '|', $first[1]->label(), '|', $first[2]->label(), "\n"; +echo $second[0]->label(), '|', $second[1]->label(), '|', $second[2]->label(), "\n"; +var_dump(UserCacheSchemaPayload::$events[0]); +var_dump(UserCacheSchemaPayload::$events[5]); + +$sharedPayload = new UserCacheSchemaPayload(4, 'four'); +var_dump($cache->store('shared-payload', [$sharedPayload, $sharedPayload])); +$sharedPayload = $cache->fetch('shared-payload'); +var_dump($sharedPayload[0] === $sharedPayload[1]); +echo $sharedPayload[0]->label(), '|', $sharedPayload[1]->label(), "\n"; + +var_dump($cache->store('classes', [new UserCacheSchemaA(5), new UserCacheSchemaB(5)])); +$classes = $cache->fetch('classes'); +echo get_class($classes[0]), ':', $classes[0]->value(), "\n"; +echo get_class($classes[1]), ':', $classes[1]->value(), "\n"; + +var_dump($cache->store('self', new UserCacheSchemaSelfState('kept'))); +$self = $cache->fetch('self'); +echo $self->name(), ',', UserCacheSchemaSelfState::$selfMatches, "\n"; +?> +--EXPECT-- +bool(true) +3,6 +1:one:4|2:two:5|3:three:8 +1:one:4|2:two:5|3:three:8 +array(2) { + [0]=> + array(4) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + [3]=> + int(0) + } + [1]=> + int(0) +} +array(2) { + [0]=> + array(4) { + [0]=> + string(2) "id" + [1]=> + string(5) "label" + [2]=> + string(6) "nested" + [3]=> + int(0) + } + [1]=> + int(0) +} +bool(true) +bool(true) +4:four:8|4:four:8 +bool(true) +UserCacheSchemaA:15 +UserCacheSchemaB:25 +bool(true) +kept,1 diff --git a/ext/user_cache/tests/user_cache_status_objects.phpt b/ext/user_cache/tests/user_cache_status_objects.phpt new file mode 100644 index 000000000000..97848b01490d --- /dev/null +++ b/ext/user_cache/tests/user_cache_status_objects.phpt @@ -0,0 +1,83 @@ +--TEST-- +UserCache\Cache: status objects +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +getPoolStatus(); + +var_dump($status instanceof UserCache\CacheStatus); +var_dump($poolStatus instanceof UserCache\CachePoolStatus); +var_dump($status->getAvailability() === UserCache\CacheAvailability::Available); +var_dump($poolStatus->getPoolName()); +var_dump($poolStatus->getEntryCount()); +var_dump($poolStatus->getEntryKeys()); +var_dump($poolStatus->getUsedMemory()); + +var_dump($cache->store('a', 1)); +var_dump($cache->store('b', 2)); + +$other = UserCache\Cache::getPool('status-objects-other'); +var_dump($other->store('a', 3)); + +$status = UserCache\Cache::getStatus(); +$poolStatus = $cache->getPoolStatus(); +$otherStatus = $other->getPoolStatus(); + +var_dump($status->getEntryCount() >= 3); +var_dump($poolStatus->getEntryCount()); +var_dump($otherStatus->getEntryCount()); +$keys = $poolStatus->getEntryKeys(); +sort($keys); +var_dump($keys); +var_dump($poolStatus->getUsedMemory() > 0); + +foreach ([$status, $poolStatus] as $object) { + try { + serialize($object); + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; + } +} + +try { + $status->entryCount = 0; +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +bool(true) +bool(true) +bool(true) +string(14) "status-objects" +int(0) +array(0) { +} +int(0) +bool(true) +bool(true) +bool(true) +bool(true) +int(2) +int(1) +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" +} +bool(true) +Exception: Serialization of 'UserCache\CacheStatus' is not allowed +Exception: Serialization of 'UserCache\CachePoolStatus' is not allowed +Error: Cannot create dynamic property UserCache\CacheStatus::$entryCount diff --git a/ext/user_cache/tests/user_cache_storable_values.phpt b/ext/user_cache/tests/user_cache_storable_values.phpt new file mode 100644 index 000000000000..e5b373d17ea4 --- /dev/null +++ b/ext/user_cache/tests/user_cache_storable_values.phpt @@ -0,0 +1,150 @@ +--TEST-- +UserCache\Cache: storable values use shared graph and opaque values are refused +--EXTENSIONS-- +user_cache +spl +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +store('storable_' . $i, $value) ? 'stored' : 'REFUSED'; + } catch (TypeError $e) { + return 'TypeError'; + } +} + +echo "array: ", user_cache_storable_tier($cache, ['a' => 1, 'b' => [2, 3]]), "\n"; +echo "stdClass: ", user_cache_storable_tier($cache, (object) ['x' => 1, 'y' => [2]]), "\n"; +echo "json object: ", user_cache_storable_tier($cache, json_decode('{"a":1,"b":{"c":2}}')), "\n"; +echo "plain object: ", user_cache_storable_tier($cache, new UserCacheStorablePlain()), "\n"; +echo "DateTime: ", user_cache_storable_tier($cache, new DateTime('2026-01-01')), "\n"; +echo "DateInterval: ", user_cache_storable_tier($cache, new DateInterval('P1D')), "\n"; +echo "ArrayObject: ", user_cache_storable_tier($cache, new ArrayObject([1, 2, 3])), "\n"; +echo "SplStack: ", user_cache_storable_tier($cache, (function () { $s = new SplStack(); $s->push(1); return $s; })()), "\n"; +echo "__sleep obj: ", user_cache_storable_tier($cache, new UserCacheStorableSleep()), "\n"; +echo "enum: ", user_cache_storable_tier($cache, UserCacheStorableSuit::Hearts), "\n"; +$ref = 1; +echo "array w/ ref: ", user_cache_storable_tier($cache, ['x' => &$ref, 'y' => &$ref]), "\n"; + +$sleepFetched = $cache->fetch('storable_9'); +echo "__sleep magic calls: "; +var_dump([UserCacheStorableSleep::$sleepCalls, UserCacheStorableSleep::$wakeupCalls]); +echo "__sleep object access: "; +var_dump($sleepFetched->a . ':' . $sleepFetched->b); + +$resource = fopen(__FILE__, 'r'); +$closure = static fn () => true; + +try { + $cache->store('resource-root', $resource); + echo "Resource: stored\n"; +} catch (TypeError $e) { + echo "Resource: TypeError\n"; +} + +try { + $cache->store('closure-root', $closure); + echo "Closure: stored\n"; +} catch (TypeError $e) { + echo "Closure: TypeError\n"; +} + +echo "nested resource: ", user_cache_storable_tier($cache, ['value' => $resource]), "\n"; +echo "nested closure: ", user_cache_storable_tier($cache, ['value' => $closure]), "\n"; +echo "object resource: ", user_cache_storable_tier($cache, new UserCacheUnsupportedBox($resource)), "\n"; +echo "object closure: ", user_cache_storable_tier($cache, new UserCacheUnsupportedBox($closure)), "\n"; +echo "fixed resource: ", user_cache_storable_tier($cache, SplFixedArray::fromArray([$resource], false)), "\n"; +echo "fixed closure: ", user_cache_storable_tier($cache, SplFixedArray::fromArray([$closure], false)), "\n"; +echo "array resource: ", user_cache_storable_tier($cache, new ArrayObject(['value' => $resource])), "\n"; +echo "array closure: ", user_cache_storable_tier($cache, new ArrayObject(['value' => $closure])), "\n"; + +$fiber = new Fiber(function () { Fiber::suspend(); }); +$fiber->start(); +echo "Fiber: ", user_cache_storable_tier($cache, $fiber), "\n"; +echo "Generator: ", user_cache_storable_tier($cache, (function () { yield 1; })()), "\n"; +echo "WeakMap: ", user_cache_storable_tier($cache, (function () { $m = new WeakMap(); $m[new stdClass()] = 1; return $m; })()), "\n"; + +fclose($resource); +?> +--EXPECT-- +array: stored +stdClass: stored +json object: stored +plain object: stored +DateTime: stored +DateInterval: stored +ArrayObject: stored +SplStack: stored +__sleep obj: stored +enum: stored +array w/ ref: stored +__sleep magic calls: array(2) { + [0]=> + int(1) + [1]=> + int(1) +} +__sleep object access: string(3) "1:2" +Resource: TypeError +Closure: TypeError +nested resource: TypeError +nested closure: TypeError +object resource: TypeError +object closure: TypeError +fixed resource: TypeError +fixed closure: TypeError +array resource: TypeError +array closure: TypeError +Fiber: TypeError +Generator: TypeError +WeakMap: TypeError diff --git a/ext/user_cache/tests/user_cache_store_multiple_rollback.phpt b/ext/user_cache/tests/user_cache_store_multiple_rollback.phpt new file mode 100644 index 000000000000..4060dfd68df8 --- /dev/null +++ b/ext/user_cache/tests/user_cache_store_multiple_rollback.phpt @@ -0,0 +1,46 @@ +--TEST-- +UserCache\Cache: storeMultiple rolls back partial writes on failure +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=1M +--FILE-- +store('existing-a', 'old-a')); +var_dump($cache->store('existing-b', 'old-b')); + +var_dump($cache->storeMultiple([ + 'existing-a' => 'new-a', + 'new-key' => 'new-value', + 'existing-b' => str_repeat('x', 4 * 1024 * 1024), +])); + +var_dump($cache->fetch('existing-a')); +var_dump($cache->fetch('existing-b')); +var_dump($cache->has('new-key')); + +$fresh = UserCache\Cache::getPool('store-multiple-rollback-fresh'); +var_dump($fresh->storeMultiple([ + 'fresh-a' => 'new-a', + 'fresh-b' => str_repeat('x', 4 * 1024 * 1024), +])); +var_dump($fresh->has('fresh-a')); +var_dump($fresh->has('fresh-b')); +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +string(5) "old-a" +string(5) "old-b" +bool(false) +bool(false) +bool(false) +bool(false) diff --git a/ext/user_cache/tests/user_cache_store_multiple_typeerror.phpt b/ext/user_cache/tests/user_cache_store_multiple_typeerror.phpt new file mode 100644 index 000000000000..14c92992b973 --- /dev/null +++ b/ext/user_cache/tests/user_cache_store_multiple_typeerror.phpt @@ -0,0 +1,39 @@ +--TEST-- +UserCache\Cache: storeMultiple() rejects unstorable values and rolls back +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store('existing', 'previous')); + +try { + $cache->storeMultiple([ + 'existing' => 'replaced', + 'fresh' => 1, + 'bad' => fn() => 1, + ]); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +var_dump($cache->fetch('existing')); +var_dump($cache->has('fresh')); + +try { + $cache->storeMultiple(['bad' => fopen('php://memory', 'r')]); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +bool(true) +UserCache\Cache::storeMultiple(): Argument #1 ($values) must not contain Closure objects +string(8) "previous" +bool(false) +UserCache\Cache::storeMultiple(): Argument #1 ($values) must contain only values of type object|array|string|int|float|bool|null, resource given diff --git a/ext/user_cache/tests/user_cache_tombstone_rehash.phpt b/ext/user_cache/tests/user_cache_tombstone_rehash.phpt new file mode 100644 index 000000000000..ebd9dbfd6e72 --- /dev/null +++ b/ext/user_cache/tests/user_cache_tombstone_rehash.phpt @@ -0,0 +1,47 @@ +--TEST-- +UserCache\Cache: rehashes the entry table after delete churn +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store("keep_$i", ['id' => $i, 'name' => "keeper_$i"]); +} + +for ($round = 0; $round < 10; $round++) { + for ($i = 0; $i < 100; $i++) { + $cache->store("churn_{$round}_{$i}", $i); + } + for ($i = 0; $i < 100; $i++) { + $cache->delete("churn_{$round}_{$i}"); + } +} + +$ok = true; +for ($i = 0; $i < 50; $i++) { + $value = $cache->fetch("keep_$i"); + if (!is_array($value) || $value['id'] !== $i || $value['name'] !== "keeper_$i") { + $ok = false; + echo "lost keep_$i\n"; + } +} + +var_dump($ok); +var_dump($cache->fetch('churn_0_0', 'gone')); +var_dump($cache->has('churn_9_99')); +var_dump($cache->store('after_rehash', 'value')); +var_dump($cache->fetch('after_rehash')); +?> +--EXPECT-- +bool(true) +string(4) "gone" +bool(false) +bool(true) +string(5) "value" diff --git a/ext/user_cache/tests/user_cache_ttl_remember.phpt b/ext/user_cache/tests/user_cache_ttl_remember.phpt new file mode 100644 index 000000000000..74ed1a1d22b8 --- /dev/null +++ b/ext/user_cache/tests/user_cache_ttl_remember.phpt @@ -0,0 +1,37 @@ +--TEST-- +UserCache\Cache: TTL and remember() +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +store('ttl', 'value', 1)); +var_dump($cache->has('ttl')); +sleep(2); +var_dump($cache->fetch('ttl', 'expired')); + +$calls = 0; +var_dump($cache->remember('remembered', function () use (&$calls) { + $calls++; + return 'computed'; +})); +var_dump($cache->remember('remembered', function () use (&$calls) { + $calls++; + return 'ignored'; +})); +var_dump($calls); +?> +--EXPECT-- +bool(true) +bool(true) +string(7) "expired" +string(8) "computed" +string(8) "computed" +int(1) diff --git a/ext/user_cache/tests/user_cache_ttl_semantics.phpt b/ext/user_cache/tests/user_cache_ttl_semantics.phpt new file mode 100644 index 000000000000..e9974b813b4c --- /dev/null +++ b/ext/user_cache/tests/user_cache_ttl_semantics.phpt @@ -0,0 +1,38 @@ +--TEST-- +UserCache\Cache: counter TTL expiry and TTL overwrite semantics +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +increment('counter', 1, 1)); + +var_dump($cache->store('overwritten', 'v1', 1)); +var_dump($cache->store('overwritten', 'v2')); + +var_dump($cache->store('shortened', 'v1')); +var_dump($cache->store('shortened', 'v2', 1)); + +sleep(2); + +var_dump($cache->fetch('counter', 'MISS')); +var_dump($cache->increment('counter')); +var_dump($cache->fetch('overwritten', 'MISS')); +var_dump($cache->fetch('shortened', 'MISS')); +?> +--EXPECT-- +int(1) +bool(true) +bool(true) +bool(true) +bool(true) +string(4) "MISS" +int(1) +string(2) "v2" +string(4) "MISS" diff --git a/ext/user_cache/tests/user_cache_uninitialized_typed_prop.phpt b/ext/user_cache/tests/user_cache_uninitialized_typed_prop.phpt new file mode 100644 index 000000000000..78fb711dd744 --- /dev/null +++ b/ext/user_cache/tests/user_cache_uninitialized_typed_prop.phpt @@ -0,0 +1,94 @@ +--TEST-- +UserCache\Cache: uninitialized typed properties round-trip as uninitialized +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +assigned = 42; + +$cache->store('box', $object); +$fetched = $cache->fetch('box'); + +$assignedProp = new ReflectionProperty($fetched, 'assigned'); +$neverProp = new ReflectionProperty($fetched, 'neverAssigned'); +$nullableProp = new ReflectionProperty($fetched, 'nullableNeverAssigned'); +$defaultProp = new ReflectionProperty($fetched, 'withDefault'); + +ok('assigned initialized', $assignedProp->isInitialized($fetched) && $fetched->assigned === 42); +ok('never-assigned stays uninitialized', !$neverProp->isInitialized($fetched)); +ok('nullable-never stays uninitialized', !$nullableProp->isInitialized($fetched)); +ok('default initialized', $defaultProp->isInitialized($fetched) && $fetched->withDefault === 'default'); + +$readError = false; +try { + $fetched->neverAssigned; +} catch (Error $e) { + $readError = str_contains($e->getMessage(), 'must not be accessed before initialization'); +} +ok('uninitialized read still errors', $readError); + +$fetched->neverAssigned = 7; +ok('can assign after fetch', $fetched->neverAssigned === 7); +$typeError = false; +try { + $fetched->assigned = 'not an int'; +} catch (TypeError $e) { + $typeError = true; +} +ok('type check preserved', $typeError); + +class UninitReadonly +{ + public readonly int $id; + public string $name; + public function __construct(string $name) + { + $this->name = $name; + } +} +$ro = new UninitReadonly('n'); +$cache->store('ro', $ro); +$fetchedRo = $cache->fetch('ro'); +$idProp = new ReflectionProperty($fetchedRo, 'id'); +ok('readonly uninitialized survives', !$idProp->isInitialized($fetchedRo) && $fetchedRo->name === 'n'); + +$graph = ['direct' => new UninitBox(), 'list' => [new UninitBox()]]; +$graph['direct']->assigned = 1; +$cache->store('graph', $graph); +$fetchedGraph = $cache->fetch('graph'); +$nestedProp = new ReflectionProperty($fetchedGraph['list'][0], 'neverAssigned'); +ok('nested uninitialized survives', !$nestedProp->isInitialized($fetchedGraph['list'][0])); +ok('serialize parity', serialize($cache->fetch('box')) === serialize($object)); +?> +--EXPECT-- +assigned initialized: OK +never-assigned stays uninitialized: OK +nullable-never stays uninitialized: OK +default initialized: OK +uninitialized read still errors: OK +can assign after fetch: OK +type check preserved: OK +readonly uninitialized survives: OK +nested uninitialized survives: OK +serialize parity: OK diff --git a/ext/user_cache/tests/user_cache_unserialize_only_contract.phpt b/ext/user_cache/tests/user_cache_unserialize_only_contract.phpt new file mode 100644 index 000000000000..125ac9bf13e6 --- /dev/null +++ b/ext/user_cache/tests/user_cache_unserialize_only_contract.phpt @@ -0,0 +1,84 @@ +--TEST-- +UserCache\Cache: __unserialize() without __serialize() is honored +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +value = $data['value']; + $this->derived = $this->value * 3; + } +} + +class SleepAndUnserializeContract +{ + public int $keep = 5; + public int $drop = 99; + public bool $wakeup = false; + public array $keys = []; + public static array $events = []; + + public function __sleep(): array + { + self::$events[] = 'sleep'; + + return ['keep']; + } + + public function __wakeup(): void + { + self::$events[] = 'wakeup'; + $this->wakeup = true; + } + + public function __unserialize(array $data): void + { + self::$events[] = 'unserialize'; + $this->keys = array_keys($data); + $this->keep = $data['keep']; + $this->drop = -1; + } +} + +$cache = UserCache\Cache::getPool('unserialize-only-contract'); + +var_dump($cache->store('one', new UnserializeOnlyContract())); +$one = $cache->fetch('one'); +var_dump($one->value, $one->derived, UnserializeOnlyContract::$calls); + +var_dump($cache->store('two', new SleepAndUnserializeContract())); +$two = $cache->fetch('two'); +var_dump($two->keep, $two->drop, $two->wakeup, $two->keys, SleepAndUnserializeContract::$events); +?> +--EXPECT-- +bool(true) +int(7) +int(21) +int(1) +bool(true) +int(5) +int(-1) +bool(false) +array(1) { + [0]=> + string(4) "keep" +} +array(2) { + [0]=> + string(5) "sleep" + [1]=> + string(11) "unserialize" +} diff --git a/ext/user_cache/tests/user_cache_unstorable_extended.phpt b/ext/user_cache/tests/user_cache_unstorable_extended.phpt new file mode 100644 index 000000000000..560095d3a882 --- /dev/null +++ b/ext/user_cache/tests/user_cache_unstorable_extended.phpt @@ -0,0 +1,51 @@ +--TEST-- +UserCache\Cache: lazy objects and not-serializable internal objects are rejected +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- + $reflector->newLazyGhost(function ($object) { $object->x = 2; }), + 'proxy' => $reflector->newLazyProxy(fn() => new LazyTarget()), + 'nested-ghost' => ['inner' => $reflector->newLazyGhost(function ($object) { $object->x = 2; })], + 'not-serializable' => UserCache\Cache::getStatus(), + 'weakref' => WeakReference::create(new LazyTarget()), + 'nested-weakref' => ['inner' => WeakReference::create(new LazyTarget())], +]; + +foreach ($values as $key => $value) { + try { + $cache->store($key, $value); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } + var_dump($cache->has($key)); +} +?> +--EXPECT-- +lazy objects cannot be stored in the user cache +bool(false) +lazy objects cannot be stored in the user cache +bool(false) +lazy objects cannot be stored in the user cache +bool(false) +objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache +bool(false) +objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache +bool(false) +objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache +bool(false) diff --git a/ext/user_cache/tests/user_cache_wakeup_only_identity.phpt b/ext/user_cache/tests/user_cache_wakeup_only_identity.phpt new file mode 100644 index 000000000000..22867c21de63 --- /dev/null +++ b/ext/user_cache/tests/user_cache_wakeup_only_identity.phpt @@ -0,0 +1,63 @@ +--TEST-- +UserCache\Cache: __wakeup()-only objects keep outer shared graph identity +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +class WakeupOnlyIdentityHolder +{ + public static int $wakeupCalls = 0; + public object $child; + + public function __wakeup(): void + { + self::$wakeupCalls++; + } +} + +class WakeupOnlyIdentityChild +{ + public int $value = 1; +} + +$child = new WakeupOnlyIdentityChild(); +$holder = new WakeupOnlyIdentityHolder(); +$holder->child = $child; + +$native = unserialize(serialize(['holder' => $holder, 'child' => $child])); +var_dump($native['holder']->child === $native['child']); + +WakeupOnlyIdentityHolder::$wakeupCalls = 0; + +var_dump($cache->store('graph', ['holder' => $holder, 'child' => $child])); + +$first = $cache->fetch('graph'); +var_dump($first['holder']->child === $first['child']); +$first['holder']->child->value = 42; +var_dump($first['child']->value); +var_dump(WakeupOnlyIdentityHolder::$wakeupCalls); + +$second = $cache->fetch('graph'); +var_dump($second['holder']->child === $second['child']); +var_dump($second['child']->value); +var_dump(WakeupOnlyIdentityHolder::$wakeupCalls); +var_dump($first['holder'] !== $second['holder']); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +int(42) +int(1) +bool(true) +int(1) +int(2) +bool(true) diff --git a/ext/user_cache/user_cache.c b/ext/user_cache/user_cache.c new file mode 100644 index 000000000000..db9cf5f81640 --- /dev/null +++ b/ext/user_cache/user_cache.c @@ -0,0 +1,3348 @@ +/* + +----------------------------------------------------------------------+ + | PHP Cache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_user_cache.h" + +#include "user_cache_internal.h" +#include "user_cache_arginfo.h" +#include "user_cache_decl.h" +#include "user_cache_shm.h" + +#include "Zend/zend_atomic.h" +#include "Zend/zend_closures.h" +#include "Zend/zend_exceptions.h" +#include "Zend/zend_smart_str.h" + +#include "ext/standard/php_var.h" +#include "ext/standard/info.h" + +#include "SAPI.h" + +#define PHP_USER_CACHE_API_VALUE_TYPE "object|array|string|int|float|bool|null" +#define PHP_USER_CACHE_STORAGE_KEY_CACHE_MAX 4096U +#define PHP_USER_CACHE_MAX_BOUNDARY_PARTITIONS 32U + +#define PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(type) \ + static type *type##_from_obj(zend_object *obj) \ + { \ + return (type *) ((char *) obj - offsetof(type, std)); \ + } + +#define PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(member) \ + php_user_cache_safe_direct_##member##_func_t php_user_cache_safe_direct_##member##_func( \ + zend_class_entry *ce) \ + { \ + const php_user_cache_safe_direct_handlers *handlers = \ + php_user_cache_safe_direct_find_handlers(ce, NULL) \ + ; \ + return handlers != NULL ? handlers->member : NULL; \ + } + +#define PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(method, field) \ + ZEND_METHOD(UserCache_CacheStatus, method) \ + { \ + php_user_cache_status_object *status; \ + \ + ZEND_PARSE_PARAMETERS_NONE(); \ + \ + status = user_cache_status_from_this(ZEND_THIS); \ + if (status == NULL) { \ + RETURN_THROWS(); \ + } \ + \ + RETURN_LONG(status->stats.field); \ + } + +typedef struct { + zend_string *scope; + zend_string *scope_prefix; + php_user_cache_context *context; + HashTable *storage_key_cache; + zend_object std; +} php_user_cache_object; + +typedef struct { + zend_string *scope; + zend_string *scope_prefix; + php_user_cache_context *context; + /* Immutable snapshot. */ + zend_long entry_count; + zend_long used_memory; + zval entry_keys; + zend_object std; +} php_user_cache_pool_status_object; + +typedef struct { + zend_ulong hash; + zend_string *key; + uint32_t index; +} php_user_cache_bulk_order; + +typedef struct { + zval *value; + php_user_cache_prepared_value prepared; + php_user_cache_store_result store_result; +} php_user_cache_bulk_store_item; + +typedef struct { + zend_long configured_memory; + zend_long shared_memory_size; + zend_long used_memory; + zend_long free_memory; + zend_long wasted_memory; + zend_long entry_count; + zend_long entry_capacity; + zend_long tombstone_count; + zend_long expunge_count; + zend_long store_failure_count; +} php_user_cache_info_stats; + +typedef struct { + /* Immutable snapshot. */ + php_user_cache_info_stats stats; + php_user_cache_reason availability_reason; + bool initialized; + zend_object std; +} php_user_cache_status_object; + +typedef struct _php_user_cache_boundary_partition { + char *boundary; + size_t boundary_len; + zend_ulong boundary_hash; + php_user_cache_partition *partition; + struct _php_user_cache_boundary_partition *next; +} php_user_cache_boundary_partition; + +#ifndef ZTS +php_user_cache_globals user_cache_globals; +#else +int user_cache_globals_id; +size_t user_cache_globals_offset; +#endif + +static HashTable php_user_cache_safe_direct_handler_table; +static bool php_user_cache_safe_direct_handlers_initialized = false; +static php_user_cache_boundary_partition *php_user_cache_boundary_partitions = NULL; +static zend_class_entry *php_user_cache_availability_ce; +static zend_class_entry *php_user_cache_ce; +static zend_object_handlers php_user_cache_object_handlers; +static zend_object_handlers php_user_cache_status_object_handlers; +static zend_object_handlers php_user_cache_pool_status_object_handlers; +static uint32_t php_user_cache_boundary_partition_count = 0; +static bool php_user_cache_boundary_limit_logged = false; +static bool php_user_cache_boundary_startup_failed_logged = false; +static uint64_t php_user_cache_self_pid = 0; +#ifndef ZEND_WIN32 +static bool php_user_cache_self_pid_uncached = false; +static bool php_user_cache_pid_atfork_registered = false; +#endif + +zend_class_entry *php_user_cache_exception_ce; +zend_class_entry *php_user_cache_status_ce; +zend_class_entry *php_user_cache_pool_status_ce; +php_user_cache_context php_user_cache_context_state = { + .storage = { .lock_file = -1 }, + .name = "user cache", + .lock_name = "php_user_cache_lock", +#ifndef ZEND_WIN32 + .sem_filename_prefix = PHP_USER_CACHE_SEM_FILENAME_PREFIX, +#endif + .clear_on_pressure = true, + .strict_store_failure = false, +}; +bool php_user_cache_runtime_opted_in = false; +/* Read-only after module startup. */ +php_user_cache_partition *php_user_cache_partitions = NULL; + +static zend_string *user_cache_storage_key( + php_user_cache_object *cache, + zend_string *key +); +static void user_cache_object_free(zend_object *obj); +static zend_object *user_cache_object_create(zend_class_entry *ce); +static void user_cache_pool_status_object_free(zend_object *obj); +static zend_object *user_cache_pool_status_object_create(zend_class_entry *ce); +static zend_object *user_cache_status_object_create(zend_class_entry *ce); +static bool user_cache_store_storage_key_prevalidated(zend_string *key, zval *value, zend_long ttl, bool add_only); + +#ifndef ZEND_WIN32 +static void user_cache_pid_atfork_child(void) +{ + php_user_cache_self_pid = 0; +} +#endif + +static bool user_cache_user_key_is_valid(zend_string *key) +{ + return ZSTR_LEN(key) != 0 && + memchr(ZSTR_VAL(key), PHP_USER_CACHE_KEY_DELIMITER_CHAR, ZSTR_LEN(key)) == NULL + ; +} + +static bool user_cache_validate_delimiter_free(zend_string *str, uint32_t arg_num, const char *empty_error) +{ + if (ZSTR_LEN(str) == 0) { + zend_argument_value_error(arg_num, "%s", empty_error); + + return false; + } + + if (memchr(ZSTR_VAL(str), PHP_USER_CACHE_KEY_DELIMITER_CHAR, ZSTR_LEN(str)) != NULL) { + zend_argument_value_error(arg_num, "must not contain the user-cache key delimiter " PHP_USER_CACHE_KEY_DELIMITER_NAME); + + return false; + } + + return true; +} + +static bool user_cache_validate_key(zend_string *key, uint32_t arg_num) +{ + return user_cache_validate_delimiter_free(key, arg_num, "must be a non-empty string"); +} + +static bool user_cache_validate_arg_value_kind( + zval *value, + uint32_t arg_num, + const char *resource_error, + const char *closure_error) +{ + ZVAL_DEREF(value); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + zend_argument_type_error(arg_num, "%s", resource_error); + + return false; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + zend_argument_type_error(arg_num, "%s", closure_error); + + return false; + } + + return true; +} + +static bool user_cache_validate_store_array_value(zval *value, uint32_t arg_num) +{ + return user_cache_validate_arg_value_kind( + value, + arg_num, + "must contain only values of type " PHP_USER_CACHE_API_VALUE_TYPE ", resource given", + "must not contain Closure objects" + ); +} + +static bool user_cache_validate_store_array(HashTable *values, uint32_t arg_num) +{ + zend_string *key; + zval *value; + + ZEND_HASH_FOREACH_STR_KEY_VAL(values, key, value) { + if (key != NULL && !user_cache_user_key_is_valid(key)) { + zend_argument_value_error(arg_num, "must be an array with non-empty string or int keys that do not contain " PHP_USER_CACHE_KEY_DELIMITER_NAME); + + return false; + } + + if (!user_cache_validate_store_array_value(value, arg_num)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static void user_cache_release_key_list(zend_string **keys, uint32_t count) +{ + uint32_t i; + + if (keys == NULL) { + return; + } + + for (i = 0; i < count; i++) { + zend_string_release(keys[i]); + } + + efree(keys); +} + +static bool user_cache_prepare_key_list( + HashTable *keys, + uint32_t arg_num, + zend_string ***prepared_keys, + uint32_t *prepared_count) +{ + zend_string **prepared; + zval *value; + uint32_t count, i = 0; + + ZEND_ASSERT(prepared_keys != NULL); + ZEND_ASSERT(prepared_count != NULL); + + count = zend_hash_num_elements(keys); + + *prepared_keys = NULL; + *prepared_count = 0; + + if (count == 0) { + return true; + } + + prepared = safe_emalloc(count, sizeof(zend_string *), 0); + + ZEND_HASH_FOREACH_VAL(keys, value) { + ZVAL_DEREF(value); + + if (Z_TYPE_P(value) == IS_STRING) { + if (!user_cache_user_key_is_valid(Z_STR_P(value))) { + zend_argument_value_error(arg_num, "must contain only non-empty string or int cache keys that do not contain " PHP_USER_CACHE_KEY_DELIMITER_NAME); + user_cache_release_key_list(prepared, i); + + return false; + } + + prepared[i++] = zend_string_copy(Z_STR_P(value)); + } else if (Z_TYPE_P(value) == IS_LONG) { + prepared[i++] = zend_long_to_str(Z_LVAL_P(value)); + } else { + zend_argument_value_error(arg_num, "must contain only non-empty string or int cache keys that do not contain " PHP_USER_CACHE_KEY_DELIMITER_NAME); + user_cache_release_key_list(prepared, i); + + return false; + } + } ZEND_HASH_FOREACH_END(); + + *prepared_keys = prepared; + *prepared_count = i; + + return true; +} + +static bool user_cache_validate_api_value(zval *value, uint32_t arg_num) +{ + return user_cache_validate_arg_value_kind( + value, + arg_num, + "must be of type " PHP_USER_CACHE_API_VALUE_TYPE ", resource given", + "must not be a Closure object" + ); +} + +static bool user_cache_validate_remember_value(zval *value) +{ + ZVAL_DEREF(value); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + zend_type_error(PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE); + + return false; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + zend_type_error(PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE); + + return false; + } + + return true; +} + +static zend_string *user_cache_build_scope_prefix(zend_string *scope) +{ + const char *prefix = "user_cache" PHP_USER_CACHE_KEY_DELIMITER; + size_t prefix_len = sizeof("user_cache" PHP_USER_CACHE_KEY_DELIMITER) - 1; + + return zend_string_concat3( + prefix, + prefix_len, + ZSTR_VAL(scope), + ZSTR_LEN(scope), + ZEND_STRL(PHP_USER_CACHE_KEY_DELIMITER) + ); +} + +PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(php_user_cache_object) + +PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(php_user_cache_pool_status_object) + +PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(php_user_cache_status_object) + +static php_user_cache_object *user_cache_object_from_this(zval *this_ptr) +{ + php_user_cache_object *cache = php_user_cache_object_from_obj(Z_OBJ_P(this_ptr)); + + ZEND_ASSERT(cache->context == php_user_cache_active_context()); + + if (cache->scope_prefix == NULL) { + zend_throw_error(NULL, "UserCache\\Cache instance was not initialized"); + + return NULL; + } + + return cache; +} + +static php_user_cache_pool_status_object *user_cache_pool_status_from_this(zval *this_ptr) +{ + php_user_cache_pool_status_object *status = + php_user_cache_pool_status_object_from_obj(Z_OBJ_P(this_ptr)) + ; + + if (status->scope == NULL || status->scope_prefix == NULL || status->context == NULL) { + zend_throw_error(NULL, "UserCache\\CachePoolStatus instance was not initialized"); + + return NULL; + } + + return status; +} + +static php_user_cache_status_object *user_cache_status_from_this(zval *this_ptr) +{ + php_user_cache_status_object *status = + php_user_cache_status_object_from_obj(Z_OBJ_P(this_ptr)) + ; + + if (!status->initialized) { + zend_throw_error(NULL, "UserCache\\CacheStatus instance was not initialized"); + + return NULL; + } + + return status; +} + +static void user_cache_register_classes(void) +{ + if (php_user_cache_ce != NULL) { + return; + } + + php_user_cache_exception_ce = zend_ce_exception; + php_user_cache_availability_ce = register_class_UserCache_CacheAvailability(); + php_user_cache_status_ce = register_class_UserCache_CacheStatus(); + php_user_cache_pool_status_ce = register_class_UserCache_CachePoolStatus(); + php_user_cache_ce = register_class_UserCache_Cache(); + + php_user_cache_ce->create_object = user_cache_object_create; + php_user_cache_status_ce->create_object = user_cache_status_object_create; + php_user_cache_pool_status_ce->create_object = user_cache_pool_status_object_create; + + memcpy( + &php_user_cache_object_handlers, + zend_get_std_object_handlers(), + sizeof(zend_object_handlers) + ); + + php_user_cache_object_handlers.offset = offsetof(php_user_cache_object, std); + php_user_cache_object_handlers.free_obj = user_cache_object_free; + php_user_cache_object_handlers.clone_obj = NULL; + + memcpy( + &php_user_cache_status_object_handlers, + zend_get_std_object_handlers(), + sizeof(zend_object_handlers) + ); + + php_user_cache_status_object_handlers.offset = offsetof(php_user_cache_status_object, std); + php_user_cache_status_object_handlers.clone_obj = NULL; + + memcpy( + &php_user_cache_pool_status_object_handlers, + zend_get_std_object_handlers(), + sizeof(zend_object_handlers) + ); + + php_user_cache_pool_status_object_handlers.offset = offsetof(php_user_cache_pool_status_object, std); + php_user_cache_pool_status_object_handlers.free_obj = user_cache_pool_status_object_free; + php_user_cache_pool_status_object_handlers.clone_obj = NULL; +} + +static void user_cache_reset_class_entries(void) +{ + php_user_cache_exception_ce = NULL; + php_user_cache_availability_ce = NULL; + php_user_cache_status_ce = NULL; + php_user_cache_pool_status_ce = NULL; + php_user_cache_ce = NULL; +} + +static void user_cache_invalidate_context(php_user_cache_context *ctx) +{ + php_user_cache_context *prev_ctx; + + prev_ctx = php_user_cache_activate_context(ctx); + + if (UC_G(shm_size) == 0 || !ctx->storage.initialized) { + php_user_cache_restore_context(prev_ctx); + + return; + } + + if (!php_user_cache_wlock()) { + php_user_cache_restore_context(prev_ctx); + + return; + } + + if (php_user_cache_header_is_initialized_locked()) { + php_user_cache_clear_locked(); + } + + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); +} + +static bool user_cache_validate_non_negative(zend_long value, uint32_t arg_num) +{ + if (value < 0) { + zend_argument_value_error(arg_num, "must be greater than or equal to 0"); + + return false; + } + + return true; +} + +static bool user_cache_can_read(void) +{ + php_user_cache_ensure_ready(); + + return php_user_cache_active_runtime()->available; +} + +static bool user_cache_begin_read(void) +{ + if (!user_cache_can_read()) { + return false; + } + + if (!php_user_cache_rlock()) { + return false; + } + + if (!php_user_cache_header_is_initialized_locked()) { + php_user_cache_unlock(); + + return false; + } + + return true; +} + +static bool user_cache_can_write(void) +{ + return user_cache_can_read(); +} + +static bool user_cache_begin_write(void) +{ + if (!php_user_cache_wlock()) { + return false; + } + + if (!php_user_cache_header_init_locked()) { + php_user_cache_unlock(); + + return false; + } + + return true; +} + +static bool user_cache_delete_storage_key_prevalidated(zend_string *key) +{ + if (!php_user_cache_wlock_for_entry_mutation(key)) { + return false; + } + + php_user_cache_delete_locked(key); + + php_user_cache_unlock(); + + return true; +} + +static void user_cache_delete_corrupt_entry_prevalidated(zend_string *key) +{ + if (EG(exception)) { + return; + } + + (void) user_cache_delete_storage_key_prevalidated(key); +} + +static bool user_cache_clear_scope_prevalidated( + zend_string *scope_prefix) +{ + if (!user_cache_begin_write()) { + return false; + } + + php_user_cache_delete_by_prefix_locked(scope_prefix); + + php_user_cache_unlock(); + php_user_cache_release_active_request_local_slots_by_prefix(scope_prefix); + + return true; +} + +static bool user_cache_fetch_if_present_api( + zend_string *key, + zval *return_value) +{ + php_user_cache_fetch_pending_seed pending_seed; + bool fetched, stack_overflowed, entry_found = false; + + if (!user_cache_can_read()) { + return false; + } + + UC_G(stack_overflowed) = false; + switch (php_user_cache_fetch_optimistic(key, return_value)) { + case PHP_USER_CACHE_OPTIMISTIC_FOUND: + return true; + case PHP_USER_CACHE_OPTIMISTIC_MISS: + return false; + case PHP_USER_CACHE_OPTIMISTIC_FALLBACK: + break; + } + + if (EG(exception)) { + stack_overflowed = UC_G(stack_overflowed); + + UC_G(stack_overflowed) = false; + zend_clear_exception(); + if (!stack_overflowed) { + user_cache_delete_corrupt_entry_prevalidated(key); + } + + return false; + } + + if (!user_cache_begin_read()) { + return false; + } + + UC_G(stack_overflowed) = false; + fetched = php_user_cache_fetch_locked(key, false, true, return_value, &entry_found, &pending_seed); + + php_user_cache_unlock(); + + if (fetched) { + /* Clone outside the shared-memory lock. */ + if (pending_seed.should_seed_request_local_slot) { + php_user_cache_fetch_finish(key, pending_seed.generation, return_value, pending_seed.flags); + } + + return true; + } + + if (!entry_found) { + return false; + } + + if (UC_G(stack_overflowed)) { + UC_G(stack_overflowed) = false; + if (EG(exception)) { + zend_clear_exception(); + } + + return false; + } + + if (EG(exception)) { + zend_clear_exception(); + } + + user_cache_delete_corrupt_entry_prevalidated(key); + + return false; +} + +static void user_cache_fetch_api( + zend_string *key, + zval *default_value, + zval *return_value) +{ + if (!user_cache_fetch_if_present_api(key, return_value)) { + ZVAL_COPY(return_value, default_value); + } +} + +static void user_cache_fetch_multiple_fill_defaults( + zend_string **prepared_keys, + zend_string **storage_keys, + uint32_t count, + zval *default_value, + zval *return_value) +{ + zval default_copy; + uint32_t i; + + for (i = 0; i < count; i++) { + ZVAL_COPY(&default_copy, default_value); + zend_symtable_update(Z_ARRVAL_P(return_value), prepared_keys[i], &default_copy); + zend_string_release(storage_keys[i]); + } +} + +/* A corrupt entry drops the batch lock and switches subsequent reads to the + * self-locking path. */ +static void user_cache_fetch_multiple_fetch_one( + zend_string *storage_key, + zval *default_value, + bool *rlock_held, + zval *out, + php_user_cache_fetch_pending_seed *pending_seed) +{ + bool fetched, found; + + pending_seed->should_seed_request_local_slot = false; + + if (!*rlock_held) { + user_cache_fetch_api(storage_key, default_value, out); + + return; + } + + UC_G(stack_overflowed) = false; + fetched = php_user_cache_fetch_locked(storage_key, false, true, out, &found, pending_seed); + if (fetched) { + return; + } + + if (!found) { + ZVAL_COPY(out, default_value); + } else if (UC_G(stack_overflowed)) { + UC_G(stack_overflowed) = false; + + if (EG(exception)) { + zend_clear_exception(); + } + + ZVAL_COPY(out, default_value); + } else { + if (EG(exception)) { + zend_clear_exception(); + } + + php_user_cache_unlock(); + *rlock_held = false; + user_cache_delete_corrupt_entry_prevalidated(storage_key); + + ZVAL_COPY(out, default_value); + } +} + +static void user_cache_finish_fetch_multiple( + zend_string **prepared_keys, + zend_string **storage_keys, + const php_user_cache_fetch_pending_seed *pending_seeds, + uint32_t count, + zval *return_value) +{ + zval *val; + uint32_t i; + + for (i = 0; i < count; i++) { + if (pending_seeds[i].should_seed_request_local_slot) { + val = zend_symtable_find(Z_ARRVAL_P(return_value), prepared_keys[i]); + if (val != NULL) { + php_user_cache_fetch_finish( + storage_keys[i], + pending_seeds[i].generation, + val, + pending_seeds[i].flags + ); + } + } + + zend_string_release(storage_keys[i]); + } +} + +static zend_result user_cache_fetch_multiple_api( + php_user_cache_object *cache, + HashTable *keys, + zval *default_value, + zval *return_value) +{ + zend_string **prepared_keys, **storage_keys, *key, *storage_key; + php_user_cache_fetch_pending_seed *pending_seeds; + zval val; + uint32_t count, i; + bool rlock_held; + + if (!user_cache_prepare_key_list(keys, 1, &prepared_keys, &count)) { + return FAILURE; + } + + /* Do not allocate while holding the read lock. */ + storage_keys = NULL; + pending_seeds = NULL; + if (count != 0) { + storage_keys = safe_emalloc(count, sizeof(zend_string *), 0); + pending_seeds = safe_emalloc(count, sizeof(php_user_cache_fetch_pending_seed), 0); + + for (i = 0; i < count; i++) { + storage_keys[i] = user_cache_storage_key(cache, prepared_keys[i]); + } + } + + array_init_size(return_value, count); + + if (!user_cache_begin_read()) { + user_cache_fetch_multiple_fill_defaults( + prepared_keys, + storage_keys, + count, + default_value, + return_value + ); + + if (storage_keys != NULL) { + efree(storage_keys); + } + + if (pending_seeds != NULL) { + efree(pending_seeds); + } + + user_cache_release_key_list(prepared_keys, count); + + return SUCCESS; + } + + rlock_held = true; + zend_try { + for (i = 0; i < count; i++) { + key = prepared_keys[i]; + storage_key = storage_keys[i]; + + user_cache_fetch_multiple_fetch_one( + storage_key, + default_value, + &rlock_held, + &val, + &pending_seeds[i] + ); + + zend_symtable_update(Z_ARRVAL_P(return_value), key, &val); + } + } zend_catch { + php_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + if (rlock_held) { + php_user_cache_unlock(); + } + + /* Clone outside the shared-memory lock. */ + user_cache_finish_fetch_multiple(prepared_keys, storage_keys, pending_seeds, count, return_value); + + if (storage_keys != NULL) { + efree(storage_keys); + } + + if (pending_seeds != NULL) { + efree(pending_seeds); + } + + user_cache_release_key_list(prepared_keys, count); + + return SUCCESS; +} + +static bool user_cache_atomic_update_api( + zend_string *key, + zend_long step, + zend_long ttl, + bool decrement, + php_user_cache_atomic_update_result *result) +{ + bool updated; + + if (!user_cache_can_write() || + !php_user_cache_wlock_for_entry_mutation(key) + ) { + memset(result, 0, sizeof(*result)); + + return false; + } + + updated = php_user_cache_atomic_update_locked(key, step, ttl, decrement, true, result); + + php_user_cache_unlock(); + + if (result->is_overflow || result->is_type_error) { + updated = false; + } + + return updated; +} + +static bool user_cache_exists_api(zend_string *key) +{ + bool exists; + + if (!user_cache_can_read()) { + return false; + } + + switch (php_user_cache_exists_optimistic(key)) { + case PHP_USER_CACHE_OPTIMISTIC_FOUND: + return true; + case PHP_USER_CACHE_OPTIMISTIC_MISS: + return false; + case PHP_USER_CACHE_OPTIMISTIC_FALLBACK: + break; + } + + if (!user_cache_begin_read()) { + return false; + } + + exists = php_user_cache_exists_locked(key); + + php_user_cache_unlock(); + + return exists; +} + +static bool user_cache_lock_api( + zend_string *key, + zend_long lease) +{ + if (!user_cache_can_write()) { + return false; + } + + return php_user_cache_try_acquire_entry_lock(key, lease); +} + +static bool user_cache_unlock_api(zend_string *key) +{ + if (!user_cache_can_write()) { + return false; + } + + return php_user_cache_release_entry_lock(key); +} + +static zend_long user_cache_count_to_zend_long(uint64_t count) +{ + return count > (uint64_t) ZEND_LONG_MAX ? ZEND_LONG_MAX : (zend_long) count; +} + +static zend_long user_cache_size_to_zend_long(size_t size) +{ + return user_cache_count_to_zend_long(size); +} + +/* Caller must hold the read lock. */ +static size_t user_cache_sum_wasted_memory_locked(php_user_cache_header *header) +{ + php_user_cache_block *block; + uint32_t block_offset, block_limit, next_offset, iter_limit, iters; + size_t wasted_memory = 0; + + block_limit = header->data_offset + header->next_free; + iter_limit = (header->data_size / ((uint32_t) sizeof(php_user_cache_block))); + for (block_offset = header->free_list, iters = 0; + block_offset != 0 && iters < iter_limit; + iters++ + ) { + if (block_offset < header->data_offset || block_offset > block_limit - sizeof(php_user_cache_block)) { + break; + } + + block = php_user_cache_block_ptr(block_offset); + next_offset = block_offset + block->size; + if (block->size < sizeof(php_user_cache_block) || next_offset > block_limit) { + break; + } + + wasted_memory += block->size; + block_offset = block->next_free; + } + + return wasted_memory; +} + +static void user_cache_collect_info_stats(php_user_cache_info_stats *stats) +{ + php_user_cache_runtime *runtime; + php_user_cache_storage *storage; + php_user_cache_header *header; + size_t free_memory = 0, wasted_memory = 0, tail_memory = 0; + + memset(stats, 0, sizeof(*stats)); + + runtime = php_user_cache_active_runtime(); + storage = &php_user_cache_active_context()->storage; + + stats->configured_memory = user_cache_size_to_zend_long(runtime->configured_memory); + stats->shared_memory_size = user_cache_size_to_zend_long(storage->size); + + if (!storage->initialized || + !storage->lock_initialized || + !php_user_cache_rlock() + ) { + return; + } + + header = php_user_cache_header_ptr(); + if (header == NULL || !php_user_cache_header_is_initialized_locked()) { + stats->free_memory = stats->shared_memory_size; + php_user_cache_unlock(); + + return; + } + + if (header->next_free <= header->data_size) { + tail_memory = header->data_size - header->next_free; + } + + wasted_memory = user_cache_sum_wasted_memory_locked(header); + free_memory = tail_memory + wasted_memory; + + stats->entry_count = (zend_long) header->count; + stats->entry_capacity = (zend_long) header->capacity; + stats->tombstone_count = (zend_long) header->tombstone_count; + stats->expunge_count = user_cache_count_to_zend_long(header->expunge_count); + stats->store_failure_count = user_cache_count_to_zend_long(header->store_failure_count); + stats->free_memory = user_cache_size_to_zend_long(free_memory); + stats->wasted_memory = user_cache_size_to_zend_long(wasted_memory); + stats->used_memory = storage->size > free_memory + ? user_cache_size_to_zend_long(storage->size - free_memory) + : 0 + ; + + php_user_cache_unlock(); +} + +/* No default: the compiler should diagnose a missing enum case. */ +static zend_object *user_cache_availability_enum_case(php_user_cache_reason reason) +{ + int case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByUnknownReason; + + switch (reason) { + case PHP_USER_CACHE_REASON_NONE: + case_id = ZEND_ENUM_UserCache_CacheAvailability_Available; + break; + case PHP_USER_CACHE_REASON_DISABLED_BY_INI: + case_id = ZEND_ENUM_UserCache_CacheAvailability_DisabledByIni; + break; + case PHP_USER_CACHE_REASON_SHM_INIT_FAILED: + case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableBySharedMemoryInitializationFailed; + break; + case PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED: + case_id = ZEND_ENUM_UserCache_CacheAvailability_DisabledBySapi; + break; + case PHP_USER_CACHE_REASON_BACKEND_NOT_INITIALIZED_BEFORE_WORKER: + case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendNotInitializedBeforeWorkerStartup; + break; + case PHP_USER_CACHE_REASON_BACKEND_INITIALIZED_AFTER_WORKER: + case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendInitializedAfterWorkerStartup; + break; + case PHP_USER_CACHE_REASON_CGI_BOUNDARY_UNAVAILABLE: + case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByCgiFastCgiBoundary; + break; + case PHP_USER_CACHE_REASON_LSAPI_BOUNDARY_UNAVAILABLE: + case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByLsapiBoundary; + break; + } + + return zend_enum_get_case_by_id(php_user_cache_availability_ce, case_id); +} + +static void user_cache_return_status(zval *return_value) +{ + php_user_cache_status_object *status; + + php_user_cache_ensure_ready(); + + object_init_ex(return_value, php_user_cache_status_ce); + status = php_user_cache_status_object_from_obj(Z_OBJ_P(return_value)); + + status->availability_reason = php_user_cache_active_runtime()->failure_reason; + user_cache_collect_info_stats(&status->stats); + status->initialized = true; +} + +static void user_cache_add_pool_memory(size_t *used_memory, size_t size) +{ + if (*used_memory >= (size_t) ZEND_LONG_MAX || + size > (size_t) ZEND_LONG_MAX - *used_memory + ) { + *used_memory = (size_t) ZEND_LONG_MAX; + + return; + } + + *used_memory += size; +} + +static size_t user_cache_payload_block_size(uint32_t payload_offset) +{ + php_user_cache_block *block; + + if (payload_offset < sizeof(php_user_cache_block)) { + return 0; + } + + block = php_user_cache_block_ptr(payload_offset - sizeof(php_user_cache_block)); + if (block->size < sizeof(php_user_cache_block)) { + return 0; + } + + return block->size; +} + +static void user_cache_account_pool_entry( + const php_user_cache_entry *entry, + zend_string *prefix, + zend_long *entry_count, + size_t *used_memory, + zval *entry_keys) +{ + const char *key; + size_t prefix_len; + bool combined_value_key; + + combined_value_key = (entry->reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + key = (const char *) php_user_cache_ptr(entry->key_offset); + prefix_len = ZSTR_LEN(prefix); + + (*entry_count)++; + + add_next_index_stringl(entry_keys, key + prefix_len, entry->key_len - prefix_len); + + user_cache_add_pool_memory(used_memory, sizeof(php_user_cache_entry)); + if (combined_value_key) { + user_cache_add_pool_memory( + used_memory, + user_cache_payload_block_size(entry->value_offset) + ); + } else { + user_cache_add_pool_memory( + used_memory, + user_cache_payload_block_size(entry->key_offset) + ); + user_cache_add_pool_memory( + used_memory, + user_cache_payload_block_size(entry->value_offset) + ); + } +} + +static void user_cache_collect_pool_status( + php_user_cache_context *ctx, + zend_string *prefix, + zend_long *entry_count, + zend_long *used_memory, + zval *entry_keys) +{ + php_user_cache_context *prev_ctx; + php_user_cache_storage *storage; + php_user_cache_header *header; + php_user_cache_entry *entries, *entry; + uint32_t i; + size_t used_size = 0; + + *entry_count = 0; + *used_memory = 0; + + prev_ctx = php_user_cache_activate_context(ctx); + + storage = &php_user_cache_active_context()->storage; + if (!storage->initialized || !storage->lock_initialized || !php_user_cache_rlock()) { + php_user_cache_restore_context(prev_ctx); + + return; + } + + header = php_user_cache_header_ptr(); + if (header == NULL || + !php_user_cache_header_is_initialized_locked() + ) { + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + return; + } + + entries = php_user_cache_entries_ptr(header); + /* Release the lock before propagating an allocation bailout. */ + zend_try { + for (i = 0; i < header->capacity; i++) { + entry = &entries[i]; + if (entry->state == PHP_USER_CACHE_ENTRY_USED && + entry->key_len >= ZSTR_LEN(prefix) && + memcmp(php_user_cache_ptr(entry->key_offset), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) == 0 + ) { + user_cache_account_pool_entry( + entry, + prefix, + entry_count, + &used_size, + entry_keys + ); + } + } + } zend_catch { + php_user_cache_unlock_if_held(); + php_user_cache_restore_context(prev_ctx); + zend_bailout(); + } zend_end_try(); + + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + *used_memory = user_cache_size_to_zend_long(used_size); +} + +static void user_cache_return_pool_status( + php_user_cache_object *cache, + zval *return_value) +{ + php_user_cache_pool_status_object *status; + + php_user_cache_ensure_ready(); + + object_init_ex(return_value, php_user_cache_pool_status_ce); + status = php_user_cache_pool_status_object_from_obj(Z_OBJ_P(return_value)); + status->scope = zend_string_copy(cache->scope); + status->scope_prefix = zend_string_copy(cache->scope_prefix); + status->context = cache->context; + + array_init(&status->entry_keys); + user_cache_collect_pool_status( + status->context, + status->scope_prefix, + &status->entry_count, + &status->used_memory, + &status->entry_keys + ); +} + +static void user_cache_object_free(zend_object *obj) +{ + php_user_cache_object *cache = php_user_cache_object_from_obj(obj); + + if (cache->scope != NULL) { + zend_string_release(cache->scope); + } + + if (cache->scope_prefix != NULL) { + zend_string_release(cache->scope_prefix); + } + + if (cache->storage_key_cache != NULL) { + zend_hash_destroy(cache->storage_key_cache); + FREE_HASHTABLE(cache->storage_key_cache); + } + + zend_object_std_dtor(&cache->std); +} + +static void user_cache_pool_status_object_free(zend_object *obj) +{ + php_user_cache_pool_status_object *status = + php_user_cache_pool_status_object_from_obj(obj) + ; + + if (status->scope != NULL) { + zend_string_release(status->scope); + } + + if (status->scope_prefix != NULL) { + zend_string_release(status->scope_prefix); + } + + zval_ptr_dtor(&status->entry_keys); + + zend_object_std_dtor(&status->std); +} + +static zend_object *user_cache_object_create(zend_class_entry *ce) +{ + php_user_cache_object *cache; + + cache = zend_object_alloc(sizeof(php_user_cache_object), ce); + + zend_object_std_init(&cache->std, ce); + object_properties_init(&cache->std, ce); + + cache->scope = NULL; + cache->scope_prefix = NULL; + cache->storage_key_cache = NULL; + cache->context = php_user_cache_owning_context(); + cache->std.handlers = &php_user_cache_object_handlers; + + return &cache->std; +} + +static zend_object *user_cache_pool_status_object_create(zend_class_entry *ce) +{ + php_user_cache_pool_status_object *status; + + status = zend_object_alloc(sizeof(php_user_cache_pool_status_object), ce); + + zend_object_std_init(&status->std, ce); + object_properties_init(&status->std, ce); + + status->scope = NULL; + status->scope_prefix = NULL; + status->context = NULL; + status->entry_count = 0; + status->used_memory = 0; + + ZVAL_UNDEF(&status->entry_keys); + + status->std.handlers = &php_user_cache_pool_status_object_handlers; + + return &status->std; +} + +static zend_object *user_cache_status_object_create(zend_class_entry *ce) +{ + php_user_cache_status_object *status; + + status = zend_object_alloc(sizeof(php_user_cache_status_object), ce); + + zend_object_std_init(&status->std, ce); + object_properties_init(&status->std, ce); + + memset(&status->stats, 0, sizeof(status->stats)); + + status->availability_reason = PHP_USER_CACHE_REASON_NONE; + status->initialized = false; + status->std.handlers = &php_user_cache_status_object_handlers; + + return &status->std; +} + +static HashTable *user_cache_pools(void) +{ + if (UC_G(pool_table) == NULL) { + ALLOC_HASHTABLE(UC_G(pool_table)); + zend_hash_init(UC_G(pool_table), 8, NULL, ZVAL_PTR_DTOR, 0); + } + + return UC_G(pool_table); +} + +static void user_cache_release_pools(void) +{ + if (UC_G(pool_table) == NULL) { + return; + } + + zend_hash_destroy(UC_G(pool_table)); + FREE_HASHTABLE(UC_G(pool_table)); + UC_G(pool_table) = NULL; +} + +static bool user_cache_validate_pool_name(zend_string *pool, uint32_t arg_num) +{ + return user_cache_validate_delimiter_free(pool, arg_num, "must not be empty"); +} + +static zend_object *user_cache_get_or_create_pool(zend_string *pool) +{ + php_user_cache_object *cache; + zend_object *obj; + zval *existing, pool_zv; + HashTable *pools = user_cache_pools(); + + existing = zend_hash_find(pools, pool); + if (existing != NULL) { + return Z_OBJ_P(existing); + } + + obj = user_cache_object_create(php_user_cache_ce); + + cache = php_user_cache_object_from_obj(obj); + cache->scope = zend_string_copy(pool); + cache->scope_prefix = user_cache_build_scope_prefix(pool); + + ZVAL_OBJ(&pool_zv, obj); + if (zend_hash_add(pools, pool, &pool_zv) == NULL) { + OBJ_RELEASE(obj); + + return NULL; + } + + return obj; +} + +static zend_string *user_cache_storage_key_cache_lookup( + php_user_cache_object *cache, + zend_string *key) +{ + zval *cached; + + if (cache->storage_key_cache == NULL) { + return NULL; + } + + cached = zend_hash_find(cache->storage_key_cache, key); + if (cached == NULL) { + return NULL; + } + + return zend_string_copy(Z_STR_P(cached)); +} + +static zend_string *user_cache_storage_key_build( + php_user_cache_object *cache, + zend_string *key) +{ + zend_string *storage_key; + zval cache_zv; + + storage_key = zend_string_concat2( + ZSTR_VAL(cache->scope_prefix), + ZSTR_LEN(cache->scope_prefix), + ZSTR_VAL(key), + ZSTR_LEN(key) + ); + zend_string_hash_val(storage_key); + + if (cache->storage_key_cache == NULL) { + ALLOC_HASHTABLE(cache->storage_key_cache); + zend_hash_init(cache->storage_key_cache, 8, NULL, ZVAL_PTR_DTOR, 0); + } + + if (zend_hash_num_elements(cache->storage_key_cache) < PHP_USER_CACHE_STORAGE_KEY_CACHE_MAX) { + ZVAL_STR_COPY(&cache_zv, storage_key); + zend_hash_add(cache->storage_key_cache, key, &cache_zv); + } + + return storage_key; +} + +static zend_string *user_cache_storage_key( + php_user_cache_object *cache, + zend_string *key) +{ + zend_string *storage_key = user_cache_storage_key_cache_lookup(cache, key); + + if (storage_key != NULL) { + return storage_key; + } + + return user_cache_storage_key_build(cache, key); +} + +static zend_string *user_cache_validated_storage_key( + php_user_cache_object *cache, + zend_string *key, + uint32_t arg_num) +{ + zend_string *storage_key = user_cache_storage_key_cache_lookup(cache, key); + + if (storage_key != NULL) { + return storage_key; + } + + if (!user_cache_validate_key(key, arg_num)) { + return NULL; + } + + return user_cache_storage_key_build(cache, key); +} + +static void user_cache_safe_direct_handlers_dtor(zval *zv) +{ + pefree(Z_PTR_P(zv), true); +} + +const php_user_cache_safe_direct_handlers *php_user_cache_safe_direct_find_handlers( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr) +{ + const php_user_cache_safe_direct_handlers *handlers; + + if (!php_user_cache_safe_direct_handlers_initialized) { + return NULL; + } + + while (ce != NULL) { + handlers = zend_hash_index_find_ptr( + &php_user_cache_safe_direct_handler_table, + (zend_ulong) (uintptr_t) ce + ); + if (handlers != NULL) { + if (base_ce_ptr != NULL) { + *base_ce_ptr = ce; + } + + return handlers; + } + + ce = ce->parent; + } + + return NULL; +} + +static bool user_cache_store_storage_key_prevalidated(zend_string *key, zval *value, zend_long ttl, bool add_only) +{ + php_user_cache_prepare_options prep_opts = { + .caller_holds_write_lock = false, + .throw_on_failure = false, + .honor_strict_store_failure = false, + }; + php_user_cache_store_options store_opts = { + .retry_after_memory_pressure = true, + .throw_on_failure = false, + .honor_strict_store_failure = false, + .capture_replaced_entry = false, + }; + php_user_cache_prepared_value prepared; + php_user_cache_store_result result; + bool stored; + + if (!php_user_cache_prepare_value(key, value, &prep_opts, &prepared)) { + php_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + if (!php_user_cache_wlock_for_entry_mutation(key)) { + php_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + if (add_only && php_user_cache_exists_locked(key)) { + php_user_cache_unlock(); + php_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + zend_try { + stored = php_user_cache_store_prepared_locked( + key, + value, + &prepared, + ttl, + &store_opts, + &result + ); + } zend_catch { + php_user_cache_unlock_if_held(); + php_user_cache_destroy_prepared_value(&prepared); + zend_bailout(); + } zend_end_try(); + + php_user_cache_unlock(); + php_user_cache_destroy_prepared_value(&prepared); + + if (stored && result.should_seed_request_local_slot) { + php_user_cache_store_request_local_slot(key, result.stored_generation, value, true); + } + + return stored; +} + +/* Entry locks must be acquired in this order. */ +static int user_cache_bulk_order_compare(const void *lhs_ptr, const void *rhs_ptr) +{ + const php_user_cache_bulk_order *lhs, *rhs; + size_t lhs_len, rhs_len, compare_len; + int result; + + lhs = lhs_ptr; + rhs = rhs_ptr; + + if (lhs->hash != rhs->hash) { + return lhs->hash < rhs->hash ? -1 : 1; + } + + lhs_len = ZSTR_LEN(lhs->key); + rhs_len = ZSTR_LEN(rhs->key); + compare_len = lhs_len < rhs_len ? lhs_len : rhs_len; + + result = memcmp(ZSTR_VAL(lhs->key), ZSTR_VAL(rhs->key), compare_len); + if (result != 0) { + return result < 0 ? -1 : 1; + } + + if (lhs_len != rhs_len) { + return lhs_len < rhs_len ? -1 : 1; + } + + return lhs->index < rhs->index ? -1 : (lhs->index > rhs->index ? 1 : 0); +} + +/* Acquires locks in bulk order and records them by original index. */ +static bool user_cache_bulk_lock_keys_ordered( + zend_string **storage_keys, + php_user_cache_bulk_order *order, + bool *acquired, + uint32_t count) +{ + zend_string **sorted_keys; + uint32_t i; + bool *sorted_acquired, result; + + for (i = 0; i < count; i++) { + order[i].hash = zend_string_hash_val(storage_keys[i]); + order[i].key = storage_keys[i]; + order[i].index = i; + } + + qsort(order, count, sizeof(*order), user_cache_bulk_order_compare); + + sorted_keys = safe_emalloc(count, sizeof(*sorted_keys), 0); + sorted_acquired = safe_emalloc(count, sizeof(*sorted_acquired), 0); + + for (i = 0; i < count; i++) { + sorted_keys[i] = storage_keys[order[i].index]; + } + + result = php_user_cache_acquire_entry_locks(sorted_keys, sorted_acquired, count); + + for (i = 0; i < count; i++) { + acquired[order[i].index] = sorted_acquired[i]; + } + + efree(sorted_acquired); + efree(sorted_keys); + + return result; +} + +static uint32_t user_cache_prepare_bulk_store_items( + php_user_cache_object *cache, + HashTable *values, + php_user_cache_bulk_store_item *items, + zend_string **storage_keys, + const php_user_cache_prepare_options *prep_opts, + bool *result) +{ + zend_ulong num_key; + zend_string *key, *storage_key; + zval *value; + uint32_t i = 0; + + *result = true; + + ZEND_HASH_FOREACH_KEY_VAL(values, num_key, key, value) { + if (key != NULL) { + storage_key = user_cache_storage_key(cache, key); + } else { + key = zend_long_to_str(num_key); + storage_key = user_cache_storage_key(cache, key); + + zend_string_release(key); + } + + storage_keys[i] = storage_key; + items[i].value = value; + + if (!php_user_cache_prepare_value(storage_key, value, prep_opts, &items[i].prepared)) { + php_user_cache_destroy_prepared_value(&items[i].prepared); + zend_string_release(storage_key); + + *result = false; + + break; + } + + i++; + } ZEND_HASH_FOREACH_END(); + + return i; +} + +/* Rolls back the entire batch if any store fails. */ +static uint32_t user_cache_commit_bulk_store_locked( + php_user_cache_bulk_store_item *items, + zend_string **storage_keys, + uint32_t prepared_count, + zend_long ttl, + const php_user_cache_store_options *store_opts, + bool *result) +{ + uint32_t i, j, stored_count = 0; + + *result = true; + + for (i = 0; i < prepared_count; i++) { + if (!php_user_cache_store_prepared_locked( + storage_keys[i], + items[i].value, + &items[i].prepared, + ttl, + store_opts, + &items[i].store_result) + ) { + *result = false; + + break; + } + + stored_count = i + 1; + } + + if (*result) { + for (i = 0; i < stored_count; i++) { + php_user_cache_discard_replaced_entry_locked( + storage_keys[i], + &items[i].store_result.replaced_entry + ); + } + } else { + for (j = stored_count; j > 0; j--) { + php_user_cache_rollback_replaced_entry_locked( + storage_keys[j - 1], + &items[j - 1].store_result.replaced_entry + ); + } + + stored_count = 0; + } + + return stored_count; +} + +static void user_cache_finish_bulk_store( + php_user_cache_bulk_store_item *items, + zend_string **storage_keys, + uint32_t stored_count) +{ + uint32_t i; + + for (i = 0; i < stored_count; i++) { + if (items[i].store_result.should_seed_request_local_slot) { + php_user_cache_store_request_local_slot( + storage_keys[i], + items[i].store_result.stored_generation, + items[i].value, + true + ); + } + } +} + +static bool user_cache_instance_store_multiple( + php_user_cache_object *cache, + HashTable *values, + zend_long ttl) +{ + php_user_cache_bulk_store_item *items; + php_user_cache_bulk_order *order; + zend_string **storage_keys; + bool *acquired; + php_user_cache_prepare_options prep_opts = { + .caller_holds_write_lock = false, + .throw_on_failure = false, + .honor_strict_store_failure = false, + }; + php_user_cache_store_options store_opts = { + .retry_after_memory_pressure = false, + .throw_on_failure = false, + .honor_strict_store_failure = false, + .capture_replaced_entry = true, + }; + uint32_t i, count, prepared_count, stored_count = 0; + bool result; + + if (!user_cache_can_write()) { + return false; + } + + count = zend_hash_num_elements(values); + if (count == 0) { + return true; + } + + items = ecalloc(count, sizeof(*items)); + order = safe_emalloc(count, sizeof(*order), 0); + storage_keys = safe_emalloc(count, sizeof(zend_string *), 0); + acquired = ecalloc(count, sizeof(bool)); + + prepared_count = user_cache_prepare_bulk_store_items( + cache, + values, + items, + storage_keys, + &prep_opts, + &result + ); + + if (result) { + result = user_cache_bulk_lock_keys_ordered(storage_keys, order, acquired, prepared_count); + } + + if (result && user_cache_begin_write()) { + zend_try { + stored_count = user_cache_commit_bulk_store_locked( + items, + storage_keys, + prepared_count, + ttl, + &store_opts, + &result + ); + } zend_catch { + php_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + php_user_cache_unlock(); + } else { + result = false; + } + + php_user_cache_release_entry_locks(storage_keys, acquired, prepared_count); + + user_cache_finish_bulk_store(items, storage_keys, stored_count); + + for (i = 0; i < prepared_count; i++) { + php_user_cache_destroy_prepared_value(&items[i].prepared); + zend_string_release(storage_keys[i]); + } + + efree(acquired); + efree(storage_keys); + efree(order); + efree(items); + + return result; +} + +static bool user_cache_instance_delete_multiple( + php_user_cache_object *cache, + HashTable *keys) +{ + php_user_cache_bulk_order *order; + zend_string **prepared_keys, **storage_keys; + uint32_t i, count; + bool *acquired, result = true; + + if (!user_cache_prepare_key_list(keys, 1, &prepared_keys, &count)) { + return false; + } + + if (!user_cache_can_write()) { + user_cache_release_key_list(prepared_keys, count); + + return !EG(exception); + } + + if (count == 0) { + user_cache_release_key_list(prepared_keys, count); + + return true; + } + + storage_keys = safe_emalloc(count, sizeof(zend_string *), 0); + order = safe_emalloc(count, sizeof(php_user_cache_bulk_order), 0); + acquired = ecalloc(count, sizeof(bool)); + + for (i = 0; i < count; i++) { + storage_keys[i] = user_cache_storage_key(cache, prepared_keys[i]); + } + + result = user_cache_bulk_lock_keys_ordered(storage_keys, order, acquired, count); + + if (result && user_cache_begin_write()) { + zend_try { + for (i = 0; i < count; i++) { + php_user_cache_delete_locked(storage_keys[i]); + } + } zend_catch { + php_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + php_user_cache_unlock(); + } else { + result = false; + } + + php_user_cache_release_entry_locks(storage_keys, acquired, count); + + for (i = 0; i < count; i++) { + zend_string_release(storage_keys[i]); + } + + efree(acquired); + efree(order); + efree(storage_keys); + + user_cache_release_key_list(prepared_keys, count); + + return result; +} + +static bool user_cache_init_partition_context(php_user_cache_partition *partition, const char *name) +{ + partition->context = php_user_cache_context_state; + partition->context.storage.lock_file = -1; + + if (name != NULL) { + partition->name = strdup(name); + if (partition->name == NULL) { + return false; + } + + partition->context.name = partition->name; + partition->context.lock_name = partition->name; + } + + return true; +} + +static bool user_cache_startup_storage_for_context(php_user_cache_context *ctx) +{ + php_user_cache_context *prev_ctx; + + prev_ctx = php_user_cache_activate_context(ctx); + + php_user_cache_reset_runtime(); + if (php_user_cache_active_runtime()->enabled && UC_G(enable)) { + if (!php_user_cache_startup_storage_before_request()) { + php_user_cache_restore_context(prev_ctx); + + return false; + } + } + + php_user_cache_restore_context(prev_ctx); + + return true; +} + +static void user_cache_partitions_shutdown(void) +{ + php_user_cache_partition *partition, *next; + php_user_cache_context *prev_ctx; + + prev_ctx = UC_G(active_context_ptr); + partition = php_user_cache_partitions; + while (partition != NULL) { + next = partition->next; + + php_user_cache_activate_context(&partition->context); + php_user_cache_shutdown_storage(); + php_user_cache_reset_runtime(); + + free(partition->name); + free(partition); + + partition = next; + } + + php_user_cache_partitions = NULL; + + php_user_cache_restore_context(prev_ctx); +} + +/* Separate identical boundary names owned by different Unix users. */ +static bool user_cache_format_boundary_key_prefix(char *prefix, size_t prefix_size, size_t *prefix_len) +{ +#if !defined(ZEND_WIN32) && defined(HAVE_UNISTD_H) + int n; + + n = snprintf( + prefix, + prefix_size, + "uid:%ld:gid:%ld:", + (long) geteuid(), + (long) getegid() + ); + + if (n < 0 || (size_t) n >= prefix_size) { + return false; + } + + *prefix_len = (size_t) n; +#else + prefix[0] = '\0'; + *prefix_len = 0; + (void) prefix_size; +#endif + + return true; +} + +static char *user_cache_build_boundary_key(const char *boundary, size_t *key_len) +{ + size_t boundary_len, prefix_len; + char *boundary_key, prefix[64]; + + boundary_len = strlen(boundary); + + if (!user_cache_format_boundary_key_prefix(prefix, sizeof(prefix), &prefix_len)) { + return NULL; + } + + if (boundary_len > SIZE_MAX - prefix_len - 1) { + return NULL; + } + + boundary_key = malloc(prefix_len + boundary_len + 1); + if (boundary_key == NULL) { + return NULL; + } + + memcpy(boundary_key, prefix, prefix_len); + memcpy(boundary_key + prefix_len, boundary, boundary_len + 1); + + *key_len = prefix_len + boundary_len; + + return boundary_key; +} + +static php_user_cache_boundary_partition *user_cache_find_boundary_partition( + const char *boundary, + size_t boundary_len, + zend_ulong boundary_hash) +{ + php_user_cache_boundary_partition *entry; + + for (entry = php_user_cache_boundary_partitions; entry != NULL; entry = entry->next) { + if (entry->boundary_hash == boundary_hash && + entry->boundary_len == boundary_len && + memcmp(entry->boundary, boundary, boundary_len) == 0 + ) { + return entry; + } + } + + return NULL; +} + +static php_user_cache_boundary_partition *user_cache_create_boundary_partition( + const char *sapi_prefix, + const char *boundary, + size_t boundary_len, + zend_ulong boundary_hash, + void (*log_message)(const char *message)) +{ + php_user_cache_boundary_partition *entry; + char partition_name[128]; + + if (php_user_cache_boundary_partition_count >= PHP_USER_CACHE_MAX_BOUNDARY_PARTITIONS) { + if (!php_user_cache_boundary_limit_logged) { + log_message("User Cache disabled for this request because the cache boundary partition limit was reached"); + php_user_cache_boundary_limit_logged = true; + } + + return NULL; + } + + entry = calloc(1, sizeof(*entry)); + if (entry == NULL) { + return NULL; + } + + entry->boundary = malloc(boundary_len + 1); + if (entry->boundary == NULL) { + free(entry); + + return NULL; + } + + memcpy(entry->boundary, boundary, boundary_len); + entry->boundary[boundary_len] = '\0'; + entry->boundary_len = boundary_len; + entry->boundary_hash = boundary_hash; + + snprintf( + partition_name, + sizeof(partition_name), + "%s:boundary:" ZEND_ULONG_FMT ":" ZEND_ULONG_FMT ":%zx", + sapi_prefix, + boundary_hash, + boundary_len > 1 ? zend_inline_hash_func(boundary + 1, boundary_len - 1) : boundary_hash, + boundary_len + ); + + entry->partition = php_user_cache_partition_create(partition_name); + if (entry->partition == NULL) { + free(entry->boundary); + free(entry); + + return NULL; + } + + entry->partition->context.boundary_identity = entry->boundary; + entry->partition->context.boundary_identity_len = entry->boundary_len; + entry->partition->context.boundary_shared = true; + + entry->next = php_user_cache_boundary_partitions; + php_user_cache_boundary_partitions = entry; + php_user_cache_boundary_partition_count++; + + return entry; +} + +uint64_t php_user_cache_cached_pid(void) +{ +#ifndef ZEND_WIN32 + if (UNEXPECTED(php_user_cache_self_pid_uncached)) { + return php_user_cache_current_pid(); + } +#endif + + if (UNEXPECTED(php_user_cache_self_pid == 0)) { + php_user_cache_self_pid = php_user_cache_current_pid(); + } + + return php_user_cache_self_pid; +} + +void php_user_cache_safe_direct_handlers_init(void) +{ + if (php_user_cache_safe_direct_handlers_initialized) { + return; + } + + zend_hash_init( + &php_user_cache_safe_direct_handler_table, + 8, + NULL, + user_cache_safe_direct_handlers_dtor, + true + ); + + php_user_cache_safe_direct_handlers_initialized = true; +} + +void php_user_cache_safe_direct_handlers_destroy(void) +{ + if (!php_user_cache_safe_direct_handlers_initialized) { + return; + } + + zend_hash_destroy(&php_user_cache_safe_direct_handler_table); + + php_user_cache_safe_direct_handlers_initialized = false; +} + +ZEND_API void php_user_cache_safe_direct_register_class( + zend_class_entry *ce, + const php_user_cache_safe_direct_handlers *handlers) +{ + php_user_cache_safe_direct_handlers handlers_copy; + + if (ce == NULL || + handlers == NULL || + handlers->copy == NULL || + handlers->state_serialize == NULL || + handlers->state_unserialize == NULL + ) { + return; + } + + php_user_cache_safe_direct_handlers_init(); + + handlers_copy = *handlers; + + zend_hash_index_update_mem( + &php_user_cache_safe_direct_handler_table, + (zend_ulong) (uintptr_t) ce, + &handlers_copy, + sizeof(handlers_copy) + ); +} + +php_user_cache_safe_direct_state_copy_func_t php_user_cache_safe_direct_state_copy_func( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr) +{ + const php_user_cache_safe_direct_handlers *handlers = + php_user_cache_safe_direct_find_handlers(ce, base_ce_ptr) + ; + + return handlers != NULL ? handlers->copy : NULL; +} + +zend_class_entry *php_user_cache_safe_direct_find_base(zend_class_entry *ce) +{ + zend_class_entry *base_ce = NULL; + + if (php_user_cache_safe_direct_state_copy_func(ce, &base_ce) == NULL) { + return NULL; + } + + return base_ce; +} + +PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(state_has_unstorable) + +PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(state_serialize) + +PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(state_unserialize) + +bool php_user_cache_safe_direct_prefers_request_local_prototype(zend_class_entry *ce) +{ + const php_user_cache_safe_direct_handlers *handlers = + php_user_cache_safe_direct_find_handlers(ce, NULL) + ; + + return handlers != NULL && handlers->prefer_request_local_prototype; +} + +ZEND_API php_user_cache_partition *php_user_cache_partition_create(const char *name) +{ + php_user_cache_partition *partition; + + partition = calloc(1, sizeof(php_user_cache_partition)); + if (partition == NULL) { + return NULL; + } + + if (!user_cache_init_partition_context(partition, name)) { + free(partition); + + return NULL; + } + + partition->next = php_user_cache_partitions; + php_user_cache_partitions = partition; + + return partition; +} + +ZEND_API bool php_user_cache_partition_startup_storage(php_user_cache_partition *partition) +{ + php_user_cache_partition *prev_partition; + bool result; + + if (partition == NULL) { + return true; + } + + prev_partition = UC_G(active_partition); + UC_G(active_partition) = partition; + + result = user_cache_startup_storage_for_context(&partition->context); + + UC_G(active_partition) = prev_partition; + + return result; +} + +ZEND_API bool php_user_cache_startup_default_context_storage(void) +{ + return user_cache_startup_storage_for_context(&php_user_cache_context_state); +} + +ZEND_API void php_user_cache_partition_activate(php_user_cache_partition *partition) +{ + UC_G(active_partition) = partition; + UC_G(active_context_ptr) = NULL; + UC_G(request_unavailable_reason) = PHP_USER_CACHE_REASON_NONE; + UC_G(runtime_resolved) = false; +} + +ZEND_API void php_user_cache_activate_request_unavailable(php_user_cache_reason reason) +{ + UC_G(active_partition) = NULL; + UC_G(active_context_ptr) = NULL; + UC_G(request_unavailable_reason) = reason; + UC_G(runtime_resolved) = false; +} + +ZEND_API php_user_cache_partition *php_user_cache_boundary_partition_get( + const char *sapi_prefix, + const char *boundary, + void (*log_message)(const char *message)) +{ + php_user_cache_boundary_partition *entry; + zend_ulong boundary_hash; + size_t boundary_len; + char *boundary_key; + + boundary_key = user_cache_build_boundary_key(boundary, &boundary_len); + if (boundary_key == NULL) { + return NULL; + } + + boundary_hash = zend_inline_hash_func(boundary_key, boundary_len); + entry = user_cache_find_boundary_partition(boundary_key, boundary_len, boundary_hash); + if (entry == NULL) { + entry = user_cache_create_boundary_partition(sapi_prefix, boundary_key, boundary_len, boundary_hash, log_message); + if (entry == NULL) { + free(boundary_key); + + return NULL; + } + } + + free(boundary_key); + + if (!php_user_cache_partition_startup_storage(entry->partition) && + !php_user_cache_boundary_startup_failed_logged + ) { + log_message("User Cache partition startup failed; User Cache will be unavailable"); + php_user_cache_boundary_startup_failed_logged = true; + } + + return entry->partition; +} + +ZEND_API void php_user_cache_activate_boundary_partition( + const char *sapi_prefix, + const char *(*get_env)(const char *name), + void (*log_message)(const char *message), + php_user_cache_reason failure_reason) +{ + php_user_cache_partition *partition = NULL; + const char *boundary; + + boundary = get_env("DOCUMENT_ROOT"); + if (boundary == NULL || boundary[0] == '\0') { + boundary = get_env("SERVER_NAME"); + } + + if (boundary != NULL && boundary[0] != '\0') { + partition = php_user_cache_boundary_partition_get(sapi_prefix, boundary, log_message); + } + + if (partition == NULL) { + php_user_cache_activate_request_unavailable(failure_reason); + + return; + } + + php_user_cache_partition_activate(partition); +} + +ZEND_API void php_user_cache_boundary_partitions_shutdown(void) +{ + php_user_cache_boundary_partition *entry, *next; + + entry = php_user_cache_boundary_partitions; + while (entry != NULL) { + next = entry->next; + free(entry->boundary); + free(entry); + entry = next; + } + + php_user_cache_boundary_partitions = NULL; + php_user_cache_boundary_partition_count = 0; + php_user_cache_boundary_limit_logged = false; + php_user_cache_boundary_startup_failed_logged = false; +} + +ZEND_API void php_user_cache_opt_in(void) +{ + php_user_cache_runtime_opted_in = true; +} + +#ifdef ZTS +static void user_cache_globals_ctor(php_user_cache_globals *user_cache_globals) +{ + memset(user_cache_globals, 0, sizeof(php_user_cache_globals)); +} + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +/* Release reader slots owned by the terminating thread. */ +static void user_cache_globals_dtor(php_user_cache_globals *user_cache_globals) +{ + php_user_cache_release_thread_reader_claims(user_cache_globals); +} +#endif + +size_t php_user_cache_globals_size(void) +{ + return sizeof(php_user_cache_globals); +} + +/* SAPI activation may access these globals before MINIT and after MSHUTDOWN. */ +void php_user_cache_globals_startup(void) +{ + user_cache_globals_id = ts_allocate_fast_id( + &user_cache_globals_id, + &user_cache_globals_offset, + sizeof(php_user_cache_globals), + (ts_allocate_ctor) user_cache_globals_ctor, +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + (ts_allocate_dtor) user_cache_globals_dtor +#else + NULL +#endif + ); +} +#endif + +void php_user_cache_minit(void) +{ + php_user_cache_context *prev_ctx; + +#ifndef ZEND_WIN32 + php_user_cache_self_pid = php_user_cache_current_pid(); + if (!php_user_cache_pid_atfork_registered && + !php_user_cache_self_pid_uncached + ) { + if (pthread_atfork(NULL, NULL, user_cache_pid_atfork_child) == 0) { + php_user_cache_pid_atfork_registered = true; + } else { + php_user_cache_self_pid = 0; + php_user_cache_self_pid_uncached = true; + } + } +#endif + + php_user_cache_runtime_opted_in = false; + + user_cache_register_classes(); + php_user_cache_safe_direct_handlers_init(); + php_user_cache_optimistic_fork_setup(); + + prev_ctx = php_user_cache_activate_context(php_user_cache_owning_context()); + + php_user_cache_reset_storage(); + + php_user_cache_restore_context(prev_ctx); +} + +void php_user_cache_mshutdown(void) +{ + php_user_cache_context *prev_ctx; + + user_cache_partitions_shutdown(); + + UC_G(active_partition) = NULL; + + prev_ctx = php_user_cache_activate_context(php_user_cache_owning_context()); + + php_user_cache_shutdown_storage(); + php_user_cache_reset_runtime(); + php_user_cache_restore_context(prev_ctx); + + UC_G(active_partition) = NULL; + php_user_cache_runtime_opted_in = false; + + php_user_cache_safe_direct_handlers_destroy(); + user_cache_reset_class_entries(); +} + +void php_user_cache_invalidate_active(void) +{ + user_cache_invalidate_context(php_user_cache_active_context()); + + php_user_cache_lookup_cache_clear(); + php_user_cache_release_active_request_local_slots(); +} + +zend_result php_user_cache_rshutdown(void) +{ + php_user_cache_unlock_if_held(); + + UC_G(request_unavailable_reason) = PHP_USER_CACHE_REASON_NONE; + UC_G(runtime_resolved) = false; + + user_cache_release_pools(); + php_user_cache_lookup_cache_clear(); + + php_user_cache_release_request_entry_locks(); + php_user_cache_release_request_local_slots(); + + if (UC_G(request_local_slot_may_cycle)) { + UC_G(request_local_slot_may_cycle) = false; + + /* No later GC run is guaranteed during shutdown. */ + if (!CG(unclean_shutdown)) { + gc_collect_cycles(); + } + } + + php_user_cache_decode_resolve_cache_release(); + php_user_cache_decode_shape_prototype_cache_release(); + php_user_cache_decode_maps_teardown(); + + return SUCCESS; +} + +zend_result php_user_cache_post_deactivate(void) +{ + php_user_cache_release_request_shared_graph_refs(); + + return SUCCESS; +} + +static void user_cache_store_method(INTERNAL_FUNCTION_PARAMETERS, bool add_only) +{ + php_user_cache_object *cache; + zend_long ttl = 0; + zend_string *key, *storage_key; + zval *value; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(key) + Z_PARAM_ZVAL(value) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_validate_api_value(value, 2)) { + zend_string_release(storage_key); + RETURN_THROWS(); + } + + if (!user_cache_validate_non_negative(ttl, 3)) { + zend_string_release(storage_key); + RETURN_THROWS(); + } + + if (!(user_cache_can_write() && + user_cache_store_storage_key_prevalidated(storage_key, value, ttl, add_only))) { + zend_string_release(storage_key); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + zend_string_release(storage_key); + + RETURN_TRUE; +} + +static void user_cache_atomic_update_method(INTERNAL_FUNCTION_PARAMETERS, bool decrement) +{ + php_user_cache_object *cache; + php_user_cache_atomic_update_result result; + zend_long step = 1, ttl = 0; + zend_string *key, *storage_key; + bool updated; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(step) + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_validate_non_negative(step, 2)) { + zend_string_release(storage_key); + + RETURN_THROWS(); + } + + if (!user_cache_validate_non_negative(ttl, 3)) { + zend_string_release(storage_key); + + RETURN_THROWS(); + } + + updated = user_cache_atomic_update_api(storage_key, step, ttl, decrement, &result); + zend_string_release(storage_key); + + if (result.is_type_error) { + zend_value_error( + "%s of user cache key \"%s\" requires the stored value to be an integer", + decrement ? "Decrement" : "Increment", + ZSTR_VAL(key) + ); + + RETURN_THROWS(); + } + + if (result.is_overflow) { + zend_throw_error( + zend_ce_arithmetic_error, + "%s of user cache key \"%s\" would exceed the range of a PHP integer", + decrement ? "Decrement" : "Increment", + ZSTR_VAL(key) + ); + + RETURN_THROWS(); + } + + if (!updated) { + RETURN_NULL(); + } + + RETURN_LONG(result.new_value); +} + +ZEND_METHOD(UserCache_CacheStatus, __construct) +{ +} + +ZEND_METHOD(UserCache_CacheStatus, getAvailability) +{ + php_user_cache_status_object *status; + zend_object *availability; + + ZEND_PARSE_PARAMETERS_NONE(); + + status = user_cache_status_from_this(ZEND_THIS); + if (status == NULL) { + RETURN_THROWS(); + } + + availability = user_cache_availability_enum_case(status->availability_reason); + + RETURN_OBJ_COPY(availability); +} + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getConfiguredMemory, configured_memory) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getSharedMemorySize, shared_memory_size) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getUsedMemory, used_memory) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getFreeMemory, free_memory) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getWastedMemory, wasted_memory) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getEntryCount, entry_count) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getEntryCapacity, entry_capacity) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getTombstoneCount, tombstone_count) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getExpungeCount, expunge_count) + +PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getStoreFailureCount, store_failure_count) + +ZEND_METHOD(UserCache_CachePoolStatus, __construct) +{ +} + +ZEND_METHOD(UserCache_CachePoolStatus, getPoolName) +{ + php_user_cache_pool_status_object *status; + + ZEND_PARSE_PARAMETERS_NONE(); + + status = user_cache_pool_status_from_this(ZEND_THIS); + if (status == NULL) { + RETURN_THROWS(); + } + + RETURN_STR_COPY(status->scope); +} + +ZEND_METHOD(UserCache_CachePoolStatus, getEntryCount) +{ + php_user_cache_pool_status_object *status; + + ZEND_PARSE_PARAMETERS_NONE(); + + status = user_cache_pool_status_from_this(ZEND_THIS); + if (status == NULL) { + RETURN_THROWS(); + } + + RETURN_LONG(status->entry_count); +} + +ZEND_METHOD(UserCache_CachePoolStatus, getEntryKeys) +{ + php_user_cache_pool_status_object *status; + + ZEND_PARSE_PARAMETERS_NONE(); + + status = user_cache_pool_status_from_this(ZEND_THIS); + if (status == NULL) { + RETURN_THROWS(); + } + + if (Z_TYPE(status->entry_keys) != IS_ARRAY) { + RETURN_EMPTY_ARRAY(); + } + + RETURN_COPY(&status->entry_keys); +} + +ZEND_METHOD(UserCache_CachePoolStatus, getUsedMemory) +{ + php_user_cache_pool_status_object *status; + + ZEND_PARSE_PARAMETERS_NONE(); + + status = user_cache_pool_status_from_this(ZEND_THIS); + if (status == NULL) { + RETURN_THROWS(); + } + + RETURN_LONG(status->used_memory); +} + +ZEND_METHOD(UserCache_Cache, __construct) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + zend_throw_error(NULL, "UserCache\\Cache instances must be obtained via UserCache\\Cache::getPool()"); +} + +ZEND_METHOD(UserCache_Cache, hasPool) +{ + zend_string *pool; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(pool) + ZEND_PARSE_PARAMETERS_END(); + + if (!user_cache_validate_pool_name(pool, 1)) { + RETURN_THROWS(); + } + + RETURN_BOOL(UC_G(pool_table) != NULL && + zend_hash_exists(UC_G(pool_table), pool)); +} + +ZEND_METHOD(UserCache_Cache, getPool) +{ + zend_string *pool; + zend_object *obj; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(pool) + ZEND_PARSE_PARAMETERS_END(); + + if (!user_cache_validate_pool_name(pool, 1)) { + RETURN_THROWS(); + } + + obj = user_cache_get_or_create_pool(pool); + if (obj == NULL) { + zend_throw_error(NULL, "Failed to register UserCache\\Cache pool"); + + RETURN_THROWS(); + } + + GC_ADDREF(obj); + + RETURN_OBJ(obj); +} + +ZEND_METHOD(UserCache_Cache, getPools) +{ + zend_string *pool; + zval *instance; + + ZEND_PARSE_PARAMETERS_NONE(); + + array_init(return_value); + + if (UC_G(pool_table) == NULL) { + return; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(UC_G(pool_table), pool, instance) { + if (pool == NULL) { + continue; + } + + Z_ADDREF_P(instance); + + /* Preserve numeric-looking pool names as string keys. */ + zend_hash_add(Z_ARRVAL_P(return_value), pool, instance); + } ZEND_HASH_FOREACH_END(); +} + +ZEND_METHOD(UserCache_Cache, getStatus) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + user_cache_return_status(return_value); + + if (EG(exception)) { + RETURN_THROWS(); + } +} + +ZEND_METHOD(UserCache_Cache, store) +{ + user_cache_store_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, false); +} + +ZEND_METHOD(UserCache_Cache, add) +{ + user_cache_store_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, true); +} + +ZEND_METHOD(UserCache_Cache, storeMultiple) +{ + php_user_cache_object *cache; + zend_long ttl = 0; + HashTable *values; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(values) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_validate_store_array(values, 1)) { + RETURN_THROWS(); + } + + if (!user_cache_validate_non_negative(ttl, 2)) { + RETURN_THROWS(); + } + + if (!user_cache_instance_store_multiple(cache, values, ttl)) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + RETURN_TRUE; +} + +ZEND_METHOD(UserCache_Cache, increment) +{ + user_cache_atomic_update_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, false); +} + +ZEND_METHOD(UserCache_Cache, decrement) +{ + user_cache_atomic_update_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, true); +} + +ZEND_METHOD(UserCache_Cache, fetch) +{ + php_user_cache_object *cache; + zend_string *key, *storage_key; + zval *default_value = NULL, default_null; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + user_cache_fetch_api(storage_key, default_value, return_value); + + zend_string_release(storage_key); +} + +ZEND_METHOD(UserCache_Cache, fetchMultiple) +{ + php_user_cache_object *cache; + zval *default_value = NULL, default_null; + HashTable *keys; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(keys) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + if (user_cache_fetch_multiple_api(cache, keys, default_value, return_value) == FAILURE) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_EMPTY_ARRAY(); + } +} + +ZEND_METHOD(UserCache_Cache, has) +{ + php_user_cache_object *cache; + zend_string *key, *storage_key; + bool exists; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + exists = user_cache_exists_api(storage_key); + + zend_string_release(storage_key); + + RETURN_BOOL(exists); +} + +ZEND_METHOD(UserCache_Cache, delete) +{ + php_user_cache_object *cache; + zend_string *key, *storage_key; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_can_write()) { + zend_string_release(storage_key); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_TRUE; + } + + if (!user_cache_delete_storage_key_prevalidated(storage_key)) { + zend_string_release(storage_key); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + zend_string_release(storage_key); + + RETURN_TRUE; +} + +ZEND_METHOD(UserCache_Cache, deleteMultiple) +{ + php_user_cache_object *cache; + HashTable *keys; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(keys) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_instance_delete_multiple(cache, keys)) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + RETURN_TRUE; +} + +ZEND_METHOD(UserCache_Cache, clear) +{ + php_user_cache_object *cache; + + ZEND_PARSE_PARAMETERS_NONE(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_can_write()) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_TRUE; + } + + if (!user_cache_clear_scope_prevalidated(cache->scope_prefix)) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + RETURN_TRUE; +} + +ZEND_METHOD(UserCache_Cache, lock) +{ + php_user_cache_object *cache; + zend_long lease = 0; + zend_string *key, *storage_key; + bool locked; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(lease) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_validate_non_negative(lease, 2)) { + zend_string_release(storage_key); + + RETURN_THROWS(); + } + + locked = user_cache_lock_api(storage_key, lease); + zend_string_release(storage_key); + + RETURN_BOOL(locked); +} + +ZEND_METHOD(UserCache_Cache, unlock) +{ + php_user_cache_object *cache; + zend_string *key, *storage_key; + bool unlocked; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + unlocked = user_cache_unlock_api(storage_key); + zend_string_release(storage_key); + + RETURN_BOOL(unlocked); +} + +static bool user_cache_invoke_remember_callback( + zend_string *key, + zend_string *storage_key, + zend_fcall_info *fci, + zend_fcall_info_cache *fcc, + zend_long ttl, + zval *result) +{ + zval key_zv; + + ZVAL_UNDEF(result); + fci->retval = result; + + ZVAL_STR(&key_zv, key); + fci->param_count = 1; + fci->params = &key_zv; + fci->named_params = NULL; + + if (zend_call_function(fci, fcc) != SUCCESS || EG(exception)) { + return true; + } + + if (Z_TYPE_P(result) == IS_UNDEF) { + return true; + } + + if (!user_cache_validate_remember_value(result)) { + return false; + } + + if (user_cache_can_write() && user_cache_store_storage_key_prevalidated(storage_key, result, ttl, false)) { + return true; + } + + /* Failing to store the memoized value is not itself an error unless it raised an exception. */ + return !EG(exception); +} + +ZEND_METHOD(UserCache_Cache, remember) +{ + php_user_cache_object *cache; + zend_long ttl = 0; + zend_string *key, *storage_key; + zend_fcall_info fci; + zend_fcall_info_cache fcc; + zval result; + bool found = false, locked = false, callback_failed = false; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(key) + Z_PARAM_FUNC(fci, fcc) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + storage_key = user_cache_validated_storage_key(cache, key, 1); + if (storage_key == NULL) { + RETURN_THROWS(); + } + + if (!user_cache_validate_non_negative(ttl, 3)) { + zend_string_release(storage_key); + + RETURN_THROWS(); + } + + found = user_cache_fetch_if_present_api(storage_key, return_value); + if (found) { + zend_string_release(storage_key); + + return; + } + + locked = user_cache_lock_api(storage_key, 0); + + /* Another request may have populated the entry while the lock was acquired. */ + found = user_cache_fetch_if_present_api(storage_key, return_value); + if (found) { + if (locked) { + locked = user_cache_unlock_api(storage_key); + } + + zend_string_release(storage_key); + + return; + } + + /* Do not return before zend_end_try() restores the bailout chain. */ + zend_try { + callback_failed = + !user_cache_invoke_remember_callback(key, storage_key, &fci, &fcc, ttl, &result); + } zend_catch { + if (locked) { + locked = user_cache_unlock_api(storage_key); + } + + zend_string_release(storage_key); + + zend_bailout(); + } zend_end_try(); + + if (callback_failed) { + zval_ptr_dtor(&result); + + locked = user_cache_unlock_api(storage_key); + + zend_string_release(storage_key); + + RETURN_THROWS(); + } + + if (locked) { + locked = user_cache_unlock_api(storage_key); + } + + zend_string_release(storage_key); + + if (EG(exception)) { + if (Z_TYPE(result) != IS_UNDEF) { + zval_ptr_dtor(&result); + } + + RETURN_THROWS(); + } + + if (Z_TYPE(result) == IS_UNDEF) { + RETURN_NULL(); + } + + RETURN_COPY_VALUE(&result); +} + +ZEND_METHOD(UserCache_Cache, getPoolStatus) +{ + php_user_cache_object *cache; + + ZEND_PARSE_PARAMETERS_NONE(); + + cache = user_cache_object_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + user_cache_return_pool_status(cache, return_value); + + if (EG(exception)) { + RETURN_THROWS(); + } +} + +static ZEND_INI_MH(OnUpdateUserCacheShmSize) +{ + zend_long *p, size; + + p = (zend_long *) ZEND_INI_GET_ADDR(); + size = zend_ini_parse_quantity_warn(new_value, entry->name); + + if (size < 0) { + zend_error(E_WARNING, "user_cache.shm_size must be greater than or equal to 0, " ZEND_LONG_FMT " given", size); + + return FAILURE; + } + + if ((zend_ulong) size > UINT32_MAX - ZEND_MM_ALIGNMENT) { + size = (zend_long) (UINT32_MAX - ZEND_MM_ALIGNMENT); + zend_error(E_WARNING, "user_cache.shm_size is limited to slightly under 4096M; clamping"); + } + + *p = size; + + return SUCCESS; +} + +PHP_INI_BEGIN() + STD_PHP_INI_ENTRY("user_cache.enable", "1", PHP_INI_SYSTEM, OnUpdateBool, enable, php_user_cache_globals, user_cache_globals) + STD_PHP_INI_ENTRY("user_cache.enable_cli", "0", PHP_INI_SYSTEM, OnUpdateBool, enable_cli, php_user_cache_globals, user_cache_globals) + STD_PHP_INI_ENTRY("user_cache.shm_size", "16M", PHP_INI_SYSTEM, OnUpdateUserCacheShmSize, shm_size, php_user_cache_globals, user_cache_globals) + STD_PHP_INI_ENTRY("user_cache.lockfile_path", "/tmp", PHP_INI_SYSTEM, OnUpdateString, lockfile_path, php_user_cache_globals, user_cache_globals) + STD_PHP_INI_ENTRY("user_cache.preferred_memory_model", "", PHP_INI_SYSTEM, OnUpdateStringUnempty, memory_model, php_user_cache_globals, user_cache_globals) +PHP_INI_END() + +ZEND_FUNCTION(user_cache_reset) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_user_cache_invalidate_active(); + + RETURN_TRUE; +} + +static PHP_MINIT_FUNCTION(cache) +{ + REGISTER_INI_ENTRIES(); + + php_user_cache_minit(); + + return SUCCESS; +} + +static PHP_MSHUTDOWN_FUNCTION(cache) +{ + php_user_cache_mshutdown(); + + UNREGISTER_INI_ENTRIES(); + + return SUCCESS; +} + +static PHP_RSHUTDOWN_FUNCTION(cache) +{ + return php_user_cache_rshutdown(); +} + +static PHP_MINFO_FUNCTION(cache) +{ + php_info_print_table_start(); + php_info_print_table_row(2, "User Cache support", "enabled"); + php_info_print_table_end(); + + DISPLAY_INI_ENTRIES(); +} + +zend_module_entry user_cache_module_entry = { + STANDARD_MODULE_HEADER, + "user_cache", + ext_functions, + PHP_MINIT(cache), + PHP_MSHUTDOWN(cache), + NULL, + PHP_RSHUTDOWN(cache), + PHP_MINFO(cache), + PHP_VERSION, + NO_MODULE_GLOBALS, + php_user_cache_post_deactivate, + STANDARD_MODULE_PROPERTIES_EX +}; diff --git a/ext/user_cache/user_cache.stub.php b/ext/user_cache/user_cache.stub.php new file mode 100644 index 000000000000..2db5970d6fa2 --- /dev/null +++ b/ext/user_cache/user_cache.stub.php @@ -0,0 +1,127 @@ + */ + public function getEntryKeys(): array {} + + public function getUsedMemory(): int {} +} + +/** @not-serializable */ +final class Cache +{ + public static function hasPool(string $pool): bool {} + + public static function getPool(string $pool): Cache {} + + /** @return array */ + public static function getPools(): array {} + + public static function getStatus(): CacheStatus {} + + private function __construct() {} + + public function store(string $key, null|bool|int|float|string|array|object $value, int $ttl = 0): bool {} + + public function add(string $key, null|bool|int|float|string|array|object $value, int $ttl = 0): bool {} + + public function storeMultiple(array $values, int $ttl = 0): bool {} + + public function increment(string $key, int $step = 1, int $ttl = 0): ?int {} + + public function decrement(string $key, int $step = 1, int $ttl = 0): ?int {} + + public function fetch(string $key, mixed $default = null): mixed {} + + public function fetchMultiple(array $keys, mixed $default = null): array {} + + public function delete(string $key): bool {} + + public function deleteMultiple(array $keys): bool {} + + public function clear(): bool {} + + public function has(string $key): bool {} + + public function lock(string $key, int $lease = 0): bool {} + + public function unlock(string $key): bool {} + + public function remember(string $key, callable $callback, int $ttl = 0): mixed {} + + public function getPoolStatus(): CachePoolStatus {} +} + +} + +namespace { + +function user_cache_reset(): bool {} + +} diff --git a/ext/user_cache/user_cache_alloc_posix.c b/ext/user_cache/user_cache_alloc_posix.c new file mode 100644 index 000000000000..5d575066af74 --- /dev/null +++ b/ext/user_cache/user_cache_alloc_posix.c @@ -0,0 +1,128 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#include "user_cache_shm.h" + +#ifdef PHP_USER_CACHE_USE_SHM_OPEN + +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + php_user_cache_shm_segment common; + int shm_fd; +} php_user_cache_shm_segment_posix; + +static int user_cache_alloc_posix_create_segments(size_t requested_size, php_user_cache_shm_segment_posix ***shared_segments_p, int *shared_segments_count, const char **error_in) +{ + php_user_cache_shm_segment_posix *shared_segment; + char shared_segment_name[sizeof("/php_user_cache.") + 20]; + int shared_segment_flags = O_RDWR|O_CREAT|O_TRUNC; + mode_t shared_segment_mode = 0600; + +#if defined(HAVE_SHM_CREATE_LARGEPAGE) + /* Prefer the largest compatible page size. */ + size_t i, shared_segment_sizes = 0, shared_segment_lg_index = 0; + size_t shared_segment_sindexes[3] = {0}; + const size_t entries = sizeof(shared_segment_sindexes) / sizeof(shared_segment_sindexes[0]); + + shared_segment_sizes = getpagesizes(shared_segment_sindexes, entries); + + if (shared_segment_sizes > 0) { + for (i = shared_segment_sizes; i-- > 0; ) { + if (shared_segment_sindexes[i] != 0 && + !(requested_size % shared_segment_sindexes[i])) { + shared_segment_lg_index = i; + break; + } + } + } +#endif + + *shared_segments_count = 1; + *shared_segments_p = (php_user_cache_shm_segment_posix **) calloc(1, sizeof(php_user_cache_shm_segment_posix) + sizeof(void *)); + if (!*shared_segments_p) { + *error_in = "calloc"; + return PHP_USER_CACHE_ALLOC_FAILURE; + } + shared_segment = (php_user_cache_shm_segment_posix *)((char *)(*shared_segments_p) + sizeof(void *)); + (*shared_segments_p)[0] = shared_segment; + + snprintf(shared_segment_name, sizeof(shared_segment_name), "/php_user_cache.%d", getpid()); +#if defined(HAVE_SHM_CREATE_LARGEPAGE) + if (shared_segment_lg_index > 0) { + shared_segment->shm_fd = shm_create_largepage(shared_segment_name, shared_segment_flags, shared_segment_lg_index, SHM_LARGEPAGE_ALLOC_DEFAULT, shared_segment_mode); + if (shared_segment->shm_fd != -1) { + goto truncate_segment; + } + } +#endif + + shared_segment->shm_fd = shm_open(shared_segment_name, shared_segment_flags, shared_segment_mode); + if (shared_segment->shm_fd == -1) { + *error_in = "shm_open"; + return PHP_USER_CACHE_ALLOC_FAILURE; + } + +#if defined(HAVE_SHM_CREATE_LARGEPAGE) +truncate_segment: +#endif + if (ftruncate(shared_segment->shm_fd, requested_size) != 0) { + *error_in = "ftruncate"; + shm_unlink(shared_segment_name); + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + shared_segment->common.p = mmap(0, requested_size, PROT_READ | PROT_WRITE, MAP_SHARED, shared_segment->shm_fd, 0); + if (shared_segment->common.p == MAP_FAILED) { + *error_in = "mmap"; + shm_unlink(shared_segment_name); + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + shm_unlink(shared_segment_name); + + shared_segment->common.pos = 0; + shared_segment->common.size = requested_size; + + return PHP_USER_CACHE_ALLOC_SUCCESS; +} + +static int user_cache_alloc_posix_detach_segment(php_user_cache_shm_segment_posix *shared_segment) +{ + munmap(shared_segment->common.p, shared_segment->common.size); + close(shared_segment->shm_fd); + + return 0; +} + +static size_t user_cache_alloc_posix_segment_type_size(void) +{ + return sizeof(php_user_cache_shm_segment_posix); +} + +const php_user_cache_shm_handlers php_user_cache_alloc_posix_handlers = { + (php_user_cache_create_segments_t)user_cache_alloc_posix_create_segments, + (php_user_cache_detach_segment_t)user_cache_alloc_posix_detach_segment, + user_cache_alloc_posix_segment_type_size +}; + +#endif /* PHP_USER_CACHE_USE_SHM_OPEN */ diff --git a/ext/user_cache/user_cache_alloc_shm.c b/ext/user_cache/user_cache_alloc_shm.c new file mode 100644 index 000000000000..5a3f271d4594 --- /dev/null +++ b/ext/user_cache/user_cache_alloc_shm.c @@ -0,0 +1,134 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#include "user_cache_shm.h" + +#ifdef PHP_USER_CACHE_USE_SHM + +#if defined(__FreeBSD__) +# include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define PHP_USER_CACHE_MIN(x, y) ((x) > (y) ? (y) : (x)) + +#define PHP_USER_CACHE_SEG_ALLOC_SIZE_MIN (2 * 1024 * 1024) + +typedef struct { + php_user_cache_shm_segment common; + int shm_id; +} php_user_cache_shm_segment_shm; + +static int user_cache_alloc_shm_create_segments(size_t requested_size, php_user_cache_shm_segment_shm ***shared_segments_p, int *shared_segments_count, const char **error_in) +{ + int i; + size_t allocate_size = 0, remaining_bytes, seg_allocate_size; + int first_segment_id = -1; + key_t first_segment_key = -1; + struct shmid_ds sds; + int shmget_flags; + php_user_cache_shm_segment_shm *shared_segments; + + shmget_flags = IPC_CREAT|SHM_R|SHM_W|IPC_EXCL; + + seg_allocate_size = requested_size; + first_segment_id = shmget(first_segment_key, seg_allocate_size, shmget_flags); + if (UNEXPECTED(first_segment_id == -1)) { + /* Fall back to decreasing power-of-two segments. */ + seg_allocate_size = PHP_USER_CACHE_SEG_ALLOC_SIZE_MIN; + while (seg_allocate_size < requested_size / 2) { + seg_allocate_size *= 2; + } + + while (seg_allocate_size >= PHP_USER_CACHE_SEG_ALLOC_SIZE_MIN) { + first_segment_id = shmget(first_segment_key, seg_allocate_size, shmget_flags); + if (first_segment_id != -1) { + break; + } + seg_allocate_size >>= 1; + } + + if (first_segment_id == -1) { + *error_in = "shmget"; + return PHP_USER_CACHE_ALLOC_FAILURE; + } + } + + *shared_segments_count = ((requested_size - 1) / seg_allocate_size) + 1; + *shared_segments_p = (php_user_cache_shm_segment_shm **) calloc(1, (*shared_segments_count) * sizeof(php_user_cache_shm_segment_shm) + sizeof(void *) * (*shared_segments_count)); + if (!*shared_segments_p) { + *error_in = "calloc"; + return PHP_USER_CACHE_ALLOC_FAILURE; + } + shared_segments = (php_user_cache_shm_segment_shm *)((char *)(*shared_segments_p) + sizeof(void *) * (*shared_segments_count)); + for (i = 0; i < *shared_segments_count; i++) { + (*shared_segments_p)[i] = shared_segments + i; + } + + remaining_bytes = requested_size; + for (i = 0; i < *shared_segments_count; i++) { + allocate_size = PHP_USER_CACHE_MIN(remaining_bytes, seg_allocate_size); + if (i != 0) { + shared_segments[i].shm_id = shmget(IPC_PRIVATE, allocate_size, shmget_flags); + } else { + shared_segments[i].shm_id = first_segment_id; + } + + if (shared_segments[i].shm_id == -1) { + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + shared_segments[i].common.p = shmat(shared_segments[i].shm_id, NULL, 0); + if (shared_segments[i].common.p == (void *)-1) { + *error_in = "shmat"; + shmctl(shared_segments[i].shm_id, IPC_RMID, &sds); + return PHP_USER_CACHE_ALLOC_FAILURE; + } + shmctl(shared_segments[i].shm_id, IPC_RMID, &sds); + + shared_segments[i].common.pos = 0; + shared_segments[i].common.size = allocate_size; + remaining_bytes -= allocate_size; + } + return PHP_USER_CACHE_ALLOC_SUCCESS; +} + +static int user_cache_alloc_shm_detach_segment(php_user_cache_shm_segment_shm *shared_segment) +{ + shmdt(shared_segment->common.p); + return 0; +} + +static size_t user_cache_alloc_shm_segment_type_size(void) +{ + return sizeof(php_user_cache_shm_segment_shm); +} + +const php_user_cache_shm_handlers php_user_cache_alloc_shm_handlers = { + (php_user_cache_create_segments_t)user_cache_alloc_shm_create_segments, + (php_user_cache_detach_segment_t)user_cache_alloc_shm_detach_segment, + user_cache_alloc_shm_segment_type_size +}; + +#endif /* PHP_USER_CACHE_USE_SHM */ diff --git a/ext/user_cache/user_cache_arginfo.h b/ext/user_cache/user_cache_arginfo.h new file mode 100644 index 000000000000..25023d384f6d --- /dev/null +++ b/ext/user_cache/user_cache_arginfo.h @@ -0,0 +1,267 @@ +/* This is a generated file, edit user_cache.stub.php instead. + * Stub hash: f475d675d1f7d8fc96ecc58b290107267010d958 + * Has decl header: yes */ + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_user_cache_reset, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_UserCache_CacheStatus___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_CacheStatus_getAvailability, 0, 0, UserCache\\CacheAvailability, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_CacheStatus_getConfiguredMemory, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_CacheStatus_getSharedMemorySize arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getUsedMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getFreeMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getWastedMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getEntryCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getEntryCapacity arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getTombstoneCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getExpungeCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CacheStatus_getStoreFailureCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +#define arginfo_class_UserCache_CachePoolStatus___construct arginfo_class_UserCache_CacheStatus___construct + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_CachePoolStatus_getPoolName, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_CachePoolStatus_getEntryCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_CachePoolStatus_getEntryKeys, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_CachePoolStatus_getUsedMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_hasPool, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, pool, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_Cache_getPool, 0, 1, UserCache\\Cache, 0) + ZEND_ARG_TYPE_INFO(0, pool, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_Cache_getPools arginfo_class_UserCache_CachePoolStatus_getEntryKeys + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_Cache_getStatus, 0, 0, UserCache\\CacheStatus, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_Cache___construct arginfo_class_UserCache_CacheStatus___construct + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_store, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_MASK(0, value, MAY_BE_NULL|MAY_BE_BOOL|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT, NULL) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_Cache_add arginfo_class_UserCache_Cache_store + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_storeMultiple, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_increment, 0, 1, IS_LONG, 1) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, step, IS_LONG, 0, "1") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_Cache_decrement arginfo_class_UserCache_Cache_increment + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_fetch, 0, 1, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_MIXED, 0, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_fetchMultiple, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, keys, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_MIXED, 0, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_delete, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_deleteMultiple, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, keys, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_Cache_clear arginfo_user_cache_reset + +#define arginfo_class_UserCache_Cache_has arginfo_class_UserCache_Cache_delete + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_lock, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, lease, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +#define arginfo_class_UserCache_Cache_unlock arginfo_class_UserCache_Cache_delete + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_remember, 0, 2, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_Cache_getPoolStatus, 0, 0, UserCache\\CachePoolStatus, 0) +ZEND_END_ARG_INFO() + +ZEND_FUNCTION(user_cache_reset); +ZEND_METHOD(UserCache_CacheStatus, __construct); +ZEND_METHOD(UserCache_CacheStatus, getAvailability); +ZEND_METHOD(UserCache_CacheStatus, getConfiguredMemory); +ZEND_METHOD(UserCache_CacheStatus, getSharedMemorySize); +ZEND_METHOD(UserCache_CacheStatus, getUsedMemory); +ZEND_METHOD(UserCache_CacheStatus, getFreeMemory); +ZEND_METHOD(UserCache_CacheStatus, getWastedMemory); +ZEND_METHOD(UserCache_CacheStatus, getEntryCount); +ZEND_METHOD(UserCache_CacheStatus, getEntryCapacity); +ZEND_METHOD(UserCache_CacheStatus, getTombstoneCount); +ZEND_METHOD(UserCache_CacheStatus, getExpungeCount); +ZEND_METHOD(UserCache_CacheStatus, getStoreFailureCount); +ZEND_METHOD(UserCache_CachePoolStatus, __construct); +ZEND_METHOD(UserCache_CachePoolStatus, getPoolName); +ZEND_METHOD(UserCache_CachePoolStatus, getEntryCount); +ZEND_METHOD(UserCache_CachePoolStatus, getEntryKeys); +ZEND_METHOD(UserCache_CachePoolStatus, getUsedMemory); +ZEND_METHOD(UserCache_Cache, hasPool); +ZEND_METHOD(UserCache_Cache, getPool); +ZEND_METHOD(UserCache_Cache, getPools); +ZEND_METHOD(UserCache_Cache, getStatus); +ZEND_METHOD(UserCache_Cache, __construct); +ZEND_METHOD(UserCache_Cache, store); +ZEND_METHOD(UserCache_Cache, add); +ZEND_METHOD(UserCache_Cache, storeMultiple); +ZEND_METHOD(UserCache_Cache, increment); +ZEND_METHOD(UserCache_Cache, decrement); +ZEND_METHOD(UserCache_Cache, fetch); +ZEND_METHOD(UserCache_Cache, fetchMultiple); +ZEND_METHOD(UserCache_Cache, delete); +ZEND_METHOD(UserCache_Cache, deleteMultiple); +ZEND_METHOD(UserCache_Cache, clear); +ZEND_METHOD(UserCache_Cache, has); +ZEND_METHOD(UserCache_Cache, lock); +ZEND_METHOD(UserCache_Cache, unlock); +ZEND_METHOD(UserCache_Cache, remember); +ZEND_METHOD(UserCache_Cache, getPoolStatus); + +static const zend_function_entry ext_functions[] = { + ZEND_FE(user_cache_reset, arginfo_user_cache_reset) + ZEND_FE_END +}; + +static const zend_function_entry class_UserCache_CacheStatus_methods[] = { + ZEND_ME(UserCache_CacheStatus, __construct, arginfo_class_UserCache_CacheStatus___construct, ZEND_ACC_PRIVATE) + ZEND_ME(UserCache_CacheStatus, getAvailability, arginfo_class_UserCache_CacheStatus_getAvailability, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getConfiguredMemory, arginfo_class_UserCache_CacheStatus_getConfiguredMemory, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getSharedMemorySize, arginfo_class_UserCache_CacheStatus_getSharedMemorySize, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getUsedMemory, arginfo_class_UserCache_CacheStatus_getUsedMemory, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getFreeMemory, arginfo_class_UserCache_CacheStatus_getFreeMemory, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getWastedMemory, arginfo_class_UserCache_CacheStatus_getWastedMemory, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getEntryCount, arginfo_class_UserCache_CacheStatus_getEntryCount, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getEntryCapacity, arginfo_class_UserCache_CacheStatus_getEntryCapacity, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getTombstoneCount, arginfo_class_UserCache_CacheStatus_getTombstoneCount, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getExpungeCount, arginfo_class_UserCache_CacheStatus_getExpungeCount, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CacheStatus, getStoreFailureCount, arginfo_class_UserCache_CacheStatus_getStoreFailureCount, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_UserCache_CachePoolStatus_methods[] = { + ZEND_ME(UserCache_CachePoolStatus, __construct, arginfo_class_UserCache_CachePoolStatus___construct, ZEND_ACC_PRIVATE) + ZEND_ME(UserCache_CachePoolStatus, getPoolName, arginfo_class_UserCache_CachePoolStatus_getPoolName, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CachePoolStatus, getEntryCount, arginfo_class_UserCache_CachePoolStatus_getEntryCount, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CachePoolStatus, getEntryKeys, arginfo_class_UserCache_CachePoolStatus_getEntryKeys, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_CachePoolStatus, getUsedMemory, arginfo_class_UserCache_CachePoolStatus_getUsedMemory, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_UserCache_Cache_methods[] = { + ZEND_ME(UserCache_Cache, hasPool, arginfo_class_UserCache_Cache_hasPool, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(UserCache_Cache, getPool, arginfo_class_UserCache_Cache_getPool, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(UserCache_Cache, getPools, arginfo_class_UserCache_Cache_getPools, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(UserCache_Cache, getStatus, arginfo_class_UserCache_Cache_getStatus, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(UserCache_Cache, __construct, arginfo_class_UserCache_Cache___construct, ZEND_ACC_PRIVATE) + ZEND_ME(UserCache_Cache, store, arginfo_class_UserCache_Cache_store, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, add, arginfo_class_UserCache_Cache_add, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, storeMultiple, arginfo_class_UserCache_Cache_storeMultiple, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, increment, arginfo_class_UserCache_Cache_increment, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, decrement, arginfo_class_UserCache_Cache_decrement, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, fetch, arginfo_class_UserCache_Cache_fetch, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, fetchMultiple, arginfo_class_UserCache_Cache_fetchMultiple, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, delete, arginfo_class_UserCache_Cache_delete, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, deleteMultiple, arginfo_class_UserCache_Cache_deleteMultiple, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, clear, arginfo_class_UserCache_Cache_clear, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, has, arginfo_class_UserCache_Cache_has, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, lock, arginfo_class_UserCache_Cache_lock, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, unlock, arginfo_class_UserCache_Cache_unlock, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, remember, arginfo_class_UserCache_Cache_remember, ZEND_ACC_PUBLIC) + ZEND_ME(UserCache_Cache, getPoolStatus, arginfo_class_UserCache_Cache_getPoolStatus, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static zend_class_entry *register_class_UserCache_CacheAvailability(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("UserCache\\CacheAvailability", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Available", NULL); + + zend_enum_add_case_cstr(class_entry, "DisabledByIni", NULL); + + zend_enum_add_case_cstr(class_entry, "DisabledBySapi", NULL); + + zend_enum_add_case_cstr(class_entry, "UnavailableBySharedMemoryInitializationFailed", NULL); + + zend_enum_add_case_cstr(class_entry, "UnavailableByBackendNotInitializedBeforeWorkerStartup", NULL); + + zend_enum_add_case_cstr(class_entry, "UnavailableByBackendInitializedAfterWorkerStartup", NULL); + + zend_enum_add_case_cstr(class_entry, "UnavailableByCgiFastCgiBoundary", NULL); + + zend_enum_add_case_cstr(class_entry, "UnavailableByLsapiBoundary", NULL); + + zend_enum_add_case_cstr(class_entry, "UnavailableByUnknownReason", NULL); + + return class_entry; +} + +static zend_class_entry *register_class_UserCache_CacheStatus(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "UserCache", "CacheStatus", class_UserCache_CacheStatus_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_READONLY_CLASS); + + return class_entry; +} + +static zend_class_entry *register_class_UserCache_CachePoolStatus(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "UserCache", "CachePoolStatus", class_UserCache_CachePoolStatus_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_READONLY_CLASS); + + return class_entry; +} + +static zend_class_entry *register_class_UserCache_Cache(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "UserCache", "Cache", class_UserCache_Cache_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NOT_SERIALIZABLE); + + return class_entry; +} diff --git a/ext/user_cache/user_cache_decl.h b/ext/user_cache/user_cache_decl.h new file mode 100644 index 000000000000..03d993c33468 --- /dev/null +++ b/ext/user_cache/user_cache_decl.h @@ -0,0 +1,19 @@ +/* This is a generated file, edit user_cache.stub.php instead. + * Stub hash: f475d675d1f7d8fc96ecc58b290107267010d958 */ + +#ifndef ZEND_USER_CACHE_DECL_f475d675d1f7d8fc96ecc58b290107267010d958_H +#define ZEND_USER_CACHE_DECL_f475d675d1f7d8fc96ecc58b290107267010d958_H + +typedef enum zend_enum_UserCache_CacheAvailability { + ZEND_ENUM_UserCache_CacheAvailability_Available = 1, + ZEND_ENUM_UserCache_CacheAvailability_DisabledByIni = 2, + ZEND_ENUM_UserCache_CacheAvailability_DisabledBySapi = 3, + ZEND_ENUM_UserCache_CacheAvailability_UnavailableBySharedMemoryInitializationFailed = 4, + ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendNotInitializedBeforeWorkerStartup = 5, + ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendInitializedAfterWorkerStartup = 6, + ZEND_ENUM_UserCache_CacheAvailability_UnavailableByCgiFastCgiBoundary = 7, + ZEND_ENUM_UserCache_CacheAvailability_UnavailableByLsapiBoundary = 8, + ZEND_ENUM_UserCache_CacheAvailability_UnavailableByUnknownReason = 9, +} zend_enum_UserCache_CacheAvailability; + +#endif /* ZEND_USER_CACHE_DECL_f475d675d1f7d8fc96ecc58b290107267010d958_H */ diff --git a/ext/user_cache/user_cache_entries.c b/ext/user_cache/user_cache_entries.c new file mode 100644 index 000000000000..c31e7be0673b --- /dev/null +++ b/ext/user_cache/user_cache_entries.c @@ -0,0 +1,4218 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#include "user_cache_internal.h" + +#include "Zend/zend_closures.h" +#include "Zend/zend_gc.h" +#include "Zend/zend_objects.h" + +#define PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE ((void *) 1) +#define PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE ((void *) 2) + +/* Release the lock before propagating a bailout. */ +#define PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(stmt) \ + do { \ + zend_try { \ + stmt \ + } zend_catch { \ + php_user_cache_unlock_if_held(); \ + zend_bailout(); \ + } zend_end_try(); \ + } while (0) + +typedef enum { + PHP_USER_CACHE_REQUEST_LOCAL_SLOT_MISS, + PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT +} php_user_cache_request_local_slot_result; + +typedef enum { + PHP_USER_CACHE_FIND_SLOT_IGNORE_EXPIRY = 0, + PHP_USER_CACHE_FIND_SLOT_SKIP_EXPIRED, + PHP_USER_CACHE_FIND_SLOT_DELETE_EXPIRED +} php_user_cache_find_slot_expiry_mode; + +typedef enum { + PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT = 1 << 0, + PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES = 1 << 1, + PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT = 1 << 2 +} php_user_cache_fetch_finish_flags; + +typedef enum { + PHP_USER_CACHE_FETCH_LOCATE_MISS, + PHP_USER_CACHE_FETCH_LOCATE_SCALAR_HIT, + PHP_USER_CACHE_FETCH_LOCATE_SLOT, + PHP_USER_CACHE_FETCH_LOCATE_UNCACHED +} php_user_cache_fetch_locate_result; + +typedef enum { + PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS, + PHP_USER_CACHE_OBJECT_SERDES, + PHP_USER_CACHE_OBJECT_OPAQUE, + PHP_USER_CACHE_OBJECT_SCAN_MEMBERS +} php_user_cache_object_storability; + +typedef enum { + PHP_USER_CACHE_STORE_ATTEMPT_STORED, + PHP_USER_CACHE_STORE_ATTEMPT_RETRY, + PHP_USER_CACHE_STORE_ATTEMPT_FAILED +} php_user_cache_store_attempt_result; + +typedef struct { + HashTable arrays; + HashTable objects; + HashTable references; + HashTable *clone_verdicts; + bool track_identity; +} php_user_cache_request_local_clone_context; + +typedef struct { + HashTable *seen_arrays; + HashTable *seen_objects; + const char **failure_message; +} php_user_cache_unstorable_context; + +static bool user_cache_clone_request_local_value( + php_user_cache_request_local_clone_context *ctx, + zval *dst, + zval *src +); + +static bool user_cache_collect_request_local_clone_verdicts_inner( + zval *value, + HashTable *seen_arrs, + HashTable *seen_objs, + HashTable *verdicts, + bool record_array_result +); + +static bool user_cache_find_unstorable_value( + zval *value, + HashTable *seen_arrs, + HashTable *seen_objs, + const char **msg +); + +static void user_cache_rehash_locked(php_user_cache_header *header); +static bool user_cache_expunge_expired_locked(void); +static bool user_cache_expunge_expired_bounded_locked(void); + +static zend_always_inline bool user_cache_value_uses_offset(uint8_t value_type) +{ + return + value_type == PHP_USER_CACHE_VALUE_STRING || + value_type == PHP_USER_CACHE_VALUE_SHARED_GRAPH + ; +} + +static zend_always_inline uint32_t user_cache_max_capacity_for_size(size_t size) +{ + size_t usable; + + if (size <= sizeof(php_user_cache_header)) { + return 0; + } + + usable = (size - sizeof(php_user_cache_header)) / sizeof(php_user_cache_entry); + + return usable > UINT32_MAX + ? UINT32_MAX + : (uint32_t) usable + ; +} + +static zend_always_inline uint8_t *user_cache_ptr_in_header( + const php_user_cache_header *header, + uint32_t offset) +{ + return (uint8_t *) header + offset; +} + +static zend_always_inline bool user_cache_key_equals( + const php_user_cache_header *header, + const php_user_cache_entry *entry, + zend_string *key, + zend_ulong hash) +{ + if (entry->state != PHP_USER_CACHE_ENTRY_USED || entry->hash != hash || entry->key_len != ZSTR_LEN(key)) { + return false; + } + + return memcmp( + user_cache_ptr_in_header(header, entry->key_offset), + ZSTR_VAL(key), + ZSTR_LEN(key) + ) == 0; +} + +static zend_always_inline uint32_t user_cache_write_payload_locked( + const php_user_cache_header *header, + uint32_t reusable_offset, + size_t size, + const void *src) +{ + if (reusable_offset != 0 && php_user_cache_block_payload_capacity(reusable_offset) >= size) { + memcpy(user_cache_ptr_in_header(header, reusable_offset), src, size); + + return reusable_offset; + } + + return php_user_cache_alloc_locked(size, src); +} + +static zend_always_inline php_user_cache_lookup_entry *user_cache_lookup_cache_set(zend_ulong hash) +{ + uint32_t set_idx = (uint32_t) (hash & (PHP_USER_CACHE_LOOKUP_SETS - 1)); + + return &UC_G(lookup_entry_storage)[set_idx * PHP_USER_CACHE_LOOKUP_WAYS]; +} + +static zend_always_inline void user_cache_lookup_entry_release_key( + php_user_cache_lookup_entry *lookup_entry) +{ + if (lookup_entry->key != NULL) { + zend_string_release(lookup_entry->key); + lookup_entry->key = NULL; + } +} + +static zend_always_inline void user_cache_lookup_cache_store( + php_user_cache_lookup_entry *lookup_entry, + zend_ulong hash, + uint64_t epoch, + uint32_t slot_idx, + uint8_t state) +{ + if (lookup_entry == NULL) { + return; + } + + user_cache_lookup_entry_release_key(lookup_entry); + + lookup_entry->hash = hash; + lookup_entry->mutation_epoch = epoch; + lookup_entry->context = php_user_cache_active_context(); + lookup_entry->slot_index = slot_idx; + lookup_entry->state = state; + lookup_entry->value_type = PHP_USER_CACHE_LOOKUP_VALUE_NONE; +} + +/* Prefer the current context, then empty, stale and miss slots. */ +static zend_always_inline php_user_cache_lookup_entry *user_cache_lookup_cache_select_slot( + php_user_cache_lookup_entry *lookup_entries, + zend_ulong hash, + uint64_t epoch, + bool allow_hit_eviction) +{ + const void *ctx = php_user_cache_active_context(); + php_user_cache_lookup_entry *preferred, *alternate; + uint32_t way = (uint32_t) ((((uint64_t) hash >> 32) ^ hash) & (PHP_USER_CACHE_LOOKUP_WAYS - 1)); + + if (lookup_entries == NULL) { + return NULL; + } + + preferred = &lookup_entries[way]; + alternate = preferred == &lookup_entries[0] ? &lookup_entries[1] : &lookup_entries[0]; + + if (preferred->state != PHP_USER_CACHE_LOOKUP_EMPTY && + preferred->hash == hash && + preferred->mutation_epoch == epoch && + preferred->context == ctx + ) { + return preferred; + } + if (alternate->state != PHP_USER_CACHE_LOOKUP_EMPTY && + alternate->hash == hash && + alternate->mutation_epoch == epoch && + alternate->context == ctx + ) { + return alternate; + } + + if (preferred->state == PHP_USER_CACHE_LOOKUP_EMPTY || + preferred->mutation_epoch != epoch || + preferred->context != ctx + ) { + return preferred; + } + if (alternate->state == PHP_USER_CACHE_LOOKUP_EMPTY || + alternate->mutation_epoch != epoch || + alternate->context != ctx + ) { + return alternate; + } + + if (preferred->state == PHP_USER_CACHE_LOOKUP_MISS) { + return preferred; + } + if (alternate->state == PHP_USER_CACHE_LOOKUP_MISS) { + return alternate; + } + + return allow_hit_eviction ? preferred : NULL; +} + +static zend_always_inline void user_cache_lookup_cache_store_miss( + php_user_cache_lookup_entry *lookup_entries, + zend_ulong hash, + uint64_t epoch, + zend_string *key) +{ + php_user_cache_lookup_entry *victim = + user_cache_lookup_cache_select_slot(lookup_entries, hash, epoch, false) + ; + + if (victim == NULL) { + return; + } + + user_cache_lookup_cache_store( + victim, + hash, + epoch, + PHP_USER_CACHE_LOOKUP_INVALID_SLOT, + PHP_USER_CACHE_LOOKUP_MISS + ); + + /* MISS entries must still distinguish hash collisions. */ + victim->key = zend_string_copy(key); +} + +static zend_always_inline void user_cache_lookup_cache_store_hit( + php_user_cache_lookup_entry *lookup_entry, + zend_ulong hash, + uint64_t epoch, + uint32_t slot_idx, + zend_string *key, + const php_user_cache_entry *entry) +{ + if (lookup_entry == NULL) { + return; + } + + user_cache_lookup_cache_store( + lookup_entry, + hash, + epoch, + slot_idx, + PHP_USER_CACHE_LOOKUP_HIT + ); + + lookup_entry->key = zend_string_copy(key); + + if (entry->expires_at != 0) { + return; + } + + switch (entry->value_type) { + case PHP_USER_CACHE_VALUE_NULL: + case PHP_USER_CACHE_VALUE_TRUE: + case PHP_USER_CACHE_VALUE_FALSE: + case PHP_USER_CACHE_VALUE_LONG: + case PHP_USER_CACHE_VALUE_DOUBLE: + lookup_entry->value_type = entry->value_type; + lookup_entry->long_value = entry->long_value; + lookup_entry->double_value = entry->double_value; + + break; + default: + break; + } +} + +static zend_always_inline void user_cache_lookup_cache_reset_entry(php_user_cache_lookup_entry *lookup_entry) +{ + if (lookup_entry == NULL) { + return; + } + + user_cache_lookup_entry_release_key(lookup_entry); + + memset(lookup_entry, 0, sizeof(*lookup_entry)); +} + +static zend_always_inline void user_cache_release_value_storage_locked(uint8_t value_type, uint32_t value_offset) +{ + bool graph_quiescent; + + if (value_offset == 0) { + return; + } + + if (value_type == PHP_USER_CACHE_VALUE_SHARED_GRAPH) { + graph_quiescent = php_user_cache_quiesce_graph_payloads_locked(); + + if (php_user_cache_shared_graph_retire_payload_locked(value_offset)) { + if (graph_quiescent) { + php_user_cache_free_locked(value_offset); + } else { + php_user_cache_shared_graph_orphan_payload_locked(value_offset); + } + } + } else if (user_cache_value_uses_offset(value_type)) { + php_user_cache_free_locked(value_offset); + } +} + +static zend_always_inline void user_cache_release_entry_storage_except_locked( + php_user_cache_entry *entry, + const php_user_cache_entry *kept_entry) +{ + bool combined, kept_combined; + + combined = (entry->reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + kept_combined = kept_entry != NULL && + (kept_entry->reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0 + ; + if (entry->key_offset != 0 && !combined && + (kept_entry == NULL || kept_combined || entry->key_offset != kept_entry->key_offset) + ) { + php_user_cache_free_locked(entry->key_offset); + } + + if (entry->value_offset != 0 && + ( + kept_entry == NULL || + entry->value_offset != kept_entry->value_offset || + entry->value_type != kept_entry->value_type + ) + ) { + user_cache_release_value_storage_locked(entry->value_type, entry->value_offset); + } +} + +static zend_always_inline void user_cache_release_entry_storage_locked(php_user_cache_entry *entry) +{ + user_cache_release_entry_storage_except_locked(entry, NULL); +} + +static zend_always_inline void user_cache_delete_entry_locked(php_user_cache_header *header, php_user_cache_entry *entry) +{ + if (entry->state == PHP_USER_CACHE_ENTRY_USED && header->count != 0) { + header->count--; + } + + if (entry->state != PHP_USER_CACHE_ENTRY_TOMBSTONE) { + header->tombstone_count++; + } + + user_cache_release_entry_storage_locked(entry); + + entry->hash = 0; + entry->key_offset = 0; + entry->key_len = 0; + entry->state = PHP_USER_CACHE_ENTRY_TOMBSTONE; + entry->value_type = PHP_USER_CACHE_VALUE_NULL; + entry->value_offset = 0; + entry->value_len = 0; + entry->reserved = 0; + entry->expires_at = 0; + entry->generation = 0; + entry->long_value = 0; + entry->double_value = 0; + + php_user_cache_bump_mutation_epoch_locked(header); +} + +static zend_always_inline void user_cache_release_request_local_slot_table(HashTable **slots_ptr) +{ + if (*slots_ptr == NULL) { + return; + } + + zend_hash_destroy(*slots_ptr); + + FREE_HASHTABLE(*slots_ptr); + + *slots_ptr = NULL; +} + +static zend_always_inline bool user_cache_is_expired(const php_user_cache_entry *entry, uint64_t now) +{ + return entry->state == PHP_USER_CACHE_ENTRY_USED && + entry->expires_at != 0 && + entry->expires_at <= now + ; +} + +static zend_always_inline bool user_cache_is_expired_now(const php_user_cache_entry *entry, uint64_t *now) +{ + if (entry->state != PHP_USER_CACHE_ENTRY_USED || entry->expires_at == 0) { + return false; + } + + if (*now == 0) { + *now = (uint64_t) time(NULL); + } + + return user_cache_is_expired(entry, *now); +} + +static zend_always_inline void user_cache_note_expired_read(void) +{ + UC_G(expired_read_observations)++; +} + +static zend_always_inline bool user_cache_scalar_to_zval( + uint8_t value_type, + zend_long lval, + double dval, + zval *return_value) +{ + switch (value_type) { + case PHP_USER_CACHE_VALUE_NULL: + ZVAL_NULL(return_value); + + return true; + case PHP_USER_CACHE_VALUE_TRUE: + ZVAL_TRUE(return_value); + + return true; + case PHP_USER_CACHE_VALUE_FALSE: + ZVAL_FALSE(return_value); + + return true; + case PHP_USER_CACHE_VALUE_LONG: + ZVAL_LONG(return_value, lval); + + return true; + case PHP_USER_CACHE_VALUE_DOUBLE: + ZVAL_DOUBLE(return_value, dval); + + return true; + default: + return false; + } +} + +static zend_always_inline bool user_cache_payload_can_fit_locked(size_t size) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + size_t total_size; + + if (!header || size == 0 || size > UINT32_MAX - sizeof(php_user_cache_block)) { + return false; + } + + total_size = PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + size); + + return total_size <= UINT32_MAX && total_size <= header->data_size; +} + +static zend_always_inline void user_cache_init_prepared_value(php_user_cache_prepared_value *prepared) +{ + memset(prepared, 0, sizeof(*prepared)); + + prepared->value_type = PHP_USER_CACHE_VALUE_NULL; +} + +static zend_always_inline bool user_cache_prepared_value_should_seed_request_local_slot( + const php_user_cache_prepared_value *prepared) +{ + return prepared->value_type == PHP_USER_CACHE_VALUE_STRING && + prepared->value_len >= PHP_USER_CACHE_REQUEST_LOCAL_STRING_MIN_LEN + ; +} + +static zend_always_inline bool user_cache_long_add_overflow( + zend_long lhs, + zend_long rhs, + zend_long *result) +{ + *result = (zend_long) ((zend_ulong) lhs + (zend_ulong) rhs); + + return (rhs > 0 && lhs > ZEND_LONG_MAX - rhs) || + (rhs < 0 && lhs < ZEND_LONG_MIN - rhs) + ; +} + +static zend_always_inline bool user_cache_long_sub_overflow( + zend_long lhs, + zend_long rhs, + zend_long *result) +{ + *result = (zend_long) ((zend_ulong) lhs - (zend_ulong) rhs); + + return (rhs > 0 && lhs < ZEND_LONG_MIN + rhs) || + (rhs < 0 && lhs > ZEND_LONG_MAX + rhs) + ; +} + +static zend_always_inline void user_cache_maybe_rehash_locked(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + + if (header != NULL && + php_user_cache_header_is_initialized_locked() && + header->tombstone_count > header->capacity / 4 + ) { + user_cache_rehash_locked(header); + } +} + +static zend_always_inline void user_cache_maybe_expunge_expired_locked(void) +{ + if (EXPECTED(UC_G(expired_read_observations) < PHP_USER_CACHE_EXPIRED_READ_EXPUNGE_THRESHOLD)) { + return; + } + + UC_G(expired_read_observations) = 0; + + (void) user_cache_expunge_expired_bounded_locked(); +} + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +static zend_always_inline bool user_cache_optimistic_payload_in_bounds( + const php_user_cache_header *header, + uint32_t offset, + uint64_t len) +{ + uint64_t limit = (uint64_t) header->data_offset + header->data_size; + + return offset >= header->data_offset + sizeof(php_user_cache_block) && + (uint64_t) offset + len <= limit + ; +} + +static zend_always_inline bool user_cache_optimistic_header( + php_user_cache_header **header_ptr, + uint64_t *seq_ptr) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + uint64_t seq; + + if (header == NULL) { + return false; + } + + seq = php_user_cache_seq_load(&header->write_seq); + if (seq < 2 || (seq & 1) != 0) { + return false; + } + + if (header->magic != PHP_USER_CACHE_MAGIC || + header->version != PHP_USER_CACHE_VERSION || + header->capacity < PHP_USER_CACHE_MIN_CAPACITY || + header->capacity > user_cache_max_capacity_for_size( + php_user_cache_active_context()->storage.size + ) + ) { + return false; + } + + if (php_user_cache_seq_reload(&header->write_seq) != seq) { + return false; + } + + *header_ptr = header; + *seq_ptr = seq; + + return true; +} + +static zend_always_inline php_user_cache_request_local_slot *user_cache_alloc_request_local_slot( + uint64_t gen, + bool no_aliases, + bool has_value) +{ + php_user_cache_request_local_slot *slot; + + slot = emalloc(sizeof(php_user_cache_request_local_slot)); + slot->generation = gen; + slot->context = php_user_cache_active_context(); + slot->needs_deep_clone = false; + slot->has_clone_verdicts = false; + slot->no_aliases = no_aliases; + slot->has_value = has_value; + + ZVAL_UNDEF(&slot->value); + + return slot; +} + +/* The caller must recheck write_seq before using the snapshot. */ +static bool user_cache_optimistic_probe( + const php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + const php_user_cache_entry *entries, + php_user_cache_entry *snapshot, + uint32_t *slot_idx, + bool *found) +{ + const php_user_cache_entry *entry; + uint64_t now = 0; + uint32_t i, step; + + i = (uint32_t) (hash % header->capacity); + + for (step = 0; step < header->capacity; step++) { + entry = &entries[i]; + + switch (entry->state) { + case PHP_USER_CACHE_ENTRY_EMPTY: + *found = false; + + return true; + case PHP_USER_CACHE_ENTRY_USED: + if (entry->hash == hash && entry->key_len == ZSTR_LEN(key)) { + if (!user_cache_optimistic_payload_in_bounds(header, entry->key_offset, entry->key_len)) { + return false; + } + + if (memcmp(user_cache_ptr_in_header(header, entry->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0) { + if (user_cache_is_expired_now(entry, &now)) { + user_cache_note_expired_read(); + *found = false; + + return true; + } + + *snapshot = *entry; + *slot_idx = i; + *found = true; + + return true; + } + } + break; + case PHP_USER_CACHE_ENTRY_TOMBSTONE: + break; + default: + return false; + } + + ++i; + if (i == header->capacity) { + i = 0; + } + } + + *found = false; + + return true; +} +#endif + +static void user_cache_request_local_slot_dtor(zval *slot_zv) +{ + php_user_cache_request_local_slot *slot = Z_PTR_P(slot_zv); + + if (slot->has_clone_verdicts) { + zend_hash_destroy(&slot->clone_verdicts); + } + + if (!Z_ISUNDEF(slot->value)) { + zval_ptr_dtor(&slot->value); + } + + efree(slot); +} + +static bool user_cache_value_needs_request_local_deep_clone_inner( + zval *value, + HashTable *seen_arrs) +{ + zend_ulong arr_key; + zval *elem; + + if (php_user_cache_stack_overflowed()) { + return true; + } + + if (Z_ISREF_P(value)) { + return true; + } + + switch (Z_TYPE_P(value)) { + case IS_OBJECT: + return true; + case IS_ARRAY: + arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value); + if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARRVAL_P(value))) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) { + if (user_cache_value_needs_request_local_deep_clone_inner(elem, seen_arrs)) { + zend_hash_index_del(seen_arrs, arr_key); + + return true; + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_index_del(seen_arrs, arr_key); + + return false; + default: + return false; + } +} + +static bool user_cache_value_needs_request_local_deep_clone(zval *value) +{ + HashTable seen_arrs; + bool result; + + if (Z_ISREF_P(value)) { + return true; + } else if (Z_TYPE_P(value) == IS_OBJECT) { + return true; + } else if (Z_TYPE_P(value) != IS_ARRAY) { + return false; + } + + zend_hash_init(&seen_arrs, 8, NULL, NULL, 0); + + result = user_cache_value_needs_request_local_deep_clone_inner(value, &seen_arrs); + zend_hash_destroy(&seen_arrs); + + return result; +} + +static bool user_cache_collect_request_local_object_clone_verdict( + zend_object *obj, + HashTable *seen_arrs, + HashTable *seen_objs, + HashTable *verdicts) +{ + zend_ulong obj_key; + zval *prop, *src, *end; + bool members_need_clone = false; + void *flag; + + if (php_user_cache_stack_overflowed()) { + /* A failed walk cannot prove that the value is acyclic. */ + UC_G(request_local_slot_may_cycle) = true; + + return true; + } + + if (obj == NULL) { + return true; + } + + obj_key = (zend_ulong) (uintptr_t) obj; + + flag = zend_hash_index_find_ptr(verdicts, obj_key); + if (flag != NULL) { + return flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE; + } + + if (!php_user_cache_seen_test_and_add(seen_objs, obj)) { + /* A seen unfinished object is a back edge. */ + UC_G(request_local_slot_may_cycle) = true; + + return true; + } + + if (obj->ce->default_properties_count) { + src = obj->properties_table; + end = src + obj->ce->default_properties_count; + + do { + if (user_cache_collect_request_local_clone_verdicts_inner( + src, + seen_arrs, + seen_objs, + verdicts, + true + ) + ) { + members_need_clone = true; + } + + src++; + } while (src != end); + } + + if (obj->properties != NULL && zend_hash_num_elements(obj->properties) != 0) { + members_need_clone = true; + + ZEND_HASH_MAP_FOREACH_VAL(obj->properties, prop) { + if (Z_TYPE_P(prop) != IS_INDIRECT) { + user_cache_collect_request_local_clone_verdicts_inner( + prop, + seen_arrs, + seen_objs, + verdicts, + true + ); + } + } ZEND_HASH_FOREACH_END(); + } + + zend_hash_index_update_ptr( + verdicts, + obj_key, + members_need_clone + ? PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE + : PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE + ); + + return members_need_clone; +} + +static bool user_cache_collect_request_local_clone_verdicts_inner( + zval *value, + HashTable *seen_arrs, + HashTable *seen_objs, + HashTable *verdicts, + bool record_array_result) +{ + zend_ulong arr_key; + zval *elem; + bool needs_deep_clone = false; + void *flag; + + if (php_user_cache_stack_overflowed()) { + UC_G(request_local_slot_may_cycle) = true; + + return true; + } + + if (Z_ISREF_P(value)) { + user_cache_collect_request_local_clone_verdicts_inner( + &Z_REF_P(value)->val, + seen_arrs, + seen_objs, + verdicts, + true + ); + + return true; + } + + switch (Z_TYPE_P(value)) { + case IS_OBJECT: + user_cache_collect_request_local_object_clone_verdict( + Z_OBJ_P(value), + seen_arrs, + seen_objs, + verdicts + ); + + return true; + case IS_ARRAY: + arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value); + flag = zend_hash_index_find_ptr(verdicts, arr_key); + if (flag != NULL) { + return flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE; + } + + if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARRVAL_P(value))) { + /* A seen active array is a back edge. */ + UC_G(request_local_slot_may_cycle) = true; + + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) { + if (user_cache_collect_request_local_clone_verdicts_inner( + elem, + seen_arrs, + seen_objs, + verdicts, + false + ) + ) { + needs_deep_clone = true; + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_index_del(seen_arrs, arr_key); + + if (needs_deep_clone || record_array_result) { + zend_hash_index_update_ptr( + verdicts, + arr_key, + needs_deep_clone + ? PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE + : PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE + ); + } + + return needs_deep_clone; + default: + return false; + } +} + +static bool user_cache_collect_request_local_clone_verdicts( + zval *value, + HashTable *verdicts) +{ + HashTable seen_arrs, seen_objs; + bool result; + + zend_hash_init(&seen_arrs, 8, NULL, NULL, 0); + zend_hash_init(&seen_objs, 8, NULL, NULL, 0); + + result = user_cache_collect_request_local_clone_verdicts_inner( + value, + &seen_arrs, + &seen_objs, + verdicts, + true + ); + + zend_hash_destroy(&seen_objs); + zend_hash_destroy(&seen_arrs); + + return result; +} + +static void user_cache_request_local_clone_array_dtor(zval *zv) +{ + zend_array *array = Z_PTR_P(zv); + + zend_array_release(array); +} + +static void user_cache_request_local_clone_context_init( + php_user_cache_request_local_clone_context *ctx, + HashTable *verdicts, + bool track_identity) +{ + zend_hash_init(&ctx->arrays, 8, NULL, user_cache_request_local_clone_array_dtor, 0); + zend_hash_init(&ctx->objects, 8, NULL, php_user_cache_object_table_dtor, 0); + zend_hash_init(&ctx->references, 8, NULL, php_user_cache_reference_table_dtor, 0); + + ctx->clone_verdicts = verdicts; + ctx->track_identity = track_identity; +} + +static void user_cache_request_local_clone_context_destroy( + php_user_cache_request_local_clone_context *ctx) +{ + zend_hash_destroy(&ctx->references); + zend_hash_destroy(&ctx->objects); + zend_hash_destroy(&ctx->arrays); +} + +static bool user_cache_value_needs_request_local_deep_clone_cached( + php_user_cache_request_local_clone_context *ctx, + zval *value) +{ + void *flag; + + if (Z_ISREF_P(value)) { + return true; + } + + if (Z_TYPE_P(value) == IS_OBJECT) { + return true; + } + + if (Z_TYPE_P(value) != IS_ARRAY) { + return false; + } + + if (ctx->clone_verdicts != NULL) { + flag = zend_hash_index_find_ptr(ctx->clone_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(value)); + if (flag != NULL) { + return flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE; + } + } + + return user_cache_value_needs_request_local_deep_clone(value); +} + +static bool user_cache_clone_request_local_array( + php_user_cache_request_local_clone_context *ctx, + zval *dst, + zval *src, + bool known_needs_deep_clone) +{ + zend_ulong key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(src); + zend_array *array; + zval *elem, cloned_elem; + void *flag; + + if (!known_needs_deep_clone) { + if (ctx->clone_verdicts != NULL) { + flag = zend_hash_index_find_ptr(ctx->clone_verdicts, key); + if (flag == PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE) { + ZVAL_COPY(dst, src); + + return true; + } else if (flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE) { + known_needs_deep_clone = true; + } + } + + if (!known_needs_deep_clone && !user_cache_value_needs_request_local_deep_clone(src)) { + ZVAL_COPY(dst, src); + + return true; + } + } + + if (ctx->track_identity) { + array = zend_hash_index_find_ptr(&ctx->arrays, key); + if (array != NULL) { + GC_ADDREF(array); + ZVAL_ARR(dst, array); + + return true; + } + } + + array = zend_array_dup(Z_ARRVAL_P(src)); + if (ctx->track_identity) { + zend_hash_index_update_ptr(&ctx->arrays, key, array); + } + + ZEND_HASH_FOREACH_VAL(array, elem) { + if (Z_TYPE_P(elem) == IS_INDIRECT || + !user_cache_value_needs_request_local_deep_clone_cached(ctx, elem) + ) { + continue; + } + + if (!user_cache_clone_request_local_value(ctx, &cloned_elem, elem)) { + if (!ctx->track_identity) { + zend_array_release(array); + } + + ZVAL_UNDEF(dst); + + return false; + } + + zval_ptr_dtor(elem); + + ZVAL_COPY_VALUE(elem, &cloned_elem); + } ZEND_HASH_FOREACH_END(); + + if (ctx->track_identity) { + GC_ADDREF(array); + } + + ZVAL_ARR(dst, array); + + return true; +} + +static bool user_cache_clone_request_local_reference( + php_user_cache_request_local_clone_context *ctx, + zval *dst, + zend_reference *src_ref) +{ + zend_ulong key = (zend_ulong) (uintptr_t) src_ref; + zend_reference *new_ref; + zval inner; + + if (ctx->track_identity) { + new_ref = zend_hash_index_find_ptr(&ctx->references, key); + if (new_ref != NULL) { + GC_ADDREF(new_ref); + ZVAL_REF(dst, new_ref); + + return true; + } + } + + ZVAL_NEW_EMPTY_REF(dst); + new_ref = Z_REF_P(dst); + ZVAL_UNDEF(&new_ref->val); + + if (ctx->track_identity) { + zend_hash_index_update_ptr(&ctx->references, key, new_ref); + } + + if (!user_cache_clone_request_local_value(ctx, &inner, &src_ref->val)) { + if (!ctx->track_identity) { + zval_ptr_dtor(dst); + } + + ZVAL_UNDEF(dst); + + return false; + } + + ZVAL_COPY_VALUE(&new_ref->val, &inner); + + if (ctx->track_identity) { + GC_ADDREF(new_ref); + ZVAL_REF(dst, new_ref); + } + + return true; +} + +static bool user_cache_clone_request_local_object_members( + php_user_cache_request_local_clone_context *ctx, + zend_object *old_obj, + zend_object *new_obj) +{ + zend_ulong num_key; + zend_string *key; + zend_property_info *prop_info; + zval *src, *dst, *end, *prop, new_prop; + uint32_t prop_idx = 0; + + if (old_obj->ce->default_properties_count) { + src = old_obj->properties_table; + dst = new_obj->properties_table; + end = src + old_obj->ce->default_properties_count; + + do { + if (!user_cache_clone_request_local_value(ctx, &new_prop, src)) { + return false; + } + + zval_ptr_dtor(dst); + + ZVAL_COPY_VALUE(dst, &new_prop); + Z_PROP_FLAG_P(dst) = Z_PROP_FLAG_P(src); + + if (Z_ISREF_P(dst) && new_obj->ce->properties_info_table != NULL) { + prop_info = new_obj->ce->properties_info_table[prop_idx]; + if (prop_info != NULL && + prop_info != ZEND_WRONG_PROPERTY_INFO && + ZEND_TYPE_IS_SET(prop_info->type) + ) { + ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(dst), prop_info); + } + } + + src++; + dst++; + prop_idx++; + } while (src != end); + } + + if (old_obj->properties != NULL && zend_hash_num_elements(old_obj->properties) != 0) { + if (new_obj->properties != NULL) { + zend_hash_clean(new_obj->properties); + zend_hash_extend(new_obj->properties, zend_hash_num_elements(old_obj->properties), 0); + } else { + new_obj->properties = zend_new_array(zend_hash_num_elements(old_obj->properties)); + zend_hash_real_init_mixed(new_obj->properties); + } + + HT_FLAGS(new_obj->properties) |= + HT_FLAGS(old_obj->properties) & HASH_FLAG_HAS_EMPTY_IND + ; + + ZEND_HASH_MAP_FOREACH_KEY_VAL(old_obj->properties, num_key, key, prop) { + if (Z_TYPE_P(prop) == IS_INDIRECT) { + ZVAL_INDIRECT( + &new_prop, + new_obj->properties_table + (Z_INDIRECT_P(prop) - old_obj->properties_table) + ); + } else if (!user_cache_clone_request_local_value(ctx, &new_prop, prop)) { + return false; + } + + if (key != NULL) { + _zend_hash_append(new_obj->properties, key, &new_prop); + } else { + zend_hash_index_add_new(new_obj->properties, num_key, &new_prop); + } + } ZEND_HASH_FOREACH_END(); + } + + return true; +} + +static bool user_cache_clone_request_local_std_object( + php_user_cache_request_local_clone_context *ctx, + zend_object *old_obj, + zend_object **new_obj_ptr) +{ + zend_object *new_obj; + zval *src, *dst, *end; + void *member_flag = NULL; + + new_obj = zend_objects_new(old_obj->ce); + + if (ctx->track_identity) { + zend_hash_index_update_ptr( + &ctx->objects, + (zend_ulong) (uintptr_t) old_obj, + new_obj + ); + } + + if (ctx->clone_verdicts != NULL) { + member_flag = zend_hash_index_find_ptr( + ctx->clone_verdicts, + (zend_ulong) (uintptr_t) old_obj + ); + } + + if (member_flag == PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE && + old_obj->properties == NULL + ) { + if (old_obj->ce->default_properties_count) { + src = old_obj->properties_table; + dst = new_obj->properties_table; + end = src + old_obj->ce->default_properties_count; + + memcpy(dst, src, sizeof(zval) * old_obj->ce->default_properties_count); + + do { + if (Z_REFCOUNTED_P(src)) { + Z_ADDREF_P(src); + } + + src++; + } while (src != end); + } + } else { + if (new_obj->ce->default_properties_count) { + dst = new_obj->properties_table; + end = dst + new_obj->ce->default_properties_count; + + do { + ZVAL_UNDEF(dst); + + dst++; + } while (dst != end); + } + + if (!user_cache_clone_request_local_object_members(ctx, old_obj, new_obj)) { + if (!ctx->track_identity) { + OBJ_RELEASE(new_obj); + } + + return false; + } + } + + if (ctx->track_identity) { + GC_ADDREF(new_obj); + } + + *new_obj_ptr = new_obj; + + return true; +} + +static bool user_cache_clone_request_local_value_callback( + void *ctx, + zval *dst, + zval *src) +{ + return user_cache_clone_request_local_value( + (php_user_cache_request_local_clone_context *) ctx, + dst, + src + ); +} + +static bool user_cache_clone_request_local_safe_direct_object( + php_user_cache_request_local_clone_context *ctx, + zend_object *old_obj, + zend_object **new_obj_ptr) +{ + php_user_cache_safe_direct_state_copy_func_t copy_func; + zend_ulong key; + zend_class_entry *ce; + zend_object *new_obj; + zval new_zv; + + ce = old_obj->ce; + + copy_func = php_user_cache_safe_direct_state_copy_func(ce, NULL); + if (copy_func == NULL) { + return false; + } + + ZVAL_UNDEF(&new_zv); + + if (object_init_ex(&new_zv, ce) != SUCCESS) { + return false; + } + + new_obj = Z_OBJ(new_zv); + key = (zend_ulong) (uintptr_t) old_obj; + + if (ctx->track_identity) { + zend_hash_index_update_ptr(&ctx->objects, key, new_obj); + } + + if (!copy_func( + ctx, + new_obj, + old_obj, + user_cache_clone_request_local_value_callback + ) + ) { + if (!ctx->track_identity) { + OBJ_RELEASE(new_obj); + } + + return false; + } + + if (!user_cache_clone_request_local_object_members(ctx, old_obj, new_obj)) { + if (!ctx->track_identity) { + OBJ_RELEASE(new_obj); + } + + return false; + } + + if (ctx->track_identity) { + GC_ADDREF(new_obj); + } + + *new_obj_ptr = new_obj; + + return true; +} + +static bool user_cache_clone_request_local_object( + php_user_cache_request_local_clone_context *ctx, + zend_object *old_obj, + zend_object **new_obj_ptr) +{ + zend_ulong key; + zend_object *new_obj; + + if (old_obj == NULL || zend_object_is_lazy(old_obj)) { + return false; + } + + if (ctx->track_identity) { + key = (zend_ulong) (uintptr_t) old_obj; + new_obj = zend_hash_index_find_ptr(&ctx->objects, key); + + if (new_obj != NULL) { + GC_ADDREF(new_obj); + *new_obj_ptr = new_obj; + + return true; + } + } + + if (old_obj->handlers == zend_get_std_object_handlers()) { + return user_cache_clone_request_local_std_object(ctx, old_obj, new_obj_ptr); + } + + return user_cache_clone_request_local_safe_direct_object(ctx, old_obj, new_obj_ptr); +} + +static bool user_cache_clone_request_local_value( + php_user_cache_request_local_clone_context *ctx, + zval *dst, + zval *src) +{ + zend_object *obj; + + if (php_user_cache_stack_overflowed()) { + ZVAL_UNDEF(dst); + + return false; + } + + if (Z_ISREF_P(src)) { + return user_cache_clone_request_local_reference(ctx, dst, Z_REF_P(src)); + } + + switch (Z_TYPE_P(src)) { + case IS_ARRAY: + return user_cache_clone_request_local_array(ctx, dst, src, false); + case IS_OBJECT: + if (!user_cache_clone_request_local_object(ctx, Z_OBJ_P(src), &obj)) { + return false; + } + + ZVAL_OBJ(dst, obj); + + return true; + default: + ZVAL_COPY(dst, src); + + return true; + } +} + +static bool user_cache_clone_request_local_slot_value_known( + zval *dst, + zval *src, + bool needs_deep_clone, + HashTable *verdicts, + bool no_aliases) +{ + php_user_cache_request_local_clone_context ctx; + bool result; + + if (!needs_deep_clone) { + ZVAL_COPY(dst, src); + + return true; + } + + user_cache_request_local_clone_context_init(&ctx, verdicts, !no_aliases); + + if (Z_TYPE_P(src) == IS_ARRAY) { + result = user_cache_clone_request_local_array(&ctx, dst, src, true); + } else { + result = user_cache_clone_request_local_value(&ctx, dst, src); + } + + user_cache_request_local_clone_context_destroy(&ctx); + + return result; +} + +static HashTable *user_cache_request_local_slots(void) +{ + HashTable **slots_ptr = &UC_G(request_local_slot_table); + + if (*slots_ptr == NULL) { + ALLOC_HASHTABLE(*slots_ptr); + zend_hash_init(*slots_ptr, 0, NULL, user_cache_request_local_slot_dtor, 0); + } + + return *slots_ptr; +} + +static zend_never_inline void user_cache_reacquire_read_lock_or_fail(const char *cache_name) +{ + if (!php_user_cache_rlock()) { + zend_error_noreturn(E_ERROR, "Unable to reacquire the %s read lock after userland execution", cache_name); + } +} + +static bool user_cache_materialize_shared_graph_locked( + const php_user_cache_header *header, + zend_string *key, + const char *cache_name, + uint8_t value_type, + uint32_t value_offset, + uint32_t value_len, + bool throw_if_missing, + zval *return_value) +{ + bool result, ref_registered, lock_safe; + + switch (value_type) { + case PHP_USER_CACHE_VALUE_SHARED_GRAPH: + ref_registered = php_user_cache_has_request_shared_graph_ref(value_offset); + if (!ref_registered) { + /* Register before decode can invoke user code and bail out. */ + php_user_cache_shared_graph_ref_reserve(); + + if (!php_user_cache_shared_graph_acquire_ref(value_offset)) { + if (throw_if_missing) { + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + ); + } + + return false; + } + + php_user_cache_register_shared_graph_ref(value_offset); + } + + lock_safe = php_user_cache_shared_graph_decode_is_lock_safe(value_offset); + if (!lock_safe) { + php_user_cache_unlock(); + } + + ZVAL_UNDEF(return_value); + + if (lock_safe) { + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + result = php_user_cache_shared_graph_decode( + user_cache_ptr_in_header(header, value_offset), + value_len, + return_value + ); + ); + } else { + result = php_user_cache_shared_graph_decode( + user_cache_ptr_in_header(header, value_offset), + value_len, + return_value + ); + } + + if (!lock_safe && !result && Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + if (!lock_safe) { + user_cache_reacquire_read_lock_or_fail(cache_name); + } + + if (!result) { + if (lock_safe && Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + /* Request cleanup releases the registered reference. */ + if (!EG(exception) && throw_if_missing) { + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + ); + } + + return false; + } + + return true; + default: + return false; + } +} + +static php_user_cache_request_local_slot *user_cache_find_request_local_slot( + zend_string *key, + uint64_t gen, + bool evict_stale) +{ + php_user_cache_request_local_slot *slot; + HashTable **slots_ptr = &UC_G(request_local_slot_table); + + if (*slots_ptr == NULL) { + return NULL; + } + + slot = zend_hash_find_ptr(*slots_ptr, key); + if (slot == NULL) { + return NULL; + } + + if (slot->generation != gen || + slot->context != (const void *) php_user_cache_active_context() + ) { + if (evict_stale) { + zend_hash_del(*slots_ptr, key); + } + + return NULL; + } + + return slot; +} + +static php_user_cache_request_local_slot_result user_cache_fetch_request_local_slot( + zend_string *key, + uint64_t gen, + zval *return_value) +{ + php_user_cache_request_local_slot *slot = user_cache_find_request_local_slot(key, gen, true); + + if (slot == NULL || !slot->has_value) { + return PHP_USER_CACHE_REQUEST_LOCAL_SLOT_MISS; + } + + if (!user_cache_clone_request_local_slot_value_known( + return_value, + &slot->value, + slot->needs_deep_clone, + slot->has_clone_verdicts ? &slot->clone_verdicts : NULL, + slot->no_aliases + ) + ) { + zend_hash_del(UC_G(request_local_slot_table), key); + + return PHP_USER_CACHE_REQUEST_LOCAL_SLOT_MISS; + } + + return PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT; +} + +static void user_cache_mark_request_local_slot(zend_string *key, uint64_t gen) +{ + php_user_cache_request_local_slot *slot = user_cache_alloc_request_local_slot(gen, true, false); + + zend_hash_update_ptr(user_cache_request_local_slots(), key, slot); +} + +static void user_cache_throw_key_not_found_guarded(zend_string *key) +{ + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key)); + ); +} + +static void user_cache_throw_unknown_value_type_guarded(zend_string *key, const char *cache_name) +{ + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" has an unknown type", cache_name, ZSTR_VAL(key)); + ); +} + +static bool user_cache_find_slot_in_header_locked( + php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + php_user_cache_find_slot_expiry_mode expiry_mode, + uint32_t *slot_idx, + bool *found) +{ + php_user_cache_entry *entries, *entry; + uint64_t now = 0; + uint32_t i, first_tombstone = UINT32_MAX, step; + + if (header == NULL) { + return false; + } + + entries = php_user_cache_entries_ptr(header); + i = (uint32_t) (hash % header->capacity); + + for (step = 0; step < header->capacity; step++) { + entry = &entries[i]; + + if (entry->state == PHP_USER_CACHE_ENTRY_EMPTY) { + *slot_idx = first_tombstone != UINT32_MAX ? first_tombstone : i; + *found = false; + + return true; + } + + if (entry->state == PHP_USER_CACHE_ENTRY_TOMBSTONE) { + if (first_tombstone == UINT32_MAX) { + first_tombstone = i; + } + } else if (expiry_mode != PHP_USER_CACHE_FIND_SLOT_IGNORE_EXPIRY && + user_cache_is_expired_now(entry, &now) + ) { + if (expiry_mode == PHP_USER_CACHE_FIND_SLOT_DELETE_EXPIRED) { + user_cache_delete_entry_locked(header, entry); + } else { + user_cache_note_expired_read(); + } + + if (first_tombstone == UINT32_MAX) { + first_tombstone = i; + } + } else if (user_cache_key_equals(header, entry, key, hash)) { + *slot_idx = i; + *found = true; + + return true; + } + + ++i; + + if (i == header->capacity) { + i = 0; + } + } + + if (first_tombstone != UINT32_MAX) { + *slot_idx = first_tombstone; + *found = false; + + return true; + } + + return false; +} + +static bool user_cache_find_slot_locked( + zend_string *key, + zend_ulong hash, + php_user_cache_find_slot_expiry_mode expiry_mode, + php_user_cache_header **header_ptr, + uint32_t *slot_idx, + bool *found) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + + if (!header || !php_user_cache_header_init_locked()) { + return false; + } + + if (header_ptr != NULL) { + *header_ptr = header; + } + + return user_cache_find_slot_in_header_locked( + header, + key, + hash, + expiry_mode, + slot_idx, + found + ); +} + +static bool user_cache_find_slot_ignore_expiry_locked( + zend_string *key, + zend_ulong hash, + php_user_cache_header **header_ptr, + uint32_t *slot_idx, + bool *found) +{ + return user_cache_find_slot_locked( + key, + hash, + PHP_USER_CACHE_FIND_SLOT_IGNORE_EXPIRY, + header_ptr, + slot_idx, + found + ); +} + +static bool user_cache_find_slot_for_read_locked( + zend_string *key, + zend_ulong hash, + php_user_cache_header **header_ptr, + uint32_t *slot_idx, + bool *found) +{ + return user_cache_find_slot_locked( + key, + hash, + PHP_USER_CACHE_FIND_SLOT_SKIP_EXPIRED, + header_ptr, + slot_idx, + found + ); +} + +static bool user_cache_find_slot_for_write_locked( + zend_string *key, + zend_ulong hash, + php_user_cache_header **header_ptr, + uint32_t *slot_idx, + bool *found) +{ + php_user_cache_header *header; + + if (!user_cache_find_slot_locked( + key, + hash, + PHP_USER_CACHE_FIND_SLOT_DELETE_EXPIRED, + header_ptr, + slot_idx, + found + ) + ) { + return false; + } + + header = *header_ptr; + if (!*found && header->count >= header->capacity - header->capacity / 8) { + return false; + } + + return true; +} + +static void user_cache_rehash_locked(php_user_cache_header *header) +{ + php_user_cache_entry *entries, *snapshot, *entry, *target; + uint32_t i, slot, step; + + entries = php_user_cache_entries_ptr(header); + snapshot = emalloc((size_t) header->capacity * sizeof(*snapshot)); + + memcpy(snapshot, entries, (size_t) header->capacity * sizeof(*snapshot)); + memset(entries, 0, (size_t) header->capacity * sizeof(*entries)); + + header->count = 0; + header->tombstone_count = 0; + + for (i = 0; i < header->capacity; i++) { + entry = &snapshot[i]; + + if (entry->state != PHP_USER_CACHE_ENTRY_USED) { + continue; + } + + slot = (uint32_t) (entry->hash % header->capacity); + for (step = 0; step < header->capacity; step++) { + target = &entries[slot]; + + if (target->state == PHP_USER_CACHE_ENTRY_EMPTY) { + *target = *entry; + header->count++; + + break; + } + + ++slot; + + if (slot == header->capacity) { + slot = 0; + } + } + } + + efree(snapshot); + + php_user_cache_bump_mutation_epoch_locked(header); +} + +static bool user_cache_expunge_expired_locked(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + php_user_cache_entry *entries; + uint64_t now; + uint32_t i; + bool removed = false; + + if (!header || !php_user_cache_header_init_locked()) { + return false; + } + + now = (uint64_t) time(NULL); + entries = php_user_cache_entries_ptr(header); + for (i = 0; i < header->capacity; i++) { + if (user_cache_is_expired(&entries[i], now)) { + user_cache_delete_entry_locked(header, &entries[i]); + removed = true; + } + } + + php_user_cache_shared_graph_reclaim_orphaned_locked(); + + return removed; +} + +/* Bound each expiry scan and resume from a process-local cursor. */ +static bool user_cache_expunge_expired_bounded_locked(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + php_user_cache_entry *entries; + uint64_t now; + uint32_t cursor, scan_len, i; + bool removed = false; + + if (!header || !php_user_cache_header_init_locked()) { + return false; + } + + now = (uint64_t) time(NULL); + entries = php_user_cache_entries_ptr(header); + + cursor = UC_G(expired_expunge_cursor); + if (cursor >= header->capacity) { + cursor = 0; + } + + scan_len = header->capacity < PHP_USER_CACHE_EXPUNGE_SCAN_MAX + ? header->capacity + : PHP_USER_CACHE_EXPUNGE_SCAN_MAX + ; + for (i = 0; i < scan_len; i++) { + if (user_cache_is_expired(&entries[cursor], now)) { + user_cache_delete_entry_locked(header, &entries[cursor]); + removed = true; + } + + ++cursor; + + if (cursor == header->capacity) { + cursor = 0; + } + } + + UC_G(expired_expunge_cursor) = cursor; + + php_user_cache_shared_graph_reclaim_orphaned_locked(); + + return removed; +} + +static void user_cache_handle_store_failure(const char *msg, bool throw_on_failure, bool honor_strict_store_failure) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + + if (honor_strict_store_failure && ctx->strict_store_failure && !throw_on_failure) { + zend_throw_exception_ex(php_user_cache_exception_ce, 0, "%s", msg); + + return; + } + + if (throw_on_failure) { + zend_throw_exception_ex(php_user_cache_exception_ce, 0, "%s", msg); + } +} + +static bool user_cache_safe_direct_value_has_unstorable( + void *ctx_ptr, + const zval *value) +{ + php_user_cache_unstorable_context *ctx = ctx_ptr; + + return user_cache_find_unstorable_value( + (zval *) value, + ctx->seen_arrays, + ctx->seen_objects, + ctx->failure_message + ); +} + +static php_user_cache_object_storability user_cache_object_class_storability( + zend_class_entry *ce) +{ + if (php_user_cache_class_uses_magic_serialize(ce) || + php_user_cache_class_uses_serialize_props(ce) + ) { + return PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS; + } + + if (php_user_cache_class_uses_magic_unserialize(ce)) { + return PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS; + } + + if (php_user_cache_class_uses_serdes(ce)) { + return PHP_USER_CACHE_OBJECT_SERDES; + } + + if ((ce->ce_flags & ZEND_ACC_ENUM) == 0 && + php_user_cache_safe_direct_state_serialize_func(ce) == NULL && + ( + (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) != 0 || + (ce->type != ZEND_USER_CLASS && ce->create_object != NULL) + ) + ) { + return PHP_USER_CACHE_OBJECT_OPAQUE; + } + + return PHP_USER_CACHE_OBJECT_SCAN_MEMBERS; +} + +static bool user_cache_find_unstorable_in_array( + zval *value, + HashTable *seen_arrs, + HashTable *seen_objs, + const char **msg) +{ + zval *elem; + + if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARR_P(value))) { + return false; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) { + if (user_cache_find_unstorable_value(elem, seen_arrs, seen_objs, msg)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static bool user_cache_find_unstorable_in_object( + zval *value, + HashTable *seen_arrs, + HashTable *seen_objs, + const char **msg) +{ + php_user_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable; + php_user_cache_unstorable_context uctx; + php_user_cache_object_storability storability; + zend_object *obj = Z_OBJ_P(value); + zval *elem, *prop, *end; + + if (zend_object_is_lazy(obj)) { + *msg = PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE; + + return true; + } + + if (!php_user_cache_seen_test_and_add(seen_objs, obj)) { + return false; + } + + storability = user_cache_object_class_storability(obj->ce); + if (storability == PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS) { + return false; + } + + state_has_unstorable = php_user_cache_safe_direct_state_has_unstorable_func(obj->ce); + if (state_has_unstorable != NULL) { + uctx.seen_arrays = seen_arrs; + uctx.seen_objects = seen_objs; + uctx.failure_message = msg; + + if (state_has_unstorable( + &uctx, + value, + user_cache_safe_direct_value_has_unstorable) + ) { + if (*msg == NULL) { + *msg = "objects of this class contain values that cannot be stored in the user cache"; + } + + return true; + } + } + + if (storability == PHP_USER_CACHE_OBJECT_SERDES) { + return false; + } + + if (storability == PHP_USER_CACHE_OBJECT_OPAQUE) { + *msg = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE; + + return true; + } + + if (obj->ce->default_properties_count != 0) { + prop = obj->properties_table; + end = prop + obj->ce->default_properties_count; + + do { + if (Z_TYPE_P(prop) != IS_UNDEF && + user_cache_find_unstorable_value( + prop, seen_arrs, seen_objs, msg + ) + ) { + return true; + } + + prop++; + } while (prop != end); + } + + if (obj->properties != NULL) { + ZEND_HASH_FOREACH_VAL(obj->properties, elem) { + if (Z_TYPE_P(elem) == IS_INDIRECT) { + elem = Z_INDIRECT_P(elem); + if (Z_TYPE_P(elem) == IS_UNDEF) { + continue; + } + } + + if (user_cache_find_unstorable_value(elem, seen_arrs, seen_objs, msg)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + return false; +} + +static bool user_cache_find_unstorable_value( + zval *value, + HashTable *seen_arrs, + HashTable *seen_objs, + const char **msg) +{ + if (php_user_cache_stack_overflowed()) { + *msg = "value is nested too deeply to be stored in the user cache"; + + return true; + } + + ZVAL_DEREF(value); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + *msg = PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE; + + return true; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + *msg = PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE; + + return true; + } + + if (Z_TYPE_P(value) == IS_ARRAY) { + return user_cache_find_unstorable_in_array(value, seen_arrs, seen_objs, msg); + } + + if (Z_TYPE_P(value) == IS_OBJECT) { + return user_cache_find_unstorable_in_object(value, seen_arrs, seen_objs, msg); + } + + return false; +} + +static bool user_cache_validate_storable_value(zval *value, bool throw_on_failure, bool honor_strict_store_failure) +{ + const char *msg = NULL; + HashTable seen_arrs, seen_objs; + bool found; + + ZVAL_DEREF(value); + + if (Z_TYPE_P(value) != IS_ARRAY && Z_TYPE_P(value) != IS_OBJECT) { + return true; + } + + zend_hash_init(&seen_arrs, 8, NULL, NULL, 0); + zend_hash_init(&seen_objs, 8, NULL, NULL, 0); + + found = user_cache_find_unstorable_value(value, &seen_arrs, &seen_objs, &msg); + + zend_hash_destroy(&seen_objs); + zend_hash_destroy(&seen_arrs); + + if (EG(exception)) { + return false; + } + + if (!found) { + return true; + } + + if (msg != NULL) { + zend_type_error("%s", msg); + } + + return false; +} + +static uint8_t *user_cache_reserve_combined_value_key_locked( + const php_user_cache_header *header, + uint32_t reusable_offset, + zend_string *key, + size_t payload_size, + uint32_t *value_offset, + uint32_t *key_offset) +{ + uint32_t base_offset; + size_t key_size, total_size; + + key_size = ZSTR_LEN(key) + 1; + if (payload_size > SIZE_MAX - key_size) { + return NULL; + } + + total_size = payload_size + key_size; + if (reusable_offset != 0 && + php_user_cache_block_payload_capacity(reusable_offset) >= total_size + ) { + base_offset = reusable_offset; + } else { + base_offset = php_user_cache_alloc_locked(total_size, NULL); + if (base_offset == 0) { + return NULL; + } + } + + *value_offset = base_offset; + *key_offset = base_offset + (uint32_t) payload_size; + + return user_cache_ptr_in_header(header, base_offset); +} + +static bool user_cache_publish_combined_value_key_locked( + const php_user_cache_header *header, + uint32_t reusable_offset, + zend_string *key, + size_t payload_size, + const void *src, + uint32_t *value_offset, + uint32_t *key_offset) +{ + uint8_t *payload; + + if (src == NULL) { + return false; + } + + payload = user_cache_reserve_combined_value_key_locked( + header, + reusable_offset, + key, + payload_size, + value_offset, + key_offset + ); + if (payload == NULL) { + return false; + } + + memcpy(payload, src, payload_size); + memcpy(payload + payload_size, ZSTR_VAL(key), ZSTR_LEN(key) + 1); + + return true; +} + +/* String roots are copied in place; other graphs normally use the staging + * buffer prepared before taking the write lock. */ +static bool user_cache_publish_prepared_shared_graph_locked( + const php_user_cache_prepared_value *prepared, + zval *value, + uint8_t *payload) +{ + zval pinned_root; + + if (prepared->payload_source != NULL && + php_user_cache_shared_graph_publish_copied_payload_locked( + payload, + prepared->payload_size, + prepared->payload_source, + prepared->payload_size, + prepared->payload_used_size, + prepared->has_verbatim_array, + prepared->fixup_offsets, + prepared->fixup_count + ) + ) { + return true; + } + + /* Use the string pinned during the sizing pass. */ + if (prepared->owned_string != NULL) { + ZVAL_STR(&pinned_root, prepared->owned_string); + value = &pinned_root; + } + + return php_user_cache_build_shared_graph_in_place( + value, + prepared->state_memo, + NULL, + payload, + prepared->payload_size, + NULL, + NULL, + NULL, + NULL + ); +} + +static bool user_cache_reclaim_space_for_store_locked( + bool can_clear, + bool *expired_retry_used, + bool *clear_retry_used) +{ + if (!*expired_retry_used) { + *expired_retry_used = true; + + if (user_cache_expunge_expired_locked()) { + return true; + } + } + + if (can_clear && php_user_cache_active_context()->clear_on_pressure && !*clear_retry_used) { + *clear_retry_used = true; + + if (php_user_cache_entry_locks_allow_clear_locked() && php_user_cache_clear_locked()) { + php_user_cache_header_ptr()->expunge_count++; + + return true; + } + } + + return false; +} + +static void user_cache_rollback_partial_store_allocs_locked( + uint32_t new_value_offset, + uint32_t old_value_offset, + uint32_t new_key_offset, + uint32_t old_key_offset, + uint16_t new_reserved) +{ + if (new_value_offset != 0 && new_value_offset != old_value_offset) { + php_user_cache_free_locked(new_value_offset); + } + + if (new_key_offset != 0 && new_key_offset != old_key_offset && + (new_reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) == 0 + ) { + php_user_cache_free_locked(new_key_offset); + } +} + +static php_user_cache_store_attempt_result user_cache_store_attempt_locked( + zend_string *key, + zval *value, + const php_user_cache_prepared_value *prepared, + zend_long ttl, + const php_user_cache_store_options *options, + php_user_cache_store_result *result, + size_t key_size, + bool *expired_retry_used, + bool *clear_retry_used) +{ + const char *msg; + php_user_cache_header *header; + php_user_cache_entry *entries, *entry, replaced_snapshot; + zend_long new_lval = 0; + uint64_t expires_at; + uint32_t slot_idx, offset = 0, graph_offset = 0, reusable_offset, old_key_offset = 0, old_value_offset = 0, + new_key_offset = 0, new_value_offset = 0, new_value_len = 0, combined_reuse_offset = 0; + uint16_t old_reserved = 0, new_reserved = 0; + uint8_t *combined_payload, old_value_type = PHP_USER_CACHE_VALUE_NULL, new_value_type = prepared->value_type; + bool found, old_combined, use_combined_publish, can_clear, capture_replaced = false, published = false; + double new_dval = 0; + + expires_at = ttl == 0 + ? 0 + : (uint64_t) time(NULL) + (uint64_t) ttl + ; + + if (!user_cache_find_slot_for_write_locked( + key, + prepared->hash, + &header, + &slot_idx, + &found + ) + ) { + if (options->retry_after_memory_pressure && + user_cache_reclaim_space_for_store_locked( + true, + expired_retry_used, + clear_retry_used + ) + ) { + return PHP_USER_CACHE_STORE_ATTEMPT_RETRY; + } + + php_user_cache_header_ptr()->store_failure_count++; + user_cache_handle_store_failure( + "cache hash table is full", + options->throw_on_failure, + options->honor_strict_store_failure + ); + + return PHP_USER_CACHE_STORE_ATTEMPT_FAILED; + } + + entries = php_user_cache_entries_ptr(header); + entry = &entries[slot_idx]; + + if (found) { + capture_replaced = options->capture_replaced_entry && result != NULL; + replaced_snapshot = *entry; + old_key_offset = entry->key_offset; + old_value_type = entry->value_type; + old_value_offset = entry->value_offset; + old_reserved = entry->reserved; + } + + old_combined = found && (old_reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + reusable_offset = 0; + + if (found && !capture_replaced && old_value_type != PHP_USER_CACHE_VALUE_SHARED_GRAPH && !old_combined) { + reusable_offset = old_value_offset; + } + + new_key_offset = found ? old_key_offset : 0; + + use_combined_publish = user_cache_value_uses_offset(prepared->value_type) && + (!found || old_combined) + ; + + if (old_combined && old_value_offset != 0 && !capture_replaced) { + if (old_value_type == PHP_USER_CACHE_VALUE_SHARED_GRAPH) { + /* The sizing pass includes worst-case alignment padding. */ + if ((prepared->payload_source != NULL || + prepared->value_type == PHP_USER_CACHE_VALUE_SHARED_GRAPH) && + php_user_cache_shared_graph_can_overwrite_payload_locked(old_value_offset) && + (prepared->value_type != PHP_USER_CACHE_VALUE_SHARED_GRAPH || + prepared->payload_source == NULL || + php_user_cache_shared_graph_copy_fits_buffer( + user_cache_ptr_in_header(header, old_value_offset), + prepared->payload_size, + prepared->payload_source, + prepared->payload_size, + prepared->payload_used_size + ) + ) + ) { + combined_reuse_offset = old_value_offset; + } + } else if (header->count == 1) { + combined_reuse_offset = old_value_offset; + } + } + + if (!use_combined_publish && (!found || old_combined)) { + new_key_offset = php_user_cache_alloc_locked(key_size, ZSTR_VAL(key)); + if (new_key_offset == 0) { + msg = "not enough shared memory left"; + can_clear = user_cache_payload_can_fit_locked(key_size); + + goto failure; + } + } + + switch (prepared->value_type) { + case PHP_USER_CACHE_VALUE_NULL: + new_value_type = PHP_USER_CACHE_VALUE_NULL; + + break; + case PHP_USER_CACHE_VALUE_TRUE: + new_value_type = PHP_USER_CACHE_VALUE_TRUE; + + break; + case PHP_USER_CACHE_VALUE_FALSE: + new_value_type = PHP_USER_CACHE_VALUE_FALSE; + + break; + case PHP_USER_CACHE_VALUE_LONG: + new_value_type = PHP_USER_CACHE_VALUE_LONG; + new_lval = prepared->long_value; + + break; + case PHP_USER_CACHE_VALUE_DOUBLE: + new_value_type = PHP_USER_CACHE_VALUE_DOUBLE; + new_dval = prepared->double_value; + + break; + case PHP_USER_CACHE_VALUE_STRING: + if (use_combined_publish) { + if (!user_cache_publish_combined_value_key_locked( + header, + combined_reuse_offset, + key, + prepared->payload_size, + prepared->payload_source, + &new_value_offset, + &new_key_offset) + ) { + msg = "not enough shared memory left"; + can_clear = user_cache_payload_can_fit_locked(prepared->payload_size + key_size); + + goto failure; + } + + new_reserved = PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + } else { + offset = user_cache_write_payload_locked( + header, + reusable_offset, + prepared->payload_size, + prepared->payload_source + ); + if (offset == 0) { + msg = "not enough shared memory left"; + can_clear = user_cache_payload_can_fit_locked(prepared->payload_size); + + goto failure; + } + + new_value_offset = offset; + } + + new_value_type = prepared->value_type; + new_value_len = prepared->value_len; + + break; + case PHP_USER_CACHE_VALUE_SHARED_GRAPH: + if (use_combined_publish) { + combined_payload = user_cache_reserve_combined_value_key_locked( + header, + combined_reuse_offset, + key, + prepared->payload_size, + &new_value_offset, + &new_key_offset + ); + + graph_offset = new_value_offset; + + if (combined_payload != NULL) { + /* A reused block remains attached so a later read can discard it. */ + zend_try { + published = user_cache_publish_prepared_shared_graph_locked( + prepared, + value, + combined_payload + ); + } zend_catch { + if (graph_offset != combined_reuse_offset) { + php_user_cache_free_locked(graph_offset); + } + php_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + if (published) { + memcpy(combined_payload + prepared->payload_size, ZSTR_VAL(key), key_size); + new_reserved = PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + new_value_type = PHP_USER_CACHE_VALUE_SHARED_GRAPH; + new_value_offset = graph_offset; + new_value_len = prepared->value_len; + + break; + } + + if (graph_offset != combined_reuse_offset) { + php_user_cache_free_locked(graph_offset); + } else { + /* Detach the partially consumed combined block first. */ + entry->value_type = PHP_USER_CACHE_VALUE_NULL; + entry->value_offset = 0; + + user_cache_delete_entry_locked(header, entry); + php_user_cache_free_locked(combined_reuse_offset); + + combined_reuse_offset = 0; + old_value_offset = 0; + old_key_offset = 0; + found = false; + } + + graph_offset = 0; + new_value_offset = 0; + new_key_offset = found ? old_key_offset : 0; + + if (EG(exception)) { + return PHP_USER_CACHE_STORE_ATTEMPT_FAILED; + } + } else if (options->retry_after_memory_pressure && + user_cache_payload_can_fit_locked(prepared->payload_size + key_size) && + user_cache_reclaim_space_for_store_locked(true, expired_retry_used, clear_retry_used) + ) { + return PHP_USER_CACHE_STORE_ATTEMPT_RETRY; + } + } else { + graph_offset = php_user_cache_alloc_locked(prepared->payload_size, NULL); + + if (graph_offset != 0) { + /* Avoid leaking the block on an allocation bailout. */ + zend_try { + published = user_cache_publish_prepared_shared_graph_locked( + prepared, + value, + user_cache_ptr_in_header(header, graph_offset) + ); + } zend_catch { + php_user_cache_free_locked(graph_offset); + php_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + if (published) { + new_value_type = PHP_USER_CACHE_VALUE_SHARED_GRAPH; + new_value_offset = graph_offset; + new_value_len = prepared->value_len; + + break; + } + + php_user_cache_free_locked(graph_offset); + + if (EG(exception)) { + return PHP_USER_CACHE_STORE_ATTEMPT_FAILED; + } + } else if (options->retry_after_memory_pressure && + user_cache_payload_can_fit_locked(prepared->payload_size) && + user_cache_reclaim_space_for_store_locked(true, expired_retry_used, clear_retry_used) + ) { + return PHP_USER_CACHE_STORE_ATTEMPT_RETRY; + } + } + + msg = "not enough shared memory left"; + can_clear = user_cache_payload_can_fit_locked(prepared->payload_size); + + goto failure; + default: + ZEND_UNREACHABLE(); + } + + if (!found && entry->state == PHP_USER_CACHE_ENTRY_TOMBSTONE && header->tombstone_count != 0) { + header->tombstone_count--; + } + + entry->hash = prepared->hash; + entry->key_offset = new_key_offset; + entry->key_len = (uint32_t) ZSTR_LEN(key); + entry->value_offset = new_value_offset; + entry->value_len = new_value_len; + entry->expires_at = expires_at; + entry->state = PHP_USER_CACHE_ENTRY_USED; + entry->value_type = new_value_type; + entry->reserved = new_reserved; + entry->long_value = new_lval; + entry->double_value = new_dval; + + if (capture_replaced) { + result->replaced_entry.found = true; + result->replaced_entry.entry = replaced_snapshot; + } else { + if (found && + old_key_offset != 0 && + old_key_offset != new_key_offset && + !old_combined + ) { + php_user_cache_free_locked(old_key_offset); + } + + if (found && + old_value_offset != 0 && + old_value_offset != new_value_offset + ) { + user_cache_release_value_storage_locked(old_value_type, old_value_offset); + } + } + + if (!found) { + header->count++; + } + + php_user_cache_bump_mutation_epoch_locked(header); + + /* Per-entry generations avoid invalidating unrelated request-local slots. */ + entry->generation = header->mutation_epoch; + + if (result != NULL) { + result->stored_generation = entry->generation; + result->should_seed_request_local_slot = user_cache_prepared_value_should_seed_request_local_slot(prepared); + } + + return PHP_USER_CACHE_STORE_ATTEMPT_STORED; + +failure: + user_cache_rollback_partial_store_allocs_locked( + new_value_offset, + old_value_offset, + new_key_offset, + old_key_offset, + new_reserved + ); + + if (options->retry_after_memory_pressure && + user_cache_reclaim_space_for_store_locked(can_clear, expired_retry_used, clear_retry_used) + ) { + return PHP_USER_CACHE_STORE_ATTEMPT_RETRY; + } + + header->store_failure_count++; + user_cache_handle_store_failure( + msg, + options->throw_on_failure, + options->honor_strict_store_failure + ); + + return PHP_USER_CACHE_STORE_ATTEMPT_FAILED; +} + +static bool user_cache_store_prepared_locked_impl( + zend_string *key, + zval *value, + const php_user_cache_prepared_value *prepared, + zend_long ttl, + const php_user_cache_store_options *options, + php_user_cache_store_result *result) +{ + php_user_cache_store_attempt_result attempt_result; + size_t key_size; + bool expired_retry_used = false, clear_retry_used = false; + + ZVAL_DEREF(value); + + if (result != NULL) { + result->stored_generation = 0; + result->should_seed_request_local_slot = false; + result->replaced_entry.found = false; + + memset(&result->replaced_entry.entry, 0, sizeof(result->replaced_entry.entry)); + } + + if (prepared == NULL || options == NULL) { + return false; + } + + key_size = ZSTR_LEN(key) + 1; + + user_cache_maybe_rehash_locked(); + + for (;;) { + attempt_result = user_cache_store_attempt_locked( + key, + value, + prepared, + ttl, + options, + result, + key_size, + &expired_retry_used, + &clear_retry_used + ); + + if (attempt_result == PHP_USER_CACHE_STORE_ATTEMPT_RETRY) { + continue; + } + + return attempt_result == PHP_USER_CACHE_STORE_ATTEMPT_STORED; + } +} + +static bool user_cache_prepare_direct_value( + zval *value, + php_user_cache_prepared_value *prepared) +{ + switch (Z_TYPE_P(value)) { + case IS_NULL: + prepared->value_type = PHP_USER_CACHE_VALUE_NULL; + + return true; + case IS_TRUE: + prepared->value_type = PHP_USER_CACHE_VALUE_TRUE; + + return true; + case IS_FALSE: + prepared->value_type = PHP_USER_CACHE_VALUE_FALSE; + + return true; + case IS_LONG: + prepared->value_type = PHP_USER_CACHE_VALUE_LONG; + prepared->long_value = Z_LVAL_P(value); + + return true; + case IS_DOUBLE: + prepared->value_type = PHP_USER_CACHE_VALUE_DOUBLE; + prepared->double_value = Z_DVAL_P(value); + + return true; + case IS_STRING: + if (Z_STRLEN_P(value) < PHP_USER_CACHE_DIRECT_STRING_MIN_LEN) { + prepared->value_type = PHP_USER_CACHE_VALUE_STRING; + prepared->value_len = (uint32_t) Z_STRLEN_P(value); + prepared->payload_size = Z_STRLEN_P(value) + 1; + prepared->payload_used_size = prepared->payload_size; + prepared->payload_source = (const uint8_t *) Z_STRVAL_P(value); + + return true; + } + + return false; + default: + return false; + } +} + +static bool user_cache_prepare_shared_graph_value( + zval *value, + const php_user_cache_prepare_options *options, + HashTable *verbatim_verdicts, + size_t verbatim_graph_len, + php_user_cache_prepared_value *prepared) +{ + size_t graph_len = 0; + bool sized, build_at_publish; + + /* Copy string roots directly under the write lock; this path cannot invoke + * user code. */ + build_at_publish = options->caller_holds_write_lock || Z_TYPE_P(value) == IS_STRING; + + if (!build_at_publish && prepared->state_memo == NULL) { + prepared->state_memo = emalloc(sizeof(HashTable)); + zend_hash_init(prepared->state_memo, 8, NULL, ZVAL_PTR_DTOR, 0); + } + + if (verbatim_graph_len != 0) { + /* The verbatim pass already computed the exact size. */ + graph_len = verbatim_graph_len; + sized = true; + } else { + sized = php_user_cache_calculate_shared_graph_size( + value, + prepared->state_memo, + verbatim_verdicts, + &graph_len + ); + } + + if (sized) { + /* Do not truncate the graph length. */ + if (graph_len > UINT32_MAX) { + return false; + } + + prepared->value_type = PHP_USER_CACHE_VALUE_SHARED_GRAPH; + prepared->value_len = (uint32_t) graph_len; + prepared->payload_size = graph_len; + + if (build_at_publish) { + if (Z_TYPE_P(value) == IS_STRING) { + prepared->owned_string = zend_string_copy(Z_STR_P(value)); + } + + return true; + } + + prepared->owned_buffer = emalloc(graph_len); + /* Snapshot hooks may invalidate address-keyed verdicts. */ + if (php_user_cache_build_shared_graph_in_place( + value, + prepared->state_memo, + zend_hash_num_elements(prepared->state_memo) == 0 ? verbatim_verdicts : NULL, + prepared->owned_buffer, + graph_len, + &prepared->payload_used_size, + &prepared->has_verbatim_array, + &prepared->fixup_offsets, + &prepared->fixup_count + ) + ) { + prepared->payload_source = prepared->owned_buffer; + + return true; + } + + if (EG(exception)) { + return false; + } + + efree(prepared->owned_buffer); + prepared->owned_buffer = NULL; + } + + if (EG(exception)) { + return false; + } + + user_cache_handle_store_failure( + "value cannot be stored in the user cache: it contains a resource, a closure, or an object with opaque internal state (e.g. Fiber, Generator, PDO)", + options->throw_on_failure, + options->honor_strict_store_failure + ); + + return false; +} + +static bool user_cache_fetch_resolve_prototype_mode_locked( + const php_user_cache_entry *entry, + bool use_request_local_slot, + bool *no_aliases) +{ + *no_aliases = true; + + if (!use_request_local_slot) { + return false; + } + + switch (entry->value_type) { + case PHP_USER_CACHE_VALUE_SHARED_GRAPH: + if (!php_user_cache_shared_graph_prefers_prototype(entry->value_offset)) { + return false; + } + + *no_aliases = !php_user_cache_shared_graph_payload_has_aliases(entry->value_offset); + + return true; + case PHP_USER_CACHE_VALUE_STRING: + /* Repeated string fetches use the request-local slot. */ + return true; + default: + /* Scalar hits are served by the lookup cache. */ + return false; + } +} + +/* Defer request-local cloning until after the read lock is released. */ +static bool user_cache_fetch_emit_value_locked( + const php_user_cache_header *header, + zend_string *key, + const char *cache_name, + php_user_cache_entry *entry, + uint64_t gen, + bool throw_if_missing, + bool use_request_local_slot, + zval *return_value, + php_user_cache_fetch_pending_seed *pending_seed) +{ + php_user_cache_request_local_slot_result slot_result; + uint32_t flags; + bool use_proto, no_aliases; + + pending_seed->should_seed_request_local_slot = false; + + use_proto = user_cache_fetch_resolve_prototype_mode_locked(entry, use_request_local_slot, &no_aliases); + + if (use_proto) { + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + slot_result = user_cache_fetch_request_local_slot(key, gen, return_value); + ); + if (slot_result == PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT) { + return true; + } + + if (EG(exception)) { + return false; + } + } + + flags = 0; + if (use_proto) { + flags |= PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT; + } + if (no_aliases) { + flags |= PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES; + } + + if (user_cache_scalar_to_zval(entry->value_type, entry->long_value, entry->double_value, return_value)) { + + return true; + } + + switch (entry->value_type) { + case PHP_USER_CACHE_VALUE_STRING: + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + ZVAL_STRINGL(return_value, (const char *) user_cache_ptr_in_header(header, entry->value_offset), entry->value_len); + ); + pending_seed->generation = gen; + pending_seed->flags = flags; + pending_seed->should_seed_request_local_slot = true; + + return true; + case PHP_USER_CACHE_VALUE_SHARED_GRAPH: + if (!user_cache_materialize_shared_graph_locked( + header, + key, + cache_name, + entry->value_type, + entry->value_offset, + entry->value_len, + throw_if_missing, + return_value) + ) { + return false; + } + + pending_seed->generation = gen; + pending_seed->flags = flags | PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT; + pending_seed->should_seed_request_local_slot = true; + + return true; + default: + if (throw_if_missing) { + user_cache_throw_unknown_value_type_guarded(key, cache_name); + } + + return false; + } +} + +static php_user_cache_fetch_locate_result user_cache_fetch_probe_lookup_cache_locked( + php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + php_user_cache_entry *entries, + php_user_cache_lookup_entry *lookup_entries, + uint64_t epoch, + zval *return_value, + uint32_t *slot_idx) +{ + const void *ctx = (const void *) php_user_cache_active_context(); + php_user_cache_lookup_entry *lookup_entry; + php_user_cache_entry *entry; + uint64_t now; + uint32_t way; + + for (way = 0; way < PHP_USER_CACHE_LOOKUP_WAYS; way++) { + lookup_entry = &lookup_entries[way]; + if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_EMPTY || + lookup_entry->hash != hash || + lookup_entry->context != ctx + ) { + continue; + } + + if (lookup_entry->mutation_epoch != epoch) { + /* Revalidate stale hits; discard stale misses. */ + if (lookup_entry->state != PHP_USER_CACHE_LOOKUP_HIT) { + user_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + } else { + if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_MISS) { + /* A colliding MISS must not hide this key. */ + if (lookup_entry->key == NULL || !zend_string_equals(lookup_entry->key, key)) { + continue; + } + + return PHP_USER_CACHE_FETCH_LOCATE_MISS; + } + + if (lookup_entry->key == key && + user_cache_scalar_to_zval( + lookup_entry->value_type, + lookup_entry->long_value, + lookup_entry->double_value, + return_value + ) + ) { + return PHP_USER_CACHE_FETCH_LOCATE_SCALAR_HIT; + } + } + + if (lookup_entry->slot_index >= header->capacity) { + user_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + + entry = &entries[lookup_entry->slot_index]; + if (!user_cache_key_equals(header, entry, key, hash)) { + user_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + + now = 0; + if (user_cache_is_expired_now(entry, &now)) { + user_cache_note_expired_read(); + user_cache_lookup_cache_reset_entry(lookup_entry); + user_cache_lookup_cache_store_miss(lookup_entries, hash, epoch, key); + + return PHP_USER_CACHE_FETCH_LOCATE_MISS; + } + + *slot_idx = lookup_entry->slot_index; + + /* Refresh the revived cache entry. */ + if (lookup_entry->mutation_epoch != epoch || lookup_entry->key != key) { + user_cache_lookup_cache_store_hit(lookup_entry, hash, epoch, *slot_idx, key, entry); + } + + return PHP_USER_CACHE_FETCH_LOCATE_SLOT; + } + + return PHP_USER_CACHE_FETCH_LOCATE_UNCACHED; +} + +static php_user_cache_fetch_locate_result user_cache_fetch_probe_entry_table_locked( + php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + php_user_cache_entry *entries, + php_user_cache_lookup_entry *lookup_entries, + uint64_t epoch, + uint32_t *slot_idx) +{ + php_user_cache_lookup_entry *lookup_entry; + bool found; + + if (!user_cache_find_slot_in_header_locked( + header, + key, + hash, + PHP_USER_CACHE_FIND_SLOT_SKIP_EXPIRED, + slot_idx, + &found + ) || + !found + ) { + user_cache_lookup_cache_store_miss(lookup_entries, hash, epoch, key); + + return PHP_USER_CACHE_FETCH_LOCATE_MISS; + } + + lookup_entry = user_cache_lookup_cache_select_slot(lookup_entries, hash, epoch, true); + user_cache_lookup_cache_store_hit(lookup_entry, hash, epoch, *slot_idx, key, &entries[*slot_idx]); + + return PHP_USER_CACHE_FETCH_LOCATE_SLOT; +} + +static bool user_cache_atomic_insert_missing_locked( + zend_string *key, + zend_long step, + zend_long ttl, + bool decrement, + php_user_cache_atomic_update_result *result) +{ + php_user_cache_prepare_options prep_opts = { + .caller_holds_write_lock = true, + .throw_on_failure = false, + .honor_strict_store_failure = false, + }; + php_user_cache_store_options store_opts = { + .retry_after_memory_pressure = true, + .throw_on_failure = false, + .honor_strict_store_failure = false, + .capture_replaced_entry = false, + }; + php_user_cache_prepared_value prepared; + zend_long updated; + zval init_val = {0}; + bool is_overflow, stored; + + is_overflow = decrement + ? user_cache_long_sub_overflow(0, step, &updated) + : user_cache_long_add_overflow(0, step, &updated) + ; + if (is_overflow) { + result->is_overflow = true; + + return false; + } + + ZVAL_LONG(&init_val, updated); + if (!php_user_cache_prepare_value(key, &init_val, &prep_opts, &prepared)) { + php_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + stored = php_user_cache_store_prepared_locked( + key, + &init_val, + &prepared, + ttl, + &store_opts, + NULL + ); + + php_user_cache_destroy_prepared_value(&prepared); + + if (stored) { + result->new_value = Z_LVAL(init_val); + + return true; + } + + return false; +} + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +/* Validate the final sequence before publishing the snapshot. */ +static php_user_cache_optimistic_result user_cache_optimistic_locate( + php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + uint64_t seq, + uint64_t epoch, + php_user_cache_lookup_entry *lookup_entries, + bool have_snapshot, + php_user_cache_entry *snapshot, + uint32_t *slot_idx) +{ + php_user_cache_lookup_entry *lookup_entry; + bool found = have_snapshot; + + if (!have_snapshot && + !user_cache_optimistic_probe( + header, + key, + hash, + php_user_cache_entries_ptr(header), + snapshot, + slot_idx, + &found + ) + ) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (php_user_cache_seq_reload(&header->write_seq) != seq) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (!found) { + user_cache_lookup_cache_store_miss(lookup_entries, hash, epoch, key); + + return PHP_USER_CACHE_OPTIMISTIC_MISS; + } + + lookup_entry = user_cache_lookup_cache_select_slot(lookup_entries, hash, epoch, true); + user_cache_lookup_cache_store_hit(lookup_entry, hash, epoch, *slot_idx, key, snapshot); + + return PHP_USER_CACHE_OPTIMISTIC_FOUND; +} + +static bool user_cache_optimistic_scan_lookup_cache( + php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + uint64_t seq, + uint64_t epoch, + php_user_cache_lookup_entry *lookup_entries, + zval *return_value, + uint32_t *hint_slot, + php_user_cache_optimistic_result *result) +{ + const void *ctx = (const void *) php_user_cache_active_context(); + php_user_cache_lookup_entry *lookup_entry; + uint32_t way; + + *hint_slot = UINT32_MAX; + + for (way = 0; way < PHP_USER_CACHE_LOOKUP_WAYS; way++) { + lookup_entry = &lookup_entries[way]; + + if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_EMPTY || + lookup_entry->hash != hash || + lookup_entry->context != ctx + ) { + continue; + } + + if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_MISS) { + if (lookup_entry->mutation_epoch != epoch) { + user_cache_lookup_cache_reset_entry(lookup_entry); + + continue; + } + + if (lookup_entry->key == NULL || !zend_string_equals(lookup_entry->key, key)) { + continue; + } + + *result = php_user_cache_seq_reload(&header->write_seq) != seq + ? PHP_USER_CACHE_OPTIMISTIC_FALLBACK + : PHP_USER_CACHE_OPTIMISTIC_MISS + ; + + return true; + } + + if (lookup_entry->mutation_epoch == epoch && + lookup_entry->key == key && + user_cache_scalar_to_zval( + lookup_entry->value_type, + lookup_entry->long_value, + lookup_entry->double_value, + return_value + ) + ) { + *result = php_user_cache_seq_reload(&header->write_seq) != seq + ? PHP_USER_CACHE_OPTIMISTIC_FALLBACK + : PHP_USER_CACHE_OPTIMISTIC_FOUND + ; + + return true; + } + + *hint_slot = lookup_entry->slot_index; + + break; + } + + return false; +} + +static bool user_cache_optimistic_try_hint_slot( + php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + uint32_t hint_slot, + php_user_cache_entry *snapshot, + uint32_t *slot_idx) +{ + const php_user_cache_entry *hint_entry; + + if (hint_slot == UINT32_MAX || hint_slot >= header->capacity) { + return false; + } + + hint_entry = &php_user_cache_entries_ptr(header)[hint_slot]; + + if (hint_entry->state == PHP_USER_CACHE_ENTRY_USED && + hint_entry->hash == hash && + hint_entry->key_len == ZSTR_LEN(key) && + user_cache_optimistic_payload_in_bounds(header, hint_entry->key_offset, hint_entry->key_len) && + memcmp(user_cache_ptr_in_header(header, hint_entry->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0 && + (hint_entry->expires_at == 0 || hint_entry->expires_at > (uint64_t) time(NULL)) + ) { + *snapshot = *hint_entry; + *slot_idx = hint_slot; + + return true; + } + + return false; +} + +static php_user_cache_optimistic_result user_cache_optimistic_emit_string( + php_user_cache_header *header, + zend_string *key, + uint64_t seq, + const php_user_cache_entry *snapshot, + zval *return_value) +{ + zend_string *str; + + if (user_cache_fetch_request_local_slot(key, snapshot->generation, return_value) == + PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT + ) { + return PHP_USER_CACHE_OPTIMISTIC_FOUND; + } + + if (!user_cache_optimistic_payload_in_bounds(header, snapshot->value_offset, snapshot->value_len)) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + str = zend_string_init( + (const char *) user_cache_ptr_in_header(header, snapshot->value_offset), + snapshot->value_len, + 0 + ); + + if (php_user_cache_seq_reload(&header->write_seq) != seq) { + zend_string_release(str); + + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + ZVAL_STR(return_value, str); + php_user_cache_fetch_finish( + key, + snapshot->generation, + return_value, + PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT | PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES + ); + + return PHP_USER_CACHE_OPTIMISTIC_FOUND; +} + +static php_user_cache_optimistic_result user_cache_optimistic_emit_shared_graph( + php_user_cache_header *header, + zend_string *key, + uint64_t seq, + const php_user_cache_entry *snapshot, + zval *return_value) +{ + uint32_t reader_slot = 0, flags; + bool use_proto, no_aliases, ref_registered; + + if (!user_cache_optimistic_payload_in_bounds( + header, + snapshot->value_offset, + sizeof(php_user_cache_shared_graph_header) + ZEND_MM_ALIGNMENT) || + !user_cache_optimistic_payload_in_bounds( + header, + snapshot->value_offset, + snapshot->value_len + ) + ) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + use_proto = php_user_cache_shared_graph_prefers_prototype(snapshot->value_offset); + no_aliases = !php_user_cache_shared_graph_payload_has_aliases(snapshot->value_offset); + + if (php_user_cache_seq_reload(&header->write_seq) != seq) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (use_proto && + user_cache_fetch_request_local_slot( + key, snapshot->generation, return_value + ) == PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT + ) { + return PHP_USER_CACHE_OPTIMISTIC_FOUND; + } + + ref_registered = php_user_cache_has_request_shared_graph_ref(snapshot->value_offset); + if (!ref_registered) { + php_user_cache_shared_graph_ref_reserve(); + + if (!php_user_cache_optimistic_reader_begin(header, &reader_slot)) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (php_user_cache_seq_reload(&header->write_seq) != seq) { + php_user_cache_optimistic_reader_end(header, reader_slot); + + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (!php_user_cache_shared_graph_acquire_ref(snapshot->value_offset)) { + php_user_cache_optimistic_reader_end(header, reader_slot); + + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + php_user_cache_register_shared_graph_ref(snapshot->value_offset); + php_user_cache_optimistic_reader_end(header, reader_slot); + } + + ZVAL_UNDEF(return_value); + + if (!php_user_cache_shared_graph_decode( + user_cache_ptr_in_header(header, snapshot->value_offset), + snapshot->value_len, + return_value + ) + ) { + if (Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + flags = PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT; + if (use_proto) { + flags |= PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT; + } + if (no_aliases) { + flags |= PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES; + } + + php_user_cache_fetch_finish(key, snapshot->generation, return_value, flags); + + return PHP_USER_CACHE_OPTIMISTIC_FOUND; +} +#endif + +bool php_user_cache_clear_locked(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + php_user_cache_entry *entries; + uint64_t epoch; + uint32_t i; + + if (!header || !php_user_cache_header_init_locked()) { + return false; + } + + epoch = header->mutation_epoch; + entries = php_user_cache_entries_ptr(header); + for (i = 0; i < header->capacity; i++) { + if (entries[i].state == PHP_USER_CACHE_ENTRY_USED) { + user_cache_release_entry_storage_locked(&entries[i]); + } + } + + memset(entries, 0, sizeof(php_user_cache_entry) * header->capacity); + + php_user_cache_shared_graph_reclaim_orphaned_locked(); + + header->count = 0; + header->tombstone_count = 0; + header->mutation_epoch = epoch + 1; + + if (header->mutation_epoch == 0) { + header->mutation_epoch = 1; + } + + return true; +} + +void php_user_cache_lookup_cache_clear(void) +{ + uint32_t i; + + for (i = 0; i < PHP_USER_CACHE_LOOKUP_BUCKETS; i++) { + user_cache_lookup_entry_release_key(&UC_G(lookup_entry_storage)[i]); + } + + memset( + UC_G(lookup_entry_storage), + 0, + sizeof(php_user_cache_lookup_entry) * PHP_USER_CACHE_LOOKUP_BUCKETS + ); +} + +bool php_user_cache_prepare_value( + zend_string *key, + zval *value, + const php_user_cache_prepare_options *options, + php_user_cache_prepared_value *prepared) +{ + HashTable verbatim_verdicts; + size_t verbatim_graph_len = 0; + bool verbatim_eligible = false, has_verbatim_verdicts = false, result; + + if (prepared == NULL || options == NULL) { + return false; + } + + user_cache_init_prepared_value(prepared); + ZVAL_DEREF(value); + prepared->hash = zend_string_hash_val(key); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + user_cache_handle_store_failure( + PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE, + options->throw_on_failure, + options->honor_strict_store_failure + ); + + return false; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + user_cache_handle_store_failure( + PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE, + options->throw_on_failure, + options->honor_strict_store_failure + ); + + return false; + } + + if (Z_TYPE_P(value) == IS_ARRAY && EXPECTED(!EG(exception))) { + zend_hash_init(&verbatim_verdicts, 8, NULL, NULL, 0); + has_verbatim_verdicts = true; + /* Prove eligibility and compute the size in one walk. */ + switch (php_user_cache_shared_graph_calc_verbatim_root( + value, + &verbatim_verdicts, + &verbatim_graph_len + )) { + case PHP_USER_CACHE_VERBATIM_ROOT_SIZED: + case PHP_USER_CACHE_VERBATIM_ROOT_ELIGIBLE_UNSIZED: + verbatim_eligible = true; + break; + case PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE: + break; + case PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED: + verbatim_eligible = php_user_cache_shared_graph_can_copy_verbatim_root( + value, + &verbatim_verdicts + ); + break; + } + } + + /* A verbatim root contains no values rejected by validation. */ + if (!verbatim_eligible && + !user_cache_validate_storable_value( + value, + options->throw_on_failure, + options->honor_strict_store_failure) + ) { + result = false; + + goto done; + } + + if (user_cache_prepare_direct_value(value, prepared)) { + result = true; + + goto done; + } + + if (!has_verbatim_verdicts) { + zend_hash_init(&verbatim_verdicts, 8, NULL, NULL, 0); + has_verbatim_verdicts = true; + } + + result = user_cache_prepare_shared_graph_value(value, options, &verbatim_verdicts, verbatim_graph_len, prepared); + +done: + if (has_verbatim_verdicts) { + zend_hash_destroy(&verbatim_verdicts); + } + + return result; +} + +void php_user_cache_destroy_prepared_value(php_user_cache_prepared_value *prepared) +{ + if (prepared == NULL) { + return; + } + + if (prepared->owned_buffer != NULL) { + efree(prepared->owned_buffer); + } + + if (prepared->fixup_offsets != NULL) { + efree(prepared->fixup_offsets); + } + + if (prepared->owned_string != NULL) { + zend_string_release(prepared->owned_string); + } + + if (prepared->state_memo != NULL) { + zend_hash_destroy(prepared->state_memo); + efree(prepared->state_memo); + } + + user_cache_init_prepared_value(prepared); +} + +/* Use zend_array_release here to avoid changing GC timing. */ +void php_user_cache_object_table_dtor(zval *zv) +{ + zend_object *obj = Z_PTR_P(zv); + + OBJ_RELEASE(obj); +} + +void php_user_cache_reference_table_dtor(zval *zv) +{ + zval ref_zv; + + ZVAL_REF(&ref_zv, (zend_reference *) Z_PTR_P(zv)); + zval_ptr_dtor(&ref_zv); +} + +void php_user_cache_store_request_local_slot(zend_string *key, uint64_t gen, zval *value, bool no_aliases) +{ + php_user_cache_request_local_slot *slot; + HashTable verdicts; + zval slot_zv; + bool needs_deep_clone = false, has_verdicts = false; + + ZVAL_DEREF(value); + + slot = user_cache_alloc_request_local_slot(gen, no_aliases, true); + + /* Collect once to avoid rescanning nested arrays during cloning. */ + if (Z_TYPE_P(value) == IS_ARRAY || Z_TYPE_P(value) == IS_OBJECT || Z_ISREF_P(value)) { + zend_hash_init(&verdicts, 8, NULL, NULL, 0); + has_verdicts = true; + needs_deep_clone = user_cache_collect_request_local_clone_verdicts(value, &verdicts); + } + + if (!user_cache_clone_request_local_slot_value_known( + &slot->value, + value, + needs_deep_clone, + has_verdicts ? &verdicts : NULL, + no_aliases + ) + ) { + if (has_verdicts) { + zend_hash_destroy(&verdicts); + } + + ZVAL_PTR(&slot_zv, slot); + + user_cache_request_local_slot_dtor(&slot_zv); + + return; + } + + slot->needs_deep_clone = needs_deep_clone; + + if (has_verdicts) { + if (needs_deep_clone) { + /* Recollect verdicts for the cloned identities. */ + zend_hash_destroy(&verdicts); + zend_hash_init(&slot->clone_verdicts, 8, NULL, NULL, 0); + user_cache_collect_request_local_clone_verdicts(&slot->value, &slot->clone_verdicts); + } else { + /* Source verdicts remain valid without a deep clone. */ + slot->clone_verdicts = verdicts; + } + + slot->has_clone_verdicts = true; + } + + zend_hash_update_ptr(user_cache_request_local_slots(), key, slot); +} + +bool php_user_cache_store_prepared_locked( + zend_string *key, + zval *value, + const php_user_cache_prepared_value *prepared, + zend_long ttl, + const php_user_cache_store_options *options, + php_user_cache_store_result *result) +{ + bool stored = false; + + user_cache_maybe_expunge_expired_locked(); + + PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT( + stored = user_cache_store_prepared_locked_impl( + key, + value, + prepared, + ttl, + options, + result + ); + ); + + return stored; +} + +bool php_user_cache_fetch_finish( + zend_string *key, + uint64_t gen, + zval *return_value, + uint32_t flags) +{ + php_user_cache_request_local_slot *slot; + bool should_store; + + if (flags & PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT) { + if (!(flags & PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT)) { + should_store = true; + } else { + /* Leave an empty marker only if no value was promoted. */ + slot = user_cache_find_request_local_slot(key, gen, true); + + if (slot == NULL) { + user_cache_mark_request_local_slot(key, gen); + should_store = false; + } else if (!slot->has_value) { + should_store = true; + } else { + /* Preserve a slot promoted by a nested fetch. */ + should_store = false; + } + } + + if (should_store) { + php_user_cache_store_request_local_slot( + key, + gen, + return_value, + (flags & PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES) != 0 + ); + } + } + + return true; +} + +bool php_user_cache_fetch_locked( + zend_string *key, + bool throw_if_missing, + bool use_request_local_slot, + zval *return_value, + bool *found, + php_user_cache_fetch_pending_seed *pending_seed) +{ + const char *cache_name = php_user_cache_active_context()->name; + php_user_cache_header *header; + php_user_cache_entry *entries, *entry; + php_user_cache_lookup_entry *lookup_entries; + php_user_cache_fetch_locate_result locate; + zend_ulong hash; + uint64_t epoch; + uint32_t slot_idx = 0; + + if (found != NULL) { + *found = false; + } + + pending_seed->should_seed_request_local_slot = false; + + hash = zend_string_hash_val(key); + + header = php_user_cache_header_ptr(); + if (!header || !php_user_cache_header_is_initialized_locked()) { + if (throw_if_missing) { + user_cache_throw_key_not_found_guarded(key); + } + + return false; + } + + entries = php_user_cache_entries_ptr(header); + epoch = header->mutation_epoch; + lookup_entries = user_cache_lookup_cache_set(hash); + + locate = user_cache_fetch_probe_lookup_cache_locked( + header, key, hash, entries, lookup_entries, epoch, return_value, &slot_idx + ); + + if (locate == PHP_USER_CACHE_FETCH_LOCATE_UNCACHED) { + locate = user_cache_fetch_probe_entry_table_locked( + header, key, hash, entries, lookup_entries, epoch, &slot_idx + ); + } + + if (locate == PHP_USER_CACHE_FETCH_LOCATE_SCALAR_HIT) { + if (found != NULL) { + *found = true; + } + + return true; + } + + if (locate == PHP_USER_CACHE_FETCH_LOCATE_MISS) { + if (throw_if_missing) { + user_cache_throw_key_not_found_guarded(key); + } + + return false; + } + + entry = &entries[slot_idx]; + + if (found != NULL) { + *found = true; + } + + return user_cache_fetch_emit_value_locked( + header, + key, + cache_name, + entry, + entry->generation, + throw_if_missing, + use_request_local_slot, + return_value, + pending_seed + ); +} + +bool php_user_cache_exists_locked(zend_string *key) +{ + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_idx; + bool found; + + if (!user_cache_find_slot_for_read_locked(key, hash, NULL, &slot_idx, &found)) { + return false; + } + + return found; +} + +void php_user_cache_discard_replaced_entry_locked( + zend_string *key, + php_user_cache_replaced_entry *replaced_entry) +{ + php_user_cache_header *header; + php_user_cache_entry *entries, *current_entry = NULL; + uint32_t slot_idx; + bool found; + + if (replaced_entry == NULL || !replaced_entry->found) { + return; + } + + if (user_cache_find_slot_ignore_expiry_locked( + key, + replaced_entry->entry.hash, + &header, + &slot_idx, + &found + ) && + found + ) { + entries = php_user_cache_entries_ptr(header); + current_entry = &entries[slot_idx]; + } + + user_cache_release_entry_storage_except_locked(&replaced_entry->entry, current_entry); + replaced_entry->found = false; + memset(&replaced_entry->entry, 0, sizeof(replaced_entry->entry)); +} + +void php_user_cache_rollback_replaced_entry_locked( + zend_string *key, + php_user_cache_replaced_entry *replaced_entry) +{ + php_user_cache_header *header; + php_user_cache_entry *entries, *entry; + uint32_t slot_idx; + bool found; + + if (!user_cache_find_slot_ignore_expiry_locked( + key, + zend_string_hash_val(key), + &header, + &slot_idx, + &found + ) + ) { + return; + } + + entries = php_user_cache_entries_ptr(header); + entry = &entries[slot_idx]; + + if (replaced_entry != NULL && replaced_entry->found) { + if (found) { + user_cache_release_entry_storage_except_locked(entry, &replaced_entry->entry); + } else { + if (entry->state == PHP_USER_CACHE_ENTRY_TOMBSTONE && header->tombstone_count != 0) { + header->tombstone_count--; + } + + header->count++; + } + + *entry = replaced_entry->entry; + replaced_entry->found = false; + + memset(&replaced_entry->entry, 0, sizeof(replaced_entry->entry)); + } else if (found) { + user_cache_delete_entry_locked(header, entry); + } +} + +void php_user_cache_delete_locked(zend_string *key) +{ + php_user_cache_header *header; + php_user_cache_entry *entries; + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_idx; + bool found; + + user_cache_maybe_expunge_expired_locked(); + + if (!user_cache_find_slot_for_write_locked(key, hash, &header, &slot_idx, &found) || !found) { + return; + } + + entries = php_user_cache_entries_ptr(header); + user_cache_delete_entry_locked(header, &entries[slot_idx]); + + user_cache_maybe_rehash_locked(); +} + +void php_user_cache_delete_by_prefix_locked(zend_string *prefix) +{ + php_user_cache_header *header; + php_user_cache_entry *entries, *entry; + uint32_t i; + + header = php_user_cache_header_ptr(); + if (!header || !php_user_cache_header_is_initialized_locked()) { + return; + } + + entries = php_user_cache_entries_ptr(header); + for (i = 0; i < header->capacity; i++) { + entry = &entries[i]; + if (entry->state != PHP_USER_CACHE_ENTRY_USED || + entry->key_len < ZSTR_LEN(prefix) || + memcmp( + user_cache_ptr_in_header(header, entry->key_offset), + ZSTR_VAL(prefix), + ZSTR_LEN(prefix) + ) != 0 + ) { + continue; + } + + user_cache_delete_entry_locked(header, entry); + } + + user_cache_maybe_rehash_locked(); +} + +bool php_user_cache_atomic_update_locked( + zend_string *key, + zend_long step, + zend_long ttl, + bool decrement, + bool insert_if_missing, + php_user_cache_atomic_update_result *result) +{ + php_user_cache_header *header; + php_user_cache_entry *entries, *entry; + zend_ulong hash = zend_string_hash_val(key); + zend_long updated; + uint32_t slot_idx; + bool found, is_overflow; + + result->new_value = 0; + result->is_overflow = false; + result->is_type_error = false; + + user_cache_maybe_expunge_expired_locked(); + + if (!user_cache_find_slot_for_write_locked(key, hash, &header, &slot_idx, &found) || !found) { + return insert_if_missing + ? user_cache_atomic_insert_missing_locked(key, step, ttl, decrement, result) + : false + ; + } + + entries = php_user_cache_entries_ptr(header); + entry = &entries[slot_idx]; + if (entry->value_type != PHP_USER_CACHE_VALUE_LONG) { + result->is_type_error = true; + + return false; + } + + is_overflow = decrement + ? user_cache_long_sub_overflow(entry->long_value, step, &updated) + : user_cache_long_add_overflow(entry->long_value, step, &updated) + ; + if (is_overflow) { + result->is_overflow = true; + + return false; + } + + entry->long_value = updated; + + php_user_cache_bump_mutation_epoch_locked(header); + entry->generation = header->mutation_epoch; + + result->new_value = entry->long_value; + + return true; +} + +void php_user_cache_release_request_local_slots(void) +{ + user_cache_release_request_local_slot_table(&UC_G(request_local_slot_table)); +} + +void php_user_cache_release_active_request_local_slots(void) +{ + /* Generation and context checks make over-invalidation safe. */ + php_user_cache_release_request_local_slots(); +} + +void php_user_cache_release_active_request_local_slots_by_prefix(zend_string *prefix) +{ + zend_string *key, **keys; + HashTable **slots_ptr = &UC_G(request_local_slot_table); + uint32_t i, slot_count, count = 0; + + if (*slots_ptr == NULL) { + return; + } + + slot_count = zend_hash_num_elements(*slots_ptr); + if (slot_count == 0) { + user_cache_release_request_local_slot_table(slots_ptr); + + return; + } + + keys = safe_emalloc(slot_count, sizeof(zend_string *), 0); + ZEND_HASH_FOREACH_STR_KEY(*slots_ptr, key) { + if (key != NULL && + ZSTR_LEN(key) >= ZSTR_LEN(prefix) && + memcmp( + ZSTR_VAL(key), + ZSTR_VAL(prefix), + ZSTR_LEN(prefix) + ) == 0 + ) { + keys[count++] = zend_string_copy(key); + } + } ZEND_HASH_FOREACH_END(); + + for (i = 0; i < count; i++) { + zend_hash_del(*slots_ptr, keys[i]); + zend_string_release(keys[i]); + } + + efree(keys); + + if (zend_hash_num_elements(*slots_ptr) == 0) { + user_cache_release_request_local_slot_table(slots_ptr); + } +} + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +php_user_cache_optimistic_result php_user_cache_fetch_optimistic( + zend_string *key, + zval *return_value) +{ + php_user_cache_header *header; + php_user_cache_entry snapshot; + php_user_cache_lookup_entry *lookup_entries; + php_user_cache_optimistic_result result; + zend_ulong hash; + uint64_t seq, epoch; + uint32_t slot_idx = 0, hint_slot; + bool have_snapshot; + + if (!user_cache_optimistic_header(&header, &seq)) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + epoch = header->mutation_epoch; + hash = zend_string_hash_val(key); + lookup_entries = user_cache_lookup_cache_set(hash); + + if (user_cache_optimistic_scan_lookup_cache( + header, key, hash, seq, epoch, lookup_entries, return_value, &hint_slot, &result + ) + ) { + return result; + } + + have_snapshot = user_cache_optimistic_try_hint_slot( + header, key, hash, hint_slot, &snapshot, &slot_idx + ); + + result = user_cache_optimistic_locate( + header, key, hash, seq, epoch, lookup_entries, have_snapshot, &snapshot, &slot_idx + ); + if (result != PHP_USER_CACHE_OPTIMISTIC_FOUND) { + return result; + } + + if (user_cache_scalar_to_zval(snapshot.value_type, snapshot.long_value, snapshot.double_value, return_value)) { + return PHP_USER_CACHE_OPTIMISTIC_FOUND; + } + + switch (snapshot.value_type) { + case PHP_USER_CACHE_VALUE_STRING: + return user_cache_optimistic_emit_string(header, key, seq, &snapshot, return_value); + case PHP_USER_CACHE_VALUE_SHARED_GRAPH: + return user_cache_optimistic_emit_shared_graph(header, key, seq, &snapshot, return_value); + default: + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } +} + +php_user_cache_optimistic_result php_user_cache_exists_optimistic(zend_string *key) +{ + const void *ctx; + php_user_cache_header *header; + php_user_cache_entry snapshot; + php_user_cache_lookup_entry *lookup_entries, *lookup_entry; + zend_ulong hash; + uint64_t seq, epoch; + uint32_t way, slot_idx = 0; + + if (!user_cache_optimistic_header(&header, &seq)) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + epoch = header->mutation_epoch; + hash = zend_string_hash_val(key); + lookup_entries = user_cache_lookup_cache_set(hash); + + ctx = (const void *) php_user_cache_active_context(); + for (way = 0; way < PHP_USER_CACHE_LOOKUP_WAYS; way++) { + lookup_entry = &lookup_entries[way]; + + if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_EMPTY || + lookup_entry->hash != hash || + lookup_entry->mutation_epoch != epoch || + lookup_entry->context != ctx + ) { + continue; + } + + if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_MISS) { + if (lookup_entry->key == NULL || !zend_string_equals(lookup_entry->key, key)) { + continue; + } + + if (php_user_cache_seq_reload(&header->write_seq) != seq) { + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + return PHP_USER_CACHE_OPTIMISTIC_MISS; + } + + break; + } + + return user_cache_optimistic_locate( + header, key, hash, seq, epoch, lookup_entries, false, &snapshot, &slot_idx + ); +} +#else +php_user_cache_optimistic_result php_user_cache_fetch_optimistic( + zend_string *key, + zval *return_value) +{ + (void) key; + (void) return_value; + + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; +} + +php_user_cache_optimistic_result php_user_cache_exists_optimistic(zend_string *key) +{ + (void) key; + + return PHP_USER_CACHE_OPTIMISTIC_FALLBACK; +} +#endif diff --git a/ext/user_cache/user_cache_internal.h b/ext/user_cache/user_cache_internal.h new file mode 100644 index 000000000000..48d4ddd20ecf --- /dev/null +++ b/ext/user_cache/user_cache_internal.h @@ -0,0 +1,1163 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_USER_CACHE_INTERNAL_H +#define PHP_USER_CACHE_INTERNAL_H + +#include "php.h" + +#include +#ifdef ZTS +# include "TSRM/TSRM.h" +#endif +#if defined(ZEND_WIN32) && defined(_MSC_VER) +# include +#endif +#if !defined(ZEND_WIN32) && defined(__linux__) +# include +#endif + +#include "Zend/zend_attributes.h" +#include "Zend/zend_ast.h" +#include "Zend/zend_atomic.h" +#include "Zend/zend_call_stack.h" +#include "Zend/zend_enum.h" +#include "Zend/zend_exceptions.h" +#include "Zend/zend_smart_str.h" +#include "Zend/zend_system_id.h" + +#include "php_user_cache.h" +#include "user_cache_shm.h" + +#include "ext/standard/php_var.h" + +#include "SAPI.h" + +#define PHP_USER_CACHE_MAGIC 0xCAC17E01U +#define PHP_USER_CACHE_VERSION 1U +#define PHP_USER_CACHE_MIN_CAPACITY 127U +#define PHP_USER_CACHE_SLOT_BYTES 256U + +#define PHP_USER_CACHE_KEY_DELIMITER "\x1f" +#define PHP_USER_CACHE_KEY_DELIMITER_CHAR '\x1f' +#define PHP_USER_CACHE_KEY_DELIMITER_NAME "0x1F" + +#define PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE "resources cannot be stored in the user cache" +#define PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE "Closure objects cannot be stored in the user cache" +#define PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE "lazy objects cannot be stored in the user cache" +#define PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE "objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache" + +#define PHP_USER_CACHE_ENTRY_EMPTY 0 +#define PHP_USER_CACHE_ENTRY_USED 1 +#define PHP_USER_CACHE_ENTRY_TOMBSTONE 2 + +#define PHP_USER_CACHE_VALUE_NULL 0 +#define PHP_USER_CACHE_VALUE_TRUE 1 +#define PHP_USER_CACHE_VALUE_FALSE 2 +#define PHP_USER_CACHE_VALUE_LONG 3 +#define PHP_USER_CACHE_VALUE_DOUBLE 4 +#define PHP_USER_CACHE_VALUE_STRING 5 +#define PHP_USER_CACHE_VALUE_SHARED_GRAPH 8 + +#define PHP_USER_CACHE_SHARED_GRAPH_MAGIC 0xCAC17E02U +#define PHP_USER_CACHE_SHARED_GRAPH_VERSION 1U +#define PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY 0x2U +#define PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT 0x4U +#define PHP_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE 0x8U +#define PHP_USER_CACHE_SHARED_GRAPH_RETIRED (1 << 30) +#define PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK (PHP_USER_CACHE_SHARED_GRAPH_RETIRED - 1) + +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF 0 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL 1 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE 2 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE 3 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG 4 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE 5 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING 6 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY 7 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT 8 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY 10 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF 11 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE 13 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF 14 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF 15 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM 16 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT 18 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT 19 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT 20 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT 21 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY 22 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT 23 +#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT 24 + +#define PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED 0x1U + +#define PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED 0x2U +#define PHP_USER_CACHE_SHARED_GRAPH_ARRAY_SHAPE_MAX_KEYS 8U + +#define PHP_USER_CACHE_LOOKUP_BUCKETS 256U +#define PHP_USER_CACHE_LOOKUP_WAYS 2U +#define PHP_USER_CACHE_LOOKUP_SETS (PHP_USER_CACHE_LOOKUP_BUCKETS / PHP_USER_CACHE_LOOKUP_WAYS) +#define PHP_USER_CACHE_LOOKUP_INVALID_SLOT UINT32_MAX + +#define PHP_USER_CACHE_REQUEST_LOCAL_STRING_MIN_LEN 256U + +#define PHP_USER_CACHE_DIRECT_STRING_MIN_LEN 4096U +#define PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS 4U + +#define PHP_USER_CACHE_EXPIRED_READ_EXPUNGE_THRESHOLD 64U + +#define PHP_USER_CACHE_EXPUNGE_SCAN_MAX 4096U + +#define PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE 1024U +#define PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US (10U * 1000U * 1000U) +#define PHP_USER_CACHE_ENTRY_LOCK_EMPTY 0 +#define PHP_USER_CACHE_ENTRY_LOCK_USED 1 +#define PHP_USER_CACHE_ENTRY_LOCK_TOMBSTONE 2 + +#if defined(__GNUC__) || defined(__clang__) +# define PHP_USER_CACHE_HAVE_OPTIMISTIC 1 +#elif defined(ZEND_WIN32) && defined(_MSC_VER) +# define PHP_USER_CACHE_HAVE_OPTIMISTIC 1 +# define PHP_USER_CACHE_OPTIMISTIC_MSVC 1 +#endif + +#if !defined(ZEND_WIN32) && defined(__linux__) +# define PHP_USER_CACHE_HAVE_SHARED_MUTEX 1 +#endif + +/* Boundary partitions require named shared memory. */ +#if !defined(ZEND_WIN32) && defined(PHP_USER_CACHE_USE_SHM_OPEN) +# define PHP_USER_CACHE_HAVE_BOUNDARY_SHM 1 +#endif + +#if defined(PHP_USER_CACHE_USE_MMAP) && !defined(ZEND_WIN32) +# define PHP_USER_CACHE_HAVE_ANON_MMAP 1 +#endif + +#define PHP_USER_CACHE_LOCK_MODEL_FCNTL 0U +#define PHP_USER_CACHE_LOCK_MODEL_MUTEX 1U + +#define PHP_USER_CACHE_READER_SLOTS 256U +#define PHP_USER_CACHE_READER_DRAIN_SPIN 1024U +#define PHP_USER_CACHE_READER_DRAIN_TIMEOUT_US 10000U +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +# define PHP_USER_CACHE_READER_CLAIM_MAX 4U +#endif + +#define PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS 32U + +#define PHP_USER_CACHE_LOOKUP_EMPTY 0 +#define PHP_USER_CACHE_LOOKUP_HIT 1 +#define PHP_USER_CACHE_LOOKUP_MISS 2 + +#define PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY 0x0001U + +#define PHP_USER_CACHE_BLOCK_FREE 1U +#define PHP_USER_CACHE_LOOKUP_VALUE_NONE 0xFFU + +#ifndef ZEND_WIN32 +# define PHP_USER_CACHE_SEM_FILENAME_PREFIX ".PhpUserCacheSem." +#endif + +/* Clear the debug-only flag before exposing a decoded table. */ +#if ZEND_DEBUG +# define PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(ht) HT_FLAGS(ht) &= ~HASH_FLAG_ALLOW_COW_VIOLATION +#else +# define PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(ht) +#endif + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +#ifdef PHP_USER_CACHE_OPTIMISTIC_MSVC +# define PHP_USER_CACHE_ATOMIC_LOAD_64(target) \ + ((uint64_t) _InterlockedOr64((volatile __int64 *) (target), 0)) +# define PHP_USER_CACHE_ATOMIC_STORE_64(target, value) \ + ((void) _InterlockedExchange64((volatile __int64 *) (target), (__int64) (value))) +# define PHP_USER_CACHE_ATOMIC_CAS_64(target, expected, desired) \ + ((uint64_t) _InterlockedCompareExchange64( \ + (volatile __int64 *) (target), \ + (__int64) (desired), \ + (__int64) (expected) \ + ) == (expected)) +# define PHP_USER_CACHE_ATOMIC_LOAD_32(target) \ + ((uint32_t) _InterlockedOr((volatile long *) (target), 0)) +# define PHP_USER_CACHE_ATOMIC_STORE_32(target, value) \ + ((void) _InterlockedExchange((volatile long *) (target), (long) (value))) +# define PHP_USER_CACHE_ATOMIC_CAS_32(target, expected, desired) \ + ((uint32_t) _InterlockedCompareExchange((volatile long *) (target), (long) (desired), (long) (expected)) == (expected)) +# define PHP_USER_CACHE_ATOMIC_FENCE_SEQ_CST() MemoryBarrier() +# define PHP_USER_CACHE_ATOMIC_FENCE_ACQUIRE() MemoryBarrier() +#else +# define PHP_USER_CACHE_ATOMIC_LOAD_64(target) \ + __atomic_load_n((target), __ATOMIC_ACQUIRE) +# define PHP_USER_CACHE_ATOMIC_STORE_64(target, value) \ + __atomic_store_n((target), (value), __ATOMIC_RELEASE) +# define PHP_USER_CACHE_ATOMIC_CAS_64(target, expected, desired) \ + __atomic_compare_exchange_n((target), &(expected), (desired), false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE) +# define PHP_USER_CACHE_ATOMIC_LOAD_32(target) \ + __atomic_load_n((target), __ATOMIC_ACQUIRE) +# define PHP_USER_CACHE_ATOMIC_STORE_32(target, value) \ + __atomic_store_n((target), (value), __ATOMIC_RELEASE) +# define PHP_USER_CACHE_ATOMIC_CAS_32(target, expected, desired) \ + __atomic_compare_exchange_n((target), &(expected), (desired), false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE) +# define PHP_USER_CACHE_ATOMIC_FENCE_SEQ_CST() __atomic_thread_fence(__ATOMIC_SEQ_CST) +# define PHP_USER_CACHE_ATOMIC_FENCE_ACQUIRE() __atomic_thread_fence(__ATOMIC_ACQUIRE) +#endif +#endif + +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX +typedef union { + pthread_mutex_t mutex; + char padding[64]; +} php_user_cache_shared_mutex; +#endif + +typedef struct { + size_t configured_memory; + php_user_cache_reason failure_reason; + bool enabled; + bool available; +} php_user_cache_runtime; + +typedef struct { + const php_user_cache_shm_handlers *handler; + php_user_cache_shm_segment **segments; + int segment_count; + size_t size; + bool initialized; + uint32_t startup_complete; + bool initialized_before_request; + bool lock_initialized; + bool use_shared_mutex; + int lock_file; + char lockfile_name[MAXPATHLEN]; + /* Cached for the lifetime of the attached segment. */ + bool layout_memo_valid; + uint32_t capacity_memo; + uint32_t data_offset_memo; +#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM + /* Cached for the lifetime of the partition. */ + bool boundary_digest_memoized; + uint8_t boundary_digest_memo[32]; +#endif +#ifdef ZTS + MUTEX_T zts_lock; +#endif +} php_user_cache_storage; + +typedef struct { + php_user_cache_storage storage; + const char *name; + const char *lock_name; +#ifndef ZEND_WIN32 + const char *sem_filename_prefix; +#endif + bool clear_on_pressure; + bool strict_store_failure; + bool boundary_shared; + const char *boundary_identity; + size_t boundary_identity_len; +} php_user_cache_context; + +struct _php_user_cache_partition { + php_user_cache_context context; + char *name; + struct _php_user_cache_partition *next; +}; + +typedef struct { + zend_ulong hash; + uint64_t owner_pid; + uint64_t owner_start_time; + uint64_t expires_at; + uint32_t key_offset; + uint32_t key_len; + uint8_t state; + uint8_t reserved[7]; +} php_user_cache_entry_lock_record; + +typedef struct { + uint64_t owner_pid; + uint64_t owner_start_time; + uint32_t active; + uint32_t reserved; +} php_user_cache_reader_slot; + +/* Cross-references are offsets from the segment base. */ +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t capacity; + uint32_t count; + uint32_t data_offset; + uint32_t data_size; + uint32_t next_free; + uint32_t free_list; + uint32_t last_block_offset; + uint64_t mutation_epoch; + uint64_t write_seq; + uint64_t expunge_count; + uint64_t store_failure_count; + uint32_t tombstone_count; + uint32_t lock_model; + uint32_t lock_model_reserved; + uint32_t orphaned_graphs_saturated; + uint32_t orphaned_graphs_reserved; + uint8_t boundary_identity_digest[32]; + uint32_t boundary_identity_digest_set; + uint32_t boundary_identity_reserved; + uint32_t orphaned_graphs[PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS]; + php_user_cache_reader_slot reader_slots[PHP_USER_CACHE_READER_SLOTS]; +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX + php_user_cache_shared_mutex global_shared_mutex; +#endif + php_user_cache_entry_lock_record entry_lock_records[PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE]; +#ifndef ZEND_WIN32 + uint32_t reserved_lock; +#endif +} php_user_cache_header; + +typedef struct { + uint32_t size; + uint32_t prev_size; + uint32_t next_free; + uint32_t prev_free; + uint32_t flags; +} php_user_cache_block; + +typedef struct { + zend_ulong hash; + uint32_t key_offset; + uint32_t key_len; + uint32_t value_offset; + uint32_t value_len; + uint64_t expires_at; + uint64_t generation; + uint8_t state; + uint8_t value_type; + uint16_t reserved; + zend_long long_value; + double double_value; +} php_user_cache_entry; + +typedef struct { + bool found; + php_user_cache_entry entry; +} php_user_cache_replaced_entry; + +typedef struct { + zend_ulong hash; + uint64_t mutation_epoch; + const void *context; + uint32_t slot_index; + uint8_t state; + uint8_t value_type; + uint8_t reserved[2]; + zend_string *key; + zend_long long_value; + double double_value; +} php_user_cache_lookup_entry; + +typedef struct { + uint64_t generation; + const void *context; + bool needs_deep_clone; + bool has_clone_verdicts; + bool no_aliases; + bool has_value; + zval value; + HashTable clone_verdicts; +} php_user_cache_request_local_slot; + +typedef struct { + zend_ulong hash; + size_t payload_size; + size_t payload_used_size; + const uint8_t *payload_source; + uint8_t *owned_buffer; + zend_string *owned_string; + /* Owned by the prepared value. */ + uint32_t *fixup_offsets; + zend_long long_value; + double double_value; + HashTable *state_memo; + uint32_t value_len; + uint32_t fixup_count; + uint8_t value_type; + bool has_verbatim_array; + uint8_t reserved[2]; +} php_user_cache_prepared_value; + +typedef struct { + bool caller_holds_write_lock; + bool throw_on_failure; + bool honor_strict_store_failure; +} php_user_cache_prepare_options; + +typedef struct { + bool retry_after_memory_pressure; + bool throw_on_failure; + bool honor_strict_store_failure; + bool capture_replaced_entry; +} php_user_cache_store_options; + +typedef struct { + php_user_cache_replaced_entry replaced_entry; + uint64_t stored_generation; + bool should_seed_request_local_slot; +} php_user_cache_store_result; + +/* Deferred until after the read lock is released. */ +typedef struct { + uint64_t generation; + uint32_t flags; + bool should_seed_request_local_slot; +} php_user_cache_fetch_pending_seed; + +typedef struct { + zend_long new_value; + bool is_overflow; + bool is_type_error; +} php_user_cache_atomic_update_result; + +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t root_offset; + uint32_t root_type; + uint32_t flags; + zend_atomic_int ref_state; +} php_user_cache_shared_graph_header; + +typedef struct { + php_user_cache_context *context; + uint32_t payload_offset; +} php_user_cache_shared_graph_ref; + +typedef struct { + php_user_cache_context *context; + uint64_t owner_pid; + zend_long lease; + bool preserve_lease; +} php_user_cache_entry_lock; + +typedef struct { + uint8_t type; + uint8_t reserved[7]; + union { + zend_long long_value; + double double_value; + uint64_t offset; + } payload; +} php_user_cache_shared_graph_value; + +typedef struct { + uint32_t name_offset; + /* Declared property index plus one for __sleep state, otherwise zero. */ + uint32_t reserved; + php_user_cache_shared_graph_value value; +} php_user_cache_shared_graph_property; + +typedef struct { + zend_ulong h; + uint32_t key_offset; + uint32_t reserved; + php_user_cache_shared_graph_value value; +} php_user_cache_shared_graph_array_element; + +typedef struct { + uint32_t count; + uint32_t next_free; + uint32_t elements_offset; + uint32_t reserved; +} php_user_cache_shared_graph_array; + +typedef struct { + uint32_t key_offset; +} php_user_cache_shared_graph_array_shape_element; + +typedef struct { + uint32_t count; + uint32_t elements_offset; + uint32_t reserved; + uint32_t reserved2; +} php_user_cache_shared_graph_array_shape; + +typedef struct { + uint32_t count; + uint32_t next_free; + uint32_t shape_offset; + uint32_t reserved; + uint32_t values_offset; + uint32_t reserved2; +} php_user_cache_shared_graph_shaped_array; + +typedef struct { + uint32_t class_name_offset; + uint32_t property_count; + uint32_t properties_offset; + uint32_t reserved; +} php_user_cache_shared_graph_object; + +typedef struct { + uint32_t class_name_offset; + uint32_t shape_offset; + uint32_t count; +} php_user_cache_shared_graph_state_schema; + +typedef struct { + uint32_t state_schema_offset; + uint32_t reserved; + uint32_t state_values_offset; + uint32_t state_next_free; +} php_user_cache_shared_graph_shaped_state_object; + +/* Shared layout for safe-direct and serialized object state. */ +typedef struct { + uint32_t class_name_offset; + uint32_t property_count; + uint32_t properties_offset; + uint32_t reserved; + php_user_cache_shared_graph_value state; +} php_user_cache_shared_graph_safe_direct_object; + +typedef struct { + uint32_t blob_len; + uint32_t flags; +} php_user_cache_shared_graph_serdes_object; + +typedef struct { + uint32_t flags; + uint32_t reserved; + php_user_cache_shared_graph_value inner; +} php_user_cache_shared_graph_reference; + +typedef struct { + uint32_t class_name_offset; + uint32_t case_name_offset; +} php_user_cache_shared_graph_enum; + +typedef struct { + size_t size; + HashTable seen_arrays; + HashTable seen_objects; + HashTable seen_references; + HashTable string_dedup; + HashTable array_shape_dedup; + HashTable state_schema_dedup; + HashTable direct_array_dedup; + HashTable direct_verdicts; + bool verbatim_arrays_allowed; + /* Distinguishes a sizing failure from an ineligible node. */ + bool reserve_failed; + /* Array address to verbatim eligibility, shared by CALC and COPY. */ + HashTable *shared_verdicts; + HashTable *state_memo; +} php_user_cache_shared_graph_calc_context; + +typedef struct { + uint8_t *buffer; + size_t size; + size_t position; + /* Absolute pointer slots to patch if the payload moves. */ + uint32_t *fixup_offsets; + uint32_t fixup_count; + uint32_t fixup_capacity; + HashTable seen_arrays; + /* Packs the aligned object offset and flags-field displacement. */ + HashTable seen_objects; + HashTable seen_references; + HashTable string_dedup; + HashTable array_shape_dedup; + HashTable state_schema_dedup; + HashTable direct_array_dedup; + HashTable direct_verdicts; + bool has_shared_identity; + bool has_object; + bool prefers_prototype; + bool has_userland_restore_object; + bool has_verbatim_array; + bool verbatim_arrays_allowed; + /* CALC verdicts remain valid only if no snapshot hook ran. */ + const HashTable *shared_verdicts; + HashTable *state_memo; +} php_user_cache_shared_graph_copy_context; + +typedef struct { + const uint8_t *old_base; + const uint8_t *new_base; + size_t len; + ptrdiff_t delta; + HashTable *seen; +} php_user_cache_shared_graph_rebase_context; + +typedef enum { + PHP_USER_CACHE_OPTIMISTIC_FALLBACK = 0, + PHP_USER_CACHE_OPTIMISTIC_FOUND, + PHP_USER_CACHE_OPTIMISTIC_MISS +} php_user_cache_optimistic_result; + +/* Result of the root-array verbatim sizing attempt. */ +typedef enum { + PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED = 0, + PHP_USER_CACHE_VERBATIM_ROOT_SIZED, + PHP_USER_CACHE_VERBATIM_ROOT_ELIGIBLE_UNSIZED, + PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE +} php_user_cache_verbatim_root_result; + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +typedef struct { + php_user_cache_header *header; + uint32_t slot_index; +} php_user_cache_reader_claim; +#endif + +typedef struct { + php_user_cache_partition *active_partition; + php_user_cache_runtime runtime_state; + php_user_cache_context *active_context_ptr; + php_user_cache_reason request_unavailable_reason; + bool lock_held; + bool lock_held_is_write; + bool runtime_resolved; + bool runtime_resolved_enabled; + const php_user_cache_context *runtime_resolved_context; + php_user_cache_shared_graph_ref *shared_graph_refs; + uint32_t shared_graph_ref_count; + uint32_t shared_graph_ref_capacity; + uint64_t shared_graph_ref_owner_pid; + HashTable *shared_graph_ref_index; + php_user_cache_lookup_entry lookup_entry_storage[PHP_USER_CACHE_LOOKUP_BUCKETS]; + HashTable *request_local_slot_table; + HashTable *entry_lock_table; + HashTable *pool_table; + HashTable *decode_identity_map; + HashTable *decode_reference_map; + HashTable *decode_array_map; + HashTable *decode_resolve_cache; + HashTable *decode_shape_prototype_cache; + /* Request-local class route cache. */ + HashTable *object_route_memo; + const void *decode_resolve_direct_keys[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS]; + void *decode_resolve_direct_values[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS]; + const void *decode_shape_prototype_direct_keys[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS]; + zend_array *decode_shape_prototype_direct_values[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS]; + uint8_t decode_resolve_direct_next; + uint8_t decode_shape_prototype_direct_next; +#ifndef ZEND_WIN32 + zend_ulong entry_lock_owner_pid; +#endif + bool write_seq_bumped; + bool stack_overflowed; + /* Request shutdown must collect cyclic slot clones. */ + bool request_local_slot_may_cycle; +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + int8_t reader_drain_state; + php_user_cache_reader_claim reader_claims[PHP_USER_CACHE_READER_CLAIM_MAX]; + uint32_t reader_claim_count; +#endif + uint32_t expired_read_observations; + /* Process-local resume point for bounded expiry scans. */ + uint32_t expired_expunge_cursor; + uint64_t entry_lock_owner_probe_pid; + uint64_t entry_lock_owner_probe_start_time; + uint64_t entry_lock_owner_probe_at; + bool entry_lock_owner_probe_dead; + /* Recomputed after fork. */ + uint64_t self_start_time_pid; + uint64_t self_start_time_token; + bool enable; + bool enable_cli; + zend_long shm_size; + char *lockfile_path; + char *memory_model; +} php_user_cache_globals; + +#ifdef ZTS +# define UC_G(v) ZEND_TSRMG_FAST(user_cache_globals_offset, php_user_cache_globals *, v) +extern int user_cache_globals_id; +extern size_t user_cache_globals_offset; +#else +# define UC_G(v) (user_cache_globals.v) +extern php_user_cache_globals user_cache_globals; +#endif + +extern zend_class_entry *php_user_cache_exception_ce; +extern zend_class_entry *php_user_cache_status_ce; +extern zend_class_entry *php_user_cache_pool_status_ce; +extern php_user_cache_context php_user_cache_context_state; +extern bool php_user_cache_runtime_opted_in; +extern php_user_cache_partition *php_user_cache_partitions; + +uint64_t php_user_cache_cached_pid(void); +void php_user_cache_reset_runtime(void); +void php_user_cache_reset_storage(void); +bool php_user_cache_header_init_locked(void); +void php_user_cache_free_locked(uint32_t payload_offset); +uint32_t php_user_cache_alloc_locked(size_t size, const void *src); +bool php_user_cache_startup_storage_before_request(void); +void php_user_cache_shutdown_storage(void); +void php_user_cache_ensure_ready_slow(void); +bool php_user_cache_rlock(void); +bool php_user_cache_wlock(void); +bool php_user_cache_wlock_for_entry_mutation(zend_string *key); +void php_user_cache_unlock(void); +void php_user_cache_unlock_if_held(void); +bool php_user_cache_try_acquire_entry_lock(zend_string *key, zend_long lease); +bool php_user_cache_release_entry_lock(zend_string *key); +/* Keys must be sorted with duplicates adjacent. */ +bool php_user_cache_acquire_entry_locks(zend_string **keys, bool *acquired, uint32_t count); +void php_user_cache_release_entry_locks(zend_string **keys, const bool *acquired, uint32_t count); +bool php_user_cache_entry_locks_allow_clear_locked(void); +void php_user_cache_release_request_entry_locks(void); +void php_user_cache_safe_direct_handlers_init(void); +void php_user_cache_safe_direct_handlers_destroy(void); +const php_user_cache_safe_direct_handlers *php_user_cache_safe_direct_find_handlers( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr +); +php_user_cache_safe_direct_state_copy_func_t php_user_cache_safe_direct_state_copy_func( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr +); +zend_class_entry *php_user_cache_safe_direct_find_base(zend_class_entry *ce); +php_user_cache_safe_direct_state_has_unstorable_func_t php_user_cache_safe_direct_state_has_unstorable_func( + zend_class_entry *ce +); +php_user_cache_safe_direct_state_serialize_func_t php_user_cache_safe_direct_state_serialize_func( + zend_class_entry *ce +); +php_user_cache_safe_direct_state_unserialize_func_t php_user_cache_safe_direct_state_unserialize_func( + zend_class_entry *ce +); +bool php_user_cache_safe_direct_prefers_request_local_prototype(zend_class_entry *ce); +bool php_user_cache_serdes_encode(zval *value, smart_str *buf, const char **failure_msg); +bool php_user_cache_serdes_decode(const uint8_t *data, size_t len, zval *dst); +uint32_t php_user_cache_serdes_declared_property_index_plus_one( + zend_class_entry *ce, + zend_string *name +); +bool php_user_cache_serdes_get_sleep_state( + zval *obj_zv, + zval *state, + const char **failure_msg +); +bool php_user_cache_serdes_call_magic_serialize(zend_object *obj, zval *state); +bool php_user_cache_shared_graph_update_object_property( + zval *obj_zv, + zend_string *prop_name, + zval *prop_val +); +bool php_user_cache_shared_graph_update_object_property_at( + zval *obj_zv, + zend_string *prop_name, + uint32_t prop_idx, + zval *prop_val +); +bool php_user_cache_shared_graph_can_copy_verbatim_root(const zval *value, HashTable *verbatim_verdicts); +php_user_cache_verbatim_root_result php_user_cache_shared_graph_calc_verbatim_root(const zval *value, HashTable *verbatim_verdicts, size_t *buf_len); +bool php_user_cache_calculate_shared_graph_size(const zval *value, HashTable *state_memo, HashTable *verbatim_verdicts, size_t *buf_len); +bool php_user_cache_build_shared_graph_in_place(const zval *value, HashTable *state_memo, const HashTable *verbatim_verdicts, uint8_t *buf, size_t buf_len, size_t *graph_len, bool *has_verbatim_array, uint32_t **fixup_offsets, uint32_t *fixup_count); +bool php_user_cache_shared_graph_copy_fits_buffer( + const uint8_t *dst_buf, + size_t dst_buf_len, + const uint8_t *src_buf, + size_t src_buf_len, + size_t src_graph_len +); +bool php_user_cache_shared_graph_decode(const uint8_t *buf, size_t buf_len, zval *dst); +bool php_user_cache_shared_graph_prefers_prototype(uint32_t payload_offset); +bool php_user_cache_shared_graph_decode_is_lock_safe(uint32_t payload_offset); +bool php_user_cache_shared_graph_payload_has_aliases(uint32_t payload_offset); +void php_user_cache_decode_resolve_cache_release(void); +void php_user_cache_decode_shape_prototype_cache_release(void); +void php_user_cache_decode_maps_teardown(void); +bool php_user_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset); +bool php_user_cache_shared_graph_publish_copied_payload_locked( + uint8_t *dst_buf, + size_t dst_buf_len, + const uint8_t *src_buf, + size_t src_buf_len, + size_t src_graph_len, + bool has_verbatim_array, + const uint32_t *fixup_offsets, + uint32_t fixup_count +); +bool php_user_cache_shared_graph_acquire_ref(uint32_t payload_offset); +bool php_user_cache_shared_graph_retire_payload_locked(uint32_t payload_offset); +bool php_user_cache_shared_graph_release_ref_locked(uint32_t payload_offset); +bool php_user_cache_has_request_shared_graph_ref(uint32_t payload_offset); +void php_user_cache_register_shared_graph_ref(uint32_t payload_offset); +bool php_user_cache_release_request_shared_graph_refs(void); +bool php_user_cache_clear_locked(void); +void php_user_cache_lookup_cache_clear(void); +bool php_user_cache_prepare_value( + zend_string *key, + zval *value, + const php_user_cache_prepare_options *options, + php_user_cache_prepared_value *prepared +); +void php_user_cache_destroy_prepared_value(php_user_cache_prepared_value *prepared); +bool php_user_cache_store_prepared_locked( + zend_string *key, + zval *value, + const php_user_cache_prepared_value *prepared, + zend_long ttl, + const php_user_cache_store_options *options, + php_user_cache_store_result *result +); +bool php_user_cache_fetch_locked( + zend_string *key, + bool throw_if_missing, + bool use_request_local_slot, + zval *return_value, + bool *found, + php_user_cache_fetch_pending_seed *pending_seed +); +bool php_user_cache_fetch_finish(zend_string *key, uint64_t gen, zval *return_value, uint32_t flags); +bool php_user_cache_exists_locked(zend_string *key); +void php_user_cache_delete_locked(zend_string *key); +void php_user_cache_discard_replaced_entry_locked(zend_string *key, php_user_cache_replaced_entry *replaced_entry); +void php_user_cache_rollback_replaced_entry_locked(zend_string *key, php_user_cache_replaced_entry *replaced_entry); +void php_user_cache_delete_by_prefix_locked(zend_string *prefix); +bool php_user_cache_atomic_update_locked( + zend_string *key, + zend_long step, + zend_long ttl, + bool decrement, + bool insert_if_missing, + php_user_cache_atomic_update_result *result +); +void php_user_cache_release_request_local_slots(void); +void php_user_cache_release_active_request_local_slots(void); +void php_user_cache_release_active_request_local_slots_by_prefix(zend_string *prefix); +void php_user_cache_store_request_local_slot(zend_string *key, uint64_t gen, zval *value, bool no_aliases); +void php_user_cache_object_table_dtor(zval *zv); +void php_user_cache_reference_table_dtor(zval *zv); +php_user_cache_optimistic_result php_user_cache_fetch_optimistic( + zend_string *key, + zval *return_value +); +php_user_cache_optimistic_result php_user_cache_exists_optimistic(zend_string *key); +bool php_user_cache_optimistic_reader_begin(php_user_cache_header *header, uint32_t *slot_idx_ptr); +void php_user_cache_optimistic_reader_end(php_user_cache_header *header, uint32_t slot_idx); +void php_user_cache_optimistic_fork_setup(void); +#if defined(ZTS) && defined(PHP_USER_CACHE_HAVE_OPTIMISTIC) +void php_user_cache_release_thread_reader_claims(php_user_cache_globals *globals); +#endif +bool php_user_cache_quiesce_graph_payloads_locked(void); +void php_user_cache_shared_graph_ref_reserve(void); +void php_user_cache_shared_graph_orphan_payload_locked(uint32_t payload_offset); +void php_user_cache_shared_graph_reclaim_orphaned_locked(void); + +static zend_always_inline uint64_t php_user_cache_current_pid(void) +{ +#ifdef ZEND_WIN32 + return (uint64_t) GetCurrentProcessId(); +#else + return (uint64_t) getpid(); +#endif +} + +static zend_always_inline bool php_user_cache_stack_overflowed(void) +{ +#ifdef ZEND_CHECK_STACK_LIMIT + bool overflowed = UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit))); + + /* Latch the failure for the rest of the request. */ + if (overflowed) { + UC_G(stack_overflowed) = true; + } + + return overflowed; +#else + return false; +#endif +} + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +static zend_always_inline uint64_t php_user_cache_atomic_load_64(const uint64_t *target) +{ + return PHP_USER_CACHE_ATOMIC_LOAD_64(target); +} + +static zend_always_inline void php_user_cache_atomic_fence_acquire(void) +{ + PHP_USER_CACHE_ATOMIC_FENCE_ACQUIRE(); +} +#endif + +static zend_always_inline php_user_cache_context *php_user_cache_owning_context(void) +{ + return UC_G(active_partition) != NULL + ? &UC_G(active_partition)->context + : &php_user_cache_context_state + ; +} + +static zend_always_inline php_user_cache_context *php_user_cache_active_context(void) +{ + if (UC_G(active_context_ptr) != NULL) { + return UC_G(active_context_ptr); + } + + return php_user_cache_owning_context(); +} + +static zend_always_inline void *php_user_cache_base(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (!storage->initialized || + storage->segment_count != 1 + ) { + return NULL; + } + + return storage->segments[0]->p; +} + +static zend_always_inline php_user_cache_header *php_user_cache_header_ptr(void) +{ + return (php_user_cache_header *) php_user_cache_base(); +} + +static zend_always_inline uint8_t *php_user_cache_ptr(uint32_t offset) +{ + return (uint8_t *) php_user_cache_base() + offset; +} + +static zend_always_inline php_user_cache_block *php_user_cache_block_ptr(uint32_t offset) +{ + return (php_user_cache_block *) php_user_cache_ptr(offset); +} + +static zend_always_inline uint32_t php_user_cache_block_payload_capacity(uint32_t payload_offset) +{ + php_user_cache_block *block; + + if (payload_offset < sizeof(php_user_cache_block)) { + return 0; + } + + block = php_user_cache_block_ptr(payload_offset - sizeof(php_user_cache_block)); + if (block->size < sizeof(php_user_cache_block)) { + return 0; + } + + return block->size - (uint32_t) sizeof(php_user_cache_block); +} + +static zend_always_inline bool php_user_cache_block_is_free(const php_user_cache_block *block) +{ + return (block->flags & PHP_USER_CACHE_BLOCK_FREE) != 0; +} + +static zend_always_inline php_user_cache_context *php_user_cache_activate_context(php_user_cache_context *context) +{ + php_user_cache_context *previous = UC_G(active_context_ptr); + + UC_G(active_context_ptr) = context; + + return previous; +} + +static zend_always_inline void php_user_cache_restore_context(php_user_cache_context *context) +{ + UC_G(active_context_ptr) = context; +} + +static zend_always_inline php_user_cache_runtime *php_user_cache_active_runtime(void) +{ + return &UC_G(runtime_state); +} + +static zend_always_inline uint64_t php_user_cache_seq_load(const uint64_t *seq) +{ +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + return php_user_cache_atomic_load_64(seq); +#else + return *seq; +#endif +} + +static zend_always_inline uint64_t php_user_cache_seq_reload(const uint64_t *seq) +{ +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + php_user_cache_atomic_fence_acquire(); + + return php_user_cache_atomic_load_64(seq); +#else + return *seq; +#endif +} + +static zend_always_inline bool php_user_cache_header_is_initialized_locked(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + + return header != NULL && + header->magic == PHP_USER_CACHE_MAGIC && + header->version == PHP_USER_CACHE_VERSION + ; +} + +static zend_always_inline void php_user_cache_bump_mutation_epoch_locked(php_user_cache_header *header) +{ + if (header == NULL) { + return; + } + + header->mutation_epoch++; + if (header->mutation_epoch == 0) { + header->mutation_epoch = 1; + } +} + +static zend_always_inline php_user_cache_entry *php_user_cache_entries_ptr(php_user_cache_header *header) +{ + return (php_user_cache_entry *) ((char *) header + sizeof(php_user_cache_header)); +} + +static zend_always_inline bool php_user_cache_seen_test_and_add(HashTable *seen, const void *ptr) +{ + zend_ulong key = (zend_ulong) (uintptr_t) ptr; + + if (zend_hash_index_exists(seen, key)) { + return false; + } + + return zend_hash_index_add_empty_element(seen, key) != NULL; +} + +static zend_always_inline void php_user_cache_ensure_ready(void) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + + if (UC_G(runtime_resolved) && + UC_G(runtime_resolved_context) == ctx && + UC_G(runtime_resolved_enabled) == UC_G(enable) + ) { + return; + } + + php_user_cache_ensure_ready_slow(); +} + +static inline bool php_user_cache_class_overrides_safe_direct_magic_serialize_ex( + zend_class_entry *ce, + const php_user_cache_safe_direct_handlers *handlers, + zend_class_entry *base_ce) +{ + if (ce->type != ZEND_USER_CLASS || + ce->__serialize == NULL || + ce->__unserialize == NULL || + (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 || + handlers == NULL || + base_ce == ce + ) { + return false; + } + + return + ( + ce->__serialize->type == ZEND_USER_FUNCTION || + ce->__unserialize->type == ZEND_USER_FUNCTION + ) && + ( + ce->__serialize->common.scope != base_ce || + ce->__unserialize->common.scope != base_ce + ) + ; +} + +static inline bool php_user_cache_class_has_safe_direct_state_funcs(zend_class_entry *ce) +{ + /* Registered as an all-or-nothing set. */ + return php_user_cache_safe_direct_find_handlers(ce, NULL) != NULL; +} + +static inline bool php_user_cache_class_magic_route_active(zend_class_entry *ce) +{ + const php_user_cache_safe_direct_handlers *handlers; + zend_class_entry *base_ce = NULL; + + if (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) { + return false; + } + + handlers = php_user_cache_safe_direct_find_handlers(ce, &base_ce); + + if (php_user_cache_class_overrides_safe_direct_magic_serialize_ex(ce, handlers, base_ce)) { + return true; + } + + return handlers == NULL; +} + +static inline bool php_user_cache_class_uses_magic_serialize(zend_class_entry *ce) +{ + return ce->__serialize != NULL && ce->__unserialize != NULL && + php_user_cache_class_magic_route_active(ce) + ; +} + +static inline bool php_user_cache_class_uses_serialize_props(zend_class_entry *ce) +{ + /* Native serialization restores this state as properties. */ + return ce->__serialize != NULL && ce->__unserialize == NULL && + php_user_cache_class_magic_route_active(ce) + ; +} + +static inline bool php_user_cache_class_uses_magic_unserialize(zend_class_entry *ce) +{ + if (ce->__serialize != NULL || ce->__unserialize == NULL) { + return false; + } + + /* Match native serialization precedence. */ + if (ce->serialize != NULL && ce->unserialize != NULL) { + return false; + } + + return php_user_cache_class_magic_route_active(ce); +} + +static inline bool php_user_cache_class_uses_serdes(zend_class_entry *ce) +{ + if (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) { + return false; + } + + if (php_user_cache_class_uses_magic_serialize(ce) || + php_user_cache_class_uses_magic_unserialize(ce) || + php_user_cache_class_has_safe_direct_state_funcs(ce) + ) { + return false; + } + + if (zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL || + zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL + ) { + return true; + } + + /* Honor native and Serializable handlers. */ + return ce->serialize != NULL && + ce->unserialize != NULL + ; +} + +#endif /* PHP_USER_CACHE_INTERNAL_H */ diff --git a/ext/user_cache/user_cache_serdes.c b/ext/user_cache/user_cache_serdes.c new file mode 100644 index 000000000000..3b2ab9b20bef --- /dev/null +++ b/ext/user_cache/user_cache_serdes.c @@ -0,0 +1,1661 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#include "user_cache_internal.h" + +#include "Zend/zend_closures.h" +#include "Zend/zend_enum.h" +#include "Zend/zend_interfaces.h" + +/* Position-independent tagged stream using native byte order. */ +#define PHP_USER_CACHE_SERDES_TAG_NULL 1 +#define PHP_USER_CACHE_SERDES_TAG_FALSE 2 +#define PHP_USER_CACHE_SERDES_TAG_TRUE 3 +#define PHP_USER_CACHE_SERDES_TAG_LONG 4 +#define PHP_USER_CACHE_SERDES_TAG_DOUBLE 5 +#define PHP_USER_CACHE_SERDES_TAG_STRING 6 +#define PHP_USER_CACHE_SERDES_TAG_EMPTY_ARRAY 7 +#define PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY 8 +#define PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY 9 +#define PHP_USER_CACHE_SERDES_TAG_OBJECT 10 +#define PHP_USER_CACHE_SERDES_TAG_SERIALIZED_OBJECT 11 +#define PHP_USER_CACHE_SERDES_TAG_CUSTOM_OBJECT 12 +#define PHP_USER_CACHE_SERDES_TAG_ENUM 13 +#define PHP_USER_CACHE_SERDES_TAG_REFERENCE 14 +#define PHP_USER_CACHE_SERDES_TAG_BACKREF 15 + +#define PHP_USER_CACHE_SERDES_KEY_LONG 0 +#define PHP_USER_CACHE_SERDES_KEY_STRING 1 + +#define PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE 0 + +#define PHP_USER_CACHE_SERDES_STRING_RAW 0 +#define PHP_USER_CACHE_SERDES_STRING_INLINE 1 +#define PHP_USER_CACHE_SERDES_STRING_REF 2 + +#define PHP_USER_CACHE_SERDES_INLINE_IDS 8 +#define PHP_USER_CACHE_SERDES_INLINE_STRINGS 32 + +typedef struct { + smart_str buf; + HashTable seen; + HashTable strings; + uint32_t string_count; + const char *failure_message; +} php_user_cache_serdes_encoder; + +typedef struct { + const uint8_t *data; + size_t len; + size_t pos; + zval *ids; + uint32_t id_count; + uint32_t id_capacity; + zend_string **strings; + uint32_t string_count; + uint32_t string_capacity; + zval inline_ids[PHP_USER_CACHE_SERDES_INLINE_IDS]; + zend_string *inline_strings[PHP_USER_CACHE_SERDES_INLINE_STRINGS]; +} php_user_cache_serdes_decoder; + +static bool user_cache_serdes_encode_value( + php_user_cache_serdes_encoder *enc, + zval *value +); + +static bool user_cache_serdes_decode_value( + php_user_cache_serdes_decoder *dec, + zval *dst +); + +static bool user_cache_serdes_decoder_register_string( + php_user_cache_serdes_decoder *dec, + zend_string *str +); + +static zend_always_inline void user_cache_serdes_put_pad(smart_str *buf, size_t align) +{ + size_t len = smart_str_get_len(buf), pad = (align - (len & (align - 1))) & (align - 1); + + if (pad != 0) { + memset(smart_str_extend(buf, pad), 0, pad); + } +} + +static zend_always_inline void user_cache_serdes_put_u8(smart_str *buf, uint8_t value) +{ + smart_str_appendc(buf, (char) value); +} + +static zend_always_inline void user_cache_serdes_put_fixed(smart_str *buf, const void *value, size_t size) +{ + user_cache_serdes_put_pad(buf, size); + smart_str_appendl(buf, (const char *) value, size); +} + +static zend_always_inline void user_cache_serdes_put_u32(smart_str *buf, uint32_t value) +{ + user_cache_serdes_put_fixed(buf, &value, sizeof(value)); +} + +static zend_always_inline void user_cache_serdes_put_u64(smart_str *buf, uint64_t value) +{ + user_cache_serdes_put_fixed(buf, &value, sizeof(value)); +} + +static zend_always_inline void user_cache_serdes_put_long(smart_str *buf, zend_long value) +{ + user_cache_serdes_put_fixed(buf, &value, sizeof(value)); +} + +static zend_always_inline void user_cache_serdes_put_double(smart_str *buf, double value) +{ + user_cache_serdes_put_fixed(buf, &value, sizeof(value)); +} + +static zend_always_inline size_t user_cache_serdes_reserve_u32(smart_str *buf) +{ + user_cache_serdes_put_pad(buf, sizeof(uint32_t)); + memset(smart_str_extend(buf, sizeof(uint32_t)), 0, sizeof(uint32_t)); + + return smart_str_get_len(buf) - sizeof(uint32_t); +} + +static zend_always_inline void user_cache_serdes_patch_u32( + smart_str *buf, + size_t pos, + uint32_t value) +{ + ZEND_ASSERT(buf->s != NULL && pos + sizeof(uint32_t) <= ZSTR_LEN(buf->s)); + + memcpy(ZSTR_VAL(buf->s) + pos, &value, sizeof(value)); +} + +static zend_always_inline bool user_cache_serdes_encode_backref_or_register( + php_user_cache_serdes_encoder *enc, + const void *container, + bool *is_backref) +{ + zval *slot; + uintptr_t id; + + slot = zend_hash_index_lookup(&enc->seen, (zend_ulong) (uintptr_t) container); + + if (Z_TYPE_P(slot) == IS_PTR) { + *is_backref = true; + + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_BACKREF); + user_cache_serdes_put_u32(&enc->buf, (uint32_t) ((uintptr_t) Z_PTR_P(slot) - 1)); + + return true; + } + + *is_backref = false; + id = (uintptr_t) zend_hash_num_elements(&enc->seen); + + if (id > UINT32_MAX) { + enc->failure_message = "value contains too many arrays or objects to be stored in the user cache"; + + return false; + } + + ZVAL_PTR(slot, (void *) id); + + return true; +} + +static zend_always_inline bool user_cache_serdes_skip_pad( + php_user_cache_serdes_decoder *dec, + size_t align) +{ + size_t pad = (align - (dec->pos & (align - 1))) & (align - 1); + + if (pad > dec->len - dec->pos) { + return false; + } + + dec->pos += pad; + + return true; +} + +static zend_always_inline bool user_cache_serdes_get_u8( + php_user_cache_serdes_decoder *dec, + uint8_t *value) +{ + if (dec->pos >= dec->len) { + return false; + } + + *value = dec->data[dec->pos++]; + + return true; +} + +static zend_always_inline bool user_cache_serdes_get_fixed( + php_user_cache_serdes_decoder *dec, + void *value, + size_t size) +{ + if (!user_cache_serdes_skip_pad(dec, size) || + size > dec->len - dec->pos + ) { + return false; + } + + memcpy(value, dec->data + dec->pos, size); + dec->pos += size; + + return true; +} + +static zend_always_inline bool user_cache_serdes_get_u32( + php_user_cache_serdes_decoder *dec, + uint32_t *value) +{ + return user_cache_serdes_get_fixed(dec, value, sizeof(*value)); +} + +static zend_always_inline bool user_cache_serdes_get_u64( + php_user_cache_serdes_decoder *dec, + uint64_t *value) +{ + return user_cache_serdes_get_fixed(dec, value, sizeof(*value)); +} + +static zend_always_inline bool user_cache_serdes_get_long( + php_user_cache_serdes_decoder *dec, + zend_long *value) +{ + return user_cache_serdes_get_fixed(dec, value, sizeof(*value)); +} + +static zend_always_inline bool user_cache_serdes_get_double( + php_user_cache_serdes_decoder *dec, + double *value) +{ + return user_cache_serdes_get_fixed(dec, value, sizeof(*value)); +} + +static zend_always_inline bool user_cache_serdes_get_bytes( + php_user_cache_serdes_decoder *dec, + size_t len, + const uint8_t **bytes) +{ + if (len > dec->len - dec->pos) { + return false; + } + + *bytes = dec->data + dec->pos; + dec->pos += len; + + return true; +} + +static zend_always_inline bool user_cache_serdes_get_str( + php_user_cache_serdes_decoder *dec, + zend_string **value) +{ + const uint8_t *bytes; + zend_string *str; + uint32_t byte_len, string_id; + uint8_t kind; + + if (!user_cache_serdes_get_u8(dec, &kind)) { + return false; + } + + if (kind == PHP_USER_CACHE_SERDES_STRING_REF) { + if (!user_cache_serdes_get_u32(dec, &string_id) || + string_id >= dec->string_count + ) { + return false; + } + + *value = zend_string_copy(dec->strings[string_id]); + + return true; + } + + if ((kind != PHP_USER_CACHE_SERDES_STRING_INLINE && + kind != PHP_USER_CACHE_SERDES_STRING_RAW) || + !user_cache_serdes_get_u32(dec, &byte_len) || + !user_cache_serdes_get_bytes(dec, byte_len, &bytes) + ) { + return false; + } + + str = zend_string_init((const char *) bytes, byte_len, 0); + if (kind == PHP_USER_CACHE_SERDES_STRING_RAW) { + *value = str; + + return true; + } + + if (!user_cache_serdes_decoder_register_string(dec, str)) { + zend_string_release(str); + + return false; + } + + *value = zend_string_copy(str); + + return true; +} + +static bool user_cache_serdes_put_str( + php_user_cache_serdes_encoder *enc, + const char *value, + size_t len) +{ + zval *string_id, new_string_id; + uint32_t id; + + string_id = zend_hash_str_find(&enc->strings, value, len); + if (string_id != NULL) { + if (Z_LVAL_P(string_id) == 0) { + if (enc->string_count >= UINT32_MAX) { + enc->failure_message = "value is too large to be stored in the user cache"; + + return false; + } + id = enc->string_count++; + + ZVAL_LONG(string_id, (zend_long) id + 1); + + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_STRING_INLINE); + user_cache_serdes_put_u32(&enc->buf, (uint32_t) len); + if (len != 0) { + smart_str_appendl(&enc->buf, value, len); + } + + return true; + } + + id = (uint32_t) (Z_LVAL_P(string_id) - 1); + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_STRING_REF); + user_cache_serdes_put_u32(&enc->buf, id); + + return true; + } + + if (len > UINT32_MAX) { + enc->failure_message = "value is too large to be stored in the user cache"; + + return false; + } + + ZVAL_LONG(&new_string_id, 0); + if (zend_hash_str_add_new(&enc->strings, value, len, &new_string_id) == NULL) { + return false; + } + + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_STRING_RAW); + user_cache_serdes_put_u32(&enc->buf, (uint32_t) len); + if (len != 0) { + smart_str_appendl(&enc->buf, value, len); + } + + return true; +} + +static zend_result user_cache_serdes_try_add_sleep_prop( + zval *obj_zv, + HashTable *props, + zend_string *name, + zend_string *error_name, + HashTable *ht) +{ + zval *val = zend_hash_find(props, name); + + if (val == NULL) { + return FAILURE; + } + + if (Z_TYPE_P(val) == IS_INDIRECT) { + val = Z_INDIRECT_P(val); + if (Z_TYPE_P(val) == IS_UNDEF) { + if (zend_get_typed_property_info_for_slot(Z_OBJ_P(obj_zv), val) != NULL) { + return SUCCESS; + } + + return FAILURE; + } + } + + if (!zend_hash_add(ht, name, val)) { + php_error_docref(NULL, E_WARNING, + "\"%s\" is returned from __sleep() multiple times", + ZSTR_VAL(error_name) + ); + + return SUCCESS; + } + + Z_TRY_ADDREF_P(val); + + return SUCCESS; +} + +static bool user_cache_serdes_get_sleep_props( + zval *obj_zv, + HashTable *sleep_retval, + zval *state) +{ + zend_result added; + zend_string *name, *tmp_name, *candidate; + zend_class_entry *ce = Z_OBJCE_P(obj_zv); + zval *name_val; + HashTable *props = zend_get_properties_for(obj_zv, ZEND_PROP_PURPOSE_SERIALIZE); + bool result = true; + + array_init_size(state, zend_hash_num_elements(sleep_retval)); + + ZEND_HASH_FOREACH_VAL_IND(sleep_retval, name_val) { + ZVAL_DEREF(name_val); + if (Z_TYPE_P(name_val) != IS_STRING) { + php_error_docref(NULL, E_WARNING, + "%s::__sleep() should return an array only containing the names of instance-variables to serialize", + ZSTR_VAL(ce->name) + ); + } + + name = zval_get_tmp_string(name_val, &tmp_name); + + /* Try unmangled, private and protected names in order. */ + added = user_cache_serdes_try_add_sleep_prop(obj_zv, props, name, name, Z_ARRVAL_P(state)); + + if (added == FAILURE && !EG(exception)) { + candidate = zend_mangle_property_name( + ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), + ZSTR_VAL(name), ZSTR_LEN(name), ce->type == ZEND_INTERNAL_CLASS + ); + + added = user_cache_serdes_try_add_sleep_prop(obj_zv, props, candidate, name, Z_ARRVAL_P(state)); + + zend_string_release(candidate); + } + + if (added == FAILURE && !EG(exception)) { + candidate = zend_mangle_property_name( + "*", 1, ZSTR_VAL(name), ZSTR_LEN(name), ce->type == ZEND_INTERNAL_CLASS + ); + + added = user_cache_serdes_try_add_sleep_prop(obj_zv, props, candidate, name, Z_ARRVAL_P(state)); + + zend_string_release(candidate); + } + + if (added == FAILURE) { + if (EG(exception)) { + zend_tmp_string_release(tmp_name); + result = false; + + break; + } + + php_error_docref( + NULL, + E_WARNING, + "\"%s\" returned as member variable from __sleep() but does not exist", + ZSTR_VAL(name) + ); + } + + zend_tmp_string_release(tmp_name); + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(props); + + return result; +} + +static bool user_cache_serdes_encode_property( + php_user_cache_serdes_encoder *enc, + zend_class_entry *ce, + zend_string *name, + zval *value) +{ + if (!user_cache_serdes_put_str(enc, ZSTR_VAL(name), ZSTR_LEN(name))) { + return false; + } + + user_cache_serdes_put_u32( + &enc->buf, + php_user_cache_serdes_declared_property_index_plus_one(ce, name) + ); + + return user_cache_serdes_encode_value(enc, value); +} + +static bool user_cache_serdes_encode_property_table( + php_user_cache_serdes_encoder *enc, + zend_class_entry *ce, + HashTable *props, + size_t count_pos) +{ + zend_ulong h; + zend_string *name; + zval *value; + uint32_t count = 0; + size_t digits_len; + char numeric_key_buf[32], *digits; + + ZEND_HASH_FOREACH_KEY_VAL(props, h, name, value) { + if (Z_TYPE_P(value) == IS_INDIRECT) { + value = Z_INDIRECT_P(value); + } + + if (Z_TYPE_P(value) == IS_UNDEF) { + continue; + } + + if (name != NULL) { + if (!user_cache_serdes_encode_property(enc, ce, name, value)) { + return false; + } + } else { + digits = zend_print_long_to_buf(numeric_key_buf + sizeof(numeric_key_buf) - 1, (zend_long) h); + digits_len = numeric_key_buf + sizeof(numeric_key_buf) - 1 - digits; + + if (!user_cache_serdes_put_str(enc, digits, digits_len)) { + return false; + } + + user_cache_serdes_put_u32(&enc->buf, PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE); + + if (!user_cache_serdes_encode_value(enc, value)) { + return false; + } + } + + count++; + } ZEND_HASH_FOREACH_END(); + + user_cache_serdes_patch_u32(&enc->buf, count_pos, count); + + return true; +} + +static bool user_cache_serdes_encode_serialized_object( + php_user_cache_serdes_encoder *enc, + zval *value) +{ + zend_object *obj = Z_OBJ_P(value); + zend_class_entry *ce = obj->ce; + zend_string *class_name = ce->name; + zval retval; + bool result; + + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_SERIALIZED_OBJECT); + if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) { + return false; + } + + if (!php_user_cache_serdes_call_magic_serialize(obj, &retval)) { + return false; + } + + result = user_cache_serdes_encode_value(enc, &retval); + + zval_ptr_dtor(&retval); + + return result; +} + +static bool user_cache_serdes_encode_custom_object( + php_user_cache_serdes_encoder *enc, + zval *value) +{ + zend_class_entry *ce = Z_OBJCE_P(value); + zend_string *class_name = ce->name; + php_serialize_data_t ser_data; + uint8_t *ser_buf = NULL; + size_t ser_len = 0; + bool result; + + if (ce->serialize == NULL || ce->unserialize == NULL) { + enc->failure_message = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE; + + return false; + } + + PHP_VAR_SERIALIZE_INIT(ser_data); + result = ce->serialize(value, &ser_buf, &ser_len, (zend_serialize_data *) ser_data) == SUCCESS; + PHP_VAR_SERIALIZE_DESTROY(ser_data); + + if (!result || ser_len > UINT32_MAX) { + if (ser_buf != NULL) { + efree(ser_buf); + } + + enc->failure_message = "the object could not be serialized for the user cache"; + + return false; + } + + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_CUSTOM_OBJECT); + if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) { + if (ser_buf != NULL) { + efree(ser_buf); + } + + return false; + } + + user_cache_serdes_put_u32(&enc->buf, (uint32_t) ser_len); + if (ser_len != 0) { + smart_str_appendl(&enc->buf, (const char *) ser_buf, ser_len); + } + + if (ser_buf != NULL) { + efree(ser_buf); + } + + return true; +} + +static bool user_cache_serdes_put_object_header( + php_user_cache_serdes_encoder *enc, + zend_string *class_name, + bool has_wakeup, + size_t *count_pos) +{ + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_OBJECT); + + if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) { + return false; + } + + user_cache_serdes_put_u8(&enc->buf, has_wakeup ? 1 : 0); + + *count_pos = user_cache_serdes_reserve_u32(&enc->buf); + + return true; +} + +static bool user_cache_serdes_encode_sleep_object( + php_user_cache_serdes_encoder *enc, + zval *value, + bool has_wakeup) +{ + zend_class_entry *ce = Z_OBJCE_P(value); + zend_string *class_name = ce->name; + zval sleep_state; + size_t count_pos; + bool result; + + if (!user_cache_serdes_put_object_header(enc, class_name, has_wakeup, &count_pos)) { + return false; + } + + if (!php_user_cache_serdes_get_sleep_state(value, &sleep_state, &enc->failure_message)) { + return false; + } + + result = user_cache_serdes_encode_property_table(enc, ce, Z_ARRVAL(sleep_state), count_pos); + + zval_ptr_dtor(&sleep_state); + + return result; +} + +static bool user_cache_serdes_encode_plain_object( + php_user_cache_serdes_encoder *enc, + zval *value, + bool has_wakeup) +{ + zend_class_entry *ce = Z_OBJCE_P(value); + zend_string *class_name = ce->name; + HashTable *props; + size_t count_pos; + bool result; + + if (ce->type != ZEND_USER_CLASS && ce->create_object != NULL && !has_wakeup) { + enc->failure_message = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE; + + return false; + } + + if (!user_cache_serdes_put_object_header(enc, class_name, has_wakeup, &count_pos)) { + return false; + } + + props = zend_get_properties_for(value, ZEND_PROP_PURPOSE_SERIALIZE); + if (props == NULL) { + user_cache_serdes_patch_u32(&enc->buf, count_pos, 0); + + return true; + } + + result = user_cache_serdes_encode_property_table(enc, ce, props, count_pos); + + zend_release_properties(props); + + return result; +} + +static bool user_cache_serdes_encode_object( + php_user_cache_serdes_encoder *enc, + zval *value) +{ + zend_object *obj = Z_OBJ_P(value); + zend_class_entry *ce = obj->ce; + zend_string *class_name = ce->name, *case_name; + bool has_sleep, has_wakeup, is_backref; + + if (ce == zend_ce_closure) { + enc->failure_message = PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE; + + return false; + } + + if (zend_object_is_lazy(obj)) { + enc->failure_message = PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE; + + return false; + } + + if (ce->ce_flags & ZEND_ACC_ENUM) { + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_ENUM); + + if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) { + return false; + } + + case_name = Z_STR_P(zend_enum_fetch_case_name(obj)); + if (!user_cache_serdes_put_str(enc, ZSTR_VAL(case_name), ZSTR_LEN(case_name))) { + return false; + } + + return true; + } + + if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) { + enc->failure_message = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE; + + return false; + } + + if (!user_cache_serdes_encode_backref_or_register(enc, obj, &is_backref)) { + return false; + } + + if (is_backref) { + return true; + } + + if (ce->__serialize != NULL && ce->__unserialize != NULL) { + return user_cache_serdes_encode_serialized_object(enc, value); + } + + /* Match native serialization precedence. */ + if (ce->serialize != NULL || ce->unserialize != NULL) { + return user_cache_serdes_encode_custom_object(enc, value); + } + + has_sleep = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL; + has_wakeup = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL; + + if (has_sleep) { + return user_cache_serdes_encode_sleep_object(enc, value, has_wakeup); + } + + return user_cache_serdes_encode_plain_object(enc, value, has_wakeup); +} + +static bool user_cache_serdes_encode_array( + php_user_cache_serdes_encoder *enc, + zval *value) +{ + zend_string *key; + zend_ulong h; + zval *elem; + HashTable *arr = Z_ARRVAL_P(value); + uint32_t count = zend_hash_num_elements(arr); + bool is_backref; + + if (count == 0 && arr->nNextFreeElement == 0) { + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_EMPTY_ARRAY); + + return true; + } + + if (!user_cache_serdes_encode_backref_or_register(enc, arr, &is_backref)) { + return false; + } + + if (is_backref) { + return true; + } + + if (HT_IS_PACKED(arr) && arr->nNumUsed == count) { + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY); + user_cache_serdes_put_u64(&enc->buf, (uint64_t) arr->nNextFreeElement); + user_cache_serdes_put_u32(&enc->buf, count); + + ZEND_HASH_PACKED_FOREACH_VAL(arr, elem) { + if (!user_cache_serdes_encode_value(enc, elem)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; + } + + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY); + user_cache_serdes_put_u64(&enc->buf, (uint64_t) arr->nNextFreeElement); + user_cache_serdes_put_u32(&enc->buf, count); + + ZEND_HASH_FOREACH_KEY_VAL(arr, h, key, elem) { + if (key != NULL) { + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_KEY_STRING); + if (!user_cache_serdes_put_str(enc, ZSTR_VAL(key), ZSTR_LEN(key))) { + return false; + } + } else { + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_KEY_LONG); + user_cache_serdes_put_u64(&enc->buf, (uint64_t) h); + } + + if (!user_cache_serdes_encode_value(enc, elem)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static bool user_cache_serdes_encode_value( + php_user_cache_serdes_encoder *enc, + zval *value) +{ + zend_reference *ref; + bool is_backref; + + if (php_user_cache_stack_overflowed()) { + enc->failure_message = "value is nested too deeply to be stored in the user cache"; + + return false; + } + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_NULL); + + return true; + case IS_FALSE: + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_FALSE); + + return true; + case IS_TRUE: + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_TRUE); + + return true; + case IS_LONG: + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_LONG); + user_cache_serdes_put_long(&enc->buf, Z_LVAL_P(value)); + + return true; + case IS_DOUBLE: + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_DOUBLE); + user_cache_serdes_put_double(&enc->buf, Z_DVAL_P(value)); + + return true; + case IS_STRING: + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_STRING); + if (!user_cache_serdes_put_str(enc, Z_STRVAL_P(value), Z_STRLEN_P(value))) { + return false; + } + + return true; + case IS_ARRAY: + return user_cache_serdes_encode_array(enc, value); + case IS_OBJECT: + return user_cache_serdes_encode_object(enc, value); + case IS_REFERENCE: + ref = Z_REF_P(value); + + if (!user_cache_serdes_encode_backref_or_register(enc, ref, &is_backref)) { + return false; + } + + if (is_backref) { + return true; + } + + user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_REFERENCE); + + return user_cache_serdes_encode_value(enc, &ref->val); + case IS_RESOURCE: + enc->failure_message = PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE; + + return false; + default: + enc->failure_message = "value cannot be stored in the user cache"; + + return false; + } +} + +static void *user_cache_serdes_registry_reserve( + void *items, + void *inline_items, + uint32_t count, + uint32_t *cap, + size_t elem_size) +{ + void *new_items; + uint32_t new_cap; + + if (count != *cap) { + return items; + } + + if (*cap > UINT32_MAX / 2) { + return NULL; + } + + new_cap = *cap * 2; + if (items == inline_items) { + new_items = safe_emalloc(new_cap, elem_size, 0); + memcpy(new_items, inline_items, count * elem_size); + } else { + new_items = safe_erealloc(items, new_cap, elem_size, 0); + } + + *cap = new_cap; + + return new_items; +} + +static bool user_cache_serdes_decoder_register_string( + php_user_cache_serdes_decoder *dec, + zend_string *str) +{ + zend_string **strings; + + if (dec->string_count == UINT32_MAX) { + return false; + } + + strings = user_cache_serdes_registry_reserve( + dec->strings, dec->inline_strings, dec->string_count, + &dec->string_capacity, sizeof(zend_string *) + ); + if (strings == NULL) { + return false; + } + + dec->strings = strings; + dec->strings[dec->string_count] = str; + dec->string_count++; + + return true; +} + +static bool user_cache_serdes_decoder_register( + php_user_cache_serdes_decoder *dec, + zval *container) +{ + zval *ids; + + if (dec->id_count == UINT32_MAX) { + return false; + } + + ids = user_cache_serdes_registry_reserve( + dec->ids, dec->inline_ids, dec->id_count, + &dec->id_capacity, sizeof(zval) + ); + if (ids == NULL) { + return false; + } + + dec->ids = ids; + ZVAL_COPY(&dec->ids[dec->id_count], container); + dec->id_count++; + + return true; +} + +static zend_class_entry *user_cache_serdes_decode_class( + php_user_cache_serdes_decoder *dec) +{ + zend_string *class_name; + zend_class_entry *ce; + + if (!user_cache_serdes_get_str(dec, &class_name)) { + return NULL; + } + + ce = zend_lookup_class(class_name); + zend_string_release(class_name); + + return ce; +} + +static bool user_cache_serdes_decode_object_properties( + php_user_cache_serdes_decoder *dec, + bool call_wakeup, + zval *dst) +{ + zend_class_entry *ce = Z_OBJCE_P(dst); + zend_string *prop_name; + zval *wakeup_zv, prop_val, retval; + uint32_t count, i, prop_idx_plus_one; + + if (!user_cache_serdes_get_u32(dec, &count)) { + return false; + } + + for (i = 0; i < count; i++) { + if (!user_cache_serdes_get_str(dec, &prop_name)) { + return false; + } + + if (!user_cache_serdes_get_u32(dec, &prop_idx_plus_one)) { + zend_string_release(prop_name); + + return false; + } + + ZVAL_UNDEF(&prop_val); + + if (!user_cache_serdes_decode_value(dec, &prop_val)) { + zval_ptr_dtor(&prop_val); + zend_string_release(prop_name); + + return false; + } + + if (prop_idx_plus_one != PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE) { + if (!php_user_cache_shared_graph_update_object_property_at( + dst, + prop_name, + prop_idx_plus_one - 1, + &prop_val + ) + ) { + zval_ptr_dtor(&prop_val); + zend_string_release(prop_name); + + return false; + } + } else if (!php_user_cache_shared_graph_update_object_property( + dst, + prop_name, + &prop_val + ) + ) { + zval_ptr_dtor(&prop_val); + + zend_string_release(prop_name); + + return false; + } + + zval_ptr_dtor(&prop_val); + + zend_string_release(prop_name); + } + + if (call_wakeup) { + wakeup_zv = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)); + if (wakeup_zv != NULL) { + zend_call_known_instance_method(Z_FUNC_P(wakeup_zv), Z_OBJ_P(dst), &retval, 0, NULL); + + zval_ptr_dtor(&retval); + + if (EG(exception)) { + return false; + } + } + } + + return true; +} + +static bool user_cache_serdes_decode_array( + php_user_cache_serdes_decoder *dec, + uint8_t tag, + zval *dst) +{ + zend_string *key = NULL; + zval elem; + uint64_t next_free, key_h = 0; + uint32_t count, i; + uint8_t key_kind; + bool result; + + if (!user_cache_serdes_get_u64(dec, &next_free) || + !user_cache_serdes_get_u32(dec, &count) + ) { + return false; + } + + /* Reject impossible counts before allocating. */ + if (count > dec->len - dec->pos) { + return false; + } + + array_init_size(dst, count); + + /* Initialize before registering the array: registration raises its refcount, + * but the first insertion requires a refcount of one. */ + if (tag == PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY) { + zend_hash_real_init_packed(Z_ARRVAL_P(dst)); + } else { + zend_hash_real_init_mixed(Z_ARRVAL_P(dst)); + } + + HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(dst)); + + if (!user_cache_serdes_decoder_register(dec, dst)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + for (i = 0; i < count; i++) { + key = NULL; + if (tag == PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY) { + if (!user_cache_serdes_get_u8(dec, &key_kind)) { + goto failure; + } + + if (key_kind == PHP_USER_CACHE_SERDES_KEY_STRING) { + if (!user_cache_serdes_get_str(dec, &key)) { + goto failure; + } + } else if (key_kind == PHP_USER_CACHE_SERDES_KEY_LONG) { + if (!user_cache_serdes_get_u64(dec, &key_h)) { + goto failure; + } + } else { + goto failure; + } + } + + ZVAL_UNDEF(&elem); + if (!user_cache_serdes_decode_value(dec, &elem)) { + zval_ptr_dtor(&elem); + if (key != NULL) { + zend_string_release(key); + } + + goto failure; + } + + if (key != NULL) { + result = zend_hash_add_new(Z_ARRVAL_P(dst), key, &elem) != NULL; + zend_string_release(key); + } else if (tag == PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY) { + result = zend_hash_index_add_new(Z_ARRVAL_P(dst), (zend_ulong) key_h, &elem) != NULL; + } else { + result = zend_hash_next_index_insert_new(Z_ARRVAL_P(dst), &elem) != NULL; + } + + if (!result) { + zval_ptr_dtor(&elem); + + goto failure; + } + } + + Z_ARRVAL_P(dst)->nNextFreeElement = (zend_long) next_free; + PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst)); + return true; + +failure: + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; +} + +static bool user_cache_serdes_decode_object( + php_user_cache_serdes_decoder *dec, + zval *dst) +{ + zend_class_entry *ce; + uint8_t call_wakeup; + + ce = user_cache_serdes_decode_class(dec); + if (ce == NULL || + (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 || + object_init_ex(dst, ce) != SUCCESS + ) { + return false; + } + + if (!user_cache_serdes_get_u8(dec, &call_wakeup) || + !user_cache_serdes_decoder_register(dec, dst) || + !user_cache_serdes_decode_object_properties(dec, call_wakeup != 0, dst) + ) { + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + return true; +} + +static bool user_cache_serdes_decode_serialized_object( + php_user_cache_serdes_decoder *dec, + zval *dst) +{ + zend_class_entry *ce; + zval state; + + ce = user_cache_serdes_decode_class(dec); + if (ce == NULL || + ce->__unserialize == NULL || + (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) != 0 || + object_init_ex(dst, ce) != SUCCESS + ) { + return false; + } + + if (!user_cache_serdes_decoder_register(dec, dst)) { + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + ZVAL_UNDEF(&state); + if (!user_cache_serdes_decode_value(dec, &state) || + Z_TYPE(state) != IS_ARRAY + ) { + zval_ptr_dtor(&state); + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + zend_call_known_instance_method_with_1_params(ce->__unserialize, Z_OBJ_P(dst), NULL, &state); + + zval_ptr_dtor(&state); + + if (EG(exception)) { + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + return true; +} + +static bool user_cache_serdes_decode_custom_object( + php_user_cache_serdes_decoder *dec, + zval *dst) +{ + const uint8_t *payload; + zend_class_entry *ce; + php_unserialize_data_t unser_data; + uint32_t payload_len; + bool result; + + ce = user_cache_serdes_decode_class(dec); + if (ce == NULL || + ce->unserialize == NULL || + (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) != 0 || + !user_cache_serdes_get_u32(dec, &payload_len) || + !user_cache_serdes_get_bytes(dec, payload_len, &payload) + ) { + return false; + } + + PHP_VAR_UNSERIALIZE_INIT(unser_data); + result = ce->unserialize(dst, ce, payload, payload_len, (zend_unserialize_data *) unser_data) == SUCCESS; + PHP_VAR_UNSERIALIZE_DESTROY(unser_data); + + if (!result || EG(exception)) { + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + if (!user_cache_serdes_decoder_register(dec, dst)) { + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + return true; +} + +static bool user_cache_serdes_decode_enum( + php_user_cache_serdes_decoder *dec, + zval *dst) +{ + zend_string *case_name; + zend_class_entry *ce; + zend_object *case_obj; + + ce = user_cache_serdes_decode_class(dec); + if (ce == NULL || + (ce->ce_flags & ZEND_ACC_ENUM) == 0 || + !user_cache_serdes_get_str(dec, &case_name) + ) { + return false; + } + + case_obj = zend_enum_get_case(ce, case_name); + + zend_string_release(case_name); + + if (case_obj == NULL) { + return false; + } + + ZVAL_OBJ_COPY(dst, case_obj); + + return true; +} + +static bool user_cache_serdes_decode_reference( + php_user_cache_serdes_decoder *dec, + zval *dst) +{ + zend_reference *ref; + zval val; + + ZVAL_NEW_EMPTY_REF(dst); + ref = Z_REF_P(dst); + ZVAL_NULL(&ref->val); + + if (!user_cache_serdes_decoder_register(dec, dst)) { + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + ZVAL_UNDEF(&val); + + if (!user_cache_serdes_decode_value(dec, &val)) { + zval_ptr_dtor(&val); + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; + } + + zval_ptr_dtor(&ref->val); + + ZVAL_COPY_VALUE(&ref->val, &val); + + return true; +} + +static bool user_cache_serdes_decode_value( + php_user_cache_serdes_decoder *dec, + zval *dst) +{ + zend_long lval; + zend_string *str; + uint32_t backref_id; + uint8_t tag; + double dval; + + ZVAL_UNDEF(dst); + + if (php_user_cache_stack_overflowed()) { + return false; + } + + if (!user_cache_serdes_get_u8(dec, &tag)) { + return false; + } + + switch (tag) { + case PHP_USER_CACHE_SERDES_TAG_NULL: + ZVAL_NULL(dst); + + return true; + case PHP_USER_CACHE_SERDES_TAG_FALSE: + ZVAL_FALSE(dst); + + return true; + case PHP_USER_CACHE_SERDES_TAG_TRUE: + ZVAL_TRUE(dst); + + return true; + case PHP_USER_CACHE_SERDES_TAG_LONG: + if (!user_cache_serdes_get_long(dec, &lval)) { + return false; + } + + ZVAL_LONG(dst, lval); + + return true; + case PHP_USER_CACHE_SERDES_TAG_DOUBLE: + if (!user_cache_serdes_get_double(dec, &dval)) { + return false; + } + + ZVAL_DOUBLE(dst, dval); + + return true; + case PHP_USER_CACHE_SERDES_TAG_STRING: + if (!user_cache_serdes_get_str(dec, &str)) { + return false; + } + + ZVAL_STR(dst, str); + + return true; + case PHP_USER_CACHE_SERDES_TAG_EMPTY_ARRAY: + ZVAL_EMPTY_ARRAY(dst); + + return true; + case PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY: + case PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY: + return user_cache_serdes_decode_array(dec, tag, dst); + case PHP_USER_CACHE_SERDES_TAG_OBJECT: + return user_cache_serdes_decode_object(dec, dst); + case PHP_USER_CACHE_SERDES_TAG_SERIALIZED_OBJECT: + return user_cache_serdes_decode_serialized_object(dec, dst); + case PHP_USER_CACHE_SERDES_TAG_CUSTOM_OBJECT: + return user_cache_serdes_decode_custom_object(dec, dst); + case PHP_USER_CACHE_SERDES_TAG_ENUM: + return user_cache_serdes_decode_enum(dec, dst); + case PHP_USER_CACHE_SERDES_TAG_REFERENCE: + return user_cache_serdes_decode_reference(dec, dst); + case PHP_USER_CACHE_SERDES_TAG_BACKREF: + if (!user_cache_serdes_get_u32(dec, &backref_id) || + backref_id >= dec->id_count + ) { + return false; + } + + ZVAL_COPY(dst, &dec->ids[backref_id]); + + return true; + default: + return false; + } +} + +uint32_t php_user_cache_serdes_declared_property_index_plus_one( + zend_class_entry *ce, + zend_string *name) +{ + zend_property_info *prop_info; + uint32_t prop_idx; + + if (ZSTR_LEN(name) == 0 || + ZSTR_VAL(name)[0] == '\0' || + ce->type != ZEND_USER_CLASS || + ce->properties_info_table == NULL + ) { + return PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE; + } + + prop_info = zend_get_property_info(ce, name, true); + if (prop_info == NULL || + prop_info == ZEND_WRONG_PROPERTY_INFO || + (prop_info->flags & (ZEND_ACC_STATIC|ZEND_ACC_VIRTUAL)) != 0 || + prop_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET + ) { + return PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE; + } + + prop_idx = OBJ_PROP_TO_NUM(prop_info->offset); + if (prop_idx >= ce->default_properties_count || prop_idx == UINT32_MAX) { + return PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE; + } + + return prop_idx + 1; +} + +bool php_user_cache_serdes_get_sleep_state( + zval *obj_zv, + zval *state, + const char **failure_msg) +{ + zend_object *obj = Z_OBJ_P(obj_zv); + zend_class_entry *ce = obj->ce; + zval *sleep_zv, retval; + bool result; + + ZVAL_UNDEF(state); + + if (failure_msg != NULL) { + *failure_msg = NULL; + } + + sleep_zv = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)); + if (sleep_zv == NULL) { + return false; + } + + GC_ADDREF(obj); + zend_call_known_instance_method(Z_FUNC_P(sleep_zv), obj, &retval, 0, NULL); + OBJ_RELEASE(obj); + + if (Z_ISUNDEF(retval) || EG(exception)) { + zval_ptr_dtor(&retval); + + return false; + } + + if (Z_TYPE(retval) != IS_ARRAY) { + zval_ptr_dtor(&retval); + + php_error_docref(NULL, E_WARNING, + "%s::__sleep() should return an array only containing the names of instance-variables to serialize", + ZSTR_VAL(ce->name) + ); + + if (failure_msg != NULL) { + *failure_msg = "__sleep() did not return an array of member names; the object cannot be stored in the user cache"; + } + + return false; + } + + /* Failure may leave a partially initialized state array. */ + result = user_cache_serdes_get_sleep_props(obj_zv, Z_ARRVAL(retval), state); + zval_ptr_dtor(&retval); + + if (!result) { + zval_ptr_dtor(state); + + ZVAL_UNDEF(state); + } + + return result; +} + +bool php_user_cache_serdes_call_magic_serialize(zend_object *obj, zval *state) +{ + zend_call_known_instance_method_with_0_params(obj->ce->__serialize, obj, state); + if (EG(exception)) { + zval_ptr_dtor(state); + + ZVAL_UNDEF(state); + + return false; + } + + if (Z_TYPE_P(state) != IS_ARRAY) { + zval_ptr_dtor(state); + + ZVAL_UNDEF(state); + + zend_type_error("%s::__serialize() must return an array", ZSTR_VAL(obj->ce->name)); + + return false; + } + + return true; +} + +bool php_user_cache_serdes_encode(zval *value, smart_str *buf, const char **failure_msg) +{ + php_user_cache_serdes_encoder enc; + bool result; + + if (failure_msg != NULL) { + *failure_msg = NULL; + } + + enc.buf = *buf; + enc.failure_message = NULL; + enc.string_count = 0; + + zend_hash_init(&enc.seen, 8, NULL, NULL, 0); + zend_hash_init(&enc.strings, 32, NULL, NULL, 0); + + result = user_cache_serdes_encode_value(&enc, value); + + zend_hash_destroy(&enc.strings); + zend_hash_destroy(&enc.seen); + + *buf = enc.buf; + + if (!result && failure_msg != NULL) { + *failure_msg = enc.failure_message; + } + + return result; +} + +bool php_user_cache_serdes_decode(const uint8_t *data, size_t len, zval *dst) +{ + php_user_cache_serdes_decoder dec; + uint32_t i; + bool result; + + ZVAL_UNDEF(dst); + + if (data == NULL || len == 0) { + return false; + } + + dec.data = data; + dec.len = len; + dec.pos = 0; + dec.ids = dec.inline_ids; + dec.id_count = 0; + dec.id_capacity = PHP_USER_CACHE_SERDES_INLINE_IDS; + dec.strings = dec.inline_strings; + dec.string_count = 0; + dec.string_capacity = PHP_USER_CACHE_SERDES_INLINE_STRINGS; + + result = user_cache_serdes_decode_value(&dec, dst); + + if (result && dec.pos != dec.len) { + result = false; + } + + for (i = 0; i < dec.id_count; i++) { + zval_ptr_dtor(&dec.ids[i]); + } + for (i = 0; i < dec.string_count; i++) { + zend_string_release(dec.strings[i]); + } + + if (dec.ids != dec.inline_ids) { + efree(dec.ids); + } + if (dec.strings != dec.inline_strings) { + efree(dec.strings); + } + + if (!result && !Z_ISUNDEF_P(dst)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + } + + return result; +} diff --git a/ext/user_cache/user_cache_shared_graph.c b/ext/user_cache/user_cache_shared_graph.c new file mode 100644 index 000000000000..4fdae3f303e7 --- /dev/null +++ b/ext/user_cache/user_cache_shared_graph.c @@ -0,0 +1,6281 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#include "user_cache_internal.h" + +#include "Zend/zend_interfaces.h" +#include "Zend/zend_closures.h" +#include "Zend/zend_enum.h" +#include "Zend/zend_operators.h" + +#if (defined(__GNUC__) && ZEND_GCC_VERSION >= 4003) || __has_attribute(hot) +# define PHP_USER_CACHE_DECODE_HOT __attribute__((hot)) +#else +# define PHP_USER_CACHE_DECODE_HOT +#endif + +/* Request-local maps preserve identity for repeated graph nodes. */ +#define PHP_USER_CACHE_DEFINE_DECODE_MAP(name, ctype, dtor, release_on_fail) \ + static zend_always_inline void user_cache_decode_##name##_map_teardown(void) \ + { \ + if (UC_G(decode_##name##_map) != NULL) { \ + zend_hash_destroy(UC_G(decode_##name##_map)); \ + efree(UC_G(decode_##name##_map)); \ + UC_G(decode_##name##_map) = NULL; \ + } \ + } \ + \ + static zend_always_inline bool user_cache_decode_##name##_map_insert(uint32_t offset, ctype *entry) \ + { \ + if (UC_G(decode_##name##_map) == NULL) { \ + UC_G(decode_##name##_map) = emalloc(sizeof(HashTable)); \ + zend_hash_init(UC_G(decode_##name##_map), 8, NULL, dtor, 0); \ + } \ + \ + GC_ADDREF(entry); \ + if (zend_hash_index_add_ptr(UC_G(decode_##name##_map), offset, entry) == NULL) { \ + release_on_fail; \ + \ + return false; \ + } \ + \ + return true; \ + } \ + \ + static zend_always_inline ctype *user_cache_decode_##name##_map_find(uint32_t offset) \ + { \ + if (UC_G(decode_##name##_map) == NULL) { \ + return NULL; \ + } \ + \ + return zend_hash_index_find_ptr(UC_G(decode_##name##_map), offset); \ + } + +/* CALC and COPY must use the same route precedence. */ +typedef enum { + PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE = 0, + PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT, + PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE, + PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS, + PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE, + PHP_USER_CACHE_OBJECT_ROUTE_SLEEP, + PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP, + PHP_USER_CACHE_OBJECT_ROUTE_SERDES, + PHP_USER_CACHE_OBJECT_ROUTE_PLAIN +} php_user_cache_object_route; + +typedef enum { + PHP_USER_CACHE_COPY_OBJECT_REF_NEW = 0, + PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED, + PHP_USER_CACHE_COPY_OBJECT_REF_ERROR +} php_user_cache_copy_object_ref_result; + +typedef bool (*php_user_cache_shared_graph_state_getter_t)( + const zval *value, + HashTable *state_memo, + zval **state_ptr, + zval *owned_state +); + +static void user_cache_decode_array_dtor(zval *zv); +static void user_cache_decode_shape_prototype_dtor(zval *zv); +static void user_cache_shared_graph_refs_check_fork(void); +static bool user_cache_shared_graph_calc_value( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value +); +static bool user_cache_shared_graph_copy_value( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + php_user_cache_shared_graph_value *dst +); +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_value( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +); +#if ZEND_DEBUG +static bool user_cache_shared_graph_rebase_verbatim_array( + php_user_cache_shared_graph_rebase_context *ctx, + zend_array *arr +); +#endif + +static zend_always_inline bool user_cache_decode_range_ok( + size_t buf_len, + uint32_t offset, + size_t need +) +{ + return offset <= buf_len && need <= buf_len - offset; +} + +/* Check without overflowing size_t. */ +static zend_always_inline bool user_cache_decode_array_range_ok( + size_t buf_len, + uint32_t offset, + uint32_t count, + size_t elem_size +) +{ + if (offset > buf_len) { + return false; + } + + return count <= (buf_len - offset) / elem_size; +} + +static zend_always_inline bool user_cache_decode_fail_zval(zval *dst) +{ + zval_ptr_dtor(dst); + + ZVAL_UNDEF(dst); + + return false; +} + +static zend_always_inline zend_string *user_cache_decode_string_at( + const uint8_t *buf, + size_t buf_len, + uint32_t offset +) +{ + zend_string *string; + + if (!user_cache_decode_range_ok(buf_len, offset, _ZSTR_HEADER_SIZE)) { + return NULL; + } + + string = (zend_string *) (void *) (buf + offset); + if (ZSTR_LEN(string) > buf_len || + !user_cache_decode_range_ok( + buf_len, + offset, + _ZSTR_STRUCT_SIZE(ZSTR_LEN(string)) + ) + ) { + return NULL; + } + + return string; +} + +static zend_always_inline size_t user_cache_decode_node_header_size(uint8_t type) +{ + switch (type) { + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + return sizeof(php_user_cache_shared_graph_array); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY: + return sizeof(php_user_cache_shared_graph_shaped_array); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT: + return sizeof(php_user_cache_shared_graph_object); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT: + return sizeof(php_user_cache_shared_graph_safe_direct_object); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT: + return sizeof(php_user_cache_shared_graph_shaped_state_object); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT: + return sizeof(php_user_cache_shared_graph_serdes_object); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM: + return sizeof(php_user_cache_shared_graph_enum); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE: + return sizeof(php_user_cache_shared_graph_reference); + default: + return 0; + } +} + +/* Address-keyed entries are valid while the decoded payload remains pinned. */ +static zend_always_inline void *user_cache_decode_resolve_cache_find(const void *addr) +{ + uint32_t i; + + for (i = 0; i < PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS; i++) { + if (UC_G(decode_resolve_direct_keys)[i] == addr) { + return UC_G(decode_resolve_direct_values)[i]; + } + } + + if (UC_G(decode_resolve_cache) == NULL) { + return NULL; + } + + return zend_hash_index_find_ptr( + UC_G(decode_resolve_cache), + (zend_ulong) (uintptr_t) addr + ); +} + +static zend_always_inline void user_cache_decode_resolve_cache_store(const void *addr, void *value) +{ + uint32_t slot; + + slot = UC_G(decode_resolve_direct_next)++ % PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS; + UC_G(decode_resolve_direct_keys)[slot] = addr; + UC_G(decode_resolve_direct_values)[slot] = value; + + if (UC_G(decode_resolve_cache) == NULL) { + UC_G(decode_resolve_cache) = emalloc(sizeof(HashTable)); + zend_hash_init(UC_G(decode_resolve_cache), 8, NULL, NULL, 0); + } + + zend_hash_index_add_ptr( + UC_G(decode_resolve_cache), + (zend_ulong) (uintptr_t) addr, + value + ); +} + +PHP_USER_CACHE_DEFINE_DECODE_MAP( + identity, + zend_object, + php_user_cache_object_table_dtor, + OBJ_RELEASE(entry) +) + +PHP_USER_CACHE_DEFINE_DECODE_MAP( + reference, + zend_reference, + php_user_cache_reference_table_dtor, + if (GC_DELREF(entry) == 0) efree_size(entry, sizeof(zend_reference)) +) + +PHP_USER_CACHE_DEFINE_DECODE_MAP( + array, + zend_array, + user_cache_decode_array_dtor, + if (GC_DELREF(entry) == 0) zend_array_destroy(entry) +) + +static zend_always_inline HashTable *user_cache_decode_shape_prototype_cache(void) +{ + if (UC_G(decode_shape_prototype_cache) == NULL) { + UC_G(decode_shape_prototype_cache) = emalloc(sizeof(HashTable)); + zend_hash_init( + UC_G(decode_shape_prototype_cache), + 8, + NULL, + user_cache_decode_shape_prototype_dtor, + 0 + ); + } + + return UC_G(decode_shape_prototype_cache); +} + +static zend_always_inline size_t user_cache_shared_graph_alignment_padding(const void *buf) +{ + uintptr_t raw_addr, aligned_addr; + + raw_addr = (uintptr_t) buf; + aligned_addr = (uintptr_t) ZEND_MM_ALIGNED_SIZE(raw_addr); + + return (size_t) (aligned_addr - raw_addr); +} + +static zend_always_inline bool user_cache_shared_graph_header_is_valid( + const php_user_cache_shared_graph_header *header +) +{ + return header->magic == PHP_USER_CACHE_SHARED_GRAPH_MAGIC && + header->version == PHP_USER_CACHE_SHARED_GRAPH_VERSION + ; +} + +static zend_always_inline const uint8_t *user_cache_shared_graph_locate( + const uint8_t *buf, + size_t buf_len, + size_t *graph_len) +{ + const php_user_cache_shared_graph_header *header; + size_t padding; + + padding = user_cache_shared_graph_alignment_padding(buf); + if (padding > buf_len || buf_len - padding < sizeof(php_user_cache_shared_graph_header)) { + return NULL; + } + + buf += padding; + buf_len -= padding; + header = (const php_user_cache_shared_graph_header *) buf; + if (!user_cache_shared_graph_header_is_valid(header)) { + return NULL; + } + + if (graph_len != NULL) { + *graph_len = buf_len; + } + + return buf; +} + +static zend_always_inline void user_cache_shared_graph_calc_init(php_user_cache_shared_graph_calc_context *ctx) +{ + ctx->size = 0; + ctx->reserve_failed = false; + ctx->shared_verdicts = NULL; + + zend_hash_init(&ctx->seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&ctx->seen_objects, 8, NULL, NULL, 0); + zend_hash_init(&ctx->seen_references, 8, NULL, NULL, 0); + zend_hash_init(&ctx->string_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->array_shape_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->state_schema_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->direct_array_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->direct_verdicts, 8, NULL, NULL, 0); +} + +static zend_always_inline void user_cache_shared_graph_calc_destroy(php_user_cache_shared_graph_calc_context *ctx) +{ + zend_hash_destroy(&ctx->direct_verdicts); + zend_hash_destroy(&ctx->direct_array_dedup); + zend_hash_destroy(&ctx->state_schema_dedup); + zend_hash_destroy(&ctx->array_shape_dedup); + zend_hash_destroy(&ctx->string_dedup); + zend_hash_destroy(&ctx->seen_references); + zend_hash_destroy(&ctx->seen_objects); + zend_hash_destroy(&ctx->seen_arrays); +} + +static zend_always_inline bool user_cache_shared_graph_calc_reserve( + php_user_cache_shared_graph_calc_context *ctx, size_t amount) +{ + size_t aligned_amount; + + aligned_amount = PHP_USER_CACHE_ALIGNED_SIZE(amount); + if (ctx->size > SIZE_MAX - aligned_amount) { + ctx->reserve_failed = true; + + return false; + } + + ctx->size += aligned_amount; + + if (UNEXPECTED(ctx->size > UINT32_MAX)) { + ctx->reserve_failed = true; + + return false; + } + + return true; +} + +static zend_always_inline bool user_cache_shared_graph_calc_reserve_string( + php_user_cache_shared_graph_calc_context *ctx, + const zend_string *string) +{ + if (zend_hash_exists(&ctx->string_dedup, (zend_string *) string)) { + return true; + } + + if (!user_cache_shared_graph_calc_reserve(ctx, _ZSTR_STRUCT_SIZE(ZSTR_LEN(string)))) { + return false; + } + + return zend_hash_add_empty_element(&ctx->string_dedup, (zend_string *) string) != NULL; +} + +static zend_always_inline void user_cache_shared_graph_key_append_u32(smart_str *key, uint32_t value) +{ + smart_str_appendl(key, (const char *) &value, sizeof(value)); +} + +static zend_always_inline bool user_cache_shared_graph_array_has_shape(const HashTable *arr) +{ + zend_string *key; + + if (arr->nNumOfElements == 0 || + HT_IS_PACKED(arr) || + arr->nNumOfElements > PHP_USER_CACHE_SHARED_GRAPH_ARRAY_SHAPE_MAX_KEYS || + arr->nNextFreeElement < 0 || + arr->nNextFreeElement > UINT32_MAX || + !HT_IS_WITHOUT_HOLES(arr) + ) { + return false; + } + + ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) { + if (key == NULL) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static zend_always_inline bool user_cache_shared_graph_can_restore_direct(zend_class_entry *ce) +{ + if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) { + return false; + } + + if (ce->type != ZEND_USER_CLASS && ce->create_object != NULL) { + return false; + } + + return true; +} + +static zend_always_inline bool user_cache_shared_graph_can_use_verbatim_arrays(void) +{ +#ifdef ZEND_WIN32 + /* Verbatim arrays contain absolute pointers and cannot cross mapping bases. */ + return false; +#else + return !php_user_cache_active_context()->boundary_shared; +#endif +} + +static zend_always_inline bool user_cache_shared_graph_is_unmangled_property_name(zend_string *prop_name) +{ + return ZSTR_LEN(prop_name) != 0 && ZSTR_VAL(prop_name)[0] != '\0'; +} + +static zend_always_inline bool user_cache_shared_graph_safe_direct_property_shadows_state( + zend_string *prop_name, zval *state) +{ + return user_cache_shared_graph_is_unmangled_property_name(prop_name) && + Z_TYPE_P(state) == IS_ARRAY && + zend_hash_exists(Z_ARRVAL_P(state), prop_name) + ; +} + +static inline bool user_cache_class_overrides_safe_direct_magic_serialize(zend_class_entry *ce) +{ + zend_class_entry *base_ce = NULL; + const php_user_cache_safe_direct_handlers *handlers; + + if (ce->type != ZEND_USER_CLASS || + ce->__serialize == NULL || + ce->__unserialize == NULL || + (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 + ) { + return false; + } + + handlers = php_user_cache_safe_direct_find_handlers(ce, &base_ce); + + return php_user_cache_class_overrides_safe_direct_magic_serialize_ex(ce, handlers, base_ce); +} + +static zend_always_inline bool user_cache_shared_graph_can_use_safe_direct(zend_class_entry *ce) +{ + zend_class_entry *base_ce = php_user_cache_safe_direct_find_base(ce); + + return base_ce != NULL && + !user_cache_class_overrides_safe_direct_magic_serialize(ce) && + php_user_cache_safe_direct_state_serialize_func(ce) != NULL && + php_user_cache_safe_direct_state_unserialize_func(ce) != NULL + ; +} + +static zend_always_inline bool user_cache_shared_graph_can_use_sleep_object(zend_class_entry *ce) +{ + if (ce->type != ZEND_USER_CLASS || + (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 || + (ce->serialize != NULL && ce->unserialize != NULL) + ) { + return false; + } + + return zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL; +} + +static zend_always_inline bool user_cache_shared_graph_can_use_wakeup_object(zend_class_entry *ce) +{ + if (ce->type != ZEND_USER_CLASS || + (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 || + ce->__serialize != NULL || + ce->__unserialize != NULL || + (ce->serialize != NULL && ce->unserialize != NULL) || + zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL + ) { + return false; + } + + return zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL; +} + +static zend_always_inline bool user_cache_shared_graph_pointer_in_range( + const void *ptr, + const uint8_t *base, + size_t len) +{ + uintptr_t addr, start; + + if (ptr == NULL || base == NULL || len == 0) { + return false; + } + + addr = (uintptr_t) ptr; + start = (uintptr_t) base; + + return addr >= start && addr - start < len; +} + +#if ZEND_DEBUG +static zend_always_inline void *user_cache_shared_graph_rebase_pointer( + void *ptr, + const uint8_t *old_base, + size_t len, + ptrdiff_t delta) +{ + if (!user_cache_shared_graph_pointer_in_range(ptr, old_base, len)) { + return ptr; + } + + return (void *) ((char *) ptr - delta); +} +#endif + +static zend_string *user_cache_shared_graph_array_shape_key(const HashTable *arr) +{ + zend_string *key; + smart_str shape_key = {0}; + uint32_t count, key_len; + + ZEND_ASSERT(user_cache_shared_graph_array_has_shape(arr)); + + count = (uint32_t) arr->nNumOfElements; + smart_str_appendl(&shape_key, "ucshape", sizeof("ucshape") - 1); + user_cache_shared_graph_key_append_u32(&shape_key, count); + + ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) { + if (ZSTR_LEN(key) > UINT32_MAX) { + smart_str_free(&shape_key); + + return NULL; + } + + key_len = (uint32_t) ZSTR_LEN(key); + + user_cache_shared_graph_key_append_u32(&shape_key, key_len); + + smart_str_appendl(&shape_key, ZSTR_VAL(key), ZSTR_LEN(key)); + } ZEND_HASH_FOREACH_END(); + + return smart_str_extract(&shape_key); +} + +static bool user_cache_shared_graph_calc_array_shape( + php_user_cache_shared_graph_calc_context *ctx, + const HashTable *arr, + zend_string *shape_key +) +{ + zend_string *key; + bool result = true, owns_shape_key = false; + + if (shape_key == NULL) { + shape_key = user_cache_shared_graph_array_shape_key(arr); + if (shape_key == NULL) { + return false; + } + + owns_shape_key = true; + } + + if (zend_hash_exists(&ctx->array_shape_dedup, shape_key)) { + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return true; + } + + if (!user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_array_shape) + ) || + !user_cache_shared_graph_calc_reserve( + ctx, + (size_t) arr->nNumOfElements * sizeof(php_user_cache_shared_graph_array_shape_element) + ) + ) { + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return false; + } + + ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) { + if (!user_cache_shared_graph_calc_reserve_string(ctx, key)) { + result = false; + + break; + } + } ZEND_HASH_FOREACH_END(); + + if (result) { + result = zend_hash_add_empty_element(&ctx->array_shape_dedup, shape_key) != NULL; + } + + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return result; +} + +static zend_string *user_cache_shared_graph_state_schema_key( + zend_class_entry *ce, + const HashTable *arr, + zend_string **shape_key_out +) +{ + zend_string *shape_key; + smart_str schema_key = {0}; + uint32_t class_name_len, shape_key_len; + + shape_key = user_cache_shared_graph_array_shape_key(arr); + if (shape_key == NULL || ZSTR_LEN(ce->name) > UINT32_MAX || ZSTR_LEN(shape_key) > UINT32_MAX) { + if (shape_key != NULL) { + zend_string_release(shape_key); + } + + return NULL; + } + + class_name_len = (uint32_t) ZSTR_LEN(ce->name); + shape_key_len = (uint32_t) ZSTR_LEN(shape_key); + + smart_str_appendl(&schema_key, "ucstate", sizeof("ucstate") - 1); + user_cache_shared_graph_key_append_u32(&schema_key, class_name_len); + smart_str_appendl(&schema_key, ZSTR_VAL(ce->name), ZSTR_LEN(ce->name)); + user_cache_shared_graph_key_append_u32(&schema_key, shape_key_len); + smart_str_appendl(&schema_key, ZSTR_VAL(shape_key), ZSTR_LEN(shape_key)); + + *shape_key_out = shape_key; + + return smart_str_extract(&schema_key); +} + +static bool user_cache_shared_graph_calc_state_schema( + php_user_cache_shared_graph_calc_context *ctx, + zend_class_entry *ce, + const HashTable *arr +) +{ + zend_string *schema_key, *shape_key = NULL; + bool result; + + schema_key = user_cache_shared_graph_state_schema_key(ce, arr, &shape_key); + if (schema_key == NULL) { + return false; + } + + if (zend_hash_exists(&ctx->state_schema_dedup, schema_key)) { + zend_string_release(shape_key); + zend_string_release(schema_key); + + return true; + } + + result = user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_state_schema) + ) && + user_cache_shared_graph_calc_reserve_string(ctx, ce->name) && + user_cache_shared_graph_calc_array_shape(ctx, arr, shape_key) + ; + + zend_string_release(shape_key); + + if (result) { + result = zend_hash_add_empty_element(&ctx->state_schema_dedup, schema_key) != NULL; + } + + zend_string_release(schema_key); + + return result; +} + +static bool user_cache_shared_graph_state_value_fits_schema( + zval *value, + const HashTable *state_array, + HashTable *seen_arrs +) +{ + zend_ulong arr_key; + zval *elem; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + ZVAL_DEREF(value); + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + case IS_STRING: + return true; + case IS_ARRAY: + if (Z_ARRVAL_P(value) == state_array) { + return false; + } + + arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value); + if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARRVAL_P(value))) { + return true; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) { + if (!user_cache_shared_graph_state_value_fits_schema( + elem, + state_array, + seen_arrs + ) + ) { + zend_hash_index_del(seen_arrs, arr_key); + + return false; + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_index_del(seen_arrs, arr_key); + + return true; + default: + return false; + } +} + +static bool user_cache_shared_graph_state_array_fits_schema(const HashTable *arr) +{ + zval *elem; + HashTable seen_arrs; + bool result = true; + + if (!user_cache_shared_graph_array_has_shape(arr)) { + return false; + } + + zend_hash_init(&seen_arrs, 8, NULL, NULL, 0); + + ZEND_HASH_FOREACH_VAL((HashTable *) arr, elem) { + if (!user_cache_shared_graph_state_value_fits_schema( + elem, + arr, + &seen_arrs + ) + ) { + result = false; + + break; + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(&seen_arrs); + + return result; +} + +static bool user_cache_shared_graph_calc_shaped_state_object( + php_user_cache_shared_graph_calc_context *ctx, + zend_class_entry *ce, + const HashTable *state_array +) +{ + zval *elem; + + if (!user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_shaped_state_object) + ) || + !user_cache_shared_graph_calc_reserve( + ctx, + (size_t) state_array->nNumOfElements * sizeof(php_user_cache_shared_graph_value) + ) || + !user_cache_shared_graph_calc_state_schema(ctx, ce, state_array) + ) { + return false; + } + + ZEND_HASH_FOREACH_VAL((HashTable *) state_array, elem) { + if (!user_cache_shared_graph_calc_value(ctx, elem)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static php_user_cache_object_route user_cache_shared_graph_classify_object_route_uncached(zend_class_entry *ce) +{ + if (user_cache_shared_graph_can_use_safe_direct(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT; + } + + if (php_user_cache_class_uses_magic_serialize(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE; + } + + if (php_user_cache_class_uses_serialize_props(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS; + } + + if (php_user_cache_class_uses_magic_unserialize(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE; + } + + if (user_cache_shared_graph_can_use_sleep_object(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_SLEEP; + } + + if (user_cache_shared_graph_can_use_wakeup_object(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP; + } + + if (php_user_cache_class_uses_serdes(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_SERDES; + } + + if (!user_cache_shared_graph_can_restore_direct(ce)) { + return PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE; + } + + return PHP_USER_CACHE_OBJECT_ROUTE_PLAIN; +} + +/* The route is immutable per class and cached for the request. */ +static php_user_cache_object_route user_cache_shared_graph_classify_object_route(zend_class_entry *ce) +{ + zval *cached, route_zv; + php_user_cache_object_route route; + + if (UC_G(object_route_memo) != NULL) { + cached = zend_hash_index_find(UC_G(object_route_memo), (zend_ulong) (uintptr_t) ce); + if (cached != NULL) { + return (php_user_cache_object_route) Z_LVAL_P(cached); + } + } else { + UC_G(object_route_memo) = emalloc(sizeof(HashTable)); + zend_hash_init(UC_G(object_route_memo), 8, NULL, NULL, 0); + } + + route = user_cache_shared_graph_classify_object_route_uncached(ce); + + ZVAL_LONG(&route_zv, (zend_long) route); + zend_hash_index_add(UC_G(object_route_memo), (zend_ulong) (uintptr_t) ce, &route_zv); + + return route; +} + +static void user_cache_shared_graph_object_route_memo_release(void) +{ + if (UC_G(object_route_memo) != NULL) { + zend_hash_destroy(UC_G(object_route_memo)); + efree(UC_G(object_route_memo)); + + UC_G(object_route_memo) = NULL; + } +} + +static bool user_cache_shared_graph_can_copy_verbatim_value(HashTable *seen_arrs, HashTable *direct_verdicts, const zval *value) +{ + const zval *packed_value; + const HashTable *arr; + const Bucket *bucket; + zend_ulong arr_key; + zval *cached, verdict; + uint32_t i; + bool result = true; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + case IS_STRING: + return true; + case IS_ARRAY: + arr = Z_ARRVAL_P(value); + if (arr->nNumOfElements == 0) { + return true; + } + + arr_key = (zend_ulong) (uintptr_t) arr; + + if (GC_REFCOUNT(arr) > 1) { + cached = zend_hash_index_find(direct_verdicts, arr_key); + if (cached != NULL) { + return Z_TYPE_P(cached) == IS_TRUE; + } + } + + if (!php_user_cache_seen_test_and_add(seen_arrs, arr)) { + return false; + } + + if (HT_IS_PACKED(arr)) { + for (i = 0; i < arr->nNumUsed; i++) { + packed_value = &arr->arPacked[i]; + if (!user_cache_shared_graph_can_copy_verbatim_value( + seen_arrs, + direct_verdicts, + packed_value + ) + ) { + result = false; + + break; + } + } + } else { + bucket = arr->arData; + + for (i = 0; i < arr->nNumUsed; i++) { + if (Z_TYPE(bucket[i].val) != IS_UNDEF && + !user_cache_shared_graph_can_copy_verbatim_value( + seen_arrs, + direct_verdicts, + &bucket[i].val + ) + ) { + result = false; + + break; + } + } + } + + zend_hash_index_del(seen_arrs, arr_key); + + if (GC_REFCOUNT(arr) > 1) { + ZVAL_BOOL(&verdict, result); + zend_hash_index_add(direct_verdicts, arr_key, &verdict); + } + + return result; + default: + return false; + } +} + +static bool user_cache_shared_graph_calc_verbatim_value( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value) +{ + const zval *packed_value; + const HashTable *arr; + const Bucket *bucket; + zend_ulong arr_key; + zval *cached, seen_marker; + uint32_t i; + size_t data_size; + bool result = true; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + return true; + case IS_STRING: + return user_cache_shared_graph_calc_reserve_string(ctx, Z_STR_P(value)); + case IS_ARRAY: + arr = Z_ARRVAL_P(value); + if (arr->nNumOfElements == 0) { + return true; + } + + arr_key = (zend_ulong) (uintptr_t) arr; + if (GC_REFCOUNT(arr) > 1) { + cached = zend_hash_index_find(&ctx->direct_array_dedup, arr_key); + if (cached != NULL) { + return true; + } + } + + if (!php_user_cache_seen_test_and_add(&ctx->seen_arrays, arr)) { + return false; + } + + data_size = HT_IS_PACKED(arr) ? HT_PACKED_USED_SIZE(arr) : HT_USED_SIZE(arr); + if (!user_cache_shared_graph_calc_reserve(ctx, sizeof(zend_array)) || + !user_cache_shared_graph_calc_reserve(ctx, data_size) + ) { + result = false; + + goto done; + } + + if (HT_IS_PACKED(arr)) { + for (i = 0; i < arr->nNumUsed; i++) { + packed_value = &arr->arPacked[i]; + if (!user_cache_shared_graph_calc_verbatim_value(ctx, packed_value)) { + result = false; + + break; + } + } + } else { + bucket = arr->arData; + for (i = 0; i < arr->nNumUsed; i++) { + if (bucket[i].key != NULL && + !user_cache_shared_graph_calc_reserve_string(ctx, bucket[i].key) + ) { + result = false; + + break; + } + + if (Z_TYPE(bucket[i].val) != IS_UNDEF && + !user_cache_shared_graph_calc_verbatim_value(ctx, &bucket[i].val) + ) { + result = false; + + break; + } + } + } + +done: + zend_hash_index_del(&ctx->seen_arrays, arr_key); + + if (result && GC_REFCOUNT(arr) > 1) { + ZVAL_TRUE(&seen_marker); + zend_hash_index_add(&ctx->direct_array_dedup, arr_key, &seen_marker); + } + + return result; + default: + return false; + } +} + +static bool user_cache_shared_graph_produce_safe_direct_state(const zval *value, zval *state) +{ + php_user_cache_safe_direct_state_serialize_func_t serialize_func; + + serialize_func = php_user_cache_safe_direct_state_serialize_func(Z_OBJCE_P(value)); + if (serialize_func == NULL || + !serialize_func(state, value) || + Z_TYPE_P(state) != IS_ARRAY + ) { + if (!Z_ISUNDEF_P(state)) { + zval_ptr_dtor(state); + ZVAL_UNDEF(state); + } + + return false; + } + + return true; +} + +static bool user_cache_shared_graph_extract_serialize_snapshot(const zval *value, zval *state) +{ + return php_user_cache_serdes_call_magic_serialize(Z_OBJ_P(value), state); +} + +static bool user_cache_shared_graph_extract_property_snapshot(const zval *value, zval *state) +{ + zend_ulong num_key; + zend_string *key; + zval *prop, elem; + HashTable *props; + bool result = true; + + props = zend_get_properties_for((zval *) value, ZEND_PROP_PURPOSE_SERIALIZE); + if (props == NULL) { + array_init(state); + + return true; + } + + array_init_size(state, zend_hash_num_elements(props)); + + ZEND_HASH_FOREACH_KEY_VAL(props, num_key, key, prop) { + if (Z_TYPE_P(prop) == IS_INDIRECT) { + prop = Z_INDIRECT_P(prop); + if (Z_TYPE_P(prop) == IS_UNDEF) { + continue; + } + } + + if (Z_ISREF_P(prop) && Z_REFCOUNT_P(prop) == 1) { + prop = Z_REFVAL_P(prop); + } + + ZVAL_COPY(&elem, prop); + if (key != NULL) { + result = zend_hash_add_new(Z_ARRVAL_P(state), key, &elem) != NULL; + } else { + result = zend_hash_index_add_new(Z_ARRVAL_P(state), num_key, &elem) != NULL; + } + + if (!result) { + zval_ptr_dtor(&elem); + + break; + } + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(props); + + if (!result) { + zval_ptr_dtor(state); + ZVAL_UNDEF(state); + } + + return result; +} + +static bool user_cache_shared_graph_extract_sleep_snapshot(const zval *value, zval *state) +{ + const char *msg = NULL; + + if (!php_user_cache_serdes_get_sleep_state((zval *) value, state, &msg)) { + if (msg != NULL) { + zend_type_error("%s", msg); + } + + return false; + } + + return true; +} + +static bool user_cache_shared_graph_extract_unserialize_route_snapshot(const zval *value, zval *state) +{ + if (zend_hash_find_known_hash(&Z_OBJCE_P(value)->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL) { + return user_cache_shared_graph_extract_sleep_snapshot(value, state); + } + + return user_cache_shared_graph_extract_property_snapshot(value, state); +} + +/* A NULL memo means the write lock is held, so state hooks may not run. */ +static bool user_cache_shared_graph_get_memoized_state( + const zval *value, + HashTable *state_memo, + bool (*produce_state)(const zval *value, zval *state), + zval **state_ptr, + zval *owned_state) +{ + zend_ulong memo_key; + zval *memo_state; + + ZVAL_UNDEF(owned_state); + *state_ptr = NULL; + + if (state_memo == NULL) { + return false; + } + + memo_key = (zend_ulong) (uintptr_t) Z_OBJ_P(value); + memo_state = zend_hash_index_find(state_memo, memo_key); + if (memo_state != NULL) { + if (Z_TYPE_P(memo_state) != IS_ARRAY) { + return false; + } + + *state_ptr = memo_state; + + return true; + } + + if (!produce_state(value, owned_state)) { + if (!Z_ISUNDEF_P(owned_state)) { + zval_ptr_dtor(owned_state); + ZVAL_UNDEF(owned_state); + } + + return false; + } + + memo_state = zend_hash_index_add(state_memo, memo_key, owned_state); + if (memo_state == NULL) { + zval_ptr_dtor(owned_state); + ZVAL_UNDEF(owned_state); + + return false; + } + + ZVAL_UNDEF(owned_state); + *state_ptr = memo_state; + + return true; +} + +/* Safe-direct state may be produced under the write lock. */ +static bool user_cache_shared_graph_get_safe_direct_state( + const zval *value, + HashTable *state_memo, + zval **state_ptr, + zval *owned_state) +{ + if (state_memo != NULL) { + return user_cache_shared_graph_get_memoized_state( + value, + state_memo, + user_cache_shared_graph_produce_safe_direct_state, + state_ptr, + owned_state + ); + } + + ZVAL_UNDEF(owned_state); + *state_ptr = NULL; + + if (!user_cache_shared_graph_produce_safe_direct_state(value, owned_state)) { + if (!Z_ISUNDEF_P(owned_state)) { + zval_ptr_dtor(owned_state); + ZVAL_UNDEF(owned_state); + } + + return false; + } + + *state_ptr = owned_state; + + return true; +} + +static bool user_cache_shared_graph_get_magic_serialized_state( + const zval *value, + HashTable *state_memo, + zval **state_ptr, + zval *owned_state) +{ + return user_cache_shared_graph_get_memoized_state( + value, + state_memo, + user_cache_shared_graph_extract_serialize_snapshot, + state_ptr, + owned_state + ); +} + +static bool user_cache_shared_graph_get_magic_unserialized_state( + const zval *value, + HashTable *state_memo, + zval **state_ptr, + zval *owned_state) +{ + return user_cache_shared_graph_get_memoized_state( + value, + state_memo, + user_cache_shared_graph_extract_unserialize_route_snapshot, + state_ptr, + owned_state + ); +} + +static bool user_cache_shared_graph_get_sleep_state( + const zval *value, + HashTable *state_memo, + zval **state_ptr, + zval *owned_state) +{ + return user_cache_shared_graph_get_memoized_state( + value, + state_memo, + user_cache_shared_graph_extract_sleep_snapshot, + state_ptr, + owned_state + ); +} + +static bool user_cache_shared_graph_get_wakeup_state( + const zval *value, + HashTable *state_memo, + zval **state_ptr, + zval *owned_state) +{ + return user_cache_shared_graph_get_memoized_state( + value, + state_memo, + user_cache_shared_graph_extract_property_snapshot, + state_ptr, + owned_state + ); +} + +static bool user_cache_shared_graph_get_serdes_blob( + const zval *value, + HashTable *state_memo, + zend_string **blob_ptr) +{ + const char *msg = NULL; + zend_ulong memo_key; + zend_string *blob; + zend_object *obj = Z_OBJ_P(value); + zval *memo_state, blob_zv; + smart_str buf = {0}; + + *blob_ptr = NULL; + + /* Serialization may invoke user code and is forbidden under the write lock. */ + if (state_memo == NULL) { + return false; + } + + memo_key = (zend_ulong) (uintptr_t) obj; + memo_state = zend_hash_index_find(state_memo, memo_key); + if (memo_state != NULL) { + if (Z_TYPE_P(memo_state) != IS_STRING) { + return false; + } + + *blob_ptr = Z_STR_P(memo_state); + + return true; + } + + if (!php_user_cache_serdes_encode((zval *) value, &buf, &msg)) { + if (msg != NULL) { + zend_type_error("%s", msg); + } + + smart_str_free(&buf); + + return false; + } + + blob = smart_str_extract(&buf); + if (ZSTR_LEN(blob) == 0 || ZSTR_LEN(blob) > UINT32_MAX) { + zend_string_release(blob); + + return false; + } + + ZVAL_STR(&blob_zv, blob); + memo_state = zend_hash_index_add(state_memo, memo_key, &blob_zv); + if (memo_state == NULL) { + zend_string_release(blob); + + return false; + } + + *blob_ptr = Z_STR_P(memo_state); + + return true; +} + +static php_user_cache_shared_graph_state_getter_t user_cache_shared_graph_route_state_getter( + php_user_cache_object_route route) +{ + switch (route) { + case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE: + case PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS: + return user_cache_shared_graph_get_magic_serialized_state; + case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE: + return user_cache_shared_graph_get_magic_unserialized_state; + case PHP_USER_CACHE_OBJECT_ROUTE_SLEEP: + return user_cache_shared_graph_get_sleep_state; + case PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP: + return user_cache_shared_graph_get_wakeup_state; + default: + return NULL; + } +} + +static bool user_cache_shared_graph_calc_magic_state_object( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value, + zend_object *obj, + php_user_cache_object_route route) +{ + php_user_cache_shared_graph_state_getter_t get_state; + zval *state_ptr, state; + bool result; + + /* Reserve shared objects only once. */ + if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) { + return true; + } + + get_state = user_cache_shared_graph_route_state_getter(route); + if (!get_state(value, ctx->state_memo, &state_ptr, &state)) { + return false; + } + + if (user_cache_shared_graph_state_array_fits_schema(Z_ARRVAL_P(state_ptr))) { + result = user_cache_shared_graph_calc_shaped_state_object( + ctx, + obj->ce, + Z_ARRVAL_P(state_ptr) + ); + } else { + result = user_cache_shared_graph_calc_reserve(ctx, + sizeof(php_user_cache_shared_graph_safe_direct_object)) && + user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name) && + user_cache_shared_graph_calc_value(ctx, state_ptr) + ; + } + + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + + return result; +} + +static bool user_cache_shared_graph_calc_sleep_state_object( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value, + zend_object *obj, + php_user_cache_object_route route) +{ + php_user_cache_shared_graph_state_getter_t get_state; + zend_ulong num_key; + zend_string *prop_name, *resolved_name; + zval *state_ptr, state, *prop_val; + HashTable *props; + uint32_t prop_count; + bool result; + + if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) { + return true; + } + + get_state = user_cache_shared_graph_route_state_getter(route); + if (!get_state(value, ctx->state_memo, &state_ptr, &state)) { + return false; + } + + props = Z_ARRVAL_P(state_ptr); + if (user_cache_shared_graph_state_array_fits_schema(props)) { + result = user_cache_shared_graph_calc_shaped_state_object( + ctx, + obj->ce, + props + ); + } else { + prop_count = zend_hash_num_elements(props); + result = user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_object) + ) && + user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name) && + (prop_count == 0 || + user_cache_shared_graph_calc_reserve( + ctx, + (size_t) prop_count * sizeof(php_user_cache_shared_graph_property) + ) + ) + ; + + if (result) { + /* Native serialization restores integer keys as property names. */ + ZEND_HASH_FOREACH_KEY_VAL(props, num_key, prop_name, prop_val) { + resolved_name = prop_name != NULL + ? zend_string_copy(prop_name) + : zend_long_to_str((zend_long) num_key) + ; + + if (!user_cache_shared_graph_calc_reserve_string(ctx, resolved_name) || + !user_cache_shared_graph_calc_value(ctx, prop_val) + ) { + zend_string_release(resolved_name); + result = false; + + break; + } + + zend_string_release(resolved_name); + } ZEND_HASH_FOREACH_END(); + } + } + + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + + return result; +} + +static bool user_cache_shared_graph_calc_safe_direct_object( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value, + zend_object *obj) +{ + zend_string *prop_name; + zval *prop_val, *src_val, *state_ptr, state; + HashTable *props; + uint32_t prop_count; + bool result; + + if (!user_cache_shared_graph_get_safe_direct_state( + value, + ctx->state_memo, + &state_ptr, + &state + ) + ) { + return false; + } + + /* Reserve shared objects only once. */ + if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) { + result = true; + + goto cleanup; + } + + if (!user_cache_shared_graph_calc_reserve(ctx, + sizeof(php_user_cache_shared_graph_safe_direct_object)) || + !user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name) || + !user_cache_shared_graph_calc_value(ctx, state_ptr) + ) { + result = false; + + goto cleanup; + } + + props = zend_std_get_properties(obj); + prop_count = props != NULL ? props->nNumOfElements : 0; + if (prop_count != 0 && + !user_cache_shared_graph_calc_reserve( + ctx, + (size_t) prop_count * sizeof(php_user_cache_shared_graph_property) + ) + ) { + result = false; + + goto cleanup; + } + + result = true; + if (props != NULL) { + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) { + if (prop_name == NULL) { + result = false; + + break; + } + + src_val = Z_TYPE_P(prop_val) == IS_INDIRECT + ? Z_INDIRECT_P(prop_val) + : prop_val + ; + + if (user_cache_shared_graph_safe_direct_property_shadows_state( + prop_name, + state_ptr + ) + ) { + continue; + } + + if (!user_cache_shared_graph_calc_reserve_string(ctx, prop_name) || + !user_cache_shared_graph_calc_value(ctx, src_val) + ) { + result = false; + + break; + } + } ZEND_HASH_FOREACH_END(); + } + +cleanup: + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + + return result; +} + +static bool user_cache_shared_graph_calc_plain_object( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value, + zend_object *obj) +{ + zend_string *prop_name; + zval *prop_val, *src_val; + HashTable *props; + uint32_t prop_count; + + if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) { + return true; + } + + if (!user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_object) + ) || + !user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name) + ) { + return false; + } + + props = zend_get_properties_for((zval *) value, ZEND_PROP_PURPOSE_SERIALIZE); + prop_count = props != NULL ? props->nNumOfElements : 0; + if (prop_count != 0 && + !user_cache_shared_graph_calc_reserve( + ctx, + (size_t) prop_count * sizeof(php_user_cache_shared_graph_property) + ) + ) { + if (props != NULL) { + zend_release_properties(props); + } + + return false; + } + + if (props != NULL) { + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) { + if (prop_name == NULL) { + zend_release_properties(props); + + return false; + } + + src_val = Z_TYPE_P(prop_val) == IS_INDIRECT + ? Z_INDIRECT_P(prop_val) + : prop_val + ; + + if (!user_cache_shared_graph_calc_reserve_string(ctx, prop_name)) { + zend_release_properties(props); + + return false; + } + + if (!user_cache_shared_graph_calc_value(ctx, src_val)) { + zend_release_properties(props); + + return false; + } + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(props); + } + + return true; +} + +static bool user_cache_shared_graph_calc_object( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value +) +{ + zend_string *case_name, *serdes_blob; + zend_class_entry *ce; + zend_object *obj; + + obj = Z_OBJ_P(value); + ce = obj->ce; + if (ce == zend_ce_closure) { + zend_type_error(PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE); + + return false; + } + + /* Do not initialize lazy objects while walking serialized state. */ + if (zend_object_is_lazy(obj)) { + zend_type_error(PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE); + + return false; + } + + if (ce->ce_flags & ZEND_ACC_ENUM) { + case_name = Z_STR_P(zend_enum_fetch_case_name(obj)); + + return user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_enum) + ) && + user_cache_shared_graph_calc_reserve_string(ctx, ce->name) && + user_cache_shared_graph_calc_reserve_string(ctx, case_name) + ; + } + + switch (user_cache_shared_graph_classify_object_route(ce)) { + case PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT: + return user_cache_shared_graph_calc_safe_direct_object(ctx, value, obj); + case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE: + return user_cache_shared_graph_calc_magic_state_object( + ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE + ); + case PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS: + return user_cache_shared_graph_calc_sleep_state_object( + ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS + ); + case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE: + return user_cache_shared_graph_calc_magic_state_object( + ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE + ); + case PHP_USER_CACHE_OBJECT_ROUTE_SLEEP: + return user_cache_shared_graph_calc_sleep_state_object( + ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_SLEEP + ); + case PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP: + return user_cache_shared_graph_calc_sleep_state_object( + ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP + ); + case PHP_USER_CACHE_OBJECT_ROUTE_SERDES: + if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) { + return true; + } + + if (!user_cache_shared_graph_get_serdes_blob(value, ctx->state_memo, &serdes_blob)) { + return false; + } + + return user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_serdes_object) + ZSTR_LEN(serdes_blob) + ); + case PHP_USER_CACHE_OBJECT_ROUTE_PLAIN: + return user_cache_shared_graph_calc_plain_object(ctx, value, obj); + case PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE: + return false; + } + + return false; +} + +static bool user_cache_shared_graph_calc_array( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value +) +{ + const HashTable *arr; + zend_ulong arr_key; + zend_string *key; + zval *elem, *verdict, verdict_zv; + bool result, verbatim; + + arr = Z_ARRVAL_P(value); + + if (arr->nNumOfElements == 0) { + if (arr->nNextFreeElement != 0) { + return user_cache_shared_graph_calc_reserve(ctx, + sizeof(php_user_cache_shared_graph_array)) + ; + } + + return true; + } + + if (ctx->verbatim_arrays_allowed) { + if (GC_FLAGS(arr) & IS_ARRAY_IMMUTABLE) { + return user_cache_shared_graph_calc_verbatim_value(ctx, value); + } + + /* A state hook may free an array keyed by address in the verdict map. */ + verdict = NULL; + if (ctx->shared_verdicts != NULL && + (ctx->state_memo == NULL || zend_hash_num_elements(ctx->state_memo) == 0) + ) { + verdict = zend_hash_index_find(ctx->shared_verdicts, (zend_ulong) (uintptr_t) arr); + } + + if (verdict != NULL) { + verbatim = Z_TYPE_P(verdict) == IS_TRUE; + } else { + verbatim = user_cache_shared_graph_can_copy_verbatim_value( + &ctx->seen_arrays, + &ctx->direct_verdicts, + value + ); + + if (ctx->shared_verdicts != NULL) { + ZVAL_BOOL(&verdict_zv, verbatim); + zend_hash_index_add(ctx->shared_verdicts, (zend_ulong) (uintptr_t) arr, &verdict_zv); + } + } + + if (verbatim) { + return user_cache_shared_graph_calc_verbatim_value(ctx, value); + } + } + + if (!php_user_cache_seen_test_and_add(&ctx->seen_arrays, arr)) { + return true; + } + + arr_key = (zend_ulong) (uintptr_t) arr; + result = true; + + if (user_cache_shared_graph_array_has_shape(arr)) { + if (!user_cache_shared_graph_calc_reserve( + ctx, + sizeof(php_user_cache_shared_graph_shaped_array) + ) || + !user_cache_shared_graph_calc_reserve( + ctx, + (size_t) arr->nNumOfElements * sizeof(php_user_cache_shared_graph_value) + ) || + !user_cache_shared_graph_calc_array_shape(ctx, arr, NULL) + ) { + result = false; + + goto done; + } + + ZEND_HASH_FOREACH_VAL((HashTable *) arr, elem) { + if (!user_cache_shared_graph_calc_value(ctx, elem)) { + result = false; + + break; + } + } ZEND_HASH_FOREACH_END(); + + goto done; + } + + if (!user_cache_shared_graph_calc_reserve(ctx, sizeof(php_user_cache_shared_graph_array)) || + !user_cache_shared_graph_calc_reserve( + ctx, + (size_t) arr->nNumOfElements * sizeof(php_user_cache_shared_graph_array_element) + ) + ) { + result = false; + + goto done; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL((HashTable *) arr, key, elem) { + if (key != NULL && !user_cache_shared_graph_calc_reserve_string(ctx, key)) { + result = false; + + break; + } + + if (!user_cache_shared_graph_calc_value(ctx, elem)) { + result = false; + + break; + } + } ZEND_HASH_FOREACH_END(); + +done: + zend_hash_index_del(&ctx->seen_arrays, arr_key); + + return result; +} + +static bool user_cache_shared_graph_calc_value( + php_user_cache_shared_graph_calc_context *ctx, + const zval *value +) +{ + zend_reference *ref; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + return true; + case IS_STRING: + return user_cache_shared_graph_calc_reserve_string(ctx, Z_STR_P(value)); + case IS_RESOURCE: + zend_type_error(PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE); + + return false; + case IS_ARRAY: + return user_cache_shared_graph_calc_array(ctx, value); + case IS_OBJECT: + return user_cache_shared_graph_calc_object(ctx, value); + case IS_REFERENCE: + ref = Z_REF_P(value); + + if (!php_user_cache_seen_test_and_add(&ctx->seen_references, ref)) { + return true; + } + + if (!user_cache_shared_graph_calc_reserve(ctx, + sizeof(php_user_cache_shared_graph_reference)) + ) { + return false; + } + + return user_cache_shared_graph_calc_value(ctx, &ref->val); + default: + return false; + } +} + +static void user_cache_shared_graph_copy_init( + php_user_cache_shared_graph_copy_context *ctx, + uint8_t *buf, + size_t size +) +{ + ctx->buffer = buf; + ctx->size = size; + ctx->position = 0; + ctx->fixup_offsets = NULL; + ctx->fixup_count = 0; + ctx->fixup_capacity = 0; + + zend_hash_init(&ctx->seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&ctx->seen_objects, 8, NULL, NULL, 0); + zend_hash_init(&ctx->seen_references, 8, NULL, NULL, 0); + zend_hash_init(&ctx->string_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->array_shape_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->state_schema_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->direct_array_dedup, 8, NULL, NULL, 0); + zend_hash_init(&ctx->direct_verdicts, 8, NULL, NULL, 0); + + ctx->has_shared_identity = false; + ctx->has_object = false; + ctx->prefers_prototype = false; + ctx->has_userland_restore_object = false; + ctx->has_verbatim_array = false; + ctx->verbatim_arrays_allowed = user_cache_shared_graph_can_use_verbatim_arrays(); + ctx->shared_verdicts = NULL; +} + +static void user_cache_shared_graph_copy_destroy(php_user_cache_shared_graph_copy_context *ctx) +{ + if (ctx->fixup_offsets != NULL) { + efree(ctx->fixup_offsets); + + ctx->fixup_offsets = NULL; + } + + zend_hash_destroy(&ctx->direct_verdicts); + zend_hash_destroy(&ctx->direct_array_dedup); + zend_hash_destroy(&ctx->state_schema_dedup); + zend_hash_destroy(&ctx->array_shape_dedup); + zend_hash_destroy(&ctx->string_dedup); + zend_hash_destroy(&ctx->seen_references); + zend_hash_destroy(&ctx->seen_objects); + zend_hash_destroy(&ctx->seen_arrays); +} + +/* Pack the flags displacement into the two free alignment bits. */ +static bool user_cache_shared_graph_seen_record_object_offsets( + php_user_cache_shared_graph_copy_context *ctx, + const zend_object *obj, + uint32_t obj_offset, + uint32_t flags_offset +) +{ + uint32_t delta; + + delta = flags_offset - obj_offset; + + ZEND_ASSERT((obj_offset & 3) == 0); + ZEND_ASSERT((delta == 4 || delta == 12) && "unexpected reserved/flags field offset"); + + return zend_hash_index_add_ptr( + &ctx->seen_objects, + (zend_ulong) (uintptr_t) obj, + (void *) (uintptr_t) (obj_offset | (delta >> 2)) + ) != NULL; +} + +static bool user_cache_shared_graph_seen_promote_object_to_shared( + php_user_cache_shared_graph_copy_context *ctx, + const zend_object *obj, + uint32_t *obj_offset +) +{ + void *seen_offset; + uint32_t packed, flags_offset; + + seen_offset = zend_hash_index_find_ptr(&ctx->seen_objects, (zend_ulong) (uintptr_t) obj); + if (seen_offset == NULL) { + return false; + } + + packed = (uint32_t) (uintptr_t) seen_offset; + *obj_offset = packed & ~(uint32_t) 3; + flags_offset = *obj_offset + ((packed & 3) << 2); + + *(uint32_t *) (ctx->buffer + flags_offset) |= PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED; + ctx->has_shared_identity = true; + + return true; +} + +static php_user_cache_copy_object_ref_result user_cache_shared_graph_copy_emit_object_ref_if_seen( + php_user_cache_shared_graph_copy_context *ctx, + const zend_object *obj, + php_user_cache_shared_graph_value *dst +) +{ + uint32_t shared_offset; + + if (zend_hash_index_find_ptr(&ctx->seen_objects, (zend_ulong) (uintptr_t) obj) == NULL) { + return PHP_USER_CACHE_COPY_OBJECT_REF_NEW; + } + + if (!user_cache_shared_graph_seen_promote_object_to_shared(ctx, obj, &shared_offset)) { + return PHP_USER_CACHE_COPY_OBJECT_REF_ERROR; + } + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF; + dst->payload.offset = shared_offset; + + return PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED; +} + +/* Callers must initialize the full unaligned region. */ +static bool user_cache_shared_graph_copy_alloc( + php_user_cache_shared_graph_copy_context *ctx, + size_t amount, + uint32_t *offset +) +{ + size_t aligned_amount; + + aligned_amount = PHP_USER_CACHE_ALIGNED_SIZE(amount); + if (ctx->position > ctx->size || aligned_amount > ctx->size - ctx->position) { + return false; + } + + *offset = (uint32_t) ctx->position; + + if (aligned_amount > amount) { + memset(ctx->buffer + ctx->position + amount, 0, aligned_amount - amount); + } + + ctx->position += aligned_amount; + + return true; +} + +/* Record absolute pointer slots for relocation. */ +static zend_always_inline void user_cache_shared_graph_copy_record_fixup( + php_user_cache_shared_graph_copy_context *ctx, + const void *slot) +{ + ZEND_ASSERT(user_cache_shared_graph_pointer_in_range(slot, ctx->buffer, ctx->size)); + ZEND_ASSERT(((uintptr_t) slot & (sizeof(uintptr_t) - 1)) == 0); + + if (ctx->fixup_count == ctx->fixup_capacity) { + ctx->fixup_capacity = ctx->fixup_capacity == 0 ? 8 : ctx->fixup_capacity * 2; + ctx->fixup_offsets = erealloc(ctx->fixup_offsets, sizeof(*ctx->fixup_offsets) * ctx->fixup_capacity); + } + + ctx->fixup_offsets[ctx->fixup_count++] = (uint32_t) ((const uint8_t *) slot - ctx->buffer); +} + +static bool user_cache_shared_graph_copy_string( + php_user_cache_shared_graph_copy_context *ctx, + const zend_string *str, + uint32_t *offset +) +{ + zend_string *new_str; + zval *cached, cached_offset; + uint32_t str_offset; + size_t str_size; + + cached = zend_hash_find(&ctx->string_dedup, (zend_string *) str); + if (cached != NULL) { + *offset = (uint32_t) Z_LVAL_P(cached); + + return true; + } + + str_size = _ZSTR_STRUCT_SIZE(ZSTR_LEN(str)); + if (!user_cache_shared_graph_copy_alloc(ctx, str_size, &str_offset)) { + return false; + } + + new_str = (zend_string *) (ctx->buffer + str_offset); + + memcpy(new_str, str, str_size); + + GC_SET_REFCOUNT(new_str, 2); + GC_TYPE_INFO(new_str) = GC_STRING | ((IS_STR_INTERNED | IS_STR_PERMANENT) << GC_FLAGS_SHIFT); + *offset = str_offset; + + ZVAL_LONG(&cached_offset, (zend_long) str_offset); + zend_hash_add(&ctx->string_dedup, (zend_string *) str, &cached_offset); + + return true; +} + +static bool user_cache_shared_graph_copy_array_shape( + php_user_cache_shared_graph_copy_context *ctx, + const HashTable *arr, + uint32_t *offset, + zend_string *shape_key +) +{ + php_user_cache_shared_graph_array_shape *graph_shape; + php_user_cache_shared_graph_array_shape_element *shape_elems, *shape_elem; + zend_string *key; + zval *cached, cached_offset; + uint32_t shape_offset, elems_offset, key_offset; + bool owns_shape_key = false; + + if (shape_key == NULL) { + shape_key = user_cache_shared_graph_array_shape_key(arr); + if (shape_key == NULL) { + return false; + } + + owns_shape_key = true; + } + + cached = zend_hash_find(&ctx->array_shape_dedup, shape_key); + if (cached != NULL) { + *offset = (uint32_t) Z_LVAL_P(cached); + + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return true; + } + + if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_shape), &shape_offset) || + !user_cache_shared_graph_copy_alloc( + ctx, + (size_t) arr->nNumOfElements * sizeof(*shape_elems), + &elems_offset + ) + ) { + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return false; + } + + graph_shape = (php_user_cache_shared_graph_array_shape *) (ctx->buffer + shape_offset); + graph_shape->count = (uint32_t) arr->nNumOfElements; + graph_shape->elements_offset = elems_offset; + graph_shape->reserved = 0; + graph_shape->reserved2 = 0; + + shape_elems = (php_user_cache_shared_graph_array_shape_element *) (ctx->buffer + elems_offset); + shape_elem = shape_elems; + + ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) { + ZEND_ASSERT(key != NULL); + + if (!user_cache_shared_graph_copy_string(ctx, key, &key_offset)) { + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return false; + } + + shape_elem->key_offset = key_offset; + + ++shape_elem; + } ZEND_HASH_FOREACH_END(); + + ZVAL_LONG(&cached_offset, (zend_long) shape_offset); + if (zend_hash_add(&ctx->array_shape_dedup, shape_key, &cached_offset) == NULL) { + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return false; + } + + *offset = shape_offset; + + if (owns_shape_key) { + zend_string_release(shape_key); + } + + return true; +} + +static bool user_cache_shared_graph_copy_state_schema( + php_user_cache_shared_graph_copy_context *ctx, + zend_class_entry *ce, + const HashTable *arr, + uint32_t *offset +) +{ + php_user_cache_shared_graph_state_schema *schema; + zend_string *schema_key, *shape_key = NULL; + zval *cached, cached_offset; + uint32_t schema_offset, class_name_offset, shape_offset; + + schema_key = user_cache_shared_graph_state_schema_key(ce, arr, &shape_key); + if (schema_key == NULL) { + return false; + } + + cached = zend_hash_find(&ctx->state_schema_dedup, schema_key); + if (cached != NULL) { + *offset = (uint32_t) Z_LVAL_P(cached); + zend_string_release(shape_key); + zend_string_release(schema_key); + + return true; + } + + if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*schema), &schema_offset) || + !user_cache_shared_graph_copy_string(ctx, ce->name, &class_name_offset) || + !user_cache_shared_graph_copy_array_shape(ctx, arr, &shape_offset, shape_key) + ) { + zend_string_release(shape_key); + zend_string_release(schema_key); + + return false; + } + + schema = (php_user_cache_shared_graph_state_schema *) (ctx->buffer + schema_offset); + schema->class_name_offset = class_name_offset; + schema->shape_offset = shape_offset; + schema->count = (uint32_t) arr->nNumOfElements; + + zend_string_release(shape_key); + + ZVAL_LONG(&cached_offset, (zend_long) schema_offset); + if (zend_hash_add(&ctx->state_schema_dedup, schema_key, &cached_offset) == NULL) { + zend_string_release(schema_key); + + return false; + } + + *offset = schema_offset; + zend_string_release(schema_key); + + return true; +} + +static bool user_cache_shared_graph_copy_shaped_state_values( + php_user_cache_shared_graph_copy_context *ctx, + const HashTable *state_array, + uint32_t values_offset +) +{ + php_user_cache_shared_graph_value *graph_vals; + zval *elem; + uint32_t i; + + graph_vals = (php_user_cache_shared_graph_value *) (ctx->buffer + values_offset); + i = 0; + + ZEND_HASH_FOREACH_VAL((HashTable *) state_array, elem) { + if (!user_cache_shared_graph_copy_value(ctx, elem, &graph_vals[i])) { + return false; + } + + ++i; + } ZEND_HASH_FOREACH_END(); + + return i == state_array->nNumOfElements; +} + +static bool user_cache_shared_graph_copy_verbatim_value( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + zval *dst) +{ + const zval *src_packed; + const HashTable *src_arr; + const Bucket *src_bucket; + zend_ulong arr_key; + zend_array *target; + zval *dst_packed, *cached, cached_offset; + Bucket *dst_bucket; + uint32_t i, string_offset, array_offset, data_offset, key_offset; + size_t data_size; + bool result = true; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + switch (Z_TYPE_P(src)) { + case IS_UNDEF: + ZVAL_UNDEF(dst); + return true; + case IS_NULL: + ZVAL_NULL(dst); + return true; + case IS_TRUE: + ZVAL_TRUE(dst); + return true; + case IS_FALSE: + ZVAL_FALSE(dst); + return true; + case IS_LONG: + ZVAL_LONG(dst, Z_LVAL_P(src)); + return true; + case IS_DOUBLE: + ZVAL_DOUBLE(dst, Z_DVAL_P(src)); + return true; + case IS_STRING: + if (!user_cache_shared_graph_copy_string(ctx, Z_STR_P(src), &string_offset)) { + return false; + } + + ZVAL_INTERNED_STR(dst, (zend_string *) (void *) (ctx->buffer + string_offset)); + + /* Stack roots do not need relocation fixups. */ + if (user_cache_shared_graph_pointer_in_range(dst, ctx->buffer, ctx->size)) { + user_cache_shared_graph_copy_record_fixup(ctx, dst); + } + + return true; + case IS_ARRAY: + src_arr = Z_ARRVAL_P(src); + if (src_arr->nNumOfElements == 0) { + ZVAL_EMPTY_ARRAY(dst); + + return true; + } + + if (GC_REFCOUNT(src_arr) > 1) { + cached = zend_hash_index_find(&ctx->direct_array_dedup, (zend_ulong) (uintptr_t) src_arr); + if (cached != NULL) { + ZVAL_ARR(dst, (zend_array *) (void *) (ctx->buffer + (uint32_t) Z_LVAL_P(cached))); + Z_TYPE_FLAGS_P(dst) = 0; + + if (user_cache_shared_graph_pointer_in_range(dst, ctx->buffer, ctx->size)) { + user_cache_shared_graph_copy_record_fixup(ctx, dst); + } + + return true; + } + } + + if (!php_user_cache_seen_test_and_add(&ctx->seen_arrays, src_arr)) { + return false; + } + + arr_key = (zend_ulong) (uintptr_t) src_arr; + data_size = HT_IS_PACKED(src_arr) ? HT_PACKED_USED_SIZE(src_arr) : HT_USED_SIZE(src_arr); + if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(zend_array), &array_offset) || + !user_cache_shared_graph_copy_alloc(ctx, data_size, &data_offset) + ) { + result = false; + + goto done; + } + + target = (zend_array *) (ctx->buffer + array_offset); + + memcpy(target, src_arr, sizeof(zend_array)); + memcpy(ctx->buffer + data_offset, HT_GET_DATA_ADDR(src_arr), data_size); + + GC_SET_REFCOUNT(target, 2); + GC_TYPE_INFO(target) = GC_ARRAY | ((IS_ARRAY_IMMUTABLE | GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT); + + HT_FLAGS(target) |= HASH_FLAG_STATIC_KEYS; + HT_SET_ITERATORS_COUNT(target, 0); + + target->pDestructor = NULL; + target->nInternalPointer = 0; + + HT_SET_DATA_ADDR(target, ctx->buffer + data_offset); + + /* arData moves with the containing buffer. */ + user_cache_shared_graph_copy_record_fixup(ctx, &target->arData); + + if (HT_IS_PACKED(src_arr)) { + dst_packed = target->arPacked; + for (i = 0; i < src_arr->nNumUsed; i++) { + src_packed = &src_arr->arPacked[i]; + if (!user_cache_shared_graph_copy_verbatim_value(ctx, src_packed, &dst_packed[i])) { + result = false; + + break; + } + } + } else { + src_bucket = src_arr->arData; + dst_bucket = target->arData; + for (i = 0; i < src_arr->nNumUsed; i++) { + if (src_bucket[i].key != NULL) { + if (!user_cache_shared_graph_copy_string( + ctx, + src_bucket[i].key, + &key_offset + ) + ) { + result = false; + + break; + } + + dst_bucket[i].key = (zend_string *) (void *) (ctx->buffer + key_offset); + + user_cache_shared_graph_copy_record_fixup(ctx, &dst_bucket[i].key); + } else { + dst_bucket[i].key = NULL; + } + + if (!user_cache_shared_graph_copy_verbatim_value(ctx, &src_bucket[i].val, &dst_bucket[i].val)) { + result = false; + + break; + } + } + } + +done: + zend_hash_index_del(&ctx->seen_arrays, arr_key); + + if (!result) { + return false; + } + + if (GC_REFCOUNT(src_arr) > 1) { + ZVAL_LONG(&cached_offset, (zend_long) array_offset); + zend_hash_index_add(&ctx->direct_array_dedup, arr_key, &cached_offset); + } + + ZVAL_ARR(dst, (zend_array *) (void *) (ctx->buffer + array_offset)); + Z_TYPE_FLAGS_P(dst) = 0; + + if (user_cache_shared_graph_pointer_in_range(dst, ctx->buffer, ctx->size)) { + user_cache_shared_graph_copy_record_fixup(ctx, dst); + } + + return true; + default: + return false; + } +} + +static bool user_cache_shared_graph_copy_shaped_state_object( + php_user_cache_shared_graph_copy_context *ctx, + zend_object *obj, + const HashTable *state_array, + uint8_t node_type, + php_user_cache_shared_graph_value *dst +) +{ + php_user_cache_shared_graph_shaped_state_object *graph_shaped_state; + uint32_t shaped_state_offset, state_schema_offset, state_values_offset; + + if (!user_cache_shared_graph_copy_alloc( + ctx, + sizeof(php_user_cache_shared_graph_shaped_state_object), + &shaped_state_offset + ) || + !user_cache_shared_graph_copy_state_schema( + ctx, + obj->ce, + state_array, + &state_schema_offset + ) || + !user_cache_shared_graph_copy_alloc( + ctx, + (size_t) state_array->nNumOfElements * sizeof(php_user_cache_shared_graph_value), + &state_values_offset + ) + ) { + return false; + } + + if (!user_cache_shared_graph_seen_record_object_offsets( + ctx, + obj, + shaped_state_offset, + shaped_state_offset + offsetof(php_user_cache_shared_graph_shaped_state_object, reserved) + ) + ) { + return false; + } + + graph_shaped_state = (php_user_cache_shared_graph_shaped_state_object *) + (ctx->buffer + shaped_state_offset) + ; + graph_shaped_state->state_schema_offset = state_schema_offset; + graph_shaped_state->reserved = 0; + graph_shaped_state->state_values_offset = state_values_offset; + graph_shaped_state->state_next_free = (uint32_t) state_array->nNextFreeElement; + + if (!user_cache_shared_graph_copy_shaped_state_values( + ctx, + state_array, + state_values_offset + ) + ) { + return false; + } + + dst->type = node_type; + dst->payload.offset = shaped_state_offset; + + return true; +} + +static bool user_cache_shared_graph_copy_magic_state_object( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + zend_object *obj, + php_user_cache_object_route route, + php_user_cache_shared_graph_value *dst) +{ + php_user_cache_shared_graph_safe_direct_object *graph_safe_direct; + php_user_cache_copy_object_ref_result ref_result; + php_user_cache_shared_graph_state_getter_t get_state; + zval *sd_state_ptr, sd_state; + uint32_t sd_offset, sd_class_offset; + bool result; + + ctx->has_userland_restore_object = true; + + ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst); + if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) { + return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED; + } + + get_state = user_cache_shared_graph_route_state_getter(route); + if (!get_state(src, ctx->state_memo, &sd_state_ptr, &sd_state)) { + return false; + } + + if (user_cache_shared_graph_state_array_fits_schema(Z_ARRVAL_P(sd_state_ptr))) { + result = user_cache_shared_graph_copy_shaped_state_object( + ctx, + obj, + Z_ARRVAL_P(sd_state_ptr), + PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT, + dst + ); + + goto cleanup; + } + + if (!user_cache_shared_graph_copy_alloc( + ctx, + sizeof(php_user_cache_shared_graph_safe_direct_object), &sd_offset + ) || + !user_cache_shared_graph_copy_string(ctx, obj->ce->name, &sd_class_offset) + ) { + result = false; + + goto cleanup; + } + + if (!user_cache_shared_graph_seen_record_object_offsets( + ctx, + obj, + sd_offset, + sd_offset + offsetof(php_user_cache_shared_graph_safe_direct_object, reserved) + ) + ) { + result = false; + + goto cleanup; + } + + graph_safe_direct = (php_user_cache_shared_graph_safe_direct_object *) (ctx->buffer + sd_offset); + graph_safe_direct->class_name_offset = sd_class_offset; + graph_safe_direct->property_count = 0; + graph_safe_direct->properties_offset = 0; + graph_safe_direct->reserved = 0; + + if (!user_cache_shared_graph_copy_value(ctx, sd_state_ptr, &graph_safe_direct->state)) { + result = false; + + goto cleanup; + } + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT; + dst->payload.offset = sd_offset; + result = true; + +cleanup: + if (!Z_ISUNDEF(sd_state)) { + zval_ptr_dtor(&sd_state); + } + + return result; +} + +static bool user_cache_shared_graph_copy_sleep_state_object( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + zend_object *obj, + php_user_cache_object_route route, + php_user_cache_shared_graph_value *dst) +{ + php_user_cache_shared_graph_object *graph_obj; + php_user_cache_shared_graph_property *graph_properties; + php_user_cache_copy_object_ref_result ref_result; + php_user_cache_shared_graph_state_getter_t get_state; + zend_ulong num_key; + zend_string *prop_name, *resolved_name; + zval *state_ptr, state, *prop_val; + HashTable *props; + uint32_t obj_offset, class_name_offset, properties_offset, + prop_idx, prop_count + ; + bool result; + + /* Values requiring restore hooks cannot use request-local cloning. */ + if (route != PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS || + zend_hash_find_known_hash(&obj->ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL + ) { + ctx->has_userland_restore_object = true; + } + + ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst); + if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) { + return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED; + } + + get_state = user_cache_shared_graph_route_state_getter(route); + if (!get_state(src, ctx->state_memo, &state_ptr, &state)) { + return false; + } + + props = Z_ARRVAL_P(state_ptr); + if (user_cache_shared_graph_state_array_fits_schema(props)) { + result = user_cache_shared_graph_copy_shaped_state_object( + ctx, + obj, + props, + PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT, + dst + ); + + goto cleanup; + } + + prop_count = zend_hash_num_elements(props); + properties_offset = 0; + + if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_obj), &obj_offset) || + !user_cache_shared_graph_copy_string(ctx, obj->ce->name, &class_name_offset) || + (prop_count != 0 && + !user_cache_shared_graph_copy_alloc( + ctx, + (size_t) prop_count * sizeof(php_user_cache_shared_graph_property), + &properties_offset + ) + ) + ) { + result = false; + + goto cleanup; + } + + if (!user_cache_shared_graph_seen_record_object_offsets( + ctx, + obj, + obj_offset, + obj_offset + offsetof(php_user_cache_shared_graph_object, reserved) + ) + ) { + result = false; + + goto cleanup; + } + + graph_obj = (php_user_cache_shared_graph_object *) (ctx->buffer + obj_offset); + graph_obj->class_name_offset = class_name_offset; + graph_obj->property_count = prop_count; + graph_obj->properties_offset = properties_offset; + graph_obj->reserved = 0; + + if (prop_count == 0) { + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT; + dst->payload.offset = obj_offset; + result = true; + + goto cleanup; + } + + graph_properties = (php_user_cache_shared_graph_property *) (ctx->buffer + properties_offset); + prop_idx = 0; + result = true; + + /* Native serialization restores integer keys as property names. */ + ZEND_HASH_FOREACH_KEY_VAL(props, num_key, prop_name, prop_val) { + resolved_name = prop_name != NULL + ? zend_string_copy(prop_name) + : zend_long_to_str((zend_long) num_key) + ; + + graph_properties[prop_idx].name_offset = 0; + graph_properties[prop_idx].reserved = + php_user_cache_serdes_declared_property_index_plus_one(obj->ce, resolved_name) + ; + graph_properties[prop_idx].value.type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF; + + if (!user_cache_shared_graph_copy_string( + ctx, + resolved_name, + &graph_properties[prop_idx].name_offset + ) || + !user_cache_shared_graph_copy_value( + ctx, + prop_val, + &graph_properties[prop_idx].value + ) + ) { + zend_string_release(resolved_name); + result = false; + + break; + } + + zend_string_release(resolved_name); + ++prop_idx; + } ZEND_HASH_FOREACH_END(); + + if (result) { + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT; + dst->payload.offset = obj_offset; + } + +cleanup: + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + + return result; +} + +static bool user_cache_shared_graph_copy_safe_direct_object( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + zend_object *obj, + php_user_cache_shared_graph_value *dst) +{ + php_user_cache_shared_graph_safe_direct_object *graph_safe_direct; + php_user_cache_shared_graph_property *graph_properties; + php_user_cache_copy_object_ref_result ref_result; + zend_string *prop_name; + zval *prop_val, *src_val, *sd_state_ptr, sd_state; + HashTable *props; + uint32_t string_offset, prop_idx, prop_count, + sd_offset, sd_class_offset, + sd_props_offset + ; + bool result; + + if (php_user_cache_safe_direct_prefers_request_local_prototype(obj->ce)) { + ctx->prefers_prototype = true; + } + + ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst); + if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) { + return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED; + } + + if (!user_cache_shared_graph_get_safe_direct_state( + src, + ctx->state_memo, + &sd_state_ptr, + &sd_state + ) + ) { + return false; + } + + if (!user_cache_shared_graph_copy_alloc( + ctx, + sizeof(php_user_cache_shared_graph_safe_direct_object), + &sd_offset + ) || + !user_cache_shared_graph_copy_string( + ctx, + obj->ce->name, + &sd_class_offset + ) + ) { + result = false; + + goto cleanup; + } + + if (!user_cache_shared_graph_seen_record_object_offsets( + ctx, + obj, + sd_offset, + sd_offset + offsetof(php_user_cache_shared_graph_safe_direct_object, reserved) + ) + ) { + result = false; + + goto cleanup; + } + + graph_safe_direct = (php_user_cache_shared_graph_safe_direct_object *) (ctx->buffer + sd_offset); + graph_safe_direct->class_name_offset = sd_class_offset; + graph_safe_direct->reserved = 0; + + if (!user_cache_shared_graph_copy_value(ctx, sd_state_ptr, &graph_safe_direct->state)) { + result = false; + + goto cleanup; + } + + props = zend_std_get_properties(obj); + prop_count = props != NULL ? props->nNumOfElements : 0; + graph_safe_direct->property_count = prop_count; + graph_safe_direct->properties_offset = 0; + result = true; + + if (prop_count != 0) { + if (!user_cache_shared_graph_copy_alloc( + ctx, + (size_t) prop_count * sizeof(php_user_cache_shared_graph_property), + &sd_props_offset + ) + ) { + result = false; + + goto cleanup; + } + + graph_safe_direct->properties_offset = sd_props_offset; + graph_properties = (php_user_cache_shared_graph_property *) (ctx->buffer + sd_props_offset); + prop_idx = 0; + + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) { + /* Zero the skipped slot; copy_alloc() only clears alignment padding. */ + memset(&graph_properties[prop_idx], 0, sizeof(graph_properties[prop_idx])); + + if (prop_name == NULL) { + result = false; + + break; + } + + src_val = Z_TYPE_P(prop_val) == IS_INDIRECT + ? Z_INDIRECT_P(prop_val) + : prop_val + ; + + if (user_cache_shared_graph_safe_direct_property_shadows_state( + prop_name, + sd_state_ptr + ) + ) { + ++prop_idx; + + continue; + } + + if (!user_cache_shared_graph_copy_string(ctx, prop_name, &string_offset)) { + result = false; + + break; + } + + graph_properties[prop_idx].name_offset = string_offset; + + if (!user_cache_shared_graph_copy_value( + ctx, + src_val, + &graph_properties[prop_idx].value + ) + ) { + result = false; + + break; + } + + ++prop_idx; + } ZEND_HASH_FOREACH_END(); + + if (!result) { + goto cleanup; + } + } + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT; + dst->payload.offset = sd_offset; + +cleanup: + if (!Z_ISUNDEF(sd_state)) { + zval_ptr_dtor(&sd_state); + } + + return result; +} + +static bool user_cache_shared_graph_copy_serdes_object( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + zend_object *obj, + php_user_cache_shared_graph_value *dst) +{ + php_user_cache_shared_graph_serdes_object *graph_serdes; + php_user_cache_copy_object_ref_result ref_result; + zend_string *serdes_blob; + uint32_t serdes_offset; + + ctx->has_userland_restore_object = true; + + ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst); + if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) { + return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED; + } + + if (!user_cache_shared_graph_get_serdes_blob(src, ctx->state_memo, &serdes_blob)) { + return false; + } + + if (!user_cache_shared_graph_copy_alloc( + ctx, + sizeof(php_user_cache_shared_graph_serdes_object) + ZSTR_LEN(serdes_blob), + &serdes_offset + ) + ) { + return false; + } + + if (!user_cache_shared_graph_seen_record_object_offsets( + ctx, + obj, + serdes_offset, + serdes_offset + offsetof(php_user_cache_shared_graph_serdes_object, flags) + ) + ) { + return false; + } + + graph_serdes = (php_user_cache_shared_graph_serdes_object *) (ctx->buffer + serdes_offset); + graph_serdes->blob_len = (uint32_t) ZSTR_LEN(serdes_blob); + graph_serdes->flags = 0; + + memcpy(graph_serdes + 1, ZSTR_VAL(serdes_blob), ZSTR_LEN(serdes_blob)); + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT; + dst->payload.offset = serdes_offset; + + return true; +} + +static bool user_cache_shared_graph_copy_plain_object( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + zend_object *obj, + php_user_cache_shared_graph_value *dst) +{ + php_user_cache_shared_graph_object *graph_obj; + php_user_cache_shared_graph_property *graph_properties; + php_user_cache_copy_object_ref_result ref_result; + zend_string *prop_name; + zval *prop_val, *src_val; + HashTable *props; + uint32_t obj_offset, class_name_offset, properties_offset, + prop_idx, prop_count + ; + + ctx->prefers_prototype = true; + + ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst); + if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) { + return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED; + } + + if (!user_cache_shared_graph_copy_alloc( + ctx, + sizeof(*graph_obj), + &obj_offset + ) || + !user_cache_shared_graph_seen_record_object_offsets( + ctx, + obj, + obj_offset, + obj_offset + offsetof(php_user_cache_shared_graph_object, reserved) + ) || + !user_cache_shared_graph_copy_string( + ctx, + obj->ce->name, + &class_name_offset + ) + ) { + return false; + } + + props = zend_get_properties_for((zval *) src, ZEND_PROP_PURPOSE_SERIALIZE); + prop_count = props != NULL ? props->nNumOfElements : 0; + properties_offset = 0; + if (prop_count != 0 && + !user_cache_shared_graph_copy_alloc( + ctx, + ((size_t) prop_count * sizeof(php_user_cache_shared_graph_property)), + &properties_offset + ) + ) { + if (props != NULL) { + zend_release_properties(props); + } + + return false; + } + + graph_obj = (php_user_cache_shared_graph_object *) (ctx->buffer + obj_offset); + graph_obj->class_name_offset = class_name_offset; + graph_obj->property_count = prop_count; + graph_obj->properties_offset = properties_offset; + graph_obj->reserved = 0; + + if (prop_count == 0) { + if (props != NULL) { + zend_release_properties(props); + } + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT; + dst->payload.offset = obj_offset; + + return true; + } + + graph_properties = (php_user_cache_shared_graph_property *) (ctx->buffer + properties_offset); + prop_idx = 0; + + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) { + if (prop_name == NULL) { + zend_release_properties(props); + + return false; + } + + graph_properties[prop_idx].name_offset = 0; + graph_properties[prop_idx].reserved = 0; + graph_properties[prop_idx].value.type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF; + + src_val = Z_TYPE_P(prop_val) == IS_INDIRECT + ? Z_INDIRECT_P(prop_val) + : prop_val + ; + + if (!user_cache_shared_graph_copy_string(ctx, prop_name, &graph_properties[prop_idx].name_offset)) { + zend_release_properties(props); + + return false; + } + + if (!user_cache_shared_graph_copy_value( + ctx, + src_val, + &graph_properties[prop_idx].value + ) + ) { + zend_release_properties(props); + + return false; + } + + ++prop_idx; + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(props); + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT; + dst->payload.offset = obj_offset; + + return true; +} + +static bool user_cache_shared_graph_copy_enum( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + php_user_cache_shared_graph_value *dst +) +{ + php_user_cache_shared_graph_enum *graph_enum; + zend_class_entry *ce; + zend_string *case_name; + uint32_t enum_offset, enum_class_offset, enum_case_offset; + + ce = Z_OBJCE_P(src); + case_name = Z_STR_P(zend_enum_fetch_case_name(Z_OBJ_P(src))); + + if (!user_cache_shared_graph_copy_alloc(ctx, + sizeof(php_user_cache_shared_graph_enum), &enum_offset) || + !user_cache_shared_graph_copy_string(ctx, ce->name, &enum_class_offset) || + !user_cache_shared_graph_copy_string(ctx, case_name, &enum_case_offset) + ) { + return false; + } + + graph_enum = (php_user_cache_shared_graph_enum *) (ctx->buffer + enum_offset); + graph_enum->class_name_offset = enum_class_offset; + graph_enum->case_name_offset = enum_case_offset; + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM; + dst->payload.offset = enum_offset; + + return true; +} + +static bool user_cache_shared_graph_copy_reference( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + php_user_cache_shared_graph_value *dst +) +{ + php_user_cache_shared_graph_reference *graph_reference; + zend_reference *ref; + uint32_t shared_offset, reference_offset; + void *seen_offset; + + ref = Z_REF_P(src); + seen_offset = zend_hash_index_find_ptr(&ctx->seen_references, (zend_ulong) (uintptr_t) ref); + + if (seen_offset != NULL) { + shared_offset = (uint32_t) (uintptr_t) seen_offset; + ((php_user_cache_shared_graph_reference *) (ctx->buffer + shared_offset))->flags |= + PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED + ; + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF; + dst->payload.offset = shared_offset; + ctx->has_shared_identity = true; + + return true; + } + + if (!user_cache_shared_graph_copy_alloc( + ctx, + sizeof(php_user_cache_shared_graph_reference), + &reference_offset + ) || + zend_hash_index_add_ptr( + &ctx->seen_references, + (zend_ulong) (uintptr_t) ref, + (void *) (uintptr_t) reference_offset + ) == NULL + ) { + return false; + } + + graph_reference = (php_user_cache_shared_graph_reference *) (ctx->buffer + reference_offset); + graph_reference->flags = 0; + graph_reference->reserved = 0; + + if (!user_cache_shared_graph_copy_value( + ctx, + &ref->val, + &graph_reference->inner + ) + ) { + return false; + } + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE; + dst->payload.offset = reference_offset; + ctx->has_shared_identity = true; + + return true; +} + +static bool user_cache_shared_graph_copy_array( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + php_user_cache_shared_graph_value *dst +) +{ + php_user_cache_shared_graph_array *graph_array; + php_user_cache_shared_graph_array_element *graph_elems, *graph_elem; + php_user_cache_shared_graph_shaped_array *graph_shaped_array; + php_user_cache_shared_graph_value *graph_vals; + zend_ulong arr_key, h; + zend_string *key; + zval *elem, *verdict, array_value; + uint32_t elem_idx, + array_offset, elems_offset, key_offset, + shape_offset, values_offset, + shared_offset + ; + bool result, verbatim; + void *seen_offset; + + result = true; + + if (Z_ARRVAL_P(src)->nNumOfElements == 0) { + if (Z_ARRVAL_P(src)->nNextFreeElement == 0) { + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY; + dst->payload.offset = 0; + + return true; + } + + if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_array), &array_offset)) { + return false; + } + + graph_array = (php_user_cache_shared_graph_array *) (ctx->buffer + array_offset); + graph_array->count = 0; + graph_array->next_free = (uint32_t) Z_ARRVAL_P(src)->nNextFreeElement; + graph_array->elements_offset = 0; + graph_array->reserved = 0; + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY; + dst->payload.offset = array_offset; + + return true; + } + + if (ctx->verbatim_arrays_allowed) { + if (GC_FLAGS(Z_ARRVAL_P(src)) & IS_ARRAY_IMMUTABLE) { + verbatim = true; + } else { + /* No state hook ran, so address-keyed verdicts are still valid. */ + verdict = ctx->shared_verdicts != NULL + ? zend_hash_index_find(ctx->shared_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(src)) + : NULL; + if (verdict != NULL) { + verbatim = Z_TYPE_P(verdict) == IS_TRUE; + } else { + verbatim = user_cache_shared_graph_can_copy_verbatim_value( + &ctx->seen_arrays, + &ctx->direct_verdicts, + src + ); + } + } + + if (verbatim) { + if (!user_cache_shared_graph_copy_verbatim_value(ctx, src, &array_value)) { + return false; + } + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY; + dst->payload.offset = (uint32_t) ((uint8_t *) Z_ARRVAL(array_value) - ctx->buffer); + + /* Verbatim arrays require relocation fixups. */ + ctx->has_verbatim_array = true; + + return true; + } + } + + arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(src); + + seen_offset = zend_hash_index_find_ptr(&ctx->seen_arrays, arr_key); + if (seen_offset != NULL) { + shared_offset = (uint32_t) (uintptr_t) seen_offset; + ((php_user_cache_shared_graph_array *) (ctx->buffer + shared_offset))->reserved |= + PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED + ; + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF; + dst->payload.offset = shared_offset; + ctx->has_shared_identity = true; + + return true; + } + + if (user_cache_shared_graph_array_has_shape(Z_ARRVAL_P(src))) { + if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_shaped_array), &array_offset) || + !user_cache_shared_graph_copy_alloc( + ctx, + (size_t) Z_ARRVAL_P(src)->nNumOfElements * sizeof(*graph_vals), + &values_offset + ) || + !user_cache_shared_graph_copy_array_shape(ctx, Z_ARRVAL_P(src), &shape_offset, NULL) + ) { + result = false; + + goto done; + } + + graph_shaped_array = (php_user_cache_shared_graph_shaped_array *) (ctx->buffer + array_offset); + graph_shaped_array->count = Z_ARRVAL_P(src)->nNumOfElements; + graph_shaped_array->next_free = (uint32_t) Z_ARRVAL_P(src)->nNextFreeElement; + graph_shaped_array->shape_offset = shape_offset; + graph_shaped_array->reserved = 0; + graph_shaped_array->values_offset = values_offset; + graph_shaped_array->reserved2 = 0; + + if (zend_hash_index_add_ptr(&ctx->seen_arrays, arr_key, (void *) (uintptr_t) array_offset) == NULL) { + result = false; + + goto done; + } + + graph_vals = (php_user_cache_shared_graph_value *) (ctx->buffer + values_offset); + elem_idx = 0; + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(src), elem) { + if (!user_cache_shared_graph_copy_value(ctx, elem, &graph_vals[elem_idx])) { + result = false; + + break; + } + + ++elem_idx; + } ZEND_HASH_FOREACH_END(); + + if (result) { + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY; + dst->payload.offset = array_offset; + } + + goto done; + } + + if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_array), &array_offset) || + !user_cache_shared_graph_copy_alloc( + ctx, + (size_t) Z_ARRVAL_P(src)->nNumOfElements * sizeof(*graph_elems), + &elems_offset + ) + ) { + result = false; + + goto done; + } + + graph_array = (php_user_cache_shared_graph_array *) (ctx->buffer + array_offset); + graph_array->count = Z_ARRVAL_P(src)->nNumOfElements; + graph_array->next_free = (uint32_t) Z_ARRVAL_P(src)->nNextFreeElement; + graph_array->elements_offset = elems_offset; + graph_array->reserved = + (HT_IS_PACKED(Z_ARRVAL_P(src)) && HT_IS_WITHOUT_HOLES(Z_ARRVAL_P(src))) ? + PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED : + 0 + ; + + if (zend_hash_index_add_ptr(&ctx->seen_arrays, arr_key, (void *) (uintptr_t) array_offset) == NULL) { + result = false; + + goto done; + } + + graph_elems = (php_user_cache_shared_graph_array_element *) (ctx->buffer + elems_offset); + graph_elem = graph_elems; + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(src), h, key, elem) { + memset(graph_elem, 0, sizeof(*graph_elem)); + graph_elem->h = h; + + if (key != NULL) { + if (!user_cache_shared_graph_copy_string(ctx, key, &key_offset)) { + result = false; + + break; + } + + graph_elem->key_offset = key_offset; + } + + if (!user_cache_shared_graph_copy_value(ctx, elem, &graph_elem->value)) { + result = false; + + break; + } + + ++graph_elem; + } ZEND_HASH_FOREACH_END(); + + if (result) { + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY; + dst->payload.offset = array_offset; + } + +done: + zend_hash_index_del(&ctx->seen_arrays, arr_key); + + return result; +} + +static bool user_cache_shared_graph_copy_object( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + php_user_cache_shared_graph_value *dst +) +{ + zend_class_entry *ce; + zend_object *obj; + + ctx->has_object = true; + ce = Z_OBJCE_P(src); + + if (ce->ce_flags & ZEND_ACC_ENUM) { + return user_cache_shared_graph_copy_enum(ctx, src, dst); + } + + obj = Z_OBJ_P(src); + + switch (user_cache_shared_graph_classify_object_route(obj->ce)) { + case PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT: + return user_cache_shared_graph_copy_safe_direct_object(ctx, src, obj, dst); + case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE: + return user_cache_shared_graph_copy_magic_state_object( + ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE, dst + ); + case PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS: + return user_cache_shared_graph_copy_sleep_state_object( + ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS, dst + ); + case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE: + return user_cache_shared_graph_copy_magic_state_object( + ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE, dst + ); + case PHP_USER_CACHE_OBJECT_ROUTE_SLEEP: + return user_cache_shared_graph_copy_sleep_state_object( + ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_SLEEP, dst + ); + case PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP: + return user_cache_shared_graph_copy_sleep_state_object( + ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP, dst + ); + case PHP_USER_CACHE_OBJECT_ROUTE_SERDES: + return user_cache_shared_graph_copy_serdes_object(ctx, src, obj, dst); + case PHP_USER_CACHE_OBJECT_ROUTE_PLAIN: + return user_cache_shared_graph_copy_plain_object(ctx, src, obj, dst); + case PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE: + return false; + } + + return false; +} + +static bool user_cache_shared_graph_copy_value( + php_user_cache_shared_graph_copy_context *ctx, + const zval *src, + php_user_cache_shared_graph_value *dst +) +{ + uint32_t string_offset; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + memset(dst, 0, sizeof(*dst)); + + switch (Z_TYPE_P(src)) { + case IS_UNDEF: + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF; + + return true; + case IS_NULL: + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL; + + return true; + case IS_TRUE: + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE; + + return true; + case IS_FALSE: + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE; + + return true; + case IS_LONG: + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG; + dst->payload.long_value = Z_LVAL_P(src); + + return true; + case IS_DOUBLE: + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE; + dst->payload.double_value = Z_DVAL_P(src); + + return true; + case IS_STRING: + if (!user_cache_shared_graph_copy_string(ctx, Z_STR_P(src), &string_offset)) { + return false; + } + + dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING; + dst->payload.offset = string_offset; + + return true; + case IS_ARRAY: + return user_cache_shared_graph_copy_array(ctx, src, dst); + case IS_OBJECT: + return user_cache_shared_graph_copy_object(ctx, src, dst); + case IS_REFERENCE: + return user_cache_shared_graph_copy_reference(ctx, src, dst); + default: + return false; + } +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_try_update_declared_property( + zend_object *obj, + zend_string *prop_name, + zend_property_info *prop_info, + zval *prop_val, + bool *failed +) +{ + zval *slot, tmp, indirect; + + *failed = false; + + if (prop_info == NULL || + prop_info == ZEND_WRONG_PROPERTY_INFO || + !zend_string_equals(prop_info->name, prop_name) || + (prop_info->flags & (ZEND_ACC_STATIC|ZEND_ACC_VIRTUAL)) != 0 || + prop_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET + ) { + return false; + } + + slot = OBJ_PROP(obj, prop_info->offset); + + if (Z_ISREF_P(prop_val)) { + if (ZEND_TYPE_IS_SET(prop_info->type)) { + if (!zend_verify_prop_assignable_by_ref(prop_info, prop_val, true)) { + *failed = true; + + return false; + } + + ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(prop_val), prop_info); + } + + zval_ptr_dtor(slot); + ZVAL_COPY(slot, prop_val); + + if (obj->properties != NULL) { + ZVAL_INDIRECT(&indirect, slot); + zend_hash_update(obj->properties, prop_name, &indirect); + } + + return true; + } + + if ((prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK)) != 0 || + (prop_info->flags & ZEND_ACC_PPP_MASK) != ZEND_ACC_PUBLIC + ) { + return false; + } + + ZVAL_COPY_DEREF(&tmp, prop_val); + + if (ZEND_TYPE_IS_SET(prop_info->type) && + !zend_verify_property_type(prop_info, &tmp, true) + ) { + zval_ptr_dtor(&tmp); + + *failed = true; + + return false; + } + + zval_ptr_dtor(slot); + ZVAL_COPY_VALUE(slot, &tmp); + + if (obj->properties != NULL) { + ZVAL_INDIRECT(&indirect, slot); + zend_hash_update(obj->properties, prop_name, &indirect); + } + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT zend_class_entry *user_cache_decode_lookup_class(zend_string *class_name) +{ + zend_class_entry *ce; + + ce = user_cache_decode_resolve_cache_find(class_name); + if (ce != NULL) { + return ce; + } + + ce = zend_lookup_class(class_name); + if (ce != NULL) { + user_cache_decode_resolve_cache_store(class_name, ce); + } + + return ce; +} + +static PHP_USER_CACHE_DECODE_HOT zend_class_entry *user_cache_decode_lookup_state_schema_class( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_state_schema *state_schema +) +{ + zend_string *class_name; + zend_class_entry *ce; + + ce = user_cache_decode_resolve_cache_find(state_schema); + if (ce != NULL) { + return ce; + } + + class_name = user_cache_decode_string_at(buf, buf_len, state_schema->class_name_offset); + if (class_name == NULL) { + return NULL; + } + + ce = user_cache_decode_lookup_class(class_name); + if (ce != NULL) { + user_cache_decode_resolve_cache_store(state_schema, ce); + } + + return ce; +} + +static void user_cache_decode_array_dtor(zval *zv) +{ + zval tmp; + + ZVAL_ARR(&tmp, (zend_array *) Z_PTR_P(zv)); + + zval_ptr_dtor(&tmp); +} + +static void user_cache_decode_shape_prototype_dtor(zval *zv) +{ + zend_array_destroy((zend_array *) Z_PTR_P(zv)); +} + +static zend_array *user_cache_decode_shape_prototype_create( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_array_shape *graph_shape +) +{ + const php_user_cache_shared_graph_array_shape_element *shape_elems, *shape_elem; + zend_string *prop_name; + zend_array *proto; + zval empty; + uint32_t i; + + if (graph_shape->count == 0 || + graph_shape->count > PHP_USER_CACHE_SHARED_GRAPH_ARRAY_SHAPE_MAX_KEYS || + !user_cache_decode_array_range_ok( + buf_len, + graph_shape->elements_offset, + graph_shape->count, + sizeof(*shape_elems) + ) + ) { + return NULL; + } + + proto = zend_new_array(graph_shape->count); + if (HT_FLAGS(proto) & HASH_FLAG_UNINITIALIZED) { + zend_hash_real_init_mixed(proto); + } + + shape_elems = + (const php_user_cache_shared_graph_array_shape_element *) (buf + graph_shape->elements_offset) + ; + + ZVAL_LONG(&empty, 0); + + for (i = 0; i < graph_shape->count; i++) { + shape_elem = &shape_elems[i]; + prop_name = user_cache_decode_string_at(buf, buf_len, shape_elem->key_offset); + if (shape_elem->key_offset == 0 || prop_name == NULL) { + zend_array_destroy(proto); + + return NULL; + } + + if (_zend_hash_append_ex(proto, prop_name, &empty, true) == NULL) { + zend_array_destroy(proto); + + return NULL; + } + } + + proto->nNextFreeElement = 0; + + return proto; +} + +static zend_always_inline void user_cache_decode_shape_prototype_direct_cache_store( + const php_user_cache_shared_graph_array_shape *graph_shape, + zend_array *proto +) +{ + uint32_t slot; + + slot = UC_G(decode_shape_prototype_direct_next)++ % PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS; + + UC_G(decode_shape_prototype_direct_keys)[slot] = graph_shape; + UC_G(decode_shape_prototype_direct_values)[slot] = proto; +} + +static zend_array *user_cache_decode_shape_prototype_get( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_array_shape *graph_shape +) +{ + zend_ulong key; + zend_array *proto; + HashTable *cache; + uint32_t i; + + for (i = 0; i < PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS; i++) { + if (UC_G(decode_shape_prototype_direct_keys)[i] == graph_shape) { + return UC_G(decode_shape_prototype_direct_values)[i]; + } + } + + key = (zend_ulong) (uintptr_t) graph_shape; + if (UC_G(decode_shape_prototype_cache) != NULL) { + proto = zend_hash_index_find_ptr(UC_G(decode_shape_prototype_cache), key); + if (proto != NULL) { + user_cache_decode_shape_prototype_direct_cache_store(graph_shape, proto); + + return proto; + } + } + + proto = user_cache_decode_shape_prototype_create(buf, buf_len, graph_shape); + if (proto == NULL) { + return NULL; + } + + cache = user_cache_decode_shape_prototype_cache(); + if (zend_hash_index_add_ptr(cache, key, proto) == NULL) { + zend_array_destroy(proto); + + return NULL; + } + + user_cache_decode_shape_prototype_direct_cache_store(graph_shape, proto); + + return proto; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_decode_shape_prototype_clone( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_array_shape *graph_shape, + uint32_t count, + uint32_t next_free, + zval *dst +) +{ + zend_array *proto, *arr; + + if (graph_shape->count != count) { + return false; + } + + proto = user_cache_decode_shape_prototype_get(buf, buf_len, graph_shape); + if (proto == NULL || proto->nNumUsed != count || proto->nNumOfElements != count) { + return false; + } + + ALLOC_HASHTABLE(arr); + + GC_SET_REFCOUNT(arr, 1); + GC_TYPE_INFO(arr) = GC_ARRAY; + + HT_FLAGS(arr) = HT_FLAGS(proto) & HASH_FLAG_MASK; + + arr->nTableMask = proto->nTableMask; + arr->nNumUsed = proto->nNumUsed; + arr->nNumOfElements = proto->nNumOfElements; + arr->nTableSize = proto->nTableSize; + arr->nInternalPointer = 0; + arr->nNextFreeElement = (zend_long) next_free; + arr->pDestructor = ZVAL_PTR_DTOR; + + HT_SET_DATA_ADDR(arr, emalloc(HT_SIZE(arr))); + + memcpy(HT_GET_DATA_ADDR(arr), HT_GET_DATA_ADDR(proto), HT_USED_SIZE(proto)); + + ZVAL_ARR(dst, arr); + + return true; +} + +static zend_always_inline bool user_cache_shared_graph_decode_simple_value( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + zend_string *str; + + switch (value->type) { + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF: + ZVAL_UNDEF(dst); + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL: + ZVAL_NULL(dst); + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE: + ZVAL_TRUE(dst); + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE: + ZVAL_FALSE(dst); + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG: + ZVAL_LONG(dst, value->payload.long_value); + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE: + ZVAL_DOUBLE(dst, value->payload.double_value); + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING: + str = user_cache_decode_string_at(buf, buf_len, (uint32_t) value->payload.offset); + if (str == NULL) { + return false; + } + + ZVAL_INTERNED_STR(dst, str); + + return true; + default: + return false; + } +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_shaped_array( + const uint8_t *buf, + size_t buf_len, + uint32_t shape_offset, + uint32_t values_offset, + uint32_t count, + uint32_t next_free, + uint32_t node_offset, + bool shared, + zval *dst +) +{ + const php_user_cache_shared_graph_array_shape *graph_shape; + const php_user_cache_shared_graph_value *graph_vals; + zval val; + Bucket *bucket; + uint32_t i; + + if (!user_cache_decode_range_ok(buf_len, shape_offset, sizeof(*graph_shape)) || + !user_cache_decode_array_range_ok(buf_len, values_offset, count, sizeof(*graph_vals)) + ) { + return false; + } + + graph_shape = (const php_user_cache_shared_graph_array_shape *) (buf + shape_offset); + if (!user_cache_decode_shape_prototype_clone( + buf, + buf_len, + graph_shape, + count, + next_free, + dst + ) + ) { + return false; + } + + if (shared) { + if (!user_cache_decode_array_map_insert(node_offset, Z_ARRVAL_P(dst))) { + return user_cache_decode_fail_zval(dst); + } + } + + HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(dst)); + + graph_vals = (const php_user_cache_shared_graph_value *) (buf + values_offset); + bucket = Z_ARRVAL_P(dst)->arData; + for (i = 0; i < count; i++) { + if (user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_vals[i], &bucket[i].val)) { + continue; + } + + ZVAL_UNDEF(&val); + if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_vals[i], &val)) { + return user_cache_decode_fail_zval(dst); + } + + ZVAL_COPY_VALUE(&bucket[i].val, &val); + } + + PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst)); + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_dynamic_array( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_array *graph_array; + const php_user_cache_shared_graph_array_element *graph_elems, *graph_elem; + zend_string *key; + zval val; + uint32_t i; + + if (!user_cache_decode_range_ok(buf_len, (uint32_t) value->payload.offset, sizeof(*graph_array))) { + return false; + } + + graph_array = (const php_user_cache_shared_graph_array *) (buf + (uint32_t) value->payload.offset); + if (!user_cache_decode_array_range_ok(buf_len, graph_array->elements_offset, graph_array->count, sizeof(*graph_elems))) { + return false; + } + + array_init_size(dst, graph_array->count); + + /* Initialize before identity registration raises the array refcount. */ + if (graph_array->count > 0) { + if (graph_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED) { + zend_hash_real_init_packed(Z_ARRVAL_P(dst)); + } else { + zend_hash_real_init_mixed(Z_ARRVAL_P(dst)); + } + + HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(dst)); + } + + if (graph_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) { + if (!user_cache_decode_array_map_insert((uint32_t) value->payload.offset, Z_ARRVAL_P(dst))) { + return user_cache_decode_fail_zval(dst); + } + } + + graph_elems = (const php_user_cache_shared_graph_array_element *) (buf + graph_array->elements_offset); + + if (graph_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED) { + for (i = 0; i < graph_array->count; i++) { + graph_elem = &graph_elems[i]; + + if (graph_elem->key_offset != 0 || graph_elem->h != i) { + return user_cache_decode_fail_zval(dst); + } + + if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_elem->value, &val)) { + ZVAL_UNDEF(&val); + + if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_elem->value, &val)) { + return user_cache_decode_fail_zval(dst); + } + } + + if (zend_hash_next_index_insert_new(Z_ARRVAL_P(dst), &val) == NULL) { + zval_ptr_dtor(&val); + return user_cache_decode_fail_zval(dst); + } + } + + Z_ARRVAL_P(dst)->nNextFreeElement = graph_array->next_free; + PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst)); + return true; + } + + for (i = 0; i < graph_array->count; i++) { + graph_elem = &graph_elems[i]; + + if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_elem->value, &val)) { + ZVAL_UNDEF(&val); + + if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_elem->value, &val)) { + return user_cache_decode_fail_zval(dst); + } + } + + if (graph_elem->key_offset != 0) { + key = user_cache_decode_string_at(buf, buf_len, graph_elem->key_offset); + if (key == NULL) { + zval_ptr_dtor(&val); + return user_cache_decode_fail_zval(dst); + } + + if (zend_hash_add_new(Z_ARRVAL_P(dst), key, &val) == NULL) { + zval_ptr_dtor(&val); + return user_cache_decode_fail_zval(dst); + } + } else { + if (zend_hash_index_add_new(Z_ARRVAL_P(dst), graph_elem->h, &val) == NULL) { + zval_ptr_dtor(&val); + return user_cache_decode_fail_zval(dst); + } + } + } + + Z_ARRVAL_P(dst)->nNextFreeElement = graph_array->next_free; + PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst)); + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_init_object( + zend_class_entry *ce, + uint32_t node_offset, + bool shared, + zval *dst +) +{ + if (object_init_ex(dst, ce) != SUCCESS) { + return false; + } + + if (shared && !user_cache_decode_identity_map_insert(node_offset, Z_OBJ_P(dst))) { + return user_cache_decode_fail_zval(dst); + } + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_apply_property( + zval *dst, + zend_string *prop_name, + uint32_t slot_idx_plus_one, + zval *prop_val +) +{ + bool result; + + if (slot_idx_plus_one != 0) { + result = php_user_cache_shared_graph_update_object_property_at( + dst, + prop_name, + slot_idx_plus_one - 1, + prop_val + ); + } else { + result = php_user_cache_shared_graph_update_object_property( + dst, + prop_name, + prop_val + ); + } + + if (!result) { + zval_ptr_dtor(prop_val); + return user_cache_decode_fail_zval(dst); + } + + zval_ptr_dtor(prop_val); + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_object_properties( + const uint8_t *buf, + size_t buf_len, + uint32_t properties_offset, + uint32_t property_count, + bool use_sleep_slots, + zval *dst +) +{ + const php_user_cache_shared_graph_property *props, *prop; + zend_string *prop_name; + zval prop_val; + uint32_t i; + + if (property_count == 0) { + return true; + } + + if (!user_cache_decode_array_range_ok(buf_len, properties_offset, property_count, sizeof(*props))) { + return user_cache_decode_fail_zval(dst); + } + + props = (const php_user_cache_shared_graph_property *) (buf + properties_offset); + for (i = 0; i < property_count; i++) { + prop = &props[i]; + if (prop->value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF) { + continue; + } + + prop_name = user_cache_decode_string_at(buf, buf_len, prop->name_offset); + if (prop_name == NULL) { + return user_cache_decode_fail_zval(dst); + } + + if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &prop->value, &prop_val)) { + ZVAL_UNDEF(&prop_val); + + if (!user_cache_shared_graph_decode_value(buf, buf_len, &prop->value, &prop_val)) { + zval_ptr_dtor(&prop_val); + return user_cache_decode_fail_zval(dst); + } + } + + if (!user_cache_shared_graph_decode_apply_property( + dst, + prop_name, + use_sleep_slots ? prop->reserved : i + 1, + &prop_val + ) + ) { + return false; + } + } + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_call_wakeup(zend_class_entry *ce, zval *dst) +{ + zval retval, *wakeup_zv; + + wakeup_zv = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)); + if (wakeup_zv != NULL) { + zend_call_known_instance_method(Z_FUNC_P(wakeup_zv), Z_OBJ_P(dst), &retval, 0, NULL); + + zval_ptr_dtor(&retval); + + if (EG(exception)) { + return user_cache_decode_fail_zval(dst); + } + } + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_call_unserialize(zend_class_entry *ce, zval *dst, zval *state) +{ + zend_call_known_instance_method_with_1_params( + ce->__unserialize, + Z_OBJ_P(dst), + NULL, + state + ); + + zval_ptr_dtor(state); + + if (EG(exception)) { + return user_cache_decode_fail_zval(dst); + } + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_object( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_object *graph_obj; + zend_string *class_name; + zend_class_entry *ce; + + graph_obj = (const php_user_cache_shared_graph_object *) (buf + (uint32_t) value->payload.offset); + class_name = user_cache_decode_string_at(buf, buf_len, graph_obj->class_name_offset); + if (class_name == NULL) { + return false; + } + ce = user_cache_decode_lookup_class(class_name); + + if (ce == NULL) { + return false; + } + + if (!user_cache_shared_graph_decode_init_object( + ce, + (uint32_t) value->payload.offset, + (graph_obj->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0, + dst + ) + ) { + return false; + } + + return user_cache_shared_graph_decode_object_properties( + buf, + buf_len, + graph_obj->properties_offset, + graph_obj->property_count, + false, + dst + ); +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_sleep_object( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_object *graph_obj; + zend_string *class_name; + zend_class_entry *ce; + + graph_obj = (const php_user_cache_shared_graph_object *) (buf + (uint32_t) value->payload.offset); + class_name = user_cache_decode_string_at(buf, buf_len, graph_obj->class_name_offset); + if (class_name == NULL) { + return false; + } + + ce = user_cache_decode_lookup_class(class_name); + if (ce == NULL) { + return false; + } + + if (!user_cache_shared_graph_decode_init_object( + ce, + (uint32_t) value->payload.offset, + (graph_obj->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0, + dst + ) + ) { + return false; + } + + if (!user_cache_shared_graph_decode_object_properties( + buf, + buf_len, + graph_obj->properties_offset, + graph_obj->property_count, + true, + dst + ) + ) { + return false; + } + + return user_cache_shared_graph_decode_call_wakeup(ce, dst); +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_sleep_shaped_object( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_shaped_state_object *graph_shaped_state; + const php_user_cache_shared_graph_state_schema *state_schema; + const php_user_cache_shared_graph_array_shape *graph_shape; + const php_user_cache_shared_graph_array_shape_element *shape_elems; + const php_user_cache_shared_graph_value *graph_vals; + zend_string *prop_name; + zend_class_entry *ce; + zval prop_val; + uint32_t i, prop_idx_plus_one; + + graph_shaped_state = (const php_user_cache_shared_graph_shaped_state_object *) (buf + (uint32_t) value->payload.offset); + if (!user_cache_decode_range_ok(buf_len, graph_shaped_state->state_schema_offset, sizeof(*state_schema))) { + return false; + } + + state_schema = (const php_user_cache_shared_graph_state_schema *) (buf + graph_shaped_state->state_schema_offset); + ce = user_cache_decode_lookup_state_schema_class(buf, buf_len, state_schema); + + if (ce == NULL) { + return false; + } + + if (!user_cache_shared_graph_decode_init_object( + ce, + (uint32_t) value->payload.offset, + (graph_shaped_state->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0, + dst + ) + ) { + return false; + } + + if (!user_cache_decode_range_ok(buf_len, state_schema->shape_offset, sizeof(*graph_shape))) { + return user_cache_decode_fail_zval(dst); + } + + graph_shape = (const php_user_cache_shared_graph_array_shape *) (buf + state_schema->shape_offset); + if (graph_shape->count != state_schema->count) { + return user_cache_decode_fail_zval(dst); + } + + if (!user_cache_decode_array_range_ok(buf_len, graph_shape->elements_offset, state_schema->count, sizeof(*shape_elems)) || + !user_cache_decode_array_range_ok(buf_len, graph_shaped_state->state_values_offset, state_schema->count, sizeof(*graph_vals)) + ) { + return user_cache_decode_fail_zval(dst); + } + + shape_elems = (const php_user_cache_shared_graph_array_shape_element *) (buf + graph_shape->elements_offset); + graph_vals = (const php_user_cache_shared_graph_value *) (buf + graph_shaped_state->state_values_offset); + for (i = 0; i < state_schema->count; i++) { + prop_name = user_cache_decode_string_at(buf, buf_len, shape_elems[i].key_offset); + if (prop_name == NULL) { + return user_cache_decode_fail_zval(dst); + } + + if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_vals[i], &prop_val)) { + ZVAL_UNDEF(&prop_val); + + if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_vals[i], &prop_val)) { + zval_ptr_dtor(&prop_val); + return user_cache_decode_fail_zval(dst); + } + } + + prop_idx_plus_one = php_user_cache_serdes_declared_property_index_plus_one(ce, prop_name); + if (!user_cache_shared_graph_decode_apply_property( + dst, + prop_name, + prop_idx_plus_one, + &prop_val + ) + ) { + return false; + } + } + + return user_cache_shared_graph_decode_call_wakeup(ce, dst); +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_safe_direct_object( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_safe_direct_object *graph_safe_direct; + zend_string *class_name; + zend_class_entry *ce; + php_user_cache_safe_direct_state_unserialize_func_t unserialize_func; + zval sd_state; + + graph_safe_direct = (const php_user_cache_shared_graph_safe_direct_object *) (buf + (uint32_t) value->payload.offset); + + class_name = user_cache_decode_string_at(buf, buf_len, graph_safe_direct->class_name_offset); + if (class_name == NULL) { + return false; + } + + ce = user_cache_decode_lookup_class(class_name); + if (ce == NULL) { + return false; + } + + unserialize_func = php_user_cache_safe_direct_state_unserialize_func(ce); + + if (unserialize_func == NULL || + !user_cache_shared_graph_decode_init_object( + ce, + (uint32_t) value->payload.offset, + (graph_safe_direct->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0, + dst + ) + ) { + return false; + } + + ZVAL_UNDEF(&sd_state); + if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_safe_direct->state, &sd_state) || + Z_TYPE(sd_state) != IS_ARRAY || + !unserialize_func(dst, &sd_state) + ) { + zval_ptr_dtor(&sd_state); + return user_cache_decode_fail_zval(dst); + } + + zval_ptr_dtor(&sd_state); + + return user_cache_shared_graph_decode_object_properties( + buf, + buf_len, + graph_safe_direct->properties_offset, + graph_safe_direct->property_count, + false, + dst + ); +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_serialized_object( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_safe_direct_object *graph_serialized; + zend_string *class_name; + zend_class_entry *ce; + zval state; + + graph_serialized = (const php_user_cache_shared_graph_safe_direct_object *) (buf + (uint32_t) value->payload.offset); + + class_name = user_cache_decode_string_at(buf, buf_len, graph_serialized->class_name_offset); + if (class_name == NULL) { + return false; + } + + ce = user_cache_decode_lookup_class(class_name); + if (ce == NULL || ce->__unserialize == NULL || (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE)) { + return false; + } + + if (!user_cache_shared_graph_decode_init_object( + ce, + (uint32_t) value->payload.offset, + (graph_serialized->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0, + dst + ) + ) { + return false; + } + + ZVAL_UNDEF(&state); + + if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_serialized->state, &state) || + Z_TYPE(state) != IS_ARRAY + ) { + zval_ptr_dtor(&state); + return user_cache_decode_fail_zval(dst); + } + + return user_cache_shared_graph_decode_call_unserialize(ce, dst, &state); +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_serialized_shaped_object( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_shaped_state_object *graph_shaped_state; + const php_user_cache_shared_graph_state_schema *state_schema; + zend_class_entry *ce; + zval state; + + graph_shaped_state = (const php_user_cache_shared_graph_shaped_state_object *) (buf + (uint32_t) value->payload.offset); + if (!user_cache_decode_range_ok(buf_len, graph_shaped_state->state_schema_offset, sizeof(*state_schema))) { + return false; + } + + state_schema = (const php_user_cache_shared_graph_state_schema *) (buf + graph_shaped_state->state_schema_offset); + ce = user_cache_decode_lookup_state_schema_class(buf, buf_len, state_schema); + if (ce == NULL || ce->__unserialize == NULL || (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE)) { + return false; + } + + if (!user_cache_shared_graph_decode_init_object( + ce, + (uint32_t) value->payload.offset, + (graph_shaped_state->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0, + dst + ) + ) { + return false; + } + + ZVAL_UNDEF(&state); + + if (!user_cache_shared_graph_decode_shaped_array( + buf, + buf_len, + state_schema->shape_offset, + graph_shaped_state->state_values_offset, + state_schema->count, + graph_shaped_state->state_next_free, + 0, + false, + &state + ) || + Z_TYPE(state) != IS_ARRAY + ) { + zval_ptr_dtor(&state); + return user_cache_decode_fail_zval(dst); + } + + return user_cache_shared_graph_decode_call_unserialize(ce, dst, &state); +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_serdes_object( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_serdes_object *graph_serdes; + + graph_serdes = + (const php_user_cache_shared_graph_serdes_object *) (buf + (uint32_t) value->payload.offset) + ; + + if (!user_cache_decode_range_ok( + buf_len, + (uint32_t) value->payload.offset + (uint32_t) sizeof(*graph_serdes), + graph_serdes->blob_len) + ) { + return false; + } + + if (!php_user_cache_serdes_decode( + (const uint8_t *) (graph_serdes + 1), + graph_serdes->blob_len, + dst + ) || + Z_TYPE_P(dst) != IS_OBJECT + ) { + return user_cache_decode_fail_zval(dst); + } + + if ((graph_serdes->flags & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) && + !user_cache_decode_identity_map_insert( + (uint32_t) value->payload.offset, + Z_OBJ_P(dst) + ) + ) { + return user_cache_decode_fail_zval(dst); + } + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_enum( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_enum *graph_enum; + zend_string *class_name, *case_name; + zend_class_entry *ce; + zend_object *case_obj; + + graph_enum = (const php_user_cache_shared_graph_enum *) (buf + (uint32_t) value->payload.offset); + case_obj = user_cache_decode_resolve_cache_find(graph_enum); + if (case_obj == NULL) { + class_name = user_cache_decode_string_at(buf, buf_len, graph_enum->class_name_offset); + case_name = user_cache_decode_string_at(buf, buf_len, graph_enum->case_name_offset); + if (class_name == NULL || case_name == NULL) { + return false; + } + + ce = user_cache_decode_lookup_class(class_name); + + if (ce == NULL || !(ce->ce_flags & ZEND_ACC_ENUM)) { + return false; + } + + case_obj = zend_enum_get_case(ce, case_name); + if (case_obj == NULL) { + return false; + } + + user_cache_decode_resolve_cache_store(graph_enum, case_obj); + } + + ZVAL_OBJ(dst, case_obj); + GC_ADDREF(case_obj); + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_reference( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_reference *graph_ref; + zend_reference *ref; + + graph_ref = (const php_user_cache_shared_graph_reference *) (buf + (uint32_t) value->payload.offset); + + ZVAL_NEW_EMPTY_REF(dst); + ref = Z_REF_P(dst); + + ZVAL_UNDEF(&ref->val); + + if ((graph_ref->flags & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) && + !user_cache_decode_reference_map_insert( + (uint32_t) value->payload.offset, + ref + ) + ) { + return user_cache_decode_fail_zval(dst); + } + + if (!user_cache_shared_graph_decode_value( + buf, + buf_len, + &graph_ref->inner, + &ref->val + ) + ) { + return user_cache_decode_fail_zval(dst); + } + + return true; +} + +static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_value( + const uint8_t *buf, + size_t buf_len, + const php_user_cache_shared_graph_value *value, + zval *dst +) +{ + const php_user_cache_shared_graph_shaped_array *graph_shaped_array; + zend_reference *shared_reference; + zend_array *shared_array; + zend_object *shared_obj; + size_t node_header_size; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + /* Validate the node header before dereferencing it. */ + node_header_size = user_cache_decode_node_header_size(value->type); + if (node_header_size != 0 && + !user_cache_decode_range_ok(buf_len, (uint32_t) value->payload.offset, node_header_size) + ) { + return false; + } + + switch (value->type) { + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING: + return user_cache_shared_graph_decode_simple_value(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY: + if ((uint32_t) value->payload.offset == 0) { + ZVAL_EMPTY_ARRAY(dst); + } else { + if (!user_cache_decode_range_ok( + buf_len, + (uint32_t) value->payload.offset, + sizeof(zend_array)) + ) { + return false; + } + + ZVAL_ARR(dst, (zend_array *) (void *) (buf + (uint32_t) value->payload.offset)); + Z_TYPE_FLAGS_P(dst) = 0; + } + + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + return user_cache_shared_graph_decode_dynamic_array(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY: + graph_shaped_array = (const php_user_cache_shared_graph_shaped_array *) (buf + (uint32_t) value->payload.offset); + + return user_cache_shared_graph_decode_shaped_array( + buf, + buf_len, + graph_shaped_array->shape_offset, + graph_shaped_array->values_offset, + graph_shaped_array->count, + graph_shaped_array->next_free, + (uint32_t) value->payload.offset, + (graph_shaped_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0, + dst + ); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF: { + shared_array = user_cache_decode_array_map_find((uint32_t) value->payload.offset); + if (shared_array == NULL) { + return false; + } + + ZVAL_ARR(dst, shared_array); + GC_ADDREF(shared_array); + + return true; + } + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + return user_cache_shared_graph_decode_object(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT: + return user_cache_shared_graph_decode_sleep_object(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT: + return user_cache_shared_graph_decode_sleep_shaped_object(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF: { + shared_obj = user_cache_decode_identity_map_find((uint32_t) value->payload.offset); + if (shared_obj == NULL) { + return false; + } + + ZVAL_OBJ(dst, shared_obj); + GC_ADDREF(shared_obj); + + return true; + } + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: + return user_cache_shared_graph_decode_safe_direct_object(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT: + return user_cache_shared_graph_decode_serialized_object(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT: + return user_cache_shared_graph_decode_serialized_shaped_object(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT: + return user_cache_shared_graph_decode_serdes_object(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM: + return user_cache_shared_graph_decode_enum(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE: + return user_cache_shared_graph_decode_reference(buf, buf_len, value, dst); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF: { + shared_reference = user_cache_decode_reference_map_find((uint32_t) value->payload.offset); + + if (shared_reference == NULL) { + return false; + } + + ZVAL_REF(dst, shared_reference); + GC_ADDREF(shared_reference); + + return true; + } + default: + return false; + } +} + +static php_user_cache_shared_graph_header *user_cache_shared_graph_payload_header(uint32_t payload_offset) +{ + const uint8_t *graph_buf; + php_user_cache_shared_graph_header *header; + size_t buf_len; + + if (payload_offset == 0) { + return NULL; + } + + buf_len = php_user_cache_block_payload_capacity(payload_offset); + if (buf_len == 0) { + return NULL; + } + + graph_buf = user_cache_shared_graph_locate( + php_user_cache_ptr(payload_offset), + buf_len, + NULL + ); + if (graph_buf == NULL) { + return NULL; + } + + header = (php_user_cache_shared_graph_header *) graph_buf; + + return header; +} + +/* Debug-only cross-check for recorded relocation fixups. */ +#if ZEND_DEBUG +static bool user_cache_shared_graph_rebase_verbatim_zval( + php_user_cache_shared_graph_rebase_context *ctx, + zval *value) +{ + zend_array *arr; + + switch (Z_TYPE_P(value)) { + case IS_STRING: + Z_STR_P(value) = (zend_string *) user_cache_shared_graph_rebase_pointer( + Z_STR_P(value), + ctx->old_base, + ctx->len, + ctx->delta + ); + + return true; + case IS_ARRAY: + arr = (zend_array *) user_cache_shared_graph_rebase_pointer( + Z_ARR_P(value), + ctx->old_base, + ctx->len, + ctx->delta + ); + + Z_ARR_P(value) = arr; + + if (!user_cache_shared_graph_pointer_in_range(arr, ctx->new_base, ctx->len)) { + return true; + } + + return user_cache_shared_graph_rebase_verbatim_array(ctx, arr); + default: + return true; + } +} + +static bool user_cache_shared_graph_rebase_verbatim_array( + php_user_cache_shared_graph_rebase_context *ctx, + zend_array *arr) +{ + zval *packed; + Bucket *bucket; + uint32_t i; + void *data; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + if (!user_cache_shared_graph_pointer_in_range( + arr, + ctx->new_base, + ctx->len + ) + ) { + return true; + } + + if (!php_user_cache_seen_test_and_add(ctx->seen, arr)) { + return true; + } + + data = HT_GET_DATA_ADDR(arr); + data = user_cache_shared_graph_rebase_pointer(data, ctx->old_base, ctx->len, ctx->delta); + + HT_SET_DATA_ADDR(arr, data); + + if (!user_cache_shared_graph_pointer_in_range(data, ctx->new_base, ctx->len)) { + return false; + } + + if (HT_IS_PACKED(arr)) { + packed = arr->arPacked; + for (i = 0; i < arr->nNumUsed; i++) { + if (!user_cache_shared_graph_rebase_verbatim_zval(ctx, &packed[i])) { + return false; + } + } + } else { + bucket = arr->arData; + for (i = 0; i < arr->nNumUsed; i++) { + if (bucket[i].key != NULL) { + bucket[i].key = (zend_string *) user_cache_shared_graph_rebase_pointer( + bucket[i].key, + ctx->old_base, + ctx->len, + ctx->delta + ); + + if (!user_cache_shared_graph_pointer_in_range(bucket[i].key, ctx->new_base, ctx->len)) { + return false; + } + } + + if (!user_cache_shared_graph_rebase_verbatim_zval(ctx, &bucket[i].val)) { + return false; + } + } + } + + return true; +} + +static bool user_cache_shared_graph_rebase_graph_value( + php_user_cache_shared_graph_rebase_context *ctx, + const php_user_cache_shared_graph_value *value) +{ + const php_user_cache_shared_graph_array *graph_array; + const php_user_cache_shared_graph_array_element *graph_elems; + const php_user_cache_shared_graph_shaped_array *graph_shaped_array; + const php_user_cache_shared_graph_value *graph_vals; + const php_user_cache_shared_graph_object *graph_obj; + const php_user_cache_shared_graph_property *props; + const php_user_cache_shared_graph_safe_direct_object *graph_safe_direct; + const php_user_cache_shared_graph_shaped_state_object *graph_shaped_state; + const php_user_cache_shared_graph_state_schema *state_schema; + zend_array *arr; + uint32_t i; + + if (php_user_cache_stack_overflowed()) { + return false; + } + + switch (value->type) { + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY: + if ((uint32_t) value->payload.offset == 0) { + return true; + } + + arr = (zend_array *) (void *) (ctx->new_base + (uint32_t) value->payload.offset); + + return user_cache_shared_graph_rebase_verbatim_array(ctx, arr); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + graph_array = (const php_user_cache_shared_graph_array *) (ctx->new_base + (uint32_t) value->payload.offset); + graph_elems = (const php_user_cache_shared_graph_array_element *) (ctx->new_base + graph_array->elements_offset); + + for (i = 0; i < graph_array->count; i++) { + if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_elems[i].value)) { + return false; + } + } + + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY: + graph_shaped_array = (const php_user_cache_shared_graph_shaped_array *) (ctx->new_base + (uint32_t) value->payload.offset); + graph_vals = (const php_user_cache_shared_graph_value *) (ctx->new_base + graph_shaped_array->values_offset); + + for (i = 0; i < graph_shaped_array->count; i++) { + if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_vals[i])) { + return false; + } + } + + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT: + graph_obj = (const php_user_cache_shared_graph_object *) (ctx->new_base + (uint32_t) value->payload.offset); + props = (const php_user_cache_shared_graph_property *) (ctx->new_base + graph_obj->properties_offset); + for (i = 0; i < graph_obj->property_count; i++) { + if (!user_cache_shared_graph_rebase_graph_value(ctx, &props[i].value)) { + return false; + } + } + + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE: + return user_cache_shared_graph_rebase_graph_value( + ctx, + &((const php_user_cache_shared_graph_reference *) (ctx->new_base + (uint32_t) value->payload.offset))->inner + ); + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT: + graph_shaped_state = + (const php_user_cache_shared_graph_shaped_state_object *) (ctx->new_base + (uint32_t) value->payload.offset) + ; + state_schema = + (const php_user_cache_shared_graph_state_schema *) (ctx->new_base + graph_shaped_state->state_schema_offset) + ; + graph_vals = (const php_user_cache_shared_graph_value *) (ctx->new_base + graph_shaped_state->state_values_offset); + + for (i = 0; i < state_schema->count; i++) { + if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_vals[i])) { + return false; + } + } + + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT: + ZEND_FALLTHROUGH; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: { + graph_safe_direct = + (const php_user_cache_shared_graph_safe_direct_object *) (ctx->new_base + (uint32_t) value->payload.offset) + ; + + if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_safe_direct->state)) { + return false; + } + + props = (const php_user_cache_shared_graph_property *) (ctx->new_base + graph_safe_direct->properties_offset); + for (i = 0; i < graph_safe_direct->property_count; i++) { + if (!user_cache_shared_graph_rebase_graph_value(ctx, &props[i].value)) { + return false; + } + } + + return true; + } + default: + return true; + } +} +#endif + +static void user_cache_shared_graph_force_retire_locked(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = user_cache_shared_graph_payload_header(payload_offset); + int state, expected; + + if (header == NULL) { + return; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + + if ((state & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0) { + return; + } + + expected = state; + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state | PHP_USER_CACHE_SHARED_GRAPH_RETIRED)) { + return; + } + } +} + +static int user_cache_shared_graph_orphan_offset_compare(const void *a, const void *b) +{ + uint32_t lhs = *(const uint32_t *) a; + uint32_t rhs = *(const uint32_t *) b; + + return lhs < rhs ? -1 : (lhs > rhs ? 1 : 0); +} + +/* Snapshot live offsets once for orphan recovery. */ +static uint32_t user_cache_shared_graph_snapshot_live_offsets_locked( + php_user_cache_header *header, + uint32_t **offsets_out +) +{ + php_user_cache_entry *entries, *entry; + uint32_t *offsets; + uint32_t i, count; + + entries = php_user_cache_entries_ptr(header); + offsets = emalloc((size_t) header->capacity * 2 * sizeof(*offsets)); + count = 0; + + for (i = 0; i < header->capacity; i++) { + entry = &entries[i]; + if (entry->state != PHP_USER_CACHE_ENTRY_USED) { + continue; + } + + if (entry->key_offset != 0) { + offsets[count++] = entry->key_offset; + } + + if (entry->value_offset != 0) { + offsets[count++] = entry->value_offset; + } + } + + qsort(offsets, count, sizeof(*offsets), user_cache_shared_graph_orphan_offset_compare); + + *offsets_out = offsets; + + return count; +} + +static bool user_cache_orphaned_graph_block_is_referenced_locked( + const uint32_t *live_offsets, + uint32_t live_offset_count, + uint32_t block_offset, + uint32_t block_size +) +{ + uint32_t payload_start, lo, hi, mid; + + payload_start = block_offset + (uint32_t) sizeof(php_user_cache_block); + lo = 0; + hi = live_offset_count; + + while (lo < hi) { + mid = lo + (hi - lo) / 2; + + if (live_offsets[mid] < payload_start) { + lo = mid + 1; + } else { + hi = mid; + } + } + + return lo < live_offset_count && live_offsets[lo] < block_offset + block_size; +} + +static bool user_cache_shared_graph_reclaim_orphaned_by_scan_locked(php_user_cache_header *header) +{ + php_user_cache_block *block; + php_user_cache_shared_graph_header *graph_header; + uint32_t *live_offsets; + uint32_t used_end, offset, block_size, payload_offset, live_offset_count; + bool reclaimed = false, restart; + + live_offset_count = user_cache_shared_graph_snapshot_live_offsets_locked(header, &live_offsets); + + do { + restart = false; + used_end = header->data_offset + header->next_free; + offset = header->data_offset; + + while (offset < used_end) { + block = php_user_cache_block_ptr(offset); + block_size = block->size; + + if (block_size < PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + 1) || + block_size > used_end - offset + ) { + goto done; + } + + if (!php_user_cache_block_is_free(block) && + !user_cache_orphaned_graph_block_is_referenced_locked( + live_offsets, live_offset_count, offset, block_size + ) + ) { + payload_offset = offset + (uint32_t) sizeof(php_user_cache_block); + graph_header = user_cache_shared_graph_payload_header(payload_offset); + + if (graph_header != NULL && + zend_atomic_int_load_ex(&graph_header->ref_state) == PHP_USER_CACHE_SHARED_GRAPH_RETIRED + ) { + php_user_cache_free_locked(payload_offset); + reclaimed = true; + restart = true; + + break; + } + } + + offset += block_size; + } + } while (restart); + +done: + efree(live_offsets); + + return reclaimed; +} + +static bool user_cache_shared_graph_load_root_value( + const php_user_cache_shared_graph_header *header, + size_t buf_len, + php_user_cache_shared_graph_value *root_value +) +{ + uint32_t root_type; + + if (header->root_offset != 0 && header->root_offset >= buf_len) { + return false; + } + + root_type = header->root_type != 0 ? header->root_type : PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT; + + memset(root_value, 0, sizeof(*root_value)); + + root_value->type = (uint8_t) root_type; + root_value->payload.offset = header->root_offset; + + switch (root_type) { + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING: + return true; + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT: + case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT: + return header->root_offset != 0; + default: + return false; + } +} + +#if ZEND_DEBUG +static bool user_cache_shared_graph_rebase_payload_pointers( + const uint8_t *buf, + size_t graph_len, + const uint8_t *old_base, + ptrdiff_t delta) +{ + const php_user_cache_shared_graph_header *header; + php_user_cache_shared_graph_rebase_context ctx; + php_user_cache_shared_graph_value root_value; + HashTable seen_arrs; + bool result; + + header = (const php_user_cache_shared_graph_header *) buf; + + /* This path has not called shared_graph_locate(). */ + if (!user_cache_shared_graph_header_is_valid(header) || + !user_cache_shared_graph_load_root_value(header, graph_len, &root_value) + ) { + return false; + } + + zend_hash_init(&seen_arrs, 8, NULL, NULL, 0); + + ctx.old_base = old_base; + ctx.new_base = buf; + ctx.len = graph_len; + ctx.delta = delta; + ctx.seen = &seen_arrs; + + result = user_cache_shared_graph_rebase_graph_value(&ctx, &root_value); + + zend_hash_destroy(&seen_arrs); + + return result; +} + +/* Verify that relocation left no pointers to rewrite. */ +static void user_cache_shared_graph_check_rebase_complete( + const uint8_t *dst_base, + size_t graph_len, + const uint8_t *src_base) +{ + uint8_t *check_buf; + bool result; + + check_buf = emalloc(graph_len); + memcpy(check_buf, dst_base, graph_len); + + result = user_cache_shared_graph_rebase_payload_pointers( + dst_base, + graph_len, + src_base, + (ptrdiff_t) (src_base - dst_base) + ); + ZEND_ASSERT(result); + ZEND_ASSERT(memcmp(check_buf, dst_base, graph_len) == 0); + + efree(check_buf); +} +#else +# define user_cache_shared_graph_check_rebase_complete(dst_base, graph_len, src_base) +#endif + +static void user_cache_destroy_shared_graph_ref_index(void) +{ + if (UC_G(shared_graph_ref_index) != NULL) { + zend_hash_destroy(UC_G(shared_graph_ref_index)); + efree(UC_G(shared_graph_ref_index)); + + UC_G(shared_graph_ref_index) = NULL; + } +} + +static void user_cache_shared_graph_refs_check_fork(void) +{ + uint64_t pid = php_user_cache_cached_pid(); + + if (UC_G(shared_graph_ref_owner_pid) == pid) { + return; + } + + if (UC_G(shared_graph_ref_owner_pid) != 0) { + if (UC_G(shared_graph_refs) != NULL) { + efree(UC_G(shared_graph_refs)); + + UC_G(shared_graph_refs) = NULL; + } + + UC_G(shared_graph_ref_count) = 0; + UC_G(shared_graph_ref_capacity) = 0; + + user_cache_destroy_shared_graph_ref_index(); + } + + UC_G(shared_graph_ref_owner_pid) = pid; +} + +static void user_cache_grow_shared_graph_refs( + php_user_cache_shared_graph_ref **refs, + uint32_t *capacity, + uint32_t initial_capacity) +{ + *capacity = *capacity == 0 ? initial_capacity : *capacity * 2; + *refs = erealloc(*refs, sizeof(**refs) * *capacity); +} + +static void user_cache_ensure_shared_graph_ref_index(void) +{ + if (UC_G(shared_graph_ref_index) == NULL) { + UC_G(shared_graph_ref_index) = emalloc(sizeof(HashTable)); + zend_hash_init(UC_G(shared_graph_ref_index), 8, NULL, NULL, 0); + } +} + +/* Include the context to disambiguate equal offsets in different pools. */ +static zend_ulong user_cache_shared_graph_ref_index_key( + const php_user_cache_context *ctx, + uint32_t payload_offset +) +{ + return ((zend_ulong) (uintptr_t) ctx) ^ (zend_ulong) payload_offset; +} + +PHP_USER_CACHE_DECODE_HOT bool php_user_cache_shared_graph_update_object_property( + zval *obj_zv, + zend_string *prop_name, + zval *prop_val +) +{ + const char *class_name, *unmangled_name; + zend_string *cname; + zend_class_entry *scope; + zend_object *obj; + zend_property_info *prop_info; + HashTable *props; + size_t unmangled_name_len; + bool failed; + + obj = Z_OBJ_P(obj_zv); + if (user_cache_shared_graph_is_unmangled_property_name(prop_name)) { + prop_info = zend_get_property_info(obj->ce, prop_name, true); + if (user_cache_shared_graph_try_update_declared_property( + obj, + prop_name, + prop_info, + prop_val, + &failed) + ) { + return true; + } + + if (failed) { + return false; + } + + if (Z_ISREF_P(prop_val)) { + props = zend_std_get_properties(obj); + + if (props != NULL) { + Z_TRY_ADDREF_P(prop_val); + + zend_hash_update(props, prop_name, prop_val); + + return true; + } + } + + scope = obj->ce; + if (prop_info != NULL && + prop_info != ZEND_WRONG_PROPERTY_INFO && + prop_info->ce != NULL + ) { + scope = prop_info->ce; + } + + zend_update_property(scope, obj, ZSTR_VAL(prop_name), ZSTR_LEN(prop_name), prop_val); + + return !EG(exception); + } + + if (zend_unmangle_property_name_ex(prop_name, &class_name, &unmangled_name, &unmangled_name_len) == SUCCESS) { + if (class_name[0] != '*') { + cname = zend_string_init(class_name, strlen(class_name), 0); + scope = zend_lookup_class(cname); + + if (scope == NULL) { + zend_string_release_ex(cname, 0); + + return false; + } + + zend_update_property(scope, obj, unmangled_name, unmangled_name_len, prop_val); + zend_string_release_ex(cname, 0); + } else { + scope = obj->ce; + prop_info = zend_hash_str_find_ptr(&obj->ce->properties_info, unmangled_name, unmangled_name_len); + if (prop_info != NULL && prop_info->ce != NULL) { + scope = prop_info->ce; + } + + zend_update_property(scope, obj, unmangled_name, unmangled_name_len, prop_val); + } + } + + return !EG(exception); +} + +PHP_USER_CACHE_DECODE_HOT bool php_user_cache_shared_graph_update_object_property_at( + zval *obj_zv, + zend_string *prop_name, + uint32_t prop_idx, + zval *prop_val +) +{ + zend_object *obj; + zend_property_info *prop_info; + bool failed; + + obj = Z_OBJ_P(obj_zv); + if (user_cache_shared_graph_is_unmangled_property_name(prop_name) && + obj->ce->type == ZEND_USER_CLASS && + obj->ce->properties_info_table != NULL && + prop_idx < obj->ce->default_properties_count + ) { + prop_info = obj->ce->properties_info_table[prop_idx]; + + if (user_cache_shared_graph_try_update_declared_property( + obj, + prop_name, + prop_info, + prop_val, + &failed + ) + ) { + return true; + } + + if (failed) { + return false; + } + } + + return php_user_cache_shared_graph_update_object_property( + obj_zv, + prop_name, + prop_val + ); +} + +void php_user_cache_decode_resolve_cache_release(void) +{ + user_cache_shared_graph_object_route_memo_release(); + + memset(UC_G(decode_resolve_direct_keys), 0, sizeof(UC_G(decode_resolve_direct_keys))); + memset(UC_G(decode_resolve_direct_values), 0, sizeof(UC_G(decode_resolve_direct_values))); + + UC_G(decode_resolve_direct_next) = 0; + + if (UC_G(decode_resolve_cache) == NULL) { + return; + } + + zend_hash_destroy(UC_G(decode_resolve_cache)); + + efree(UC_G(decode_resolve_cache)); + + UC_G(decode_resolve_cache) = NULL; +} + +void php_user_cache_decode_shape_prototype_cache_release(void) +{ + memset(UC_G(decode_shape_prototype_direct_keys), 0, sizeof(UC_G(decode_shape_prototype_direct_keys))); + memset(UC_G(decode_shape_prototype_direct_values), 0, sizeof(UC_G(decode_shape_prototype_direct_values))); + + UC_G(decode_shape_prototype_direct_next) = 0; + + if (UC_G(decode_shape_prototype_cache) == NULL) { + return; + } + + zend_hash_destroy(UC_G(decode_shape_prototype_cache)); + efree(UC_G(decode_shape_prototype_cache)); + + UC_G(decode_shape_prototype_cache) = NULL; +} + +/* A bailout may bypass normal decode-map cleanup. */ +void php_user_cache_decode_maps_teardown(void) +{ + user_cache_decode_identity_map_teardown(); + user_cache_decode_reference_map_teardown(); + user_cache_decode_array_map_teardown(); +} + +bool php_user_cache_shared_graph_prefers_prototype(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = + user_cache_shared_graph_payload_header(payload_offset) + ; + + if (header == NULL) { + return true; + } + + return (header->flags & PHP_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE) != 0; +} + +bool php_user_cache_shared_graph_payload_has_aliases(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = + user_cache_shared_graph_payload_header(payload_offset) + ; + + if (header == NULL) { + return true; + } + + return (header->flags & PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY) != 0; +} + +bool php_user_cache_shared_graph_decode_is_lock_safe(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = + user_cache_shared_graph_payload_header(payload_offset) + ; + + return header != NULL && + (header->flags & PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT) == 0 + ; +} + +/* Prove verbatim eligibility and compute the root size in one walk. */ +php_user_cache_verbatim_root_result php_user_cache_shared_graph_calc_verbatim_root( + const zval *value, + HashTable *verbatim_verdicts, + size_t *buf_len) +{ + php_user_cache_shared_graph_calc_context calc_ctx; + php_user_cache_verbatim_root_result result; + zval verdict_zv; + bool immutable, completed; + + if (!user_cache_shared_graph_can_use_verbatim_arrays() || + Z_ARRVAL_P(value)->nNumOfElements == 0 + ) { + return PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED; + } + + immutable = (GC_FLAGS(Z_ARRVAL_P(value)) & IS_ARRAY_IMMUTABLE) != 0; + + user_cache_shared_graph_calc_init(&calc_ctx); + calc_ctx.verbatim_arrays_allowed = true; + calc_ctx.state_memo = NULL; + + completed = user_cache_shared_graph_calc_reserve( + &calc_ctx, + sizeof(php_user_cache_shared_graph_header) + ) && + user_cache_shared_graph_calc_verbatim_value(&calc_ctx, value) + ; + + if (completed && calc_ctx.size <= UINT32_MAX - (ZEND_MM_ALIGNMENT - 1)) { + *buf_len = calc_ctx.size + (ZEND_MM_ALIGNMENT - 1); + result = PHP_USER_CACHE_VERBATIM_ROOT_SIZED; + } else if (immutable || completed) { + /* Immutable arrays cannot fail verbatim eligibility. */ + result = PHP_USER_CACHE_VERBATIM_ROOT_ELIGIBLE_UNSIZED; + } else if (calc_ctx.reserve_failed) { + result = PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED; + } else { + result = PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE; + } + + user_cache_shared_graph_calc_destroy(&calc_ctx); + + if (!immutable && result != PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED) { + ZVAL_BOOL(&verdict_zv, result != PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE); + zend_hash_index_add(verbatim_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(value), &verdict_zv); + } + + return result; +} + +bool php_user_cache_shared_graph_can_copy_verbatim_root(const zval *value, HashTable *verbatim_verdicts) +{ + HashTable seen_arrs, direct_verdicts; + zval verdict_zv; + bool eligible; + + if (!user_cache_shared_graph_can_use_verbatim_arrays()) { + return false; + } + + if (GC_FLAGS(Z_ARRVAL_P(value)) & IS_ARRAY_IMMUTABLE) { + return true; + } + + zend_hash_init(&seen_arrs, 8, NULL, NULL, 0); + zend_hash_init(&direct_verdicts, 8, NULL, NULL, 0); + + eligible = user_cache_shared_graph_can_copy_verbatim_value( + &seen_arrs, + &direct_verdicts, + value + ); + + zend_hash_destroy(&direct_verdicts); + zend_hash_destroy(&seen_arrs); + + ZVAL_BOOL(&verdict_zv, eligible); + zend_hash_index_add(verbatim_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(value), &verdict_zv); + + return eligible; +} + +bool php_user_cache_calculate_shared_graph_size( + const zval *value, + HashTable *state_memo, + HashTable *verbatim_verdicts, + size_t *buf_len +) +{ + php_user_cache_shared_graph_calc_context calc_ctx; + zend_class_entry *root_ce; + bool result; + + if (!buf_len) { + return false; + } + + *buf_len = 0; + + if (Z_TYPE_P(value) == IS_OBJECT) { + root_ce = Z_OBJCE_P(value); + if (!(root_ce->ce_flags & ZEND_ACC_ENUM) && + user_cache_shared_graph_classify_object_route(root_ce) == PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE + ) { + return false; + } + } else if (Z_TYPE_P(value) != IS_ARRAY && Z_TYPE_P(value) != IS_STRING) { + return false; + } + + user_cache_shared_graph_calc_init(&calc_ctx); + + calc_ctx.verbatim_arrays_allowed = user_cache_shared_graph_can_use_verbatim_arrays(); + calc_ctx.shared_verdicts = verbatim_verdicts; + calc_ctx.state_memo = state_memo; + result = user_cache_shared_graph_calc_reserve( + &calc_ctx, + sizeof(php_user_cache_shared_graph_header) + ); + + if (result) { + result = user_cache_shared_graph_calc_value(&calc_ctx, value); + } + + if (result) { + if (calc_ctx.size > UINT32_MAX - (ZEND_MM_ALIGNMENT - 1)) { + result = false; + } else { + calc_ctx.size += ZEND_MM_ALIGNMENT - 1; + } + } + + if (result) { + *buf_len = calc_ctx.size; + } + + user_cache_shared_graph_calc_destroy(&calc_ctx); + + return result; +} + +bool php_user_cache_build_shared_graph_in_place( + const zval *value, + HashTable *state_memo, + const HashTable *verbatim_verdicts, + uint8_t *buf, + size_t buf_len, + size_t *graph_len, + bool *has_verbatim_array, + uint32_t **fixup_offsets, + uint32_t *fixup_count +) +{ + php_user_cache_shared_graph_copy_context copy_ctx; + php_user_cache_shared_graph_header *header; + php_user_cache_shared_graph_value root_value; + uint32_t header_offset, root_offset, root_type; + size_t padding; + bool result; + + if (buf == NULL) { + return false; + } + + padding = user_cache_shared_graph_alignment_padding(buf); + if (padding > buf_len || buf_len - padding < sizeof(php_user_cache_shared_graph_header)) { + return false; + } + + if (padding != 0) { + memset(buf, 0, padding); + } + + buf += padding; + buf_len -= padding; + + user_cache_shared_graph_copy_init(©_ctx, buf, buf_len); + + copy_ctx.shared_verdicts = verbatim_verdicts; + copy_ctx.state_memo = state_memo; + root_offset = 0; + root_type = 0; + + result = user_cache_shared_graph_copy_alloc(©_ctx, sizeof(*header), &header_offset) && header_offset == 0; + if (result) { + if (Z_TYPE_P(value) == IS_OBJECT) { + result = user_cache_shared_graph_copy_value(©_ctx, value, &root_value) && + ( + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM + ) + ; + } else if (Z_TYPE_P(value) == IS_ARRAY) { + result = user_cache_shared_graph_copy_value(©_ctx, value, &root_value) && + ( + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY || + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY + ) + ; + } else if (Z_TYPE_P(value) == IS_STRING) { + result = user_cache_shared_graph_copy_value(©_ctx, value, &root_value) && + root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING + ; + } else { + result = false; + } + + if (result) { + root_type = root_value.type; + root_offset = (uint32_t) root_value.payload.offset; + } + } + + if (!result) { + user_cache_shared_graph_copy_destroy(©_ctx); + + return false; + } + + header = (php_user_cache_shared_graph_header *) buf; + header->magic = PHP_USER_CACHE_SHARED_GRAPH_MAGIC; + header->version = PHP_USER_CACHE_SHARED_GRAPH_VERSION; + header->root_offset = root_offset; + header->root_type = root_type; + header->flags = + (copy_ctx.has_shared_identity ? PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY : 0) | + (copy_ctx.has_object ? PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT : 0) | + ((copy_ctx.prefers_prototype && !copy_ctx.has_userland_restore_object) ? PHP_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE : 0) + ; + + ZEND_ATOMIC_INT_INIT(&header->ref_state, 0); + + if (graph_len != NULL) { + *graph_len = copy_ctx.position; + } + + if (has_verbatim_array != NULL) { + *has_verbatim_array = copy_ctx.has_verbatim_array; + } + + ZEND_ASSERT(copy_ctx.has_verbatim_array || copy_ctx.fixup_count == 0); + + /* Transfer relocation fixups to the prepared value. */ + if (fixup_offsets != NULL) { + *fixup_offsets = copy_ctx.fixup_offsets; + *fixup_count = copy_ctx.fixup_count; + + copy_ctx.fixup_offsets = NULL; + copy_ctx.fixup_count = 0; + copy_ctx.fixup_capacity = 0; + } + + user_cache_shared_graph_copy_destroy(©_ctx); + + return true; +} + +bool php_user_cache_shared_graph_copy_fits_buffer( + const uint8_t *dst_buf, + size_t dst_buf_len, + const uint8_t *src_buf, + size_t src_buf_len, + size_t src_graph_len +) +{ + size_t dst_padding; + + if (src_buf == NULL || dst_buf == NULL || src_graph_len == 0 || src_graph_len > src_buf_len) { + return false; + } + + dst_padding = user_cache_shared_graph_alignment_padding(dst_buf); + + return dst_padding <= dst_buf_len && + (src_graph_len <= dst_buf_len - dst_padding) + ; +} + +bool php_user_cache_shared_graph_decode( + const uint8_t *buf, + size_t buf_len, + zval *dst +) +{ + const php_user_cache_shared_graph_header *header; + const uint8_t *graph_buf; + php_user_cache_shared_graph_value root_value; + HashTable *saved_identity_map, *saved_reference_map, *saved_array_map; + bool result; + + graph_buf = user_cache_shared_graph_locate( + buf, + buf_len, + &buf_len + ); + if (graph_buf == NULL) { + return false; + } + + buf = graph_buf; + + header = (const php_user_cache_shared_graph_header *) buf; + if (!user_cache_shared_graph_load_root_value(header, buf_len, &root_value)) { + return false; + } + + /* Restore hooks may re-enter fetch. */ + saved_identity_map = UC_G(decode_identity_map); + UC_G(decode_identity_map) = NULL; + saved_reference_map = UC_G(decode_reference_map); + UC_G(decode_reference_map) = NULL; + saved_array_map = UC_G(decode_array_map); + UC_G(decode_array_map) = NULL; + + result = user_cache_shared_graph_decode_value(buf, buf_len, &root_value, dst); + + user_cache_decode_identity_map_teardown(); + user_cache_decode_reference_map_teardown(); + user_cache_decode_array_map_teardown(); + + UC_G(decode_identity_map) = saved_identity_map; + UC_G(decode_reference_map) = saved_reference_map; + UC_G(decode_array_map) = saved_array_map; + + /* Drop address-keyed caches after releasing a failed payload. */ + if (!result) { + php_user_cache_decode_resolve_cache_release(); + php_user_cache_decode_shape_prototype_cache_release(); + } + + return result; +} + +bool php_user_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = + user_cache_shared_graph_payload_header(payload_offset) + ; + + if (header == NULL) { + return false; + } + + if (!php_user_cache_quiesce_graph_payloads_locked()) { + return false; + } + + return zend_atomic_int_load_ex(&header->ref_state) == 0; +} + +bool php_user_cache_shared_graph_publish_copied_payload_locked( + uint8_t *dst_buf, + size_t dst_buf_len, + const uint8_t *src_buf, + size_t src_buf_len, + size_t src_graph_len, + bool has_verbatim_array, + const uint32_t *fixup_offsets, + uint32_t fixup_count) +{ + const uint8_t *src_base; + php_user_cache_shared_graph_header *header; + uint8_t *dst_base; + uintptr_t delta; + size_t src_padding, dst_padding; + uint32_t i; + + if (!php_user_cache_shared_graph_copy_fits_buffer( + dst_buf, + dst_buf_len, + src_buf, + src_buf_len, + src_graph_len + ) + ) { + return false; + } + + src_padding = user_cache_shared_graph_alignment_padding(src_buf); + if (src_padding > src_buf_len || + src_buf_len - src_padding < src_graph_len || + src_graph_len < sizeof(*header) + ) { + return false; + } + + src_base = src_buf + src_padding; + dst_padding = user_cache_shared_graph_alignment_padding(dst_buf); + if (dst_padding != 0) { + memset(dst_buf, 0, dst_padding); + } + + dst_base = dst_buf + dst_padding; + + if (src_base != dst_base) { + memcpy(dst_base, src_base, src_graph_len); + } + + header = (php_user_cache_shared_graph_header *) dst_base; + ZEND_ATOMIC_INT_INIT(&header->ref_state, 0); + + if (src_base == dst_base) { + return true; + } + + /* Only non-empty verbatim arrays contain absolute pointers. */ + if (!has_verbatim_array) { + ZEND_ASSERT(fixup_count == 0); + + user_cache_shared_graph_check_rebase_complete(dst_base, src_graph_len, src_base); + + return true; + } + + /* Patch every recorded absolute pointer slot. */ + ZEND_ASSERT(fixup_offsets != NULL && fixup_count > 0); + + delta = (uintptr_t) dst_base - (uintptr_t) src_base; + for (i = 0; i < fixup_count; i++) { + ZEND_ASSERT(fixup_offsets[i] <= src_graph_len - sizeof(uintptr_t)); + ZEND_ASSERT((((uintptr_t) dst_base + fixup_offsets[i]) & (sizeof(uintptr_t) - 1)) == 0); + + *(uintptr_t *) (void *) (dst_base + fixup_offsets[i]) += delta; + } + + user_cache_shared_graph_check_rebase_complete(dst_base, src_graph_len, src_base); + + return true; +} + +void php_user_cache_shared_graph_orphan_payload_locked(uint32_t payload_offset) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + uint32_t i; + + if (header == NULL || payload_offset == 0) { + return; + } + + user_cache_shared_graph_force_retire_locked(payload_offset); + + for (i = 0; i < PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS; i++) { + if (header->orphaned_graphs[i] == payload_offset) { + return; + } + + if (header->orphaned_graphs[i] == 0) { + header->orphaned_graphs[i] = payload_offset; + + return; + } + } + + header->orphaned_graphs_saturated = 1; +} + +void php_user_cache_shared_graph_reclaim_orphaned_locked(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + php_user_cache_shared_graph_header *graph_header; + uint32_t i, payload_offset; + bool checked_quiescent = false; + + if (header == NULL) { + return; + } + + for (i = 0; i < PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS; i++) { + payload_offset = header->orphaned_graphs[i]; + if (payload_offset == 0) { + continue; + } + + if (!checked_quiescent) { + if (!php_user_cache_quiesce_graph_payloads_locked()) { + return; + } + + checked_quiescent = true; + } + + graph_header = user_cache_shared_graph_payload_header(payload_offset); + + if (graph_header == NULL) { + header->orphaned_graphs[i] = 0; + + continue; + } + + if (zend_atomic_int_load_ex(&graph_header->ref_state) != + PHP_USER_CACHE_SHARED_GRAPH_RETIRED + ) { + header->orphaned_graphs[i] = 0; + + continue; + } + + header->orphaned_graphs[i] = 0; + + php_user_cache_free_locked(payload_offset); + } + + if (header->orphaned_graphs_saturated != 0) { + if (!checked_quiescent) { + if (!php_user_cache_quiesce_graph_payloads_locked()) { + return; + } + + checked_quiescent = true; + } + + user_cache_shared_graph_reclaim_orphaned_by_scan_locked(header); + + header->orphaned_graphs_saturated = 0; + } +} + +bool php_user_cache_shared_graph_acquire_ref(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = user_cache_shared_graph_payload_header(payload_offset); + int state, refcount, expected; + + if (header == NULL) { + return false; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + refcount = state & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if ((state & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0 || + refcount == PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK + ) { + return false; + } + + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state + 1)) { + return true; + } + } +} + +/* Abnormally terminated owners can leave retired payloads pinned. Reclaiming + * them safely requires per-owner reference records and a layout version bump. */ +bool php_user_cache_shared_graph_retire_payload_locked(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = user_cache_shared_graph_payload_header(payload_offset); + int state, refcount, expected; + + if (header == NULL) { + return true; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + refcount = state & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if (refcount == 0) { + return true; + } + + if ((state & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0) { + return false; + } + + if (zend_atomic_int_compare_exchange_ex( + &header->ref_state, + &expected, + (state | PHP_USER_CACHE_SHARED_GRAPH_RETIRED) + ) + ) { + return false; + } + } +} + +bool php_user_cache_shared_graph_release_ref_locked(uint32_t payload_offset) +{ + php_user_cache_shared_graph_header *header = user_cache_shared_graph_payload_header(payload_offset); + int state, refcount, expected, desired; + + if (header == NULL) { + return false; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + refcount = state & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if (refcount == 0) { + return false; + } + + desired = (state & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) | (refcount - 1); + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, desired)) { + return (desired & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0 && + (desired & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK) == 0 + ; + } + } +} + +bool php_user_cache_has_request_shared_graph_ref(uint32_t payload_offset) +{ + php_user_cache_context *ctx; + zval *index_zv; + uint32_t i; + + user_cache_shared_graph_refs_check_fork(); + + if (UC_G(shared_graph_ref_count) == 0) { + return false; + } + + ctx = php_user_cache_active_context(); + + if (UC_G(shared_graph_ref_index) != NULL) { + index_zv = zend_hash_index_find( + UC_G(shared_graph_ref_index), + user_cache_shared_graph_ref_index_key(ctx, payload_offset) + ); + if (index_zv == NULL) { + return false; + } + + i = (uint32_t) Z_LVAL_P(index_zv); + if (i < UC_G(shared_graph_ref_count) && + UC_G(shared_graph_refs)[i].context == ctx && + UC_G(shared_graph_refs)[i].payload_offset == payload_offset + ) { + return true; + } + } + + for (i = 0; i < UC_G(shared_graph_ref_count); i++) { + if (UC_G(shared_graph_refs)[i].context == ctx && + UC_G(shared_graph_refs)[i].payload_offset == payload_offset + ) { + return true; + } + } + + return false; +} + +void php_user_cache_shared_graph_ref_reserve(void) +{ + user_cache_shared_graph_refs_check_fork(); + + if (UC_G(shared_graph_ref_count) == UC_G(shared_graph_ref_capacity)) { + user_cache_grow_shared_graph_refs(&UC_G(shared_graph_refs), &UC_G(shared_graph_ref_capacity), 8); + } + + user_cache_ensure_shared_graph_ref_index(); + + zend_hash_extend( + UC_G(shared_graph_ref_index), + zend_hash_num_elements(UC_G(shared_graph_ref_index)) + 1, + 0 + ); +} + +void php_user_cache_register_shared_graph_ref(uint32_t payload_offset) +{ + php_user_cache_context *ctx; + zval index_zv; + + user_cache_shared_graph_refs_check_fork(); + + if (UC_G(shared_graph_ref_count) == UC_G(shared_graph_ref_capacity)) { + user_cache_grow_shared_graph_refs(&UC_G(shared_graph_refs), &UC_G(shared_graph_ref_capacity), 8); + } + + ctx = php_user_cache_active_context(); + + UC_G(shared_graph_refs)[UC_G(shared_graph_ref_count)].context = ctx; + UC_G(shared_graph_refs)[UC_G(shared_graph_ref_count)].payload_offset = payload_offset; + + user_cache_ensure_shared_graph_ref_index(); + + ZVAL_LONG(&index_zv, (zend_long) UC_G(shared_graph_ref_count)); + + zend_hash_index_add( + UC_G(shared_graph_ref_index), + user_cache_shared_graph_ref_index_key(ctx, payload_offset), + &index_zv + ); + + UC_G(shared_graph_ref_count)++; +} + +bool php_user_cache_release_request_shared_graph_refs(void) +{ + php_user_cache_shared_graph_ref *ref; + php_user_cache_context *ctx, *prev_ctx; + uint32_t i, inner; + bool released = false; + + user_cache_shared_graph_refs_check_fork(); + + if (UC_G(shared_graph_ref_count) == 0) { + user_cache_destroy_shared_graph_ref_index(); + + return false; + } + + for (i = 0; i < UC_G(shared_graph_ref_count); i++) { + ctx = UC_G(shared_graph_refs)[i].context; + + if (ctx == NULL) { + continue; + } + + prev_ctx = php_user_cache_activate_context(ctx); + + if (php_user_cache_wlock()) { + if (php_user_cache_header_is_initialized_locked()) { + for (inner = i; inner < UC_G(shared_graph_ref_count); inner++) { + ref = &UC_G(shared_graph_refs)[inner]; + + if (ref->context != ctx) { + continue; + } + + if (ref->payload_offset == 0) { + ref->context = NULL; + continue; + } + + released = true; + if (php_user_cache_shared_graph_release_ref_locked(ref->payload_offset)) { + if (php_user_cache_quiesce_graph_payloads_locked()) { + php_user_cache_free_locked(ref->payload_offset); + } else { + php_user_cache_shared_graph_orphan_payload_locked(ref->payload_offset); + } + } + + ref->context = NULL; + } + + php_user_cache_shared_graph_reclaim_orphaned_locked(); + } + + php_user_cache_unlock(); + } + + php_user_cache_restore_context(prev_ctx); + } + + efree(UC_G(shared_graph_refs)); + + UC_G(shared_graph_refs) = NULL; + UC_G(shared_graph_ref_count) = 0; + UC_G(shared_graph_ref_capacity) = 0; + + user_cache_destroy_shared_graph_ref_index(); + + return released; +} diff --git a/ext/user_cache/user_cache_shm.h b/ext/user_cache/user_cache_shm.h new file mode 100644 index 000000000000..d87eceb54e70 --- /dev/null +++ b/ext/user_cache/user_cache_shm.h @@ -0,0 +1,112 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_USER_CACHE_SHM_H +#define PHP_USER_CACHE_SHM_H + +#include "php.h" + +#if defined(__APPLE__) && defined(__MACH__) /* Darwin */ +# ifdef HAVE_SHM_MMAP_POSIX +# define PHP_USER_CACHE_USE_SHM_OPEN 1 +# endif +# ifdef HAVE_SHM_MMAP_ANON +# define PHP_USER_CACHE_USE_MMAP 1 +# endif +#elif defined(__linux__) || defined(_AIX) +# ifdef HAVE_SHM_MMAP_POSIX +# define PHP_USER_CACHE_USE_SHM_OPEN 1 +# endif +# ifdef HAVE_SHM_IPC +# define PHP_USER_CACHE_USE_SHM 1 +# endif +# ifdef HAVE_SHM_MMAP_ANON +# define PHP_USER_CACHE_USE_MMAP 1 +# endif +#elif defined(__sparc) || defined(__sun) +# ifdef HAVE_SHM_MMAP_POSIX +# define PHP_USER_CACHE_USE_SHM_OPEN 1 +# endif +# ifdef HAVE_SHM_IPC +# define PHP_USER_CACHE_USE_SHM 1 +# endif +# if defined(__i386) +# ifdef HAVE_SHM_MMAP_ANON +# define PHP_USER_CACHE_USE_MMAP 1 +# endif +# endif +#else +# ifdef HAVE_SHM_MMAP_POSIX +# define PHP_USER_CACHE_USE_SHM_OPEN 1 +# endif +# ifdef HAVE_SHM_MMAP_ANON +# define PHP_USER_CACHE_USE_MMAP 1 +# endif +# ifdef HAVE_SHM_IPC +# define PHP_USER_CACHE_USE_SHM 1 +# endif +#endif + +#define PHP_USER_CACHE_ALLOC_FAILURE 0 +#define PHP_USER_CACHE_ALLOC_SUCCESS 1 + +/* Part of the shared-memory layout; bump PHP_USER_CACHE_VERSION if changed. */ +typedef union { + void *ptr; + double dbl; + zend_long lng; +} php_user_cache_align_test; + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L +# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (alignof(php_user_cache_align_test) < 8 ? 8 : alignof(php_user_cache_align_test)) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (_Alignof(php_user_cache_align_test) < 8 ? 8 : _Alignof(php_user_cache_align_test)) +#elif ZEND_GCC_VERSION >= 2000 || defined(__clang__) +# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (__alignof__(php_user_cache_align_test) < 8 ? 8 : __alignof__(php_user_cache_align_test)) +#else +# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (sizeof(php_user_cache_align_test)) +#endif + +#define PHP_USER_CACHE_ALIGNED_SIZE(size) \ + ZEND_MM_ALIGNED_SIZE_EX(size, PHP_USER_CACHE_PLATFORM_ALIGNMENT) + +typedef struct { + size_t size; + size_t end; + size_t pos; + void *p; +} php_user_cache_shm_segment; + +typedef int (*php_user_cache_create_segments_t)(size_t requested_size, php_user_cache_shm_segment ***shared_segments, int *shared_segment_count, const char **error_in); +typedef int (*php_user_cache_detach_segment_t)(php_user_cache_shm_segment *shared_segment); + +typedef struct { + php_user_cache_create_segments_t create_segments; + php_user_cache_detach_segment_t detach_segment; + size_t (*segment_type_size)(void); +} php_user_cache_shm_handlers; + +typedef struct { + const char *name; + const php_user_cache_shm_handlers *handler; +} php_user_cache_shm_handler_entry; + +#ifdef PHP_USER_CACHE_USE_SHM +extern const php_user_cache_shm_handlers php_user_cache_alloc_shm_handlers; +#endif +#ifdef PHP_USER_CACHE_USE_SHM_OPEN +extern const php_user_cache_shm_handlers php_user_cache_alloc_posix_handlers; +#endif + +#endif /* PHP_USER_CACHE_SHM_H */ diff --git a/ext/user_cache/user_cache_storage.c b/ext/user_cache/user_cache_storage.c new file mode 100644 index 000000000000..df37742c1361 --- /dev/null +++ b/ext/user_cache/user_cache_storage.c @@ -0,0 +1,3947 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#include "user_cache_internal.h" +#include "user_cache_storage_portability.h" + +#include "ext/hash/php_hash.h" +#include "ext/hash/php_hash_sha.h" + +typedef enum { + PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP, + PHP_USER_CACHE_ENTRY_LOCK_RELEASE_CLEAR, + PHP_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES +} php_user_cache_entry_lock_release_mode; + +typedef struct { + char *key; + uint32_t key_len; + zend_ulong hash; + uint64_t owner_pid; + uint64_t owner_start_time; + uint64_t expires_at; +} php_user_cache_recovered_entry_lock; + +typedef struct { + zend_string *key; + php_user_cache_entry_lock *lock; +} php_user_cache_entry_lock_release_pair; + +static const uint32_t php_user_cache_capacity_primes[] = { + 127U, + 251U, + 509U, + 1021U, + 2039U, + 4093U, + 8191U, + 16381U, + 32749U, + 65521U, + 131071U, + 262139U, + 524287U, + 1048573U, + 2097143U, + 4194301U, + 8388593U, + 16777213U, +}; + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +static uint64_t php_user_cache_reader_incarnation = 0; +#endif + +static php_user_cache_startup_lock php_user_cache_startup_storage_lock_state = + PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER +; + +static inline uint64_t user_cache_process_start_time_token(uint64_t pid); + +static zend_always_inline bool user_cache_startup_failure_forced(void) +{ + const char *value = getenv("PHP_USER_CACHE_FORCE_STARTUP_FAILURE"); + + return value != NULL && value[0] != '\0' && value[0] != '0'; +} + +static zend_always_inline bool user_cache_requires_pre_request_storage(void) +{ + if (sapi_module.name == NULL) { + return false; + } + + return strcmp(sapi_module.name, "fpm-fcgi") == 0 || + strcmp(sapi_module.name, "apache2handler") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 + ; +} + +static zend_always_inline bool user_cache_is_opted_in(void) +{ + return php_user_cache_runtime_opted_in; +} + +static zend_always_inline bool user_cache_cache_is_disabled_for_sapi(void) +{ + if (!UC_G(enable)) { + return true; + } + + return !UC_G(enable_cli) && + sapi_module.name != NULL && + (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "phpdbg") == 0) + ; +} + +static zend_always_inline void user_cache_set_unavailable(php_user_cache_reason reason) +{ + php_user_cache_runtime *runtime = php_user_cache_active_runtime(); + + runtime->available = false; + runtime->failure_reason = reason; +} + +static zend_always_inline void user_cache_set_available(void) +{ + php_user_cache_runtime *runtime = php_user_cache_active_runtime(); + + runtime->available = true; + runtime->failure_reason = PHP_USER_CACHE_REASON_NONE; +} + +static zend_always_inline HashTable **user_cache_entry_lock_table_ptr(void) +{ + return &UC_G(entry_lock_table); +} + +static zend_always_inline uint32_t user_cache_entry_lock_table_index(zend_ulong hash) +{ + return (uint32_t) (hash % PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE); +} + +static zend_always_inline uint32_t user_cache_used_end_offset_locked(const php_user_cache_header *header) +{ + return header->data_offset + header->next_free; +} + +static zend_always_inline void user_cache_block_mark_free(php_user_cache_block *block) +{ + block->flags |= PHP_USER_CACHE_BLOCK_FREE; +} + +static zend_always_inline void user_cache_block_mark_used(php_user_cache_block *block) +{ + block->flags &= ~PHP_USER_CACHE_BLOCK_FREE; + block->next_free = 0; + block->prev_free = 0; +} + +static zend_always_inline bool user_cache_entry_lock_record_key_matches( + const php_user_cache_entry_lock_record *record, + zend_string *key, + zend_ulong hash) +{ + return record->state == PHP_USER_CACHE_ENTRY_LOCK_USED && + record->hash == hash && + record->key_len == ZSTR_LEN(key) && + memcmp(php_user_cache_ptr(record->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0 + ; +} + +static zend_always_inline php_user_cache_entry_lock *user_cache_find_local_entry_lock( + const php_user_cache_context *ctx, + zend_string *key) +{ + HashTable **locks_ptr = user_cache_entry_lock_table_ptr(); + php_user_cache_entry_lock *lock; + + if (*locks_ptr == NULL) { + return NULL; + } + + lock = zend_hash_find_ptr(*locks_ptr, key); + if (lock == NULL || lock->context != ctx) { + return NULL; + } + + return lock; +} + +/* Do not cache failures or values inherited across fork. */ +static zend_always_inline uint64_t user_cache_cached_self_start_time_token(uint64_t self_pid) +{ + if (UC_G(self_start_time_pid) != self_pid || UC_G(self_start_time_token) == 0) { + UC_G(self_start_time_pid) = self_pid; + UC_G(self_start_time_token) = user_cache_process_start_time_token(self_pid); + } + + return UC_G(self_start_time_token); +} + +#if defined(ZTS) && defined(PHP_USER_CACHE_HAVE_OPTIMISTIC) +static zend_always_inline bool user_cache_storage_holds_header( + const php_user_cache_storage *storage, + const php_user_cache_header *header) +{ + return storage->initialized && + storage->segment_count == 1 && + storage->segments[0]->p == (const void *) header + ; +} +#endif + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +static zend_always_inline void user_cache_atomic_store_64(uint64_t *target, uint64_t value) +{ + PHP_USER_CACHE_ATOMIC_STORE_64(target, value); +} + +static zend_always_inline bool user_cache_atomic_cas_64(uint64_t *target, uint64_t expected, uint64_t desired) +{ + return PHP_USER_CACHE_ATOMIC_CAS_64(target, expected, desired); +} + +static zend_always_inline uint32_t user_cache_atomic_load_32(const uint32_t *target) +{ + return PHP_USER_CACHE_ATOMIC_LOAD_32(target); +} + +static zend_always_inline void user_cache_atomic_store_32(uint32_t *target, uint32_t value) +{ + PHP_USER_CACHE_ATOMIC_STORE_32(target, value); +} + +static zend_always_inline bool user_cache_atomic_cas_32(uint32_t *target, uint32_t expected, uint32_t desired) +{ + return PHP_USER_CACHE_ATOMIC_CAS_32(target, expected, desired); +} + +static zend_always_inline void user_cache_atomic_fence_seq_cst(void) +{ + PHP_USER_CACHE_ATOMIC_FENCE_SEQ_CST(); +} +#endif + +static zend_always_inline void user_cache_seq_announce(uint64_t *seq, uint64_t value) +{ +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + user_cache_atomic_store_64(seq, value); + user_cache_atomic_fence_seq_cst(); +#else + *seq = value; +#endif +} + +static zend_always_inline void user_cache_seq_publish(uint64_t *seq, uint64_t value) +{ +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + user_cache_atomic_store_64(seq, value); +#else + *seq = value; +#endif +} + +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX +static zend_always_inline bool user_cache_header_is_initialized_acquire(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + + if (header == NULL) { + return false; + } + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + if (user_cache_atomic_load_32(&header->magic) != PHP_USER_CACHE_MAGIC) { + return false; + } + + php_user_cache_atomic_fence_acquire(); + + return user_cache_atomic_load_32(&header->version) == PHP_USER_CACHE_VERSION; +#else + return php_user_cache_header_is_initialized_locked(); +#endif +} +#endif + +static inline bool user_cache_zts_lock_init(php_user_cache_storage *storage) +{ +#ifdef ZTS + storage->zts_lock = tsrm_mutex_alloc(); + + return storage->zts_lock != NULL; +#else + (void) storage; + + return true; +#endif +} + +static inline void user_cache_zts_lock_destroy(php_user_cache_storage *storage) +{ +#ifdef ZTS + tsrm_mutex_free(storage->zts_lock); + storage->zts_lock = NULL; +#else + (void) storage; +#endif +} + +static inline bool user_cache_zts_lock(php_user_cache_storage *storage) +{ +#ifdef ZTS + return tsrm_mutex_lock(storage->zts_lock) == 0; +#else + (void) storage; + + return true; +#endif +} + +static inline void user_cache_zts_unlock(php_user_cache_storage *storage) +{ +#ifdef ZTS + tsrm_mutex_unlock(storage->zts_lock); +#else + (void) storage; +#endif +} + +static inline void user_cache_startup_lock_acquire(php_user_cache_startup_lock *lock) +{ +#ifdef ZTS +# ifdef ZEND_WIN32 + AcquireSRWLockExclusive(lock); +# else + pthread_mutex_lock(lock); +# endif +#else + (void) lock; +#endif +} + +static inline void user_cache_startup_lock_release(php_user_cache_startup_lock *lock) +{ +#ifdef ZTS +# ifdef ZEND_WIN32 + ReleaseSRWLockExclusive(lock); +# else + pthread_mutex_unlock(lock); +# endif +#else + (void) lock; +#endif +} + +static inline uint32_t user_cache_sleep_entry_lock_retry_interval(void) +{ +#ifdef ZEND_WIN32 + Sleep(PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US / 1000); +#elif defined(HAVE_UNISTD_H) + usleep(PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US); +#endif + + return PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US; +} + +static inline uint64_t user_cache_process_start_time_token(uint64_t pid) +{ +#ifdef ZEND_WIN32 + FILETIME creation, exit_time, kernel_time, user_time; + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) pid); + uint64_t start_time = 0; + + if (process == NULL) { + return 0; + } + + if (GetProcessTimes(process, &creation, &exit_time, &kernel_time, &user_time)) { + start_time = ((uint64_t) creation.dwHighDateTime << 32) | creation.dwLowDateTime; + } + + CloseHandle(process); + + return start_time; +#elif defined(__linux__) + const char *p; + ssize_t stat_len; + int fd, field; + unsigned long long start_time = 0; + char path[64], stat_buf[1024]; + + snprintf(path, sizeof(path), "/proc/%llu/stat", (unsigned long long) pid); + fd = open(path, O_RDONLY); + if (fd < 0) { + return 0; + } + + stat_len = read(fd, stat_buf, sizeof(stat_buf) - 1); + close(fd); + if (stat_len <= 0) { + return 0; + } + + stat_buf[stat_len] = '\0'; + + /* Field 22 follows the closing parenthesis of comm. */ + p = strrchr(stat_buf, ')'); + if (p == NULL) { + return 0; + } + + p++; + for (field = 0; field < 19 && *p != '\0'; field++) { + while (*p == ' ') { + p++; + } + while (*p != '\0' && *p != ' ') { + p++; + } + } + + if (sscanf(p, " %llu", &start_time) != 1) { + return 0; + } + + return (uint64_t) start_time; +#elif defined(__APPLE__) || defined(__FreeBSD__) + struct kinfo_proc info; + size_t size = sizeof(info); + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int) pid }; + + if (sysctl(mib, 4, &info, &size, NULL, 0) != 0 || size < sizeof(info)) { + return 0; + } + + /* Only stable, non-zero equality is required. */ +# ifdef __APPLE__ + return (((uint64_t) info.kp_proc.p_starttime.tv_sec << 20) + | (uint64_t) info.kp_proc.p_starttime.tv_usec) + 1 + ; +# else + return (((uint64_t) info.ki_start.tv_sec << 20) + | (uint64_t) info.ki_start.tv_usec) + 1 + ; +# endif +#else + (void) pid; + + return 0; +#endif +} + +static inline bool user_cache_process_has_exited(uint64_t pid) +{ +#ifdef ZEND_WIN32 + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) pid); + DWORD exit_code = 0; + + if (process == NULL) { + return GetLastError() == ERROR_INVALID_PARAMETER; + } + + if (GetExitCodeProcess(process, &exit_code) && exit_code != STILL_ACTIVE) { + CloseHandle(process); + + return true; + } + + CloseHandle(process); + + return false; +#else + return kill((pid_t) pid, 0) == -1 && errno == ESRCH; +#endif +} + +static inline bool user_cache_startup_storage_is_complete(void) +{ +#if defined(ZTS) && defined(PHP_USER_CACHE_HAVE_OPTIMISTIC) + return user_cache_atomic_load_32(&php_user_cache_active_context()->storage.startup_complete) != 0; +#else + return false; +#endif +} + +static inline void user_cache_startup_storage_lock(void) +{ + user_cache_startup_lock_acquire(&php_user_cache_startup_storage_lock_state); +} + +static inline void user_cache_startup_storage_unlock(void) +{ + user_cache_startup_lock_release(&php_user_cache_startup_storage_lock_state); +} + +#ifdef ZEND_WIN32 +static inline void user_cache_win32_set_segment( + php_user_cache_win32_segment *segment, + HANDLE memfile, + void *mapping_base, + size_t mapping_size, + size_t requested_size +) +{ + segment->memfile = memfile; + segment->mapping_base = mapping_base; + segment->mapping_size = mapping_size; + segment->segment.p = mapping_base; + segment->segment.pos = 0; + segment->segment.size = requested_size; +} +#endif + +#if defined(PHP_USER_CACHE_HAVE_ANON_MMAP) || defined(PHP_USER_CACHE_HAVE_BOUNDARY_SHM) +static int user_cache_wrap_mapped_segment( + void *mapping, + size_t requested_size, + php_user_cache_shm_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +) +{ + php_user_cache_shm_segment *segment; + + *shared_segments_count = 1; + *shared_segments_p = (php_user_cache_shm_segment **) calloc(1, sizeof(php_user_cache_shm_segment *) + sizeof(php_user_cache_shm_segment)); + if (*shared_segments_p == NULL) { + munmap(mapping, requested_size); + *error_in = "calloc"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + segment = (php_user_cache_shm_segment *) ((char *) *shared_segments_p + sizeof(php_user_cache_shm_segment *)); + (*shared_segments_p)[0] = segment; + + segment->p = mapping; + segment->pos = 0; + segment->size = requested_size; + segment->end = requested_size; + + return PHP_USER_CACHE_ALLOC_SUCCESS; +} + +static int user_cache_munmap_detach_segment(php_user_cache_shm_segment *shared_segment) +{ + munmap(shared_segment->p, shared_segment->size); + + return 0; +} + +static size_t user_cache_mmap_segment_type_size(void) +{ + return sizeof(php_user_cache_shm_segment); +} +#endif + +#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP +static int user_cache_mmap_create_segments( + size_t requested_size, + php_user_cache_shm_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +) +{ + void *mapping; + + mapping = mmap(NULL, requested_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); + if (mapping == MAP_FAILED) { + *error_in = "mmap"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + return user_cache_wrap_mapped_segment(mapping, requested_size, shared_segments_p, shared_segments_count, error_in); +} +#endif + +#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM +static void user_cache_shared_boundary_digest( + const php_user_cache_context *ctx, + size_t requested_size, + uint8_t digest[32] +) +{ + const char *identity; + PHP_SHA256_CTX sha_ctx; + size_t identity_len, update_len = 0; + int prefix_len; + char prefix[64]; + + identity = ctx->boundary_identity != NULL + ? ctx->boundary_identity + : (ctx->lock_name != NULL ? ctx->lock_name : "") + ; + identity_len = ctx->boundary_identity != NULL + ? ctx->boundary_identity_len + : strlen(identity) + ; + prefix_len = snprintf( + prefix, + sizeof(prefix), + "PhpUserCache.boundary-shm|%u|%zu|%zu|", + PHP_USER_CACHE_VERSION, + requested_size, + identity_len + ); + + PHP_SHA256Init(&sha_ctx); + if (prefix_len > 0) { + update_len = (size_t) prefix_len; + if (update_len >= sizeof(prefix)) { + update_len = sizeof(prefix) - 1; + } + + PHP_SHA256Update(&sha_ctx, (const uint8_t *) prefix, update_len); + } + + PHP_SHA256Update(&sha_ctx, (const uint8_t *) identity, identity_len); + PHP_SHA256Final(digest, &sha_ctx); +} + +static void user_cache_shared_boundary_digest_to_identity( + const uint8_t digest[32], + uint64_t *identity_high, + uint32_t *identity_low +) +{ + *identity_high = + ((uint64_t) digest[0] << 56) | + ((uint64_t) digest[1] << 48) | + ((uint64_t) digest[2] << 40) | + ((uint64_t) digest[3] << 32) | + ((uint64_t) digest[4] << 24) | + ((uint64_t) digest[5] << 16) | + ((uint64_t) digest[6] << 8) | + (uint64_t) digest[7] + ; + *identity_low = + ((uint32_t) digest[8] << 24) | + ((uint32_t) digest[9] << 16) | + ((uint32_t) digest[10] << 8) | + (uint32_t) digest[11] + ; +} + +static void user_cache_shared_boundary_identity( + const php_user_cache_context *ctx, + size_t requested_size, + uint64_t *identity_high, + uint32_t *identity_low +) +{ + uint8_t digest[32]; + + user_cache_shared_boundary_digest(ctx, requested_size, digest); + user_cache_shared_boundary_digest_to_identity(digest, identity_high, identity_low); +} + +static void user_cache_shared_boundary_shm_name(char *buf, size_t buf_size, size_t requested_size) +{ + uint64_t identity_high; + uint32_t identity_low; + + user_cache_shared_boundary_identity( + php_user_cache_active_context(), + requested_size, + &identity_high, + &identity_low + ); + + snprintf(buf, buf_size, "/ZUC.%016" PRIx64 "%08" PRIx32, identity_high, identity_low); +} + +static void user_cache_shared_boundary_lock_name(char *buf, size_t buf_size, size_t requested_size) +{ + uint64_t identity_high; + uint32_t identity_low; + + user_cache_shared_boundary_identity( + php_user_cache_active_context(), + requested_size, + &identity_high, + &identity_low + ); + + snprintf( + buf, + buf_size, + "%s/.ZendUserCacheBnd.%016" PRIx64 "%08" PRIx32 ".lock", + UC_G(lockfile_path), + identity_high, + identity_low + ); +} + +static bool user_cache_shared_boundary_fd_is_trusted(int fd, struct stat *st) +{ + if (fstat(fd, st) != 0) { + return false; + } + + return st->st_uid == geteuid() && (st->st_mode & 0077) == 0; +} + +/* Recreate a zero-length shm object left by a failed creator. */ +static int user_cache_shared_boundary_open_shm_fd( + const char *shm_name, + size_t requested_size, + const char **error_in +) +{ + struct stat st; + uint32_t attempts; + int fd, create_attempts; + + for (create_attempts = 0; create_attempts < 2; create_attempts++) { + fd = shm_open(shm_name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + if (ftruncate(fd, (off_t) requested_size) != 0) { + close(fd); + shm_unlink(shm_name); + *error_in = "ftruncate"; + + return -1; + } + + return fd; + } + + if (errno != EEXIST) { + *error_in = "shm_open"; + + return -1; + } + + fd = shm_open(shm_name, O_RDWR, 0600); + if (fd < 0) { + *error_in = "shm_open"; + + return -1; + } + + if (!user_cache_shared_boundary_fd_is_trusted(fd, &st)) { + close(fd); + *error_in = "shm ownership"; + + return -1; + } + + for (attempts = 0; (size_t) st.st_size != requested_size && attempts < 1000; attempts++) { + if (st.st_size != 0) { + break; + } + + usleep(1000); + + if (fstat(fd, &st) != 0) { + break; + } + } + + if ((size_t) st.st_size == requested_size) { + return fd; + } + + close(fd); + + if (st.st_size == 0 && create_attempts == 0) { + shm_unlink(shm_name); + + continue; + } + + *error_in = "shm size"; + + return -1; + } + + *error_in = "shm size"; + + return -1; +} + +static int user_cache_shared_boundary_create_segments( + size_t requested_size, + php_user_cache_shm_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +) +{ + int fd; + char shm_name[64]; + void *mapping; + + if (requested_size > (size_t) SSIZE_MAX) { + *error_in = "size overflow"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + user_cache_shared_boundary_shm_name(shm_name, sizeof(shm_name), requested_size); + + fd = user_cache_shared_boundary_open_shm_fd(shm_name, requested_size, error_in); + if (fd < 0) { + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + mapping = mmap(NULL, requested_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + if (mapping == MAP_FAILED) { + *error_in = "mmap"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + return user_cache_wrap_mapped_segment(mapping, requested_size, shared_segments_p, shared_segments_count, error_in); +} + +static const php_user_cache_shm_handler_entry *user_cache_shared_boundary_handler_entry(void) +{ + static const php_user_cache_shm_handlers handlers = { + user_cache_shared_boundary_create_segments, + user_cache_munmap_detach_segment, + user_cache_mmap_segment_type_size + }; + static const php_user_cache_shm_handler_entry entry = { + "boundary-shm", &handlers + }; + + return &entry; +} +#endif + +#ifdef ZEND_WIN32 +static void user_cache_win32_format_name(char *buf, size_t buf_size, const char *name, size_t unique_id) +{ + const char *sapi_name = sapi_module.name != NULL ? sapi_module.name : ""; + php_user_cache_context *ctx = php_user_cache_active_context(); + + snprintf( + buf, + buf_size, + "%s@%.32s@%.20s@%.32s@%s@%zx", + name, + zend_system_id, + sapi_name, + zend_system_id, + ctx->lock_name, + unique_id + ); +} + +static int user_cache_win32_reattach_segment( + php_user_cache_win32_segment *segment, + HANDLE memfile, + size_t requested_size, + const char **error_in +) +{ + MEMORY_BASIC_INFORMATION info; + void *mapping_base; + + mapping_base = MapViewOfFileEx(memfile, FILE_MAP_ALL_ACCESS, 0, 0, 0, NULL); + if (mapping_base == NULL) { + *error_in = "MapViewOfFileEx"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + /* The mapped object must cover the requested range. */ + if (VirtualQuery(mapping_base, &info, sizeof(info)) == 0 || + info.RegionSize < requested_size + ) { + UnmapViewOfFile(mapping_base); + *error_in = "VirtualQuery"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + user_cache_win32_set_segment(segment, memfile, mapping_base, requested_size, requested_size); + + return PHP_USER_CACHE_ALLOC_SUCCESS; +} + +static int user_cache_win32_create_segment( + php_user_cache_win32_segment *segment, + const char *mapping_name, + size_t requested_size, + const char **error_in +) +{ + HANDLE memfile; + DWORD size_high, size_low; + int result; + void *mapping_base; + +#if defined(_WIN64) + size_high = (DWORD) (requested_size >> 32); + size_low = (DWORD) (requested_size & 0xffffffff); +#else + if (requested_size > UINT32_MAX) { + *error_in = "size overflow"; + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + size_high = 0; + size_low = (DWORD) requested_size; +#endif + + memfile = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE | SEC_COMMIT, size_high, size_low, mapping_name); + if (memfile == NULL) { + *error_in = "CreateFileMappingA"; + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + if (GetLastError() == ERROR_ALREADY_EXISTS) { + result = user_cache_win32_reattach_segment(segment, memfile, requested_size, error_in); + if (result != PHP_USER_CACHE_ALLOC_SUCCESS) { + CloseHandle(memfile); + } + + return result; + } + + mapping_base = MapViewOfFileEx(memfile, FILE_MAP_ALL_ACCESS, 0, 0, 0, NULL); + if (mapping_base == NULL) { + CloseHandle(memfile); + *error_in = "MapViewOfFileEx"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + user_cache_win32_set_segment(segment, memfile, mapping_base, requested_size, requested_size); + + return PHP_USER_CACHE_ALLOC_SUCCESS; +} + +static int user_cache_win32_create_segments( + size_t requested_size, + php_user_cache_shm_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +) +{ + php_user_cache_win32_segment *segment; + HANDLE mutex = NULL, memfile = NULL; + DWORD wait_result; + bool mutex_acquired = false; + int result = PHP_USER_CACHE_ALLOC_FAILURE; + char mapping_name[MAXPATHLEN], mutex_name[MAXPATHLEN]; + + *shared_segments_count = 1; + *shared_segments_p = (php_user_cache_shm_segment **) calloc( + 1, + sizeof(php_user_cache_shm_segment *) + sizeof(php_user_cache_win32_segment) + ); + if (*shared_segments_p == NULL) { + *error_in = "calloc"; + + return PHP_USER_CACHE_ALLOC_FAILURE; + } + + segment = (php_user_cache_win32_segment *) ((char *) *shared_segments_p + sizeof(php_user_cache_shm_segment *)); + (*shared_segments_p)[0] = (php_user_cache_shm_segment *) segment; + + user_cache_win32_format_name( + mapping_name, + sizeof(mapping_name), + PHP_USER_CACHE_WIN32_MAPPING_NAME, + requested_size + ); + user_cache_win32_format_name( + mutex_name, + sizeof(mutex_name), + PHP_USER_CACHE_WIN32_MAPPING_MUTEX_NAME, + requested_size + ); + + mutex = CreateMutexA(NULL, FALSE, mutex_name); + if (mutex == NULL) { + *error_in = "CreateMutexA"; + + goto failure; + } + + wait_result = WaitForSingleObject(mutex, INFINITE); + if (wait_result != WAIT_OBJECT_0 && wait_result != WAIT_ABANDONED) { + *error_in = "WaitForSingleObject"; + + goto failure; + } + + mutex_acquired = true; + + memfile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, mapping_name); + if (memfile != NULL) { + result = user_cache_win32_reattach_segment(segment, memfile, requested_size, error_in); + if (result != PHP_USER_CACHE_ALLOC_SUCCESS) { + CloseHandle(memfile); + } + } else { + result = user_cache_win32_create_segment(segment, mapping_name, requested_size, error_in); + } + + if (mutex_acquired) { + ReleaseMutex(mutex); + mutex_acquired = false; + } + + CloseHandle(mutex); + mutex = NULL; + + if (result == PHP_USER_CACHE_ALLOC_SUCCESS) { + return PHP_USER_CACHE_ALLOC_SUCCESS; + } + +failure: + if (mutex_acquired) { + ReleaseMutex(mutex); + } + + if (mutex != NULL) { + CloseHandle(mutex); + } + + free(*shared_segments_p); + + *shared_segments_p = NULL; + *shared_segments_count = 0; + + return PHP_USER_CACHE_ALLOC_FAILURE; +} + +static int user_cache_win32_detach_segment(php_user_cache_shm_segment *shared_segment) +{ + php_user_cache_win32_segment *segment = (php_user_cache_win32_segment *) shared_segment; + + if (segment->mapping_base != NULL) { + UnmapViewOfFile(segment->mapping_base); + segment->mapping_base = NULL; + } + + if (segment->memfile != NULL) { + CloseHandle(segment->memfile); + segment->memfile = NULL; + } + + return 0; +} + +static size_t user_cache_win32_segment_type_size(void) +{ + return sizeof(php_user_cache_win32_segment); +} +#endif + +static const php_user_cache_shm_handler_entry *user_cache_handler_table(void) +{ +#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP + static const php_user_cache_shm_handlers mmap_handlers = { + user_cache_mmap_create_segments, + user_cache_munmap_detach_segment, + user_cache_mmap_segment_type_size + }; +#endif +#ifdef ZEND_WIN32 + static const php_user_cache_shm_handlers win32_handlers = { + user_cache_win32_create_segments, + user_cache_win32_detach_segment, + user_cache_win32_segment_type_size + }; +#endif + static const php_user_cache_shm_handler_entry handlers[] = { +#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP + { "mmap", &mmap_handlers }, +#endif +#ifdef PHP_USER_CACHE_USE_SHM + { "shm", &php_user_cache_alloc_shm_handlers }, +#endif +#ifdef PHP_USER_CACHE_USE_SHM_OPEN + { "posix", &php_user_cache_alloc_posix_handlers }, +#endif +#ifdef ZEND_WIN32 + { "win32", &win32_handlers }, +#endif + { NULL, NULL } + }; + + return handlers; +} + +static void user_cache_cleanup_segments(const php_user_cache_shm_handlers *handler, php_user_cache_shm_segment **segments, int segment_count) +{ + int i; + + if (!handler || !segments) { + return; + } + + for (i = 0; i < segment_count; i++) { + if (segments[i]->p && segments[i]->p != (void *) -1) { + handler->detach_segment(segments[i]); + } + } + + free(segments); +} + +static bool user_cache_entry_lock_owner_is_dead_uncached(uint64_t owner_pid, uint64_t owner_start_time) +{ + uint64_t current_start_time; + + if (user_cache_process_has_exited(owner_pid)) { + return true; + } + + if (owner_start_time != 0) { + current_start_time = user_cache_process_start_time_token(owner_pid); + if (current_start_time != 0 && current_start_time != owner_start_time) { + return true; + } + } + + return false; +} + +static bool user_cache_entry_lock_owner_is_dead(uint64_t owner_pid, uint64_t owner_start_time) +{ + uint64_t now = (uint64_t) time(NULL); + + if (owner_pid == UC_G(entry_lock_owner_probe_pid) && + owner_start_time == UC_G(entry_lock_owner_probe_start_time) && + now == UC_G(entry_lock_owner_probe_at) + ) { + return UC_G(entry_lock_owner_probe_dead); + } + + UC_G(entry_lock_owner_probe_dead) = user_cache_entry_lock_owner_is_dead_uncached(owner_pid, owner_start_time); + UC_G(entry_lock_owner_probe_pid) = owner_pid; + UC_G(entry_lock_owner_probe_start_time) = owner_start_time; + UC_G(entry_lock_owner_probe_at) = now; + + return UC_G(entry_lock_owner_probe_dead); +} + +static bool user_cache_recovery_lock_key_is_readable( + const php_user_cache_header *header, + const php_user_cache_entry_lock_record *record) +{ + uint32_t data_end; + + if (record->key_offset == 0 || record->key_len == 0 || record->key_offset < header->data_offset) { + return false; + } + + if (header->data_size > UINT32_MAX - header->data_offset) { + return false; + } + + data_end = header->data_offset + header->data_size; + if (record->key_offset > data_end) { + return false; + } + + return record->key_len <= data_end - record->key_offset; +} + +/* Dead owners may remain live until their lease expires. */ +static bool user_cache_entry_lock_record_is_active_locked( + php_user_cache_entry_lock_record *record, + uint64_t now, + bool demote_dead_owner) +{ + if (record->state != PHP_USER_CACHE_ENTRY_LOCK_USED) { + return false; + } + + if (record->expires_at != 0 && record->expires_at <= now) { + return false; + } + + if (record->owner_pid == 0) { + return record->expires_at != 0; + } + + if (user_cache_entry_lock_owner_is_dead(record->owner_pid, record->owner_start_time)) { + if (!demote_dead_owner) { + return false; + } + + record->owner_pid = 0; + record->owner_start_time = 0; + + return record->expires_at != 0; + } + + return true; +} + +static php_user_cache_recovered_entry_lock *user_cache_collect_recovery_entry_locks( + php_user_cache_header *header, + uint32_t *count_ptr) +{ + php_user_cache_recovered_entry_lock *locks; + php_user_cache_entry_lock_record *record; + uint64_t now = (uint64_t) time(NULL); + uint32_t i, count = 0; + char *key; + + for (i = 0; i < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; i++) { + record = &header->entry_lock_records[i]; + if (user_cache_entry_lock_record_is_active_locked(record, now, false) && + user_cache_recovery_lock_key_is_readable(header, record) + ) { + count++; + } + } + + *count_ptr = 0; + if (count == 0) { + return NULL; + } + + locks = (php_user_cache_recovered_entry_lock *) calloc(count, sizeof(*locks)); + if (locks == NULL) { + return NULL; + } + + for (i = 0; i < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; i++) { + record = &header->entry_lock_records[i]; + if (!user_cache_entry_lock_record_is_active_locked(record, now, false) || + !user_cache_recovery_lock_key_is_readable(header, record) + ) { + continue; + } + + key = (char *) malloc(record->key_len); + if (key == NULL) { + continue; + } + + memcpy(key, php_user_cache_ptr(record->key_offset), record->key_len); + + locks[*count_ptr].key = key; + locks[*count_ptr].key_len = record->key_len; + locks[*count_ptr].hash = record->hash; + locks[*count_ptr].owner_pid = record->owner_pid; + locks[*count_ptr].owner_start_time = record->owner_start_time; + locks[*count_ptr].expires_at = record->expires_at; + + (*count_ptr)++; + } + + return locks; +} + +static void user_cache_free_recovery_entry_locks( + php_user_cache_recovered_entry_lock *locks, + uint32_t count) +{ + uint32_t i; + + if (locks == NULL) { + return; + } + + for (i = 0; i < count; i++) { + free(locks[i].key); + } + + free(locks); +} + +static void user_cache_restore_recovery_entry_locks( + php_user_cache_header *header, + php_user_cache_recovered_entry_lock *locks, + uint32_t count) +{ + php_user_cache_entry_lock_record *record; + uint32_t i, probe, slot_idx, key_offset; + + for (i = 0; i < count; i++) { + if (locks[i].key == NULL) { + continue; + } + + for (probe = 0; probe < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; probe++) { + slot_idx = (user_cache_entry_lock_table_index(locks[i].hash) + probe) % + PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE + ; + + record = &header->entry_lock_records[slot_idx]; + if (record->state == PHP_USER_CACHE_ENTRY_LOCK_USED) { + continue; + } + + key_offset = php_user_cache_alloc_locked(locks[i].key_len, locks[i].key); + if (key_offset == 0) { + break; + } + + memset(record, 0, sizeof(*record)); + + record->hash = locks[i].hash; + record->owner_pid = locks[i].owner_pid; + record->owner_start_time = locks[i].owner_start_time; + record->expires_at = locks[i].expires_at; + record->key_offset = key_offset; + record->key_len = locks[i].key_len; + record->state = PHP_USER_CACHE_ENTRY_LOCK_USED; + + break; + } + } +} + +#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM +static bool user_cache_header_boundary_identity_matches_locked( + const php_user_cache_header *header, + size_t requested_size) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + php_user_cache_storage *storage = &ctx->storage; + + if (!ctx->boundary_shared) { + return header->boundary_identity_digest_set == 0; + } + + if (header->boundary_identity_digest_set == 0) { + return false; + } + + /* The boundary identity is immutable for the partition lifetime. */ + if (!storage->boundary_digest_memoized) { + user_cache_shared_boundary_digest(ctx, requested_size, storage->boundary_digest_memo); + storage->boundary_digest_memoized = true; + } + + return memcmp(header->boundary_identity_digest, storage->boundary_digest_memo, sizeof(storage->boundary_digest_memo)) == 0; +} +#endif + +/* Recover an interrupted write while holding the exclusive lock. */ +static void user_cache_recover_after_owner_death(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + php_user_cache_recovered_entry_lock *locks; + php_user_cache_reader_slot *slot; + uint64_t owner_pid, owner_start_time; + uint32_t lock_count = 0, i; + + if (header == NULL || !php_user_cache_header_is_initialized_locked()) { + return; + } + + if ((header->write_seq & 1) == 0) { + return; + } + + locks = user_cache_collect_recovery_entry_locks(header, &lock_count); + + memset( + php_user_cache_entries_ptr(header), + 0, + (size_t) header->capacity * sizeof(php_user_cache_entry) + ); + memset(header->entry_lock_records, 0, sizeof(header->entry_lock_records)); + memset(header->orphaned_graphs, 0, sizeof(header->orphaned_graphs)); + + for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) { + slot = &header->reader_slots[i]; + + /* Reader slots may be claimed lock-free. */ +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + owner_pid = php_user_cache_atomic_load_64(&slot->owner_pid); + owner_start_time = php_user_cache_atomic_load_64(&slot->owner_start_time); +#else + owner_pid = slot->owner_pid; + owner_start_time = slot->owner_start_time; +#endif + + if (owner_pid != 0 && + user_cache_entry_lock_owner_is_dead(owner_pid, owner_start_time) + ) { +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + /* Clear start time before pid so scanners cannot combine owners. */ + user_cache_atomic_cas_32(&slot->active, 1, 0); + user_cache_atomic_store_64(&slot->owner_start_time, 0); + user_cache_atomic_cas_64(&slot->owner_pid, owner_pid, 0); +#else + memset(slot, 0, sizeof(*slot)); +#endif + } + } + + header->count = 0; + header->tombstone_count = 0; + header->next_free = 0; + header->free_list = 0; + header->last_block_offset = 0; + header->orphaned_graphs_saturated = 0; + + user_cache_restore_recovery_entry_locks(header, locks, lock_count); + user_cache_free_recovery_entry_locks(locks, lock_count); + + php_user_cache_bump_mutation_epoch_locked(header); + + user_cache_seq_publish(&header->write_seq, header->write_seq + 1); +} + +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX +static bool user_cache_shared_mutex_init(php_user_cache_header *header) +{ + pthread_mutexattr_t attr; + bool result; + + if (pthread_mutexattr_init(&attr) != 0) { + return false; + } + + result = pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) == 0 && + pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST) == 0 && + pthread_mutex_init(&header->global_shared_mutex.mutex, &attr) == 0 + ; + + pthread_mutexattr_destroy(&attr); + + return result; +} + +static bool user_cache_shared_mutex_lock(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + int result; + + if (header == NULL) { + return false; + } + + result = pthread_mutex_lock(&header->global_shared_mutex.mutex); + if (result == EOWNERDEAD) { + /* A failed robust-mutex recovery makes the segment unusable. */ + if (pthread_mutex_consistent(&header->global_shared_mutex.mutex) != 0) { + php_error_docref(NULL, E_WARNING, "Cache: unable to make the shared lock consistent after owner death"); + pthread_mutex_unlock(&header->global_shared_mutex.mutex); + + return false; + } + + user_cache_recover_after_owner_death(); + + result = 0; + } + + if (result != 0) { + return false; + } + + UC_G(lock_held) = true; + + return true; +} +#endif + +#ifndef ZEND_WIN32 +static bool user_cache_lock_impl(short lock_type) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + struct flock mem_lock; + + if (!storage->lock_initialized) { + return false; + } + +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX + if (storage->use_shared_mutex) { + return user_cache_shared_mutex_lock(); + } +#endif + + if (!user_cache_zts_lock(storage)) { + return false; + } + + mem_lock.l_type = lock_type; + mem_lock.l_whence = SEEK_SET; + mem_lock.l_start = 0; + mem_lock.l_len = 1; + + while (fcntl(storage->lock_file, F_SETLKW, &mem_lock) == -1) { + if (errno != EINTR) { + user_cache_zts_unlock(storage); + + return false; + } + } + + UC_G(lock_held) = true; + + return true; +} + +static bool user_cache_create_lock(void) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + php_user_cache_storage *storage = &ctx->storage; + int val; +#ifdef PHP_USER_CACHE_USE_SHM_OPEN + struct stat lock_st; +#endif + + if (storage->lock_initialized) { + return true; + } + + if (!user_cache_zts_lock_init(storage)) { + return false; + } + +#ifdef PHP_USER_CACHE_USE_SHM_OPEN + if (ctx->boundary_shared) { + user_cache_shared_boundary_lock_name( + storage->lockfile_name, + sizeof(storage->lockfile_name), + storage->size + ); + + /* Refuse symlinks in the predictable world-writable lock path. */ + storage->lock_file = open(storage->lockfile_name, O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600); + if (storage->lock_file >= 0 && + !user_cache_shared_boundary_fd_is_trusted(storage->lock_file, &lock_st) + ) { + close(storage->lock_file); + storage->lock_file = -1; + } + + if (storage->lock_file < 0) { + user_cache_zts_lock_destroy(storage); + + return false; + } + + storage->lock_initialized = true; + + return true; + } +#endif + +#if defined(__linux__) && defined(HAVE_MEMFD_CREATE) && defined(MFD_CLOEXEC) + storage->lock_file = memfd_create(ctx->lock_name, MFD_CLOEXEC); + if (storage->lock_file >= 0) { + storage->lock_initialized = true; + + return true; + } +#endif + +#ifdef O_TMPFILE + storage->lock_file = open(UC_G(lockfile_path), O_RDWR | O_TMPFILE | O_EXCL | O_CLOEXEC, 0666); + if (storage->lock_file >= 0) { + storage->lock_initialized = true; + + return true; + } +#endif + + snprintf( + storage->lockfile_name, + sizeof(storage->lockfile_name), + "%s/%sXXXXXX", + UC_G(lockfile_path), + ctx->sem_filename_prefix + ); + + storage->lock_file = mkstemp(storage->lockfile_name); + if (storage->lock_file == -1) { + user_cache_zts_lock_destroy(storage); + + return false; + } + + if (fchmod(storage->lock_file, 0666) == -1) { + close(storage->lock_file); + storage->lock_file = -1; + user_cache_zts_lock_destroy(storage); + + return false; + } + + val = fcntl(storage->lock_file, F_GETFD, 0); + val |= FD_CLOEXEC; + fcntl(storage->lock_file, F_SETFD, val); + unlink(storage->lockfile_name); + + storage->lock_initialized = true; + + return true; +} + +static void user_cache_destroy_lock(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return; + } + + if (storage->lock_file >= 0) { + close(storage->lock_file); + storage->lock_file = -1; + } + + user_cache_zts_lock_destroy(storage); + + storage->lock_initialized = false; +} + +static bool user_cache_rlock_impl(void) +{ + return user_cache_lock_impl(F_RDLCK); +} + +static bool user_cache_wlock_impl(void) +{ + return user_cache_lock_impl(F_WRLCK); +} + +static void user_cache_unlock_impl(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + struct flock mem_unlock; + + if (!storage->lock_initialized) { + return; + } + +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX + if (storage->use_shared_mutex) { + php_user_cache_header *header = php_user_cache_header_ptr(); + + if (header != NULL) { + pthread_mutex_unlock(&header->global_shared_mutex.mutex); + } + + UC_G(lock_held) = false; + + return; + } +#endif + + mem_unlock.l_type = F_UNLCK; + mem_unlock.l_whence = SEEK_SET; + mem_unlock.l_start = 0; + mem_unlock.l_len = 1; + fcntl(storage->lock_file, F_SETLK, &mem_unlock); + + user_cache_zts_unlock(storage); + UC_G(lock_held) = false; +} +#else +static bool user_cache_win32_open_lock_file_at(php_user_cache_storage *storage, const char *dir, const char *base_name) +{ + size_t dir_len; + const char *sep; + + if (dir == NULL || dir[0] == '\0') { + return false; + } + + dir_len = strlen(dir); + sep = dir[dir_len - 1] == '/' || dir[dir_len - 1] == '\\' + ? "" + : "/" + ; + + snprintf( + storage->lockfile_name, + sizeof(storage->lockfile_name), + "%s%s%s.lock", + dir, + sep, + base_name + ); + + storage->lock_file = php_win32_ioutil_open(storage->lockfile_name, O_RDWR | O_CREAT | O_BINARY, 0666); + + return storage->lock_file >= 0; +} + +static bool user_cache_win32_open_lock_file(php_user_cache_context *ctx) +{ + php_user_cache_storage *storage = &ctx->storage; + DWORD tmp_path_w_len; + wchar_t tmp_path_w[MAXPATHLEN]; + size_t tmp_path_len; + bool opened; + char base_name[MAXPATHLEN], *tmp_path, *p; + + user_cache_win32_format_name( + base_name, + sizeof(base_name), + PHP_USER_CACHE_WIN32_LOCK_FILE_NAME, + storage->size + ); + + for (p = base_name; *p != '\0'; p++) { + if ((uint8_t) *p < 32 || + *p == '<' || + *p == '>' || + *p == ':' || + *p == '"' || + *p == '/' || + *p == '\\' || + *p == '|' || + *p == '?' || + *p == '*' + ) { + *p = '_'; + } + } + + /* Preserve temporary paths outside the ANSI codepage. */ + tmp_path_w_len = GetTempPathW(MAXPATHLEN, tmp_path_w); + if (tmp_path_w_len == 0 || tmp_path_w_len >= MAXPATHLEN) { + return false; + } + + tmp_path = php_win32_ioutil_conv_w_to_any(tmp_path_w, tmp_path_w_len, &tmp_path_len); + if (tmp_path == NULL) { + return false; + } + + opened = user_cache_win32_open_lock_file_at(storage, tmp_path, base_name); + free(tmp_path); + + return opened; +} + +static bool user_cache_win32_lock_range(php_user_cache_storage *storage, DWORD offset, bool exclusive, bool blocking) +{ + OVERLAPPED overlapped; + HANDLE handle; + DWORD flags = 0; + + if (!storage->lock_initialized || storage->lock_file < 0) { + return false; + } + + handle = (HANDLE) _get_osfhandle(storage->lock_file); + if (handle == INVALID_HANDLE_VALUE) { + return false; + } + + memset(&overlapped, 0, sizeof(overlapped)); + overlapped.Offset = offset; + + if (exclusive) { + flags |= LOCKFILE_EXCLUSIVE_LOCK; + } + + if (!blocking) { + flags |= LOCKFILE_FAIL_IMMEDIATELY; + } + + return LockFileEx(handle, flags, 0, 1, 0, &overlapped) == TRUE; +} + +static void user_cache_win32_unlock_range(php_user_cache_storage *storage, DWORD offset) +{ + OVERLAPPED overlapped; + HANDLE handle; + + if (!storage->lock_initialized || storage->lock_file < 0) { + return; + } + + handle = (HANDLE) _get_osfhandle(storage->lock_file); + if (handle == INVALID_HANDLE_VALUE) { + return; + } + + memset(&overlapped, 0, sizeof(overlapped)); + overlapped.Offset = offset; + + UnlockFileEx(handle, 0, 1, 0, &overlapped); +} + +static bool user_cache_lock_impl(bool exclusive) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return false; + } + + if (!user_cache_zts_lock(storage)) { + return false; + } + + if (!user_cache_win32_lock_range(storage, 0, exclusive, true)) { + user_cache_zts_unlock(storage); + + return false; + } + + UC_G(lock_held) = true; + + return true; +} + +static bool user_cache_create_lock(void) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + php_user_cache_storage *storage = &ctx->storage; + + if (storage->lock_initialized) { + return true; + } + + if (!user_cache_zts_lock_init(storage)) { + return false; + } + + if (!user_cache_win32_open_lock_file(ctx)) { + user_cache_zts_lock_destroy(storage); + + return false; + } + + storage->lock_initialized = true; + + return true; +} + +static void user_cache_destroy_lock(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return; + } + + if (storage->lock_file >= 0) { + php_win32_ioutil_close(storage->lock_file); + storage->lock_file = -1; + } + + user_cache_zts_lock_destroy(storage); + + storage->lock_initialized = false; +} + +static bool user_cache_rlock_impl(void) +{ + return user_cache_lock_impl(false); +} + +static bool user_cache_wlock_impl(void) +{ + return user_cache_lock_impl(true); +} + +static void user_cache_unlock_impl(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return; + } + + user_cache_win32_unlock_range(storage, 0); + + user_cache_zts_unlock(storage); + + UC_G(lock_held) = false; +} +#endif + +static uint64_t user_cache_entry_lock_expires_at(zend_long lease) +{ + uint64_t now, lease_seconds; + + ZEND_ASSERT(lease > 0); + + now = (uint64_t) time(NULL); + lease_seconds = (uint64_t) lease; + if (lease_seconds > UINT64_MAX - now) { + return UINT64_MAX; + } + + return now + lease_seconds; +} + +static void user_cache_remove_entry_lock_record_locked( + php_user_cache_entry_lock_record *record) +{ + if (record->state == PHP_USER_CACHE_ENTRY_LOCK_USED && record->key_offset != 0) { + php_user_cache_free_locked(record->key_offset); + } + + memset(record, 0, sizeof(*record)); + + record->state = PHP_USER_CACHE_ENTRY_LOCK_TOMBSTONE; +} + +static bool user_cache_find_entry_lock_record_slot_locked( + php_user_cache_header *header, + zend_string *key, + zend_ulong hash, + uint32_t *slot_idx, + bool *found) +{ + php_user_cache_entry_lock_record *record; + uint64_t now = 0; + uint32_t first_avail = UINT32_MAX, i, probe; + + *found = false; + + for (probe = 0; probe < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; probe++) { + i = (user_cache_entry_lock_table_index(hash) + probe) % PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; + record = &header->entry_lock_records[i]; + + if (record->state == PHP_USER_CACHE_ENTRY_LOCK_EMPTY) { + *slot_idx = first_avail != UINT32_MAX ? first_avail : i; + + return true; + } + + if (record->state == PHP_USER_CACHE_ENTRY_LOCK_TOMBSTONE) { + if (first_avail == UINT32_MAX) { + first_avail = i; + } + + continue; + } + + /* Avoid a clock read for an empty table. */ + if (now == 0) { + now = (uint64_t) time(NULL); + } + + if (!user_cache_entry_lock_record_is_active_locked(record, now, true)) { + user_cache_remove_entry_lock_record_locked(record); + + if (first_avail == UINT32_MAX) { + first_avail = i; + } + + continue; + } + + if (user_cache_entry_lock_record_key_matches(record, key, hash)) { + *slot_idx = i; + *found = true; + + return true; + } + } + + if (first_avail != UINT32_MAX) { + *slot_idx = first_avail; + + return true; + } + + return false; +} + +static bool user_cache_insert_entry_lock_record_locked( + php_user_cache_header *header, + uint32_t slot_idx, + zend_string *key, + zend_ulong hash, + zend_long lease) +{ + php_user_cache_entry_lock_record *record = &header->entry_lock_records[slot_idx]; + uint64_t owner_pid; + uint32_t key_offset; + + if (ZSTR_LEN(key) > UINT32_MAX) { + return false; + } + + key_offset = php_user_cache_alloc_locked(ZSTR_LEN(key), ZSTR_VAL(key)); + if (key_offset == 0) { + return false; + } + + owner_pid = php_user_cache_cached_pid(); + + memset(record, 0, sizeof(*record)); + + record->hash = hash; + record->owner_pid = owner_pid; + record->owner_start_time = user_cache_cached_self_start_time_token(owner_pid); + record->expires_at = lease > 0 ? user_cache_entry_lock_expires_at(lease) : 0; + record->key_offset = key_offset; + record->key_len = (uint32_t) ZSTR_LEN(key); + record->state = PHP_USER_CACHE_ENTRY_LOCK_USED; + + return true; +} + +static bool user_cache_update_entry_lock_record_lease_locked( + php_user_cache_header *header, + zend_string *key, + zend_long lease) +{ + php_user_cache_entry_lock_record *record; + zend_ulong hash; + uint64_t expires_at; + uint32_t slot_idx; + bool found; + + if (lease <= 0) { + return true; + } + + hash = zend_string_hash_val(key); + if (!user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found) || !found) { + return false; + } + + record = &header->entry_lock_records[slot_idx]; + expires_at = user_cache_entry_lock_expires_at(lease); + if (record->expires_at < expires_at) { + record->expires_at = expires_at; + } + + return true; +} + +static php_user_cache_entry_lock *user_cache_create_local_entry_lock( + php_user_cache_context *ctx, + zend_long lease, + bool preserve_lease) +{ + php_user_cache_entry_lock *lock; + + lock = emalloc(sizeof(php_user_cache_entry_lock)); + lock->context = ctx; + lock->owner_pid = php_user_cache_cached_pid(); + lock->lease = lease; + lock->preserve_lease = preserve_lease; + + return lock; +} + +static bool user_cache_add_local_entry_lock( + HashTable *locks, + zend_string *key, + php_user_cache_entry_lock *lock) +{ + bool added = false; + + zend_try { + added = zend_hash_add_ptr(locks, key, lock) != NULL; + } zend_catch { + efree(lock); + zend_bailout(); + } zend_end_try(); + + return added; +} + +static void user_cache_release_entry_lock_records_locked( + php_user_cache_header *header, + const php_user_cache_entry_lock_release_pair *pairs, + uint32_t pair_count, + php_user_cache_entry_lock_release_mode mode) +{ + php_user_cache_entry_lock *lock; + zend_ulong hash; + zend_string *key; + uint32_t i, slot_idx; + bool found; + + for (i = 0; i < pair_count; i++) { + key = pairs[i].key; + lock = pairs[i].lock; + + hash = zend_string_hash_val(key); + if (!user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found) || !found) { + continue; + } + + if (header->entry_lock_records[slot_idx].owner_pid != lock->owner_pid) { + continue; + } + + if (mode == PHP_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES && + lock->preserve_lease && + lock->lease > 0 + ) { + header->entry_lock_records[slot_idx].owner_pid = 0; + header->entry_lock_records[slot_idx].owner_start_time = 0; + header->entry_lock_records[slot_idx].expires_at = + user_cache_entry_lock_expires_at(lock->lease) + ; + } else { + user_cache_remove_entry_lock_record_locked(&header->entry_lock_records[slot_idx]); + } + } +} + +static void user_cache_destroy_entry_locks_if_empty(HashTable **locks_ptr) +{ + if (*locks_ptr != NULL && zend_hash_num_elements(*locks_ptr) == 0) { + zend_hash_destroy(*locks_ptr); + FREE_HASHTABLE(*locks_ptr); + *locks_ptr = NULL; + } +} + +static void user_cache_release_entry_locks_for_context( + php_user_cache_context *ctx, + HashTable **locks_ptr, + php_user_cache_entry_lock_release_mode mode) +{ + php_user_cache_context *prev_ctx; + php_user_cache_entry_lock *lock; + php_user_cache_header *header; + php_user_cache_entry_lock_release_pair *pairs; + zend_string *key; + uint32_t i, lock_count, pair_count = 0; + + if (*locks_ptr == NULL) { + return; + } + + lock_count = zend_hash_num_elements(*locks_ptr); + if (lock_count == 0 || mode == PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP) { + zend_hash_destroy(*locks_ptr); + FREE_HASHTABLE(*locks_ptr); + *locks_ptr = NULL; + + return; + } + + pairs = safe_emalloc(lock_count, sizeof(php_user_cache_entry_lock_release_pair), 0); + + ZEND_HASH_FOREACH_STR_KEY_PTR(*locks_ptr, key, lock) { + if (key == NULL || lock == NULL || lock->context != ctx) { + continue; + } + + pairs[pair_count].key = zend_string_copy(key); + pairs[pair_count].lock = lock; + + pair_count++; + } ZEND_HASH_FOREACH_END(); + + if (pair_count == 0) { + efree(pairs); + + return; + } + + prev_ctx = php_user_cache_activate_context(ctx); + if (php_user_cache_wlock()) { + header = php_user_cache_header_ptr(); + + if (php_user_cache_header_init_locked()) { + user_cache_release_entry_lock_records_locked( + header, + pairs, + pair_count, + mode + ); + } + + php_user_cache_unlock(); + } + + php_user_cache_restore_context(prev_ctx); + + for (i = 0; i < pair_count; i++) { + zend_hash_del(*locks_ptr, pairs[i].key); + zend_string_release(pairs[i].key); + } + + efree(pairs); + + user_cache_destroy_entry_locks_if_empty(locks_ptr); +} + +static void user_cache_release_entry_locks_all_contexts( + HashTable **locks_ptr, + php_user_cache_entry_lock_release_mode mode) +{ + php_user_cache_context *ctx = NULL; + php_user_cache_entry_lock *lock; + + while (*locks_ptr != NULL) { + ctx = NULL; + + ZEND_HASH_FOREACH_PTR(*locks_ptr, lock) { + if (lock != NULL && lock->context != NULL) { + ctx = lock->context; + + break; + } + } ZEND_HASH_FOREACH_END(); + + if (ctx == NULL) { + user_cache_release_entry_locks_for_context( + php_user_cache_owning_context(), + locks_ptr, + PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP + ); + + return; + } + + user_cache_release_entry_locks_for_context(ctx, locks_ptr, mode); + } +} + +static void user_cache_ensure_entry_lock_owner(void) +{ +#ifndef ZEND_WIN32 + zend_ulong cur_pid = (zend_ulong) php_user_cache_cached_pid(); + + if (UC_G(entry_lock_owner_pid) == 0) { + UC_G(entry_lock_owner_pid) = cur_pid; + + return; + } + + if (UC_G(entry_lock_owner_pid) == cur_pid) { + return; + } + + user_cache_release_entry_locks_for_context( + php_user_cache_owning_context(), + &UC_G(entry_lock_table), + PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP + ); + + UC_G(entry_lock_owner_pid) = cur_pid; +#endif +} + +static void user_cache_entry_lock_dtor(zval *lock_zv) +{ + php_user_cache_entry_lock *lock = Z_PTR_P(lock_zv); + + if (lock != NULL) { + efree(lock); + } +} + +static HashTable *user_cache_prepare_entry_locks_for_insert(uint32_t reserve) +{ + HashTable **locks_ptr = user_cache_entry_lock_table_ptr(); + + if (*locks_ptr == NULL) { + ALLOC_HASHTABLE(*locks_ptr); + zend_hash_init(*locks_ptr, 0, NULL, user_cache_entry_lock_dtor, 0); + } + + zend_hash_extend(*locks_ptr, zend_hash_num_elements(*locks_ptr) + reserve, 0); + + return *locks_ptr; +} + +static bool user_cache_upgrade_held_entry_lock( + php_user_cache_context *ctx, + zend_string *key, + zend_long lease, + bool preserve_lease, + php_user_cache_entry_lock *lock) +{ + php_user_cache_context *prev_ctx; + php_user_cache_header *header; + bool updated; + + if (lease > lock->lease) { + prev_ctx = php_user_cache_activate_context(ctx); + if (!php_user_cache_wlock()) { + php_user_cache_restore_context(prev_ctx); + + return false; + } + + header = php_user_cache_header_ptr(); + updated = php_user_cache_header_init_locked() && + user_cache_update_entry_lock_record_lease_locked(header, key, lease) + ; + + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + if (!updated) { + return false; + } + + lock->lease = lease; + } + + lock->preserve_lease = lock->preserve_lease || preserve_lease; + + return true; +} + +static bool user_cache_remove_owned_entry_lock_record( + php_user_cache_context *ctx, + zend_string *key, + zend_ulong hash, + uint64_t owner_pid) +{ + php_user_cache_context *prev_ctx; + php_user_cache_header *header; + uint32_t slot_idx; + bool found; + + prev_ctx = php_user_cache_activate_context(ctx); + if (!php_user_cache_wlock()) { + php_user_cache_restore_context(prev_ctx); + + return false; + } + + header = php_user_cache_header_ptr(); + + if (php_user_cache_header_is_initialized_locked() && + user_cache_find_entry_lock_record_slot_locked( + header, + key, + hash, + &slot_idx, + &found + ) && + found && + header->entry_lock_records[slot_idx].owner_pid == owner_pid + ) { + user_cache_remove_entry_lock_record_locked( + &header->entry_lock_records[slot_idx] + ); + } + + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + return true; +} + +/* Release all flagged entry locks under one write lock. */ +static void user_cache_release_entry_locks_for_keys( + php_user_cache_context *ctx, + zend_string **keys, + const bool *acquired, + uint32_t count) +{ + php_user_cache_context *prev_ctx; + php_user_cache_entry_lock *lock; + php_user_cache_header *header; + zend_ulong hash; + HashTable **locks_ptr = user_cache_entry_lock_table_ptr(); + uint32_t i, slot_idx; + bool found, any_held = false; + + user_cache_ensure_entry_lock_owner(); + + for (i = 0; i < count; i++) { + if (acquired[i] && user_cache_find_local_entry_lock(ctx, keys[i]) != NULL) { + any_held = true; + + break; + } + } + + if (!any_held) { + return; + } + + prev_ctx = php_user_cache_activate_context(ctx); + + if (!php_user_cache_wlock()) { + php_user_cache_restore_context(prev_ctx); + + return; + } + + header = php_user_cache_header_ptr(); + + if (php_user_cache_header_is_initialized_locked()) { + for (i = 0; i < count; i++) { + if (!acquired[i]) { + continue; + } + + lock = user_cache_find_local_entry_lock(ctx, keys[i]); + if (lock == NULL) { + continue; + } + + hash = zend_string_hash_val(keys[i]); + if (user_cache_find_entry_lock_record_slot_locked(header, keys[i], hash, &slot_idx, &found) && + found && + header->entry_lock_records[slot_idx].owner_pid == lock->owner_pid + ) { + user_cache_remove_entry_lock_record_locked(&header->entry_lock_records[slot_idx]); + } + } + } + + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + for (i = 0; i < count; i++) { + if (acquired[i] && user_cache_find_local_entry_lock(ctx, keys[i]) != NULL) { + zend_hash_del(*locks_ptr, keys[i]); + } + } + + user_cache_destroy_entry_locks_if_empty(locks_ptr); +} + +static bool user_cache_acquire_entry_lock_record( + php_user_cache_context *ctx, + zend_string *key, + zend_long lease, + bool preserve_lease, + bool blocking) +{ + php_user_cache_context *prev_ctx; + php_user_cache_entry_lock *lock; + php_user_cache_header *header; + zend_ulong hash = zend_string_hash_val(key); + HashTable *locks, **locks_ptr = user_cache_entry_lock_table_ptr(); + uint64_t waited_us = 0; + uint32_t slot_idx; + bool found, inserted, insert_failed; + + user_cache_ensure_entry_lock_owner(); + + lock = user_cache_find_local_entry_lock(ctx, key); + if (lock != NULL) { + return user_cache_upgrade_held_entry_lock(ctx, key, lease, preserve_lease, lock); + } + + locks = user_cache_prepare_entry_locks_for_insert(1); + lock = user_cache_create_local_entry_lock(ctx, lease, preserve_lease); + + for (;;) { + inserted = false; + found = false; + insert_failed = false; + prev_ctx = php_user_cache_activate_context(ctx); + + if (!php_user_cache_wlock()) { + php_user_cache_restore_context(prev_ctx); + efree(lock); + user_cache_destroy_entry_locks_if_empty(locks_ptr); + + return false; + } + + header = php_user_cache_header_ptr(); + + if (!php_user_cache_header_init_locked()) { + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + efree(lock); + user_cache_destroy_entry_locks_if_empty(locks_ptr); + + return false; + } + + if (user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found)) { + if (!found && + !(inserted = user_cache_insert_entry_lock_record_locked(header, slot_idx, key, hash, lease)) + ) { + insert_failed = true; + } + } + + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + if (inserted) { + if (user_cache_add_local_entry_lock(locks, key, lock)) { + return true; + } + + /* Roll back a record that cannot be tracked locally. */ + user_cache_remove_owned_entry_lock_record(ctx, key, hash, lock->owner_pid); + efree(lock); + user_cache_destroy_entry_locks_if_empty(locks_ptr); + + return false; + } + + if (insert_failed) { + efree(lock); + user_cache_destroy_entry_locks_if_empty(locks_ptr); + + return false; + } + + if (!blocking || waited_us >= PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US) { + efree(lock); + user_cache_destroy_entry_locks_if_empty(locks_ptr); + + return false; + } + + waited_us += user_cache_sleep_entry_lock_retry_interval(); + } +} + +/* Keys are sorted so concurrent bulk operations acquire locks in one order. */ +static bool user_cache_acquire_entry_lock_records( + php_user_cache_context *ctx, + zend_string **keys, + bool *acquired, + uint32_t count) +{ + php_user_cache_context *prev_ctx; + php_user_cache_entry_lock *existing, **pending_locks; + php_user_cache_header *header; + zend_ulong hash; + HashTable *locks, **locks_ptr = user_cache_entry_lock_table_ptr(); + uint64_t waited_us = 0; + uint32_t i, slot_idx, start = 0, stop, insert_end, blocked_idx = UINT32_MAX, pending_count = 0; + bool *collisions, found, blocked, insert_failed, add_failed; + + user_cache_ensure_entry_lock_owner(); + + pending_locks = safe_emalloc(count, sizeof(*pending_locks), 0); + collisions = safe_emalloc(count, sizeof(*collisions), 0); + + for (i = 0; i < count; i++) { + acquired[i] = false; + pending_locks[i] = NULL; + collisions[i] = false; + + /* Collapse adjacent duplicates. */ + if (i > 0 && zend_string_equals(keys[i], keys[i - 1])) { + continue; + } + + existing = *locks_ptr != NULL ? zend_hash_find_ptr(*locks_ptr, keys[i]) : NULL; + if (existing != NULL && existing->context == ctx) { + continue; + } + + /* Local tracking will reject a record from another context. */ + collisions[i] = existing != NULL; + pending_locks[i] = user_cache_create_local_entry_lock(ctx, 0, false); + + pending_count++; + } + + if (pending_count == 0) { + efree(collisions); + efree(pending_locks); + + return true; + } + + locks = user_cache_prepare_entry_locks_for_insert(pending_count); + + for (;;) { + stop = count; + insert_end = count; + blocked = false; + insert_failed = false; + add_failed = false; + prev_ctx = php_user_cache_activate_context(ctx); + + if (!php_user_cache_wlock()) { + php_user_cache_restore_context(prev_ctx); + + goto bailout; + } + + header = php_user_cache_header_ptr(); + + if (!php_user_cache_header_init_locked()) { + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + goto bailout; + } + + /* Stop before waiting so the global lock is not held. */ + for (i = start; i < count; i++) { + if (pending_locks[i] == NULL) { + continue; + } + + hash = zend_string_hash_val(keys[i]); + + if (!user_cache_find_entry_lock_record_slot_locked(header, keys[i], hash, &slot_idx, &found) || found) { + blocked = true; + stop = i; + insert_end = i; + + break; + } + + if (!user_cache_insert_entry_lock_record_locked(header, slot_idx, keys[i], hash, 0)) { + insert_failed = true; + stop = i; + insert_end = i; + + break; + } + + if (collisions[i]) { + /* Do not insert records beyond the known failure. */ + stop = i; + insert_end = i + 1; + + break; + } + } + + php_user_cache_unlock(); + php_user_cache_restore_context(prev_ctx); + + /* Roll back records that cannot be tracked locally. */ + for (i = start; i < insert_end; i++) { + if (pending_locks[i] == NULL) { + continue; + } + + if (add_failed || !user_cache_add_local_entry_lock(locks, keys[i], pending_locks[i])) { + user_cache_remove_owned_entry_lock_record( + ctx, + keys[i], + zend_string_hash_val(keys[i]), + pending_locks[i]->owner_pid + ); + efree(pending_locks[i]); + pending_locks[i] = NULL; + add_failed = true; + + continue; + } + + pending_locks[i] = NULL; + acquired[i] = true; + } + + if (add_failed || insert_failed) { + goto bailout; + } + + if (!blocked) { + efree(collisions); + efree(pending_locks); + + return true; + } + + if (blocked_idx != stop) { + /* Each blocked key gets a fresh wait budget. */ + blocked_idx = stop; + waited_us = 0; + } + + if (waited_us >= PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US) { + goto bailout; + } + + waited_us += user_cache_sleep_entry_lock_retry_interval(); + start = stop; + } + +bailout: + for (i = 0; i < count; i++) { + if (pending_locks[i] != NULL) { + efree(pending_locks[i]); + } + } + + efree(collisions); + efree(pending_locks); + user_cache_destroy_entry_locks_if_empty(locks_ptr); + + return false; +} + +static uint32_t user_cache_calculate_capacity(size_t size) +{ + uint32_t capacity = PHP_USER_CACHE_MIN_CAPACITY; + size_t target = size / PHP_USER_CACHE_SLOT_BYTES, data_offset; + unsigned int i; + + /* Leave room for value data after the entry table. */ + for (i = 0; i < sizeof(php_user_cache_capacity_primes) / sizeof(php_user_cache_capacity_primes[0]); i++) { + if (php_user_cache_capacity_primes[i] > target) { + break; + } + + data_offset = PHP_USER_CACHE_ALIGNED_SIZE( + sizeof(php_user_cache_header) + + (size_t) php_user_cache_capacity_primes[i] * sizeof(php_user_cache_entry) + ); + if (data_offset >= size) { + break; + } + + capacity = php_user_cache_capacity_primes[i]; + } + + return capacity; +} + +static void user_cache_free_list_remove_locked(php_user_cache_header *header, uint32_t block_offset) +{ + php_user_cache_block *block = php_user_cache_block_ptr(block_offset); + + if (block->prev_free != 0) { + php_user_cache_block_ptr(block->prev_free)->next_free = block->next_free; + } else { + header->free_list = block->next_free; + } + + if (block->next_free != 0) { + php_user_cache_block_ptr(block->next_free)->prev_free = block->prev_free; + } + + block->next_free = 0; + block->prev_free = 0; + block->flags &= ~PHP_USER_CACHE_BLOCK_FREE; +} + +static void user_cache_free_list_insert_locked(php_user_cache_header *header, uint32_t block_offset) +{ + php_user_cache_block *block = php_user_cache_block_ptr(block_offset); + + block->prev_free = 0; + block->next_free = header->free_list; + + if (header->free_list != 0) { + php_user_cache_block_ptr(header->free_list)->prev_free = block_offset; + } + + user_cache_block_mark_free(block); + + header->free_list = block_offset; +} + +static void user_cache_update_following_prev_size_locked( + php_user_cache_header *header, + uint32_t block_offset, + const php_user_cache_block *block +) +{ + uint32_t next_offset = block_offset + block->size; + + if (next_offset < user_cache_used_end_offset_locked(header)) { + php_user_cache_block_ptr(next_offset)->prev_size = block->size; + } +} + +static void user_cache_trim_tail_free_blocks_locked( + php_user_cache_header *header, + uint32_t block_offset +) +{ + php_user_cache_block *block = php_user_cache_block_ptr(block_offset); + uint32_t prev_offset; + + while (block_offset >= header->data_offset && + header->last_block_offset == block_offset && + php_user_cache_block_is_free(block) && + block_offset + block->size == user_cache_used_end_offset_locked(header) + ) { + prev_offset = 0; + user_cache_free_list_remove_locked(header, block_offset); + header->next_free -= block->size; + + if (block->prev_size != 0 && block_offset > header->data_offset) { + prev_offset = block_offset - block->prev_size; + } + + header->last_block_offset = prev_offset; + + if (prev_offset == 0) { + break; + } + + block_offset = prev_offset; + block = php_user_cache_block_ptr(block_offset); + } +} + +static void user_cache_write_section_enter(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + + UC_G(write_seq_bumped) = false; + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + UC_G(reader_drain_state) = 0; +#endif + + if (header == NULL || !php_user_cache_header_is_initialized_locked()) { + return; + } + + user_cache_seq_announce(&header->write_seq, header->write_seq + 1); + + UC_G(write_seq_bumped) = true; +} + +static void user_cache_write_section_leave(void) +{ + php_user_cache_header *header; + + if (!UC_G(write_seq_bumped)) { + return; + } + + UC_G(write_seq_bumped) = false; + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + UC_G(reader_drain_state) = 0; +#endif + + header = php_user_cache_header_ptr(); + if (header == NULL) { + return; + } + + user_cache_seq_publish(&header->write_seq, header->write_seq + 1); +} + +static void user_cache_write_section_note_header_initialized(php_user_cache_header *header) +{ + if (!UC_G(lock_held_is_write) || UC_G(write_seq_bumped)) { + return; + } + + user_cache_seq_announce(&header->write_seq, header->write_seq + 1); + UC_G(write_seq_bumped) = true; + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + UC_G(reader_drain_state) = 0; +#endif +} + +/* Publish a fresh segment by storing its magic last. */ +static bool user_cache_header_format_fresh_locked( + php_user_cache_header *header, + uint32_t capacity, + uint32_t data_offset) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (data_offset >= storage->size) { + return false; + } + + memset(header, 0, data_offset); + + header->capacity = capacity; + header->data_offset = data_offset; + header->data_size = (uint32_t) (storage->size - data_offset); + header->next_free = 0; + header->free_list = 0; + header->last_block_offset = 0; + header->count = 0; + header->mutation_epoch = 1; + /* Preserve an existing odd sequence until the write section ends. */ + header->write_seq = UC_G(write_seq_bumped) ? 1u : 2u; +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX + header->lock_model = user_cache_shared_mutex_init(header) + ? PHP_USER_CACHE_LOCK_MODEL_MUTEX + : PHP_USER_CACHE_LOCK_MODEL_FCNTL + ; +#else + header->lock_model = PHP_USER_CACHE_LOCK_MODEL_FCNTL; +#endif + if (php_user_cache_active_context()->boundary_shared) { +#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM + user_cache_shared_boundary_digest( + php_user_cache_active_context(), + storage->size, + header->boundary_identity_digest + ); + + header->boundary_identity_digest_set = 1; +#else + header->boundary_identity_digest_set = 0; +#endif + } + + header->version = PHP_USER_CACHE_VERSION; +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + user_cache_atomic_fence_seq_cst(); +#endif + header->magic = PHP_USER_CACHE_MAGIC; + + user_cache_write_section_note_header_initialized(header); + + return true; +} + +static uint32_t user_cache_alloc_from_free_list_locked( + php_user_cache_header *header, + size_t size, + uint32_t total_size, + const void *src) +{ + php_user_cache_block *block, *remainder; + uint32_t min_split_size, best_offset = 0, best_size = UINT32_MAX, *free_offset_ptr; + + min_split_size = (uint32_t) PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + 1); + + free_offset_ptr = &header->free_list; + while (*free_offset_ptr != 0) { + block = php_user_cache_block_ptr(*free_offset_ptr); + + if (block->size >= total_size && block->size < best_size) { + best_offset = *free_offset_ptr; + best_size = block->size; + if (best_size == total_size) { + break; + } + } + + free_offset_ptr = &block->next_free; + } + + if (best_offset == 0) { + return 0; + } + + block = php_user_cache_block_ptr(best_offset); + user_cache_free_list_remove_locked(header, best_offset); + + if (block->size >= total_size + min_split_size) { + remainder = php_user_cache_block_ptr(best_offset + total_size); + remainder->size = block->size - total_size; + remainder->prev_size = total_size; + remainder->next_free = 0; + remainder->prev_free = 0; + remainder->flags = PHP_USER_CACHE_BLOCK_FREE; + + block->size = total_size; + + user_cache_update_following_prev_size_locked(header, best_offset + total_size, remainder); + user_cache_free_list_insert_locked(header, best_offset + total_size); + + if (header->last_block_offset == best_offset) { + header->last_block_offset = best_offset + total_size; + } + } else { + user_cache_update_following_prev_size_locked(header, best_offset, block); + } + + user_cache_block_mark_used(block); + + if (src != NULL) { + memcpy(php_user_cache_ptr(best_offset + sizeof(php_user_cache_block)), src, size); + } + + return best_offset + (uint32_t) sizeof(php_user_cache_block); +} + +static uint32_t user_cache_alloc_from_tail_locked( + php_user_cache_header *header, + size_t size, + uint32_t total_size, + const void *src) +{ + php_user_cache_block *block; + uint32_t block_offset; + + if (header->next_free > header->data_size || total_size > header->data_size - header->next_free) { + return 0; + } + + block_offset = header->data_offset + header->next_free; + block = php_user_cache_block_ptr(block_offset); + block->size = total_size; + block->prev_size = header->last_block_offset != 0 ? php_user_cache_block_ptr(header->last_block_offset)->size : 0; + block->next_free = 0; + block->prev_free = 0; + block->flags = 0; + + if (src != NULL) { + memcpy(php_user_cache_ptr(block_offset + sizeof(php_user_cache_block)), src, size); + } + + header->next_free += total_size; + header->last_block_offset = block_offset; + + return block_offset + (uint32_t) sizeof(php_user_cache_block); +} + +static void user_cache_log_storage_handler_failure(const char *error_in, int error_code) +{ +#ifdef ZEND_WIN32 + char *msg = php_win32_error_to_msg(error_code); + + php_error_docref( + NULL, E_WARNING, + "Cache: shared memory initialization failed: %s: %s (%d)", + error_in != NULL ? error_in : "unknown", + msg, + error_code + ); + + php_win32_error_msg_free(msg); +#else + php_error_docref( + NULL, E_WARNING, + "Cache: shared memory initialization failed: %s: %s (%d)", + error_in != NULL ? error_in : "unknown", + strerror(error_code), + error_code + ); +#endif +} + +static bool user_cache_try_storage_handler( + const php_user_cache_shm_handler_entry *handler_entry, + const char **error_in_ptr, + int *error_code_ptr +) +{ + const char *error_in = NULL; + php_user_cache_context *ctx = php_user_cache_active_context(); + php_user_cache_runtime *runtime = php_user_cache_active_runtime(); + php_user_cache_storage *storage = &ctx->storage; + php_user_cache_shm_segment **segments = NULL; + int segment_count = 0; + + if (handler_entry->handler->create_segments( + runtime->configured_memory, + &segments, + &segment_count, + &error_in + ) != PHP_USER_CACHE_ALLOC_SUCCESS + ) { + /* Preserve the error across cleanup. */ +#ifdef ZEND_WIN32 + *error_code_ptr = (int) GetLastError(); +#else + *error_code_ptr = errno; +#endif + *error_in_ptr = error_in; + user_cache_cleanup_segments(handler_entry->handler, segments, segment_count); + + return false; + } + + storage->handler = handler_entry->handler; + storage->segments = segments; + storage->segment_count = segment_count; + storage->size = runtime->configured_memory; + storage->initialized = true; + + return true; +} + +/* Boundary contexts must not fall back to process-local storage. */ +static bool user_cache_select_storage_handler(void) +{ + const php_user_cache_shm_handler_entry *handler_entry; + const char *model = UC_G(memory_model); + const char *error_in = NULL; + int saved_error = 0; + +#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM + if (php_user_cache_active_context()->boundary_shared) { + if (user_cache_try_storage_handler( + user_cache_shared_boundary_handler_entry(), + &error_in, + &saved_error + ) + ) { + return true; + } + + user_cache_log_storage_handler_failure(error_in, saved_error); + + return false; + } +#endif + + if (model && model[0]) { + if (strcmp(model, "cgi") == 0) { + model = "shm"; + } + + for (handler_entry = user_cache_handler_table(); handler_entry->name; handler_entry++) { + if (strcmp(model, handler_entry->name) == 0 && + user_cache_try_storage_handler(handler_entry, &error_in, &saved_error) + ) { + return true; + } + } + } + + for (handler_entry = user_cache_handler_table(); handler_entry->name; handler_entry++) { + if (model && model[0] && strcmp(model, handler_entry->name) == 0) { + continue; + } + + if (user_cache_try_storage_handler(handler_entry, &error_in, &saved_error)) { + return true; + } + } + + user_cache_log_storage_handler_failure(error_in, saved_error); + + return false; +} + +static bool user_cache_negotiate_lock_model(void) +{ +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (!user_cache_wlock_impl()) { + return false; + } + + if (user_cache_header_is_initialized_acquire() && + php_user_cache_header_ptr()->lock_model == PHP_USER_CACHE_LOCK_MODEL_MUTEX + ) { + user_cache_unlock_impl(); + storage->use_shared_mutex = true; + + if (!php_user_cache_wlock()) { + return false; + } + } else { + UC_G(lock_held_is_write) = true; + user_cache_recover_after_owner_death(); + user_cache_write_section_enter(); + } + + return true; +#else + return php_user_cache_wlock(); +#endif +} + +static bool user_cache_startup_storage_impl(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (storage->initialized) { + return true; + } + + if (!user_cache_select_storage_handler()) { + return false; + } + + if (storage->segment_count != 1) { + goto bailout; + } + + if (!user_cache_create_lock()) { + goto bailout; + } + + if (!user_cache_negotiate_lock_model()) { + user_cache_destroy_lock(); + + goto bailout; + } + + if (!php_user_cache_header_init_locked()) { + php_user_cache_unlock(); + user_cache_destroy_lock(); + + goto bailout; + } + + php_user_cache_unlock(); + +#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX + storage->use_shared_mutex = + php_user_cache_header_ptr() != NULL && + php_user_cache_header_ptr()->lock_model == PHP_USER_CACHE_LOCK_MODEL_MUTEX + ; +#endif + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC + user_cache_atomic_store_32(&storage->startup_complete, 1); +#else + storage->startup_complete = 1; +#endif + + return true; + +bailout: + user_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + php_user_cache_reset_storage(); + + return false; +} + +static bool user_cache_startup_storage(void) +{ + bool result; + + if (user_cache_startup_storage_is_complete()) { + return true; + } + + user_cache_startup_storage_lock(); + result = user_cache_startup_storage_impl(); + user_cache_startup_storage_unlock(); + + return result; +} + +static void user_cache_resolve_runtime(void) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + php_user_cache_runtime *runtime; + php_user_cache_storage *storage = &ctx->storage; + + php_user_cache_reset_runtime(); + + runtime = php_user_cache_active_runtime(); + if (!runtime->enabled) { + return; + } + + if (UC_G(request_unavailable_reason) != PHP_USER_CACHE_REASON_NONE) { + user_cache_set_unavailable(UC_G(request_unavailable_reason)); + + return; + } + + if (!user_cache_is_opted_in()) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED); + + return; + } + + if (user_cache_cache_is_disabled_for_sapi()) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_DISABLED_BY_INI); + + return; + } + + if (!storage->initialized && + user_cache_requires_pre_request_storage() + ) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_BACKEND_NOT_INITIALIZED_BEFORE_WORKER); + + return; + } + + if (!user_cache_startup_storage()) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_SHM_INIT_FAILED); + + return; + } + + if (user_cache_requires_pre_request_storage() && + !storage->initialized_before_request + ) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_BACKEND_INITIALIZED_AFTER_WORKER); + + return; + } + + user_cache_set_available(); +} + +#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC +static bool user_cache_reader_slot_owner_is_dead(const php_user_cache_reader_slot *slot) +{ + /* Reader slots are claimed lock-free. */ + uint64_t owner_pid = php_user_cache_atomic_load_64(&slot->owner_pid), + owner_start_time = php_user_cache_atomic_load_64(&slot->owner_start_time) + ; + + return user_cache_entry_lock_owner_is_dead(owner_pid, owner_start_time); +} + +#ifndef ZEND_WIN32 +static void user_cache_optimistic_atfork_child(void) +{ + UC_G(reader_claim_count) = 0; +} +#endif + +static int32_t user_cache_claim_reader_slot(php_user_cache_header *header) +{ + php_user_cache_reader_slot *slot; + uint64_t my_pid, expected; + uint32_t i; + + for (i = 0; i < UC_G(reader_claim_count); i++) { + if (UC_G(reader_claims)[i].header == header) { + return (int32_t) UC_G(reader_claims)[i].slot_index; + } + } + + if (UC_G(reader_claim_count) == PHP_USER_CACHE_READER_CLAIM_MAX || + php_user_cache_reader_incarnation == 0 + ) { + return -1; + } + + my_pid = php_user_cache_cached_pid(); + + for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) { + slot = &header->reader_slots[i]; + + expected = php_user_cache_atomic_load_64(&slot->owner_pid); + + if (expected != 0) { + if (!user_cache_reader_slot_owner_is_dead(slot) || + user_cache_atomic_load_32(&slot->active) != 0 + ) { + continue; + } + + /* Clear start time before pid so scanners cannot combine owners. */ + user_cache_atomic_store_64(&slot->owner_start_time, 0); + if (!user_cache_atomic_cas_64(&slot->owner_pid, expected, 0)) { + continue; + } + } + + if (user_cache_atomic_cas_64(&slot->owner_pid, 0, my_pid)) { + user_cache_atomic_store_64(&slot->owner_start_time, user_cache_cached_self_start_time_token(my_pid)); + + goto found; + } + } + + return -1; + +found: + UC_G(reader_claims)[UC_G(reader_claim_count)].header = header; + UC_G(reader_claims)[UC_G(reader_claim_count)].slot_index = i; + UC_G(reader_claim_count)++; + + return (int32_t) i; +} + +bool php_user_cache_quiesce_graph_payloads_locked(void) +{ + php_user_cache_header *header; + php_user_cache_reader_slot *slot; + uint64_t waited_us = 0; + uint32_t i, count, spin = 0; + + if (!UC_G(write_seq_bumped)) { + return true; + } + + if (UC_G(reader_drain_state) != 0) { + return UC_G(reader_drain_state) > 0; + } + + header = php_user_cache_header_ptr(); + if (header == NULL) { + return true; + } + + user_cache_atomic_fence_seq_cst(); + + for (;;) { + count = 0; + for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) { + if (user_cache_atomic_load_32(&header->reader_slots[i].active) != 0) { + count++; + } + } + + if (count == 0) { + UC_G(reader_drain_state) = 1; + + return true; + } + + spin++; + + if ((spin & 0xFFU) == 0) { + for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) { + slot = &header->reader_slots[i]; + + if (user_cache_atomic_load_32(&slot->active) == 0 || + php_user_cache_atomic_load_64(&slot->owner_pid) == 0 + ) { + continue; + } + + if (user_cache_reader_slot_owner_is_dead(slot)) { + user_cache_atomic_cas_32(&slot->active, 1, 0); + } + } + } + + if (spin > PHP_USER_CACHE_READER_DRAIN_SPIN) { +#ifdef ZEND_WIN32 + Sleep(1); + waited_us += 1000; +#else + usleep(50); + waited_us += 50; +#endif + + if (waited_us > PHP_USER_CACHE_READER_DRAIN_TIMEOUT_US) { + UC_G(reader_drain_state) = -1; + + return false; + } + } + } +} + +void php_user_cache_optimistic_fork_setup(void) +{ + static bool registered = false; + + if (php_user_cache_reader_incarnation == 0) { + php_user_cache_reader_incarnation = + ((uint64_t) time(NULL) << 20) ^ + (uint64_t) (uintptr_t) &php_user_cache_reader_incarnation ^ + php_user_cache_current_pid() + ; + + if (php_user_cache_reader_incarnation == 0) { + php_user_cache_reader_incarnation = 1; + } + } + + if (registered) { + return; + } + +#ifndef ZEND_WIN32 + if (pthread_atfork(NULL, NULL, user_cache_optimistic_atfork_child) != 0) { + return; + } +#endif + + registered = true; +} + +bool php_user_cache_optimistic_reader_begin(php_user_cache_header *header, uint32_t *slot_idx_ptr) +{ + int32_t slot_idx = user_cache_claim_reader_slot(header); + + if (slot_idx < 0) { + return false; + } + + *slot_idx_ptr = (uint32_t) slot_idx; + + user_cache_atomic_store_32(&header->reader_slots[slot_idx].active, 1); + user_cache_atomic_fence_seq_cst(); + + return true; +} + +void php_user_cache_optimistic_reader_end(php_user_cache_header *header, uint32_t slot_idx) +{ + user_cache_atomic_store_32(&header->reader_slots[slot_idx].active, 0); +} + +#ifdef ZTS +static bool user_cache_reader_claim_header_is_attached(const php_user_cache_header *header) +{ + const php_user_cache_partition *partition; + + if (user_cache_storage_holds_header(&php_user_cache_context_state.storage, header)) { + return true; + } + + for (partition = php_user_cache_partitions; partition != NULL; partition = partition->next) { + if (user_cache_storage_holds_header(&partition->context.storage, header)) { + return true; + } + } + + return false; +} + +/* Release per-thread reader slots before their live process pid makes them + * permanently unclaimable. Clear active and start time before owner_pid. */ +void php_user_cache_release_thread_reader_claims(php_user_cache_globals *globals) +{ + php_user_cache_reader_slot *slot; + uint64_t my_pid; + uint32_t i; + + if (globals->reader_claim_count == 0) { + return; + } + + my_pid = php_user_cache_cached_pid(); + + for (i = 0; i < globals->reader_claim_count; i++) { + if (!user_cache_reader_claim_header_is_attached(globals->reader_claims[i].header)) { + continue; + } + + slot = &globals->reader_claims[i].header->reader_slots[globals->reader_claims[i].slot_index]; + + user_cache_atomic_store_32(&slot->active, 0); + user_cache_atomic_store_64(&slot->owner_start_time, 0); + user_cache_atomic_cas_64(&slot->owner_pid, my_pid, 0); + } + + globals->reader_claim_count = 0; +} +#endif +#else +bool php_user_cache_quiesce_graph_payloads_locked(void) +{ + return true; +} + +void php_user_cache_optimistic_fork_setup(void) +{ +} + +bool php_user_cache_optimistic_reader_begin(php_user_cache_header *header, uint32_t *slot_idx_ptr) +{ + (void) header; + (void) slot_idx_ptr; + + return false; +} + +void php_user_cache_optimistic_reader_end(php_user_cache_header *header, uint32_t slot_idx) +{ + (void) header; + (void) slot_idx; +} +#endif + +void php_user_cache_reset_runtime(void) +{ + php_user_cache_runtime *runtime = php_user_cache_active_runtime(); + + UC_G(runtime_resolved) = false; + + memset(runtime, 0, sizeof(*runtime)); + + runtime->configured_memory = UC_G(shm_size); + + runtime->enabled = runtime->configured_memory != 0; + if (!runtime->enabled) { + runtime->available = false; + runtime->failure_reason = PHP_USER_CACHE_REASON_DISABLED_BY_INI; + } +} + +void php_user_cache_reset_storage(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + memset(storage, 0, sizeof(*storage)); + + storage->lock_file = -1; +} + +bool php_user_cache_header_init_locked(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + php_user_cache_header *header = php_user_cache_header_ptr(); + uint32_t capacity, data_offset; + + if (!header) { + return false; + } + + /* Cache the layout for the lifetime of the attachment. */ + if (!storage->layout_memo_valid) { + storage->capacity_memo = user_cache_calculate_capacity(storage->size); + storage->data_offset_memo = (uint32_t) PHP_USER_CACHE_ALIGNED_SIZE( + sizeof(php_user_cache_header) + storage->capacity_memo * sizeof(php_user_cache_entry) + ); + storage->layout_memo_valid = true; + } + capacity = storage->capacity_memo; + data_offset = storage->data_offset_memo; + + if (header->magic == PHP_USER_CACHE_MAGIC && header->version == PHP_USER_CACHE_VERSION) { + /* Reformat segments whose compile-time layout differs. */ + if (header->capacity == capacity && header->data_offset == data_offset) { +#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM + return user_cache_header_boundary_identity_matches_locked(header, storage->size); +#else + return true; +#endif + } + } + + return user_cache_header_format_fresh_locked(header, capacity, data_offset); +} + +void php_user_cache_free_locked(uint32_t payload_offset) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + php_user_cache_block *block, *adjacent; + uint32_t block_offset, original_block_offset, next_offset, prev_offset; + + if (!header || payload_offset < sizeof(php_user_cache_block)) { + return; + } + + block_offset = payload_offset - (uint32_t) sizeof(php_user_cache_block); + original_block_offset = block_offset; + block = php_user_cache_block_ptr(block_offset); + if (php_user_cache_block_is_free(block)) { + return; + } + + user_cache_block_mark_free(block); + + next_offset = block_offset + block->size; + if (next_offset < user_cache_used_end_offset_locked(header)) { + adjacent = php_user_cache_block_ptr(next_offset); + + if (php_user_cache_block_is_free(adjacent)) { + user_cache_free_list_remove_locked(header, next_offset); + block->size += adjacent->size; + + if (header->last_block_offset == next_offset) { + header->last_block_offset = block_offset; + } + } + } + + if (block->prev_size != 0 && block_offset > header->data_offset) { + prev_offset = block_offset - block->prev_size; + + adjacent = php_user_cache_block_ptr(prev_offset); + if (php_user_cache_block_is_free(adjacent)) { + user_cache_free_list_remove_locked(header, prev_offset); + + block->size += adjacent->size; + adjacent->size = block->size; + block = adjacent; + block_offset = prev_offset; + + if (header->last_block_offset == original_block_offset) { + header->last_block_offset = block_offset; + } + } + } + + user_cache_update_following_prev_size_locked(header, block_offset, block); + user_cache_free_list_insert_locked(header, block_offset); + user_cache_trim_tail_free_blocks_locked(header, block_offset); +} + +uint32_t php_user_cache_alloc_locked(size_t size, const void *src) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + uint32_t total_size, offset; + size_t aligned_size; + + if (!header || size == 0 || size > UINT32_MAX - sizeof(php_user_cache_block)) { + return 0; + } + + aligned_size = PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + size); + if (aligned_size > UINT32_MAX) { + return 0; + } + + total_size = (uint32_t) aligned_size; + + offset = user_cache_alloc_from_free_list_locked(header, size, total_size, src); + if (offset != 0) { + return offset; + } + + return user_cache_alloc_from_tail_locked(header, size, total_size, src); +} + +bool php_user_cache_startup_storage_before_request(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + if (user_cache_startup_failure_forced()) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_SHM_INIT_FAILED); + + return false; + } + + if (!user_cache_is_opted_in()) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED); + + return true; + } + + if (!user_cache_startup_storage()) { + user_cache_set_unavailable(PHP_USER_CACHE_REASON_SHM_INIT_FAILED); + + return false; + } + + storage->initialized_before_request = true; + + return true; +} + +void php_user_cache_shutdown_storage(void) +{ + php_user_cache_storage *storage = &php_user_cache_active_context()->storage; + + user_cache_destroy_lock(); + user_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + php_user_cache_reset_storage(); +} + +void php_user_cache_ensure_ready_slow(void) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + + user_cache_resolve_runtime(); + + UC_G(runtime_resolved) = true; + UC_G(runtime_resolved_context) = ctx; + UC_G(runtime_resolved_enabled) = UC_G(enable); +} + +bool php_user_cache_rlock(void) +{ + php_user_cache_header *header; + uint32_t recovery_attempts = 0; + + for (;;) { + if (!user_cache_rlock_impl()) { + return false; + } + + UC_G(lock_held_is_write) = false; + + header = php_user_cache_header_ptr(); + if (!UNEXPECTED( + header != NULL && + php_user_cache_header_is_initialized_locked() && + (php_user_cache_seq_load(&header->write_seq) & 1) != 0 + ) + ) { + return true; + } + + php_user_cache_unlock(); + + if (++recovery_attempts > 8) { + return false; + } + + if (!php_user_cache_wlock()) { + return false; + } + + php_user_cache_unlock(); + } +} + +bool php_user_cache_wlock(void) +{ + if (!user_cache_wlock_impl()) { + return false; + } + + UC_G(lock_held_is_write) = true; + + user_cache_recover_after_owner_death(); + + user_cache_write_section_enter(); + + return true; +} + +bool php_user_cache_wlock_for_entry_mutation(zend_string *key) +{ + php_user_cache_entry_lock *local_lock; + php_user_cache_header *header; + zend_ulong hash = zend_string_hash_val(key); + uint64_t waited_us = 0; + uint32_t slot_idx; + bool found; + + user_cache_ensure_entry_lock_owner(); + + for (;;) { + if (!php_user_cache_wlock()) { + return false; + } + + if (!php_user_cache_header_init_locked()) { + php_user_cache_unlock(); + + return false; + } + + header = php_user_cache_header_ptr(); + if (!user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found) || !found) { + return true; + } + + local_lock = user_cache_find_local_entry_lock(php_user_cache_active_context(), key); + if (local_lock != NULL && + header->entry_lock_records[slot_idx].owner_pid == local_lock->owner_pid + ) { + return true; + } + + php_user_cache_unlock(); + + if (waited_us >= PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US) { + return false; + } + + waited_us += user_cache_sleep_entry_lock_retry_interval(); + } +} + +void php_user_cache_unlock(void) +{ + if (UC_G(lock_held_is_write)) { + user_cache_write_section_leave(); + } + + UC_G(lock_held_is_write) = false; + + user_cache_unlock_impl(); +} + +void php_user_cache_unlock_if_held(void) +{ + if (UC_G(lock_held)) { + php_user_cache_unlock(); + } +} + +bool php_user_cache_try_acquire_entry_lock(zend_string *key, zend_long lease) +{ + return user_cache_acquire_entry_lock_record( + php_user_cache_active_context(), + key, + lease, + true, + false + ); +} + +bool php_user_cache_acquire_entry_locks(zend_string **keys, bool *acquired, uint32_t count) +{ + return user_cache_acquire_entry_lock_records( + php_user_cache_active_context(), + keys, + acquired, + count + ); +} + +bool php_user_cache_release_entry_lock(zend_string *key) +{ + php_user_cache_context *ctx = php_user_cache_active_context(); + php_user_cache_entry_lock *lock; + zend_ulong hash; + HashTable **locks_ptr = user_cache_entry_lock_table_ptr(); + + user_cache_ensure_entry_lock_owner(); + + lock = user_cache_find_local_entry_lock(ctx, key); + if (lock == NULL) { + return false; + } + + hash = zend_string_hash_val(key); + if (!user_cache_remove_owned_entry_lock_record(ctx, key, hash, lock->owner_pid)) { + return false; + } + + zend_hash_del(*locks_ptr, key); + user_cache_destroy_entry_locks_if_empty(locks_ptr); + + return true; +} + +void php_user_cache_release_entry_locks(zend_string **keys, const bool *acquired, uint32_t count) +{ + user_cache_release_entry_locks_for_keys( + php_user_cache_active_context(), + keys, + acquired, + count + ); +} + +bool php_user_cache_entry_locks_allow_clear_locked(void) +{ + php_user_cache_header *header = php_user_cache_header_ptr(); + php_user_cache_entry_lock_record *record; + uint64_t owner_pid, now; + uint32_t i; + + user_cache_ensure_entry_lock_owner(); + + if (header == NULL || !php_user_cache_header_is_initialized_locked()) { + return true; + } + + owner_pid = php_user_cache_cached_pid(); + now = (uint64_t) time(NULL); + + for (i = 0; i < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; i++) { + record = &header->entry_lock_records[i]; + if (record->state != PHP_USER_CACHE_ENTRY_LOCK_USED) { + continue; + } + + if (!user_cache_entry_lock_record_is_active_locked(record, now, true)) { + user_cache_remove_entry_lock_record_locked(record); + continue; + } + + if (record->owner_pid != owner_pid) { + return false; + } + } + + return true; +} + +void php_user_cache_release_request_entry_locks(void) +{ + user_cache_release_entry_locks_all_contexts( + &UC_G(entry_lock_table), + PHP_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES + ); +#ifndef ZEND_WIN32 + UC_G(entry_lock_owner_pid) = 0; +#endif +} diff --git a/ext/user_cache/user_cache_storage_portability.h b/ext/user_cache/user_cache_storage_portability.h new file mode 100644 index 000000000000..e48df067de76 --- /dev/null +++ b/ext/user_cache/user_cache_storage_portability.h @@ -0,0 +1,96 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo . | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_USER_CACHE_STORAGE_PORTABILITY_H +#define PHP_USER_CACHE_STORAGE_PORTABILITY_H + +#include "user_cache_internal.h" + +#ifdef ZEND_WIN32 +# include "zend_execute.h" +# include "zend_system_id.h" +# include "win32/ioutil.h" + +# include +# include +# include +#else +# include +# include +# include +# include +# include +# include +# ifdef HAVE_UNISTD_H +# include +# endif +# if defined(PHP_USER_CACHE_HAVE_ANON_MMAP) || \ + defined(PHP_USER_CACHE_HAVE_BOUNDARY_SHM) || \ + (defined(__linux__) && defined(HAVE_MEMFD_CREATE)) +# include +# endif +#endif + +#if defined(__APPLE__) || defined(__FreeBSD__) +# include +# ifdef __FreeBSD__ +# include +# include +# endif +#endif + +#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP +# if defined(MAP_ANON) && !defined(MAP_ANONYMOUS) +# define MAP_ANONYMOUS MAP_ANON +# endif +#endif + +#ifdef ZEND_WIN32 +# define PHP_USER_CACHE_WIN32_MAPPING_NAME "PhpUserCache.SharedMemoryArea" +# define PHP_USER_CACHE_WIN32_MAPPING_MUTEX_NAME "PhpUserCache.SharedMemoryMutex" +# define PHP_USER_CACHE_WIN32_LOCK_FILE_NAME "PhpUserCache.LockFile" +#endif + +#define PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US 10000U + +#ifdef ZTS +# ifdef ZEND_WIN32 +# define PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER SRWLOCK_INIT +# else +# define PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER PTHREAD_MUTEX_INITIALIZER +# endif +#else +# define PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER 0 +#endif + +#ifdef ZEND_WIN32 +typedef struct _php_user_cache_win32_segment { + php_user_cache_shm_segment segment; + HANDLE memfile; + void *mapping_base; + size_t mapping_size; +} php_user_cache_win32_segment; +#endif + +#ifdef ZTS +# ifdef ZEND_WIN32 +typedef SRWLOCK php_user_cache_startup_lock; +# else +typedef pthread_mutex_t php_user_cache_startup_lock; +# endif +#else +typedef char php_user_cache_startup_lock; +#endif + +#endif /* PHP_USER_CACHE_STORAGE_PORTABILITY_H */ diff --git a/main/main.c b/main/main.c index 9e77c99a45df..9fcd9a10929d 100644 --- a/main/main.c +++ b/main/main.c @@ -52,6 +52,7 @@ #include "ext/random/php_random_csprng.h" #include "ext/random/php_random_zend_utils.h" #include "ext/opcache/ZendAccelerator.h" +#include "ext/user_cache/php_user_cache.h" #ifdef HAVE_JIT # include "ext/opcache/jit/zend_jit.h" #endif @@ -2828,6 +2829,7 @@ PHPAPI void php_reserve_tsrm_memory(void) TSRM_ALIGNED_SIZE(sizeof(php_core_globals)) + TSRM_ALIGNED_SIZE(sizeof(sapi_globals_struct)) + TSRM_ALIGNED_SIZE(sizeof(zend_accel_globals)) + + TSRM_ALIGNED_SIZE(php_user_cache_globals_size()) + #ifdef HAVE_JIT TSRM_ALIGNED_SIZE(sizeof(zend_jit_globals)) + #endif @@ -2845,6 +2847,9 @@ PHPAPI bool php_tsrm_startup_ex(int expected_threads) TSRM_ALIGNED_SIZE(sizeof(zend_compiler_globals)) + TSRM_ALIGNED_SIZE(sizeof(zend_executor_globals))); (void)ts_resource(0); + /* Allocated here rather than from the OPcache module wiring: SAPI + * activate hooks consume these globals before the module MINIT runs. */ + php_user_cache_globals_startup(); return ret; } diff --git a/sapi/apache2handler/sapi_apache2.c b/sapi/apache2handler/sapi_apache2.c index 83b3f02fb743..9bb58d7adda0 100644 --- a/sapi/apache2handler/sapi_apache2.c +++ b/sapi/apache2handler/sapi_apache2.c @@ -32,6 +32,7 @@ #include "zend_smart_str.h" #include "ext/standard/php_standard.h" +#include "ext/user_cache/php_user_cache.h" #include "apr_strings.h" #include "ap_config.h" @@ -64,6 +65,14 @@ char *apache2_php_ini_path_override = NULL; ZEND_TSRMLS_CACHE_DEFINE() #endif +typedef struct _php_apache_user_cache_partition_entry { + const server_rec *server; + php_user_cache_partition *partition; + struct _php_apache_user_cache_partition_entry *next; +} php_apache_user_cache_partition_entry; + +static php_apache_user_cache_partition_entry *php_apache_user_cache_partitions = NULL; + static size_t php_apache_sapi_ub_write(const char *str, size_t str_length) { @@ -418,6 +427,7 @@ static sapi_module_struct apache2_sapi_module = { static apr_status_t php_apache_server_shutdown(void *tmp) { apache2_sapi_module.shutdown(&apache2_sapi_module); + php_apache_user_cache_partitions = NULL; sapi_shutdown(); #ifdef ZTS tsrm_shutdown(); @@ -458,6 +468,62 @@ static int php_pre_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp return OK; } +/* Linear over the vhost list, run once per request. */ +static php_user_cache_partition *php_apache_user_cache_partition_for_server(const server_rec *server) +{ + php_apache_user_cache_partition_entry *entry; + + for (entry = php_apache_user_cache_partitions; entry != NULL; entry = entry->next) { + if (entry->server == server) { + return entry->partition; + } + } + + return NULL; +} + +static void php_apache_user_cache_init_partitions(apr_pool_t *pconf, server_rec *server) +{ + const char *hostname, *partition_name; + php_apache_user_cache_partition_entry *entry; + server_rec *cur; + unsigned int i; + + /* The partition entries are pool-allocated (freed with pconf); the + * php_user_cache_partition objects created below are owned by + * OPcache and released together in its MSHUTDOWN (partitions_shutdown), + * so no cgi/lsapi-style SAPI shutdown hook is required here. */ + php_apache_user_cache_partitions = NULL; + php_user_cache_opt_in(); + + i = 0; + for (cur = server; cur != NULL; cur = cur->next, i++) { + hostname = cur->server_hostname != NULL ? cur->server_hostname : "default"; + partition_name = apr_psprintf( + pconf, + "apache2handler:%u:%s:%u", + i, + hostname, + (unsigned int) cur->port + ); + + entry = apr_pcalloc(pconf, sizeof(*entry)); + entry->server = cur; + entry->partition = php_user_cache_partition_create(partition_name); + if (entry->partition == NULL) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cur, "Unable to allocate OPcache User Cache partition"); + continue; + } + + if (!php_user_cache_partition_startup_storage(entry->partition)) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cur, "OPcache User Cache partition startup failed; User Cache will be unavailable"); + } + + entry->next = php_apache_user_cache_partitions; + php_apache_user_cache_partitions = entry; + } +} + static int php_apache_server_startup(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { @@ -500,6 +566,7 @@ php_apache_server_startup(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp if (apache2_sapi_module.startup(&apache2_sapi_module) != SUCCESS) { return DONE; } + php_apache_user_cache_init_partitions(pconf, s); apr_pool_cleanup_register(pconf, NULL, php_apache_server_shutdown, apr_pool_cleanup_null); php_apache_add_version(pconf); @@ -548,12 +615,21 @@ static int php_apache_request_ctor(request_rec *r, php_struct *ctx) ctx->r->user = apr_pstrdup(ctx->r->pool, SG(request_info).auth_user); - return php_request_startup(); + php_user_cache_partition_activate(php_apache_user_cache_partition_for_server(r->server)); + + if (php_request_startup() == FAILURE) { + php_user_cache_partition_activate(NULL); + + return FAILURE; + } + + return SUCCESS; } static void php_apache_request_dtor(request_rec *r) { php_request_shutdown(NULL); + php_user_cache_partition_activate(NULL); } static void php_apache_ini_dtor(request_rec *r, request_rec *p) diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index 7efd8f159d59..06310bab17b7 100644 --- a/sapi/cgi/cgi_main.c +++ b/sapi/cgi/cgi_main.c @@ -63,6 +63,7 @@ #include "ext/standard/php_standard.h" #include "ext/standard/dl_arginfo.h" #include "ext/standard/url.h" +#include "ext/user_cache/php_user_cache.h" #ifdef PHP_WIN32 # include @@ -778,6 +779,41 @@ static void sapi_cgi_log_message(const char *message, int syslog_type_int) } } +static const char *cgi_user_cache_getenv(const char *name) +{ + const char *value; + fcgi_request *request; + int name_len; + + if (SG(server_context) != NULL && SG(server_context) != (void *) 1) { + request = (fcgi_request*) SG(server_context); + if (fcgi_has_env(request)) { + name_len = (int) strlen(name); + value = fcgi_getenv(request, name, name_len); + if (value != NULL) { + return value; + } + } + } + + return getenv(name); +} + +static void cgi_user_cache_log_message(const char *message) +{ + sapi_cgi_log_message(message, 0); +} + +static void cgi_user_cache_activate_request_partition(void) +{ + php_user_cache_activate_boundary_partition( + "cgi-fcgi", + cgi_user_cache_getenv, + cgi_user_cache_log_message, + PHP_USER_CACHE_REASON_CGI_BOUNDARY_UNAVAILABLE + ); +} + /* {{{ php_cgi_ini_activate_user_config */ static void php_cgi_ini_activate_user_config(char *path, size_t path_len, const char *doc_root, size_t doc_root_len) { @@ -860,6 +896,11 @@ static void php_cgi_ini_activate_user_config(char *path, size_t path_len, const static int sapi_cgi_activate(void) { + /* Resolve the user-cache partition before any early return below: the + * activate return value is ignored by sapi_activate(), so bailing out + * first would leave the request without a partition. */ + cgi_user_cache_activate_request_partition(); + /* PATH_TRANSLATED should be defined at this stage but better safe than sorry :) */ if (!SG(request_info).path_translated) { return FAILURE; @@ -963,12 +1004,20 @@ static int sapi_cgi_deactivate(void) sapi_cgi_flush(SG(server_context)); } } + php_user_cache_partition_activate(NULL); + return SUCCESS; } static int php_cgi_startup(sapi_module_struct *sapi_module_ptr) { - return php_module_startup(sapi_module_ptr, &cgi_module_entry); + if (php_module_startup(sapi_module_ptr, &cgi_module_entry) == FAILURE) { + return FAILURE; + } + + php_user_cache_opt_in(); + + return SUCCESS; } /* {{{ sapi_module_struct cgi_sapi_module */ @@ -1547,6 +1596,7 @@ static PHP_MINIT_FUNCTION(cgi) static PHP_MSHUTDOWN_FUNCTION(cgi) { zend_hash_destroy(&CGIG(user_config_cache)); + php_user_cache_boundary_partitions_shutdown(); UNREGISTER_INI_ENTRIES(); return SUCCESS; diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index e5e573d1533c..205a063d2c58 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -52,6 +52,9 @@ #include "fopen_wrappers.h" #include "ext/standard/php_standard.h" #include "ext/standard/dl_arginfo.h" + +#include "ext/user_cache/php_user_cache.h" + #include "cli.h" #ifdef PHP_WIN32 #include @@ -390,7 +393,14 @@ static void sapi_cli_send_header(sapi_header_struct *sapi_header, void *server_c static int php_cli_startup(sapi_module_struct *sapi_module_ptr) /* {{{ */ { - return php_module_startup(sapi_module_ptr, NULL); + int result; + + result = php_module_startup(sapi_module_ptr, NULL); + if (result == SUCCESS) { + php_user_cache_opt_in(); + } + + return result; } /* }}} */ diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 797979b67305..92c43a9ec9e9 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -44,6 +44,8 @@ #include +#include "ext/user_cache/php_user_cache.h" + #ifdef HAVE_DLFCN_H #include #endif @@ -499,7 +501,14 @@ const zend_function_entry server_additional_functions[] = { static int sapi_cli_server_startup(sapi_module_struct *sapi_module_ptr) /* {{{ */ { - return php_module_startup(sapi_module_ptr, &cli_server_module_entry); + int result; + + result = php_module_startup(sapi_module_ptr, &cli_server_module_entry); + if (result == SUCCESS) { + php_user_cache_opt_in(); + } + + return result; } /* }}} */ static size_t sapi_cli_server_ub_write(const char *str, size_t str_length) /* {{{ */ @@ -2571,6 +2580,10 @@ static zend_result php_cli_server_ctor(php_cli_server *server, const char *addr, } server->server_sock = server_sock; + if (!php_user_cache_startup_default_context_storage()) { + php_cli_server_logf(PHP_CLI_SERVER_LOG_ERROR, "OPcache User Cache startup failed; User Cache will be unavailable"); + } + php_cli_server_startup_workers(); php_cli_server_poller_ctor(&server->poller); diff --git a/sapi/fpm/config.m4 b/sapi/fpm/config.m4 index 89c53a0c4d28..3f7590337b35 100644 --- a/sapi/fpm/config.m4 +++ b/sapi/fpm/config.m4 @@ -462,6 +462,7 @@ if test "$PHP_FPM" != "no"; then fpm/fpm_status.c \ fpm/fpm_stdio.c \ fpm/fpm_unix.c \ + fpm/fpm_user_cache.c \ fpm/fpm_worker_pool.c \ fpm/zlog.c \ fpm/events/select.c \ diff --git a/sapi/fpm/fpm/fpm.c b/sapi/fpm/fpm/fpm.c index cb86f403b89c..eec526874abd 100644 --- a/sapi/fpm/fpm/fpm.c +++ b/sapi/fpm/fpm/fpm.c @@ -19,6 +19,7 @@ #include "fpm_scoreboard.h" #include "fpm_stdio.h" #include "fpm_log.h" +#include "fpm_user_cache.h" #include "zlog.h" struct fpm_globals_s fpm_globals = { @@ -64,6 +65,7 @@ enum fpm_init_return_status fpm_init(int argc, char **argv, char *config, char * 0 > fpm_children_init_main() || 0 > fpm_sockets_init_main() || 0 > fpm_worker_pool_init_main() || + 0 > fpm_user_cache_init_main() || 0 > fpm_event_init_main()) { if (fpm_globals.test_successful) { diff --git a/sapi/fpm/fpm/fpm_children.c b/sapi/fpm/fpm/fpm_children.c index 285df91a9b69..8c02ea5d5d7a 100644 --- a/sapi/fpm/fpm/fpm_children.c +++ b/sapi/fpm/fpm/fpm_children.c @@ -29,6 +29,8 @@ #include "zlog.h" +#include "ext/user_cache/php_user_cache.h" + static time_t *last_faults; static int fault; @@ -189,6 +191,7 @@ static void fpm_child_init(struct fpm_worker_pool_s *wp) /* {{{ */ { fpm_globals.max_requests = wp->config->pm_max_requests; fpm_globals.listening_socket = dup(wp->listening_socket); + php_user_cache_partition_activate(wp->user_cache_partition); if (0 > fpm_stdio_init_child(wp) || 0 > fpm_log_init_child(wp) || diff --git a/sapi/fpm/fpm/fpm_user_cache.c b/sapi/fpm/fpm/fpm_user_cache.c new file mode 100644 index 000000000000..240b3499047e --- /dev/null +++ b/sapi/fpm/fpm/fpm_user_cache.c @@ -0,0 +1,43 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "fpm_config.h" + +#include "fpm.h" +#include "fpm_worker_pool.h" +#include "fpm_user_cache.h" +#include "zlog.h" + +#include "ext/user_cache/php_user_cache.h" + +int fpm_user_cache_init_main(void) +{ + struct fpm_worker_pool_s *wp; + + php_user_cache_opt_in(); + + for (wp = fpm_worker_all_pools; wp; wp = wp->next) { + wp->user_cache_partition = php_user_cache_partition_create(wp->config->name); + if (wp->user_cache_partition == NULL) { + zlog(ZLOG_ERROR, "[pool %s] unable to allocate OPcache User Cache partition", wp->config->name); + return -1; + } + + if (!php_user_cache_partition_startup_storage(wp->user_cache_partition)) { + zlog(ZLOG_WARNING, "[pool %s] OPcache User Cache partition startup failed; User Cache will be unavailable", wp->config->name); + } + } + + return 0; +} diff --git a/sapi/fpm/fpm/fpm_user_cache.h b/sapi/fpm/fpm/fpm_user_cache.h new file mode 100644 index 000000000000..60e7fae39804 --- /dev/null +++ b/sapi/fpm/fpm/fpm_user_cache.h @@ -0,0 +1,20 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#ifndef FPM_USER_CACHE_H +#define FPM_USER_CACHE_H 1 + +int fpm_user_cache_init_main(void); + +#endif diff --git a/sapi/fpm/fpm/fpm_worker_pool.h b/sapi/fpm/fpm/fpm_worker_pool.h index efb8640cd32f..07a05b45b7c5 100644 --- a/sapi/fpm/fpm/fpm_worker_pool.h +++ b/sapi/fpm/fpm/fpm_worker_pool.h @@ -5,6 +5,7 @@ #include "fpm_conf.h" #include "fpm_shm.h" +#include "ext/user_cache/php_user_cache.h" struct fpm_worker_pool_s; struct fpm_child_s; @@ -38,6 +39,7 @@ struct fpm_worker_pool_s { struct fpm_scoreboard_s *scoreboard; int log_fd; char **limit_extensions; + php_user_cache_partition *user_cache_partition; /* for ondemand PM */ struct fpm_event_s *ondemand_event; diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc index 907988654337..3856f8b1612f 100644 --- a/sapi/fpm/tests/tester.inc +++ b/sapi/fpm/tests/tester.inc @@ -548,7 +548,7 @@ class Tester $this->masterProcess = proc_open($cmd, $desc, $pipes, null, $envVars); register_shutdown_function( - function ($masterProcess) use ($configFile) { + static function ($masterProcess) use ($configFile) { @unlink($configFile); if (is_resource($masterProcess)) { @proc_terminate($masterProcess); diff --git a/sapi/litespeed/lsapi_main.c b/sapi/litespeed/lsapi_main.c index e7aec268489b..becf3f10410d 100644 --- a/sapi/litespeed/lsapi_main.c +++ b/sapi/litespeed/lsapi_main.c @@ -26,6 +26,8 @@ #include "lsapilib.h" #include "lsapi_main_arginfo.h" +#include "ext/user_cache/php_user_cache.h" + #include #include @@ -63,6 +65,7 @@ typedef struct _user_config_cache_entry { time_t expires; HashTable user_config; } user_config_cache_entry; + static HashTable user_config_cache; static int lsapi_mode = 0; @@ -176,6 +179,8 @@ static int sapi_lsapi_deactivate(void) SG(request_info).path_translated = NULL; } + php_user_cache_partition_activate(NULL); + return SUCCESS; } /* }}} */ @@ -513,6 +518,26 @@ static void sapi_lsapi_log_message(const char *message, int syslog_type_int) } /* }}} */ +static void lsapi_user_cache_log_message(const char *message) +{ + sapi_lsapi_log_message(message, 0); +} + +static const char *lsapi_user_cache_get_boundary_value(const char *name) +{ + return sapi_lsapi_getenv(name, 0); +} + +static void lsapi_user_cache_activate_request_partition(void) +{ + php_user_cache_activate_boundary_partition( + "litespeed", + lsapi_user_cache_get_boundary_value, + lsapi_user_cache_log_message, + PHP_USER_CACHE_REASON_LSAPI_BOUNDARY_UNAVAILABLE + ); +} + /* Set to 1 to turn on log messages useful during development: */ #if 0 @@ -541,6 +566,11 @@ static int sapi_lsapi_activate(void) char *path, *server_name; size_t path_len, server_name_len; + /* Resolve the user-cache partition before any early return below: the + * activate return value is ignored by sapi_activate(), so bailing out + * first would leave the request without a partition. */ + lsapi_user_cache_activate_request_partition(); + /* PATH_TRANSLATED should be defined at this stage but better safe than sorry :) */ if (!SG(request_info).path_translated) { return FAILURE; @@ -1517,6 +1547,8 @@ int main( int argc, char * argv[] ) return FAILURE; } + php_user_cache_opt_in(); + if ( climode ) { return cli_main(argc, argv); } @@ -1627,6 +1659,7 @@ static PHP_MINIT_FUNCTION(litespeed) static PHP_MSHUTDOWN_FUNCTION(litespeed) { zend_hash_destroy(&user_config_cache); + php_user_cache_boundary_partitions_shutdown(); /* UNREGISTER_INI_ENTRIES(); */ return SUCCESS; diff --git a/sapi/phpdbg/phpdbg.c b/sapi/phpdbg/phpdbg.c index 3c0d5e836dd0..aa325233a7a6 100644 --- a/sapi/phpdbg/phpdbg.c +++ b/sapi/phpdbg/phpdbg.c @@ -32,6 +32,8 @@ #include "ext/standard/basic_functions.h" +#include "ext/user_cache/php_user_cache.h" + #if defined(PHP_WIN32) && defined(HAVE_OPENSSL_EXT) # include "openssl/applink.c" #endif @@ -690,6 +692,8 @@ static inline int php_sapi_phpdbg_module_startup(sapi_module_struct *module) /* return FAILURE; } + php_user_cache_opt_in(); + phpdbg_booted = 1; return SUCCESS; From 423666f9953b2bb84b36ca9ca175e819cc9d01e7 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Tue, 14 Jul 2026 16:58:17 +0000 Subject: [PATCH 02/12] fix: include pthread.h --- ext/user_cache/user_cache_internal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/user_cache/user_cache_internal.h b/ext/user_cache/user_cache_internal.h index 48d4ddd20ecf..e9fc98cd3aa2 100644 --- a/ext/user_cache/user_cache_internal.h +++ b/ext/user_cache/user_cache_internal.h @@ -24,7 +24,7 @@ #if defined(ZEND_WIN32) && defined(_MSC_VER) # include #endif -#if !defined(ZEND_WIN32) && defined(__linux__) +#ifndef ZEND_WIN32 # include #endif From a8e4ec392d9b2db6a2213c888a83924f3e7816aa Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Tue, 14 Jul 2026 17:00:22 +0000 Subject: [PATCH 03/12] fix: void ptr cast --- ext/user_cache/user_cache_shared_graph.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/user_cache/user_cache_shared_graph.c b/ext/user_cache/user_cache_shared_graph.c index 4fdae3f303e7..3d8c4f654821 100644 --- a/ext/user_cache/user_cache_shared_graph.c +++ b/ext/user_cache/user_cache_shared_graph.c @@ -5429,7 +5429,7 @@ void php_user_cache_decode_resolve_cache_release(void) { user_cache_shared_graph_object_route_memo_release(); - memset(UC_G(decode_resolve_direct_keys), 0, sizeof(UC_G(decode_resolve_direct_keys))); + memset((void *) UC_G(decode_resolve_direct_keys), 0, sizeof(UC_G(decode_resolve_direct_keys))); memset(UC_G(decode_resolve_direct_values), 0, sizeof(UC_G(decode_resolve_direct_values))); UC_G(decode_resolve_direct_next) = 0; @@ -5447,7 +5447,7 @@ void php_user_cache_decode_resolve_cache_release(void) void php_user_cache_decode_shape_prototype_cache_release(void) { - memset(UC_G(decode_shape_prototype_direct_keys), 0, sizeof(UC_G(decode_shape_prototype_direct_keys))); + memset((void *) UC_G(decode_shape_prototype_direct_keys), 0, sizeof(UC_G(decode_shape_prototype_direct_keys))); memset(UC_G(decode_shape_prototype_direct_values), 0, sizeof(UC_G(decode_shape_prototype_direct_values))); UC_G(decode_shape_prototype_direct_next) = 0; From 98c6d6a16041ec394e1f45be8964a9e4663f5289 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Tue, 14 Jul 2026 17:02:27 +0000 Subject: [PATCH 04/12] fix: skip 32-bit over shm size test --- ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt b/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt index 43bb0800d00e..a8741d1d39ae 100644 --- a/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt +++ b/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt @@ -2,6 +2,10 @@ UserCache\Cache: shm size clamp emits a PHP warning --EXTENSIONS-- user_cache +--SKIPIF-- + --INI-- user_cache.enable=1 user_cache.enable_cli=1 From a7906867608223e1656a1099f52644be797d4737 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Tue, 14 Jul 2026 17:40:54 +0000 Subject: [PATCH 05/12] fix: repeat test failure --- ext/user_cache/tests/user_cache_add.phpt | 1 + ext/user_cache/tests/user_cache_cgi_boundary.phpt | 6 +++++- ext/user_cache/tests/user_cache_info_counters.phpt | 11 +++++------ .../tests/user_cache_litespeed_boundary.phpt | 8 +++++++- ext/user_cache/tests/user_cache_ttl_semantics.phpt | 1 + 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ext/user_cache/tests/user_cache_add.phpt b/ext/user_cache/tests/user_cache_add.phpt index 376c7e05bae7..e516e57c82df 100644 --- a/ext/user_cache/tests/user_cache_add.phpt +++ b/ext/user_cache/tests/user_cache_add.phpt @@ -10,6 +10,7 @@ user_cache.shm_size=16M --FILE-- clear(); var_dump($cache->add('key', 'first')); var_dump($cache->add('key', 'second')); diff --git a/ext/user_cache/tests/user_cache_cgi_boundary.phpt b/ext/user_cache/tests/user_cache_cgi_boundary.phpt index b81791f482d3..ceb43e238374 100644 --- a/ext/user_cache/tests/user_cache_cgi_boundary.phpt +++ b/ext/user_cache/tests/user_cache_cgi_boundary.phpt @@ -236,7 +236,9 @@ $complexKey = 'cgi-boundary-complex-key'; $action = $_GET['action'] ?? 'fetch'; $host = $_SERVER['SERVER_NAME'] ?? 'unknown'; -if ($action === 'seed') { +if ($action === 'clear') { + $cache->clear(); +} elseif ($action === 'seed') { $cache->store($key, $host . '-value'); $cache->store($complexKey, ['host' => $host, 'nested' => ['value' => 42]]); } @@ -273,6 +275,8 @@ try { stream_set_blocking($pipes[2], false); user_cache_cgi_wait($process, $pipes, $port, $alphaScript, $alphaRoot, 'alpha.local'); + user_cache_cgi_request($port, $alphaScript, $alphaRoot, 'alpha.local', 'action=clear'); + user_cache_cgi_request($port, $betaScript, $betaRoot, 'beta.local', 'action=clear'); $checks = [ [$alphaScript, $alphaRoot, 'alpha.local', 'action=seed', 'alpha.local:alpha.local-value:alpha.local:42:available'], diff --git a/ext/user_cache/tests/user_cache_info_counters.phpt b/ext/user_cache/tests/user_cache_info_counters.phpt index 8a00282620a0..f448565d9957 100644 --- a/ext/user_cache/tests/user_cache_info_counters.phpt +++ b/ext/user_cache/tests/user_cache_info_counters.phpt @@ -10,10 +10,11 @@ user_cache.shm_size=2M --FILE-- clear(); $info = UserCache\Cache::getStatus(); -var_dump($info->getExpungeCount()); -var_dump($info->getStoreFailureCount()); +$expungeBase = $info->getExpungeCount(); +$storeFailureBase = $info->getStoreFailureCount(); /* Fill until pressure resets the partition. */ $payload = str_repeat('x', 64 * 1024); @@ -22,15 +23,13 @@ for ($i = 0; $i < 64; $i++) { } $info = UserCache\Cache::getStatus(); -var_dump($info->getExpungeCount() >= 1); +var_dump($info->getExpungeCount() > $expungeBase); var_dump($cache->store('too-big', str_repeat('y', 4 * 1024 * 1024))); $info = UserCache\Cache::getStatus(); -var_dump($info->getStoreFailureCount() >= 1); +var_dump($info->getStoreFailureCount() > $storeFailureBase); ?> --EXPECT-- -int(0) -int(0) bool(true) bool(false) bool(true) diff --git a/ext/user_cache/tests/user_cache_litespeed_boundary.phpt b/ext/user_cache/tests/user_cache_litespeed_boundary.phpt index 226ad2d4bdd4..c0dcd5a32882 100644 --- a/ext/user_cache/tests/user_cache_litespeed_boundary.phpt +++ b/ext/user_cache/tests/user_cache_litespeed_boundary.phpt @@ -308,7 +308,9 @@ $key = 'litespeed-boundary-key'; $action = $_GET['action'] ?? 'fetch'; $host = $_SERVER['SERVER_NAME'] ?? 'unknown'; -if ($action === 'seed') { +if ($action === 'clear') { + $cache->clear(); +} elseif ($action === 'seed') { $cache->store($key, $host . '-value'); } @@ -338,6 +340,10 @@ try { user_cache_lsapi_wait($processes[0][0], $processes[0][1], $alphaPort, $alphaScript, $alphaRoot, 'alpha.local'); user_cache_lsapi_wait($processes[1][0], $processes[1][1], $betaPort, $betaScript, $betaRoot, 'beta.local'); + user_cache_lsapi_request($alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=clear'); + user_cache_lsapi_request($alphaPort, $otherScript, $otherRoot, 'other.local', 'action=clear'); + user_cache_lsapi_request($betaPort, $betaScript, $betaRoot, 'beta.local', 'action=clear'); + $checks = [ [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=seed', 'alpha.local:alpha.local-value:default'], [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:default'], diff --git a/ext/user_cache/tests/user_cache_ttl_semantics.phpt b/ext/user_cache/tests/user_cache_ttl_semantics.phpt index e9974b813b4c..88780b85d3c7 100644 --- a/ext/user_cache/tests/user_cache_ttl_semantics.phpt +++ b/ext/user_cache/tests/user_cache_ttl_semantics.phpt @@ -10,6 +10,7 @@ user_cache.shm_size=16M --FILE-- clear(); var_dump($cache->increment('counter', 1, 1)); From da8686c1828e4cab11f83b1f6a12008b52ea2993 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Tue, 14 Jul 2026 18:27:13 +0000 Subject: [PATCH 06/12] fix: add CONFLICTS for Windows --- ext/user_cache/tests/CONFLICTS | 1 + 1 file changed, 1 insertion(+) create mode 100644 ext/user_cache/tests/CONFLICTS diff --git a/ext/user_cache/tests/CONFLICTS b/ext/user_cache/tests/CONFLICTS new file mode 100644 index 000000000000..3b242bdd032d --- /dev/null +++ b/ext/user_cache/tests/CONFLICTS @@ -0,0 +1 @@ +user_cache From d15fc4ff930ee33caa3107b6588c0cb048720e89 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Wed, 15 Jul 2026 02:04:15 +0000 Subject: [PATCH 07/12] chore: coding style --- ext/date/php_date.c | 10 ++++---- ext/spl/spl_array.c | 49 +++++++++++++++++++++++++++--------- ext/spl/spl_dllist.c | 17 ++++++++----- ext/spl/spl_fixedarray.c | 20 +++++++++------ ext/spl/spl_heap.c | 27 +++++++++++++------- ext/user_cache/config.m4 | 2 -- ext/user_cache/user_cache.c | 50 +++++++++++++++++++++++++------------ 7 files changed, 117 insertions(+), 58 deletions(-) diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 1e4112db9cc6..4a62e92dd1fa 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -17,7 +17,7 @@ #include "php_ini.h" #include "ext/standard/info.h" #include "ext/standard/php_versioning.h" -#include "ext/user_cache/php_user_cache.h" +#include "ext/user_cache/php_user_cache.h" /* For user_cache safe direct path */ #include "php_date.h" #include "zend_attributes.h" #include "zend_interfaces.h" @@ -4764,10 +4764,10 @@ static void php_date_interval_initialize_from_hash(php_interval_obj *intobj, con intobj->initialized = true; } /* }}} */ -#define PHP_DATE_USER_CACHE_STATE_TIMEZONE "timezone" -#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_TIMEZONE) - 1) -#define PHP_DATE_USER_CACHE_STATE_CTIME "ctime" -#define PHP_DATE_USER_CACHE_STATE_CTIME_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_CTIME) - 1) +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE "timezone" +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_TIMEZONE) - 1) +#define PHP_DATE_USER_CACHE_STATE_CTIME "ctime" +#define PHP_DATE_USER_CACHE_STATE_CTIME_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_CTIME) - 1) static void php_date_user_cache_add_assoc_int64(zval *arr, const char *name, int64_t value) { diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index a223bdd1964f..691a5eae99cb 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -18,7 +18,7 @@ #include "php.h" #include "ext/standard/php_var.h" -#include "ext/user_cache/php_user_cache.h" +#include "ext/user_cache/php_user_cache.h" #include "ext/user_cache/php_user_cache.h" /* For user_cache safe direct path */ #include "zend_smart_str.h" #include "zend_interfaces.h" #include "zend_exceptions.h" @@ -1540,19 +1540,28 @@ static void spl_array_object_user_cache_serialize_state(zval *object, zval *retu static void spl_array_object_user_cache_unserialize_state(zval *object, HashTable *data) { spl_array_object *intern = Z_SPLARRAY_P(object); + zend_class_entry *ce; zval *flags_zv, *storage_zv, *iterator_class_zv; zend_long flags; - flags_zv = zend_hash_index_find(data, 0); - storage_zv = zend_hash_index_find(data, 1); + flags_zv = zend_hash_index_find(data, 0); + storage_zv = zend_hash_index_find(data, 1); iterator_class_zv = zend_hash_index_find(data, 2); if (!flags_zv || !storage_zv || Z_TYPE_P(flags_zv) != IS_LONG || - (iterator_class_zv && (Z_TYPE_P(iterator_class_zv) != IS_NULL && - Z_TYPE_P(iterator_class_zv) != IS_STRING))) { - zend_throw_exception(spl_ce_UnexpectedValueException, - "Incomplete or ill-typed serialization data", 0); + (iterator_class_zv && + ( + Z_TYPE_P(iterator_class_zv) != IS_NULL && + Z_TYPE_P(iterator_class_zv) != IS_STRING + ) + ) + ) { + zend_throw_exception( + spl_ce_UnexpectedValueException, + "Incomplete or ill-typed serialization data", + 0 + ); return; } @@ -1562,29 +1571,40 @@ static void spl_array_object_user_cache_unserialize_state(zval *object, HashTabl if (flags & SPL_ARRAY_IS_SELF) { zval_ptr_dtor(&intern->array); + ZVAL_UNDEF(&intern->array); } else { if (Z_TYPE_P(storage_zv) != IS_OBJECT && Z_TYPE_P(storage_zv) != IS_ARRAY) { zend_throw_exception(spl_ce_InvalidArgumentException, "Passed variable is not an array or object", 0); + return; } + spl_array_set_array(object, intern, storage_zv, 0L, true); } if (iterator_class_zv && Z_TYPE_P(iterator_class_zv) == IS_STRING) { - zend_class_entry *ce = zend_lookup_class(Z_STR_P(iterator_class_zv)); + ce = zend_lookup_class(Z_STR_P(iterator_class_zv)); if (!ce) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, + zend_throw_exception_ex( + spl_ce_UnexpectedValueException, + 0, "Cannot deserialize ArrayObject with iterator class '%s'; no such class exists", - ZSTR_VAL(Z_STR_P(iterator_class_zv))); + ZSTR_VAL(Z_STR_P(iterator_class_zv)) + ); + return; } if (!instanceof_function(ce, spl_ce_ArrayIterator)) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, + zend_throw_exception_ex( + spl_ce_UnexpectedValueException, + 0, "Cannot deserialize ArrayObject with iterator class '%s'; this class is not derived from ArrayIterator", - ZSTR_VAL(Z_STR_P(iterator_class_zv))); + ZSTR_VAL(Z_STR_P(iterator_class_zv)) + ); + return; } @@ -1619,7 +1639,9 @@ static bool spl_array_object_copy_user_cache_state( if (old_intern->ar_flags & SPL_ARRAY_IS_SELF) { zval_ptr_dtor(&new_intern->array); + ZVAL_UNDEF(&new_intern->array); + result = true; goto cleanup; @@ -1664,11 +1686,14 @@ static bool spl_array_object_user_cache_state_has_unstorable( static bool spl_array_object_serialize_user_cache_state(zval *state, const zval *object) { ZVAL_UNDEF(state); + spl_array_object_user_cache_serialize_state((zval *) object, state); + if (EG(exception) || Z_TYPE_P(state) != IS_ARRAY) { if (Z_TYPE_P(state) != IS_UNDEF) { zval_ptr_dtor(state); } + ZVAL_UNDEF(state); return false; diff --git a/ext/spl/spl_dllist.c b/ext/spl/spl_dllist.c index f43f14f34b1c..bf3d586fda42 100644 --- a/ext/spl/spl_dllist.c +++ b/ext/spl/spl_dllist.c @@ -17,7 +17,7 @@ #endif #include "php.h" -#include "ext/user_cache/php_user_cache.h" +#include "ext/user_cache/php_user_cache.h" /* For user_cache safe direct support path */ #include "zend_interfaces.h" #include "zend_exceptions.h" #include "zend_hash.h" @@ -1112,17 +1112,16 @@ static void spl_dllist_object_user_cache_serialize_state(zval *object, zval *ret array_init(return_value); - /* flags */ ZVAL_LONG(&tmp, intern->flags); zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); - /* elements */ array_init_size(&tmp, intern->llist->count); while (current) { zend_hash_next_index_insert(Z_ARRVAL(tmp), ¤t->data); Z_TRY_ADDREF(current->data); current = current->next; } + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); } @@ -1131,13 +1130,15 @@ static void spl_dllist_object_user_cache_serialize_state(zval *object, zval *ret static bool spl_dllist_object_user_cache_unserialize_state(zval *object, HashTable *data) { spl_dllist_object *intern = Z_SPLDLLIST_P(object); - zval *flags_zv, *storage_zv, *elem; zend_long flags; + zval *flags_zv, *storage_zv, *elem; flags_zv = zend_hash_index_find(data, 0); storage_zv = zend_hash_index_find(data, 1); - if (!flags_zv || !storage_zv || - Z_TYPE_P(flags_zv) != IS_LONG || Z_TYPE_P(storage_zv) != IS_ARRAY + if (!flags_zv || + !storage_zv || + Z_TYPE_P(flags_zv) != IS_LONG || + Z_TYPE_P(storage_zv) != IS_ARRAY ) { return false; } @@ -1152,6 +1153,7 @@ static bool spl_dllist_object_user_cache_unserialize_state(zval *object, HashTab } intern->flags = (int) flags; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(storage_zv), elem) { spl_ptr_llist_push(intern->llist, elem); } ZEND_HASH_FOREACH_END(); @@ -1183,12 +1185,15 @@ static bool spl_dllist_object_copy_user_cache_state( current = old_intern->llist->head; while (current) { ZVAL_UNDEF(&cloned_elem); + if (!clone_value(ctx, &cloned_elem, ¤t->data)) { return false; } spl_ptr_llist_push(new_intern->llist, &cloned_elem); + zval_ptr_dtor(&cloned_elem); + current = current->next; } diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c index aa1884e66e9e..d8d4ffa5c3c2 100644 --- a/ext/spl/spl_fixedarray.c +++ b/ext/spl/spl_fixedarray.c @@ -26,7 +26,7 @@ #include "spl_fixedarray.h" #include "spl_exceptions.h" #include "ext/json/php_json.h" /* For php_json_serializable_ce */ -#include "ext/user_cache/php_user_cache.h" +#include "ext/user_cache/php_user_cache.h" /* For user_cache safe direct support path */ static zend_object_handlers spl_handler_SplFixedArray; PHPAPI zend_class_entry *spl_ce_SplFixedArray; @@ -653,18 +653,18 @@ PHP_METHOD(SplFixedArray, __unserialize) } } -/* Builds an array containing only the fixed-size elements, without the object - * properties (those are handled separately by the user cache). */ static void spl_fixedarray_object_build_user_cache_elements(spl_fixedarray_object *intern, zval *return_value) { - zval *current; zend_long i; + zval *current; array_init_size(return_value, intern->array.size); for (i = 0; i < intern->array.size; i++) { current = &intern->array.elements[i]; + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), current); + Z_TRY_ADDREF_P(current); } } @@ -699,11 +699,13 @@ static bool spl_fixedarray_object_copy_user_cache_state( for (i = 0; i < size; i++) { ZVAL_UNDEF(&cloned_elem); + if (!clone_value(ctx, &cloned_elem, &old_intern->array.elements[i])) { return false; } ZVAL_COPY_DEREF(&new_intern->array.elements[i], &cloned_elem); + zval_ptr_dtor(&cloned_elem); } @@ -723,7 +725,9 @@ static bool spl_fixedarray_object_user_cache_state_has_unstorable( } spl_fixedarray_object_build_user_cache_elements(Z_SPLFIXEDARRAY_P((zval *) object), &state_zv); + result = value_has_unstorable(ctx, &state_zv); + zval_ptr_dtor(&state_zv); return result; @@ -736,25 +740,25 @@ static bool spl_fixedarray_object_serialize_user_cache_state(zval *state, const return true; } -/* Restores the fixed-size elements from the user-cache state (elements only, no - * object properties). */ static void spl_fixedarray_object_user_cache_unserialize_state(zval *object, HashTable *data) { spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(object); - zval *elem; zend_long size; + zval *elem; if (intern->array.size != 0) { return; } size = zend_hash_num_elements(data); - spl_fixedarray_init_non_empty_struct(&intern->array, size); if (!size) { return; } + spl_fixedarray_init_non_empty_struct(&intern->array, size); + intern->array.size = 0; + ZEND_HASH_FOREACH_VAL(data, elem) { ZVAL_COPY_DEREF(&intern->array.elements[intern->array.size], elem); intern->array.size++; diff --git a/ext/spl/spl_heap.c b/ext/spl/spl_heap.c index e58b4d6eb8ac..2d95caa4ef35 100644 --- a/ext/spl/spl_heap.c +++ b/ext/spl/spl_heap.c @@ -1265,13 +1265,13 @@ static bool spl_heap_object_is_pqueue(zend_class_entry *ce) return instanceof_function(ce, spl_ce_SplPriorityQueue); } -/* Grows the heap backing store to hold at least `count` elements; used by the - * user-cache safe-direct bulk restore path. */ static void spl_heap_user_cache_ensure_capacity(spl_ptr_heap *heap, size_t count) { + size_t alloc_size; + while (count > heap->max_size) { - size_t alloc_size = heap->max_size * heap->elem_size; - /* we need to allocate more memory */ + alloc_size = heap->max_size * heap->elem_size; + heap->elements = safe_erealloc(heap->elements, 2, alloc_size, 0); memset((char *) heap->elements + alloc_size, 0, alloc_size); heap->max_size *= 2; @@ -1335,9 +1335,11 @@ static bool spl_heap_object_copy_user_cache_state( ZVAL_UNDEF(&cloned_data); ZVAL_UNDEF(&cloned_priority); + if (!clone_value(ctx, &cloned_data, &old_elem->data)) { return false; } + if (!clone_value(ctx, &cloned_priority, &old_elem->priority)) { zval_ptr_dtor(&cloned_data); @@ -1350,7 +1352,9 @@ static bool spl_heap_object_copy_user_cache_state( zval_ptr_dtor(&cloned_priority); } else { zv_old_elem = spl_heap_elem(old_intern->heap, i); + ZVAL_UNDEF(&cloned_elem); + if (!clone_value(ctx, &cloned_elem, zv_old_elem)) { return false; } @@ -1390,7 +1394,8 @@ static bool spl_heap_object_user_cache_state_has_unstorable( elem = spl_heap_elem(intern->heap, i); if (value_has_unstorable(ctx, &elem->data) || - value_has_unstorable(ctx, &elem->priority)) { + value_has_unstorable(ctx, &elem->priority) + ) { return true; } } else { @@ -1409,8 +1414,7 @@ static bool spl_heap_object_serialize_user_cache_state(zval *state, const zval * { spl_heap_object *intern; spl_pqueue_elem *pqueue_elem; - zval *heap_elem; - zval elems, entry, tmp; + zval *heap_elem, elems, entry, tmp; size_t i; bool is_pqueue; @@ -1432,8 +1436,10 @@ static bool spl_heap_object_serialize_user_cache_state(zval *state, const zval * pqueue_elem = spl_heap_elem(intern->heap, i); array_init_size(&entry, 2); + Z_TRY_ADDREF(pqueue_elem->data); zend_hash_index_add_new(Z_ARRVAL(entry), 0, &pqueue_elem->data); + Z_TRY_ADDREF(pqueue_elem->priority); zend_hash_index_add_new(Z_ARRVAL(entry), 1, &pqueue_elem->priority); zend_hash_next_index_insert(Z_ARRVAL(elems), &entry); @@ -1465,8 +1471,11 @@ static bool spl_heap_object_unserialize_user_cache_state(zval *object, zval *sta data = Z_ARRVAL_P(state); flags_zv = zend_hash_index_find(data, 0); elements_zv = zend_hash_index_find(data, 1); - if (!flags_zv || !elements_zv || - Z_TYPE_P(flags_zv) != IS_LONG || Z_TYPE_P(elements_zv) != IS_ARRAY + + if (!flags_zv || + !elements_zv || + Z_TYPE_P(flags_zv) != IS_LONG || + Z_TYPE_P(elements_zv) != IS_ARRAY ) { return false; } diff --git a/ext/user_cache/config.m4 b/ext/user_cache/config.m4 index a7c45f7bc826..0cae97ab8e52 100644 --- a/ext/user_cache/config.m4 +++ b/ext/user_cache/config.m4 @@ -1,5 +1,3 @@ -dnl config.m4 for extension user_cache - PHP_NEW_EXTENSION([user_cache], m4_normalize([ user_cache.c user_cache_storage.c diff --git a/ext/user_cache/user_cache.c b/ext/user_cache/user_cache.c index db9cf5f81640..8bab95562ad8 100644 --- a/ext/user_cache/user_cache.c +++ b/ext/user_cache/user_cache.c @@ -1,6 +1,4 @@ /* - +----------------------------------------------------------------------+ - | PHP Cache | +----------------------------------------------------------------------+ | Copyright © The PHP Group and Contributors. | +----------------------------------------------------------------------+ @@ -10,9 +8,9 @@ | | | SPDX-License-Identifier: BSD-3-Clause | +----------------------------------------------------------------------+ - | Author: Go Kudo | + | Authors: Go Kudo | +----------------------------------------------------------------------+ -*/ + */ #include "php.h" #include "php_user_cache.h" @@ -32,9 +30,9 @@ #include "SAPI.h" -#define PHP_USER_CACHE_API_VALUE_TYPE "object|array|string|int|float|bool|null" -#define PHP_USER_CACHE_STORAGE_KEY_CACHE_MAX 4096U -#define PHP_USER_CACHE_MAX_BOUNDARY_PARTITIONS 32U +#define PHP_USER_CACHE_API_VALUE_TYPE "object|array|string|int|float|bool|null" +#define PHP_USER_CACHE_STORAGE_KEY_CACHE_MAX 4096U +#define PHP_USER_CACHE_MAX_BOUNDARY_PARTITIONS 32U #define PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(type) \ static type *type##_from_obj(zend_object *obj) \ @@ -785,8 +783,8 @@ static zend_result user_cache_fetch_multiple_api( zval *default_value, zval *return_value) { - zend_string **prepared_keys, **storage_keys, *key, *storage_key; php_user_cache_fetch_pending_seed *pending_seeds; + zend_string **prepared_keys, **storage_keys, *key, *storage_key; zval val; uint32_t count, i; bool rlock_held; @@ -832,6 +830,7 @@ static zend_result user_cache_fetch_multiple_api( } rlock_held = true; + zend_try { for (i = 0; i < count; i++) { key = prepared_keys[i]; @@ -1086,6 +1085,7 @@ static void user_cache_return_status(zval *return_value) status->availability_reason = php_user_cache_active_runtime()->failure_reason; user_cache_collect_info_stats(&status->stats); + status->initialized = true; } @@ -1192,6 +1192,7 @@ static void user_cache_collect_pool_status( } entries = php_user_cache_entries_ptr(header); + /* Release the lock before propagating an allocation bailout. */ zend_try { for (i = 0; i < header->capacity; i++) { @@ -1230,12 +1231,14 @@ static void user_cache_return_pool_status( php_user_cache_ensure_ready(); object_init_ex(return_value, php_user_cache_pool_status_ce); + status = php_user_cache_pool_status_object_from_obj(Z_OBJ_P(return_value)); status->scope = zend_string_copy(cache->scope); status->scope_prefix = zend_string_copy(cache->scope_prefix); status->context = cache->context; array_init(&status->entry_keys); + user_cache_collect_pool_status( status->context, status->scope_prefix, @@ -1360,6 +1363,7 @@ static void user_cache_release_pools(void) zend_hash_destroy(UC_G(pool_table)); FREE_HASHTABLE(UC_G(pool_table)); + UC_G(pool_table) = NULL; } @@ -1387,6 +1391,7 @@ static zend_object *user_cache_get_or_create_pool(zend_string *pool) cache->scope_prefix = user_cache_build_scope_prefix(pool); ZVAL_OBJ(&pool_zv, obj); + if (zend_hash_add(pools, pool, &pool_zv) == NULL) { OBJ_RELEASE(obj); @@ -1427,6 +1432,7 @@ static zend_string *user_cache_storage_key_build( ZSTR_VAL(key), ZSTR_LEN(key) ); + zend_string_hash_val(storage_key); if (cache->storage_key_cache == NULL) { @@ -1555,6 +1561,7 @@ static bool user_cache_store_storage_key_prevalidated(zend_string *key, zval *va } zend_catch { php_user_cache_unlock_if_held(); php_user_cache_destroy_prepared_value(&prepared); + zend_bailout(); } zend_end_try(); @@ -1572,7 +1579,7 @@ static bool user_cache_store_storage_key_prevalidated(zend_string *key, zval *va static int user_cache_bulk_order_compare(const void *lhs_ptr, const void *rhs_ptr) { const php_user_cache_bulk_order *lhs, *rhs; - size_t lhs_len, rhs_len, compare_len; + size_t lhs_len, rhs_len, cmp_len; int result; lhs = lhs_ptr; @@ -1584,9 +1591,9 @@ static int user_cache_bulk_order_compare(const void *lhs_ptr, const void *rhs_pt lhs_len = ZSTR_LEN(lhs->key); rhs_len = ZSTR_LEN(rhs->key); - compare_len = lhs_len < rhs_len ? lhs_len : rhs_len; + cmp_len = lhs_len < rhs_len ? lhs_len : rhs_len; - result = memcmp(ZSTR_VAL(lhs->key), ZSTR_VAL(rhs->key), compare_len); + result = memcmp(ZSTR_VAL(lhs->key), ZSTR_VAL(rhs->key), cmp_len); if (result != 0) { return result < 0 ? -1 : 1; } @@ -1666,6 +1673,7 @@ static uint32_t user_cache_prepare_bulk_store_items( if (!php_user_cache_prepare_value(storage_key, value, prep_opts, &items[i].prepared)) { php_user_cache_destroy_prepared_value(&items[i].prepared); + zend_string_release(storage_key); *result = false; @@ -1757,7 +1765,6 @@ static bool user_cache_instance_store_multiple( php_user_cache_bulk_store_item *items; php_user_cache_bulk_order *order; zend_string **storage_keys; - bool *acquired; php_user_cache_prepare_options prep_opts = { .caller_holds_write_lock = false, .throw_on_failure = false, @@ -1770,7 +1777,7 @@ static bool user_cache_instance_store_multiple( .capture_replaced_entry = true, }; uint32_t i, count, prepared_count, stored_count = 0; - bool result; + bool result, *acquired; if (!user_cache_can_write()) { return false; @@ -1811,6 +1818,7 @@ static bool user_cache_instance_store_multiple( ); } zend_catch { php_user_cache_unlock_if_held(); + zend_bailout(); } zend_end_try(); @@ -1825,6 +1833,7 @@ static bool user_cache_instance_store_multiple( for (i = 0; i < prepared_count; i++) { php_user_cache_destroy_prepared_value(&items[i].prepared); + zend_string_release(storage_keys[i]); } @@ -1878,6 +1887,7 @@ static bool user_cache_instance_delete_multiple( } } zend_catch { php_user_cache_unlock_if_held(); + zend_bailout(); } zend_end_try(); @@ -1945,6 +1955,7 @@ static void user_cache_partitions_shutdown(void) php_user_cache_context *prev_ctx; prev_ctx = UC_G(active_context_ptr); + partition = php_user_cache_partitions; while (partition != NULL) { next = partition->next; @@ -2071,6 +2082,7 @@ static php_user_cache_boundary_partition *user_cache_create_boundary_partition( } memcpy(entry->boundary, boundary, boundary_len); + entry->boundary[boundary_len] = '\0'; entry->boundary_len = boundary_len; entry->boundary_hash = boundary_hash; @@ -2098,6 +2110,7 @@ static php_user_cache_boundary_partition *user_cache_create_boundary_partition( entry->partition->context.boundary_shared = true; entry->next = php_user_cache_boundary_partitions; + php_user_cache_boundary_partitions = entry; php_user_cache_boundary_partition_count++; @@ -2316,8 +2329,8 @@ ZEND_API void php_user_cache_activate_boundary_partition( void (*log_message)(const char *message), php_user_cache_reason failure_reason) { - php_user_cache_partition *partition = NULL; const char *boundary; + php_user_cache_partition *partition = NULL; boundary = get_env("DOCUMENT_ROOT"); if (boundary == NULL || boundary[0] == '\0') { @@ -2417,6 +2430,7 @@ void php_user_cache_minit(void) php_user_cache_runtime_opted_in = false; user_cache_register_classes(); + php_user_cache_safe_direct_handlers_init(); php_user_cache_optimistic_fork_setup(); @@ -2442,6 +2456,7 @@ void php_user_cache_mshutdown(void) php_user_cache_restore_context(prev_ctx); UC_G(active_partition) = NULL; + php_user_cache_runtime_opted_in = false; php_user_cache_safe_direct_handlers_destroy(); @@ -2527,7 +2542,8 @@ static void user_cache_store_method(INTERNAL_FUNCTION_PARAMETERS, bool add_only) } if (!(user_cache_can_write() && - user_cache_store_storage_key_prevalidated(storage_key, value, ttl, add_only))) { + user_cache_store_storage_key_prevalidated(storage_key, value, ttl, add_only)) + ) { zend_string_release(storage_key); if (EG(exception)) { @@ -2580,6 +2596,7 @@ static void user_cache_atomic_update_method(INTERNAL_FUNCTION_PARAMETERS, bool d } updated = user_cache_atomic_update_api(storage_key, step, ttl, decrement, &result); + zend_string_release(storage_key); if (result.is_type_error) { @@ -2735,7 +2752,8 @@ ZEND_METHOD(UserCache_Cache, hasPool) } RETURN_BOOL(UC_G(pool_table) != NULL && - zend_hash_exists(UC_G(pool_table), pool)); + zend_hash_exists(UC_G(pool_table), pool) + ); } ZEND_METHOD(UserCache_Cache, getPool) From 9471400c4000187333e77dc1b0019130d861b31d Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Wed, 15 Jul 2026 02:18:04 +0000 Subject: [PATCH 08/12] chore: coding style 2 --- sapi/fpm/fpm/fpm_user_cache.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sapi/fpm/fpm/fpm_user_cache.h b/sapi/fpm/fpm/fpm_user_cache.h index 60e7fae39804..bae3d9e0432a 100644 --- a/sapi/fpm/fpm/fpm_user_cache.h +++ b/sapi/fpm/fpm/fpm_user_cache.h @@ -13,8 +13,8 @@ */ #ifndef FPM_USER_CACHE_H -#define FPM_USER_CACHE_H 1 +# define FPM_USER_CACHE_H int fpm_user_cache_init_main(void); -#endif +#endif /* FPM_USER_CACHE_H */ From 1d9d1fedd37d21b6ff578d66702101706dc492a5 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Wed, 15 Jul 2026 02:21:27 +0000 Subject: [PATCH 09/12] test: add CONFLICTS all into user_cache fpm tests --- ext/user_cache/tests/fpm/CONFLICTS | 1 + 1 file changed, 1 insertion(+) create mode 100644 ext/user_cache/tests/fpm/CONFLICTS diff --git a/ext/user_cache/tests/fpm/CONFLICTS b/ext/user_cache/tests/fpm/CONFLICTS new file mode 100644 index 000000000000..0702cb5bfbb0 --- /dev/null +++ b/ext/user_cache/tests/fpm/CONFLICTS @@ -0,0 +1 @@ +all From 1c0049c8dd585811c406ade95e5c6f8d1eac2fad Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Wed, 15 Jul 2026 04:22:20 +0000 Subject: [PATCH 10/12] fix: minimize upstream changes and add support DatePeriod and DateInterval --- ext/date/php_date.c | 2268 +++++++++-------- ext/spl/spl_array.c | 2 +- .../tests/fpm/user_cache_fpm_dateperiod.phpt | 112 + .../user_cache_dateinterval_safe_direct.phpt | 87 + .../user_cache_dateperiod_safe_direct.phpt | 156 ++ 5 files changed, 1519 insertions(+), 1106 deletions(-) create mode 100644 ext/user_cache/tests/fpm/user_cache_fpm_dateperiod.phpt create mode 100644 ext/user_cache/tests/user_cache_dateinterval_safe_direct.phpt create mode 100644 ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 4a62e92dd1fa..15332a1aac81 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -4764,1460 +4764,1518 @@ static void php_date_interval_initialize_from_hash(php_interval_obj *intobj, con intobj->initialized = true; } /* }}} */ -#define PHP_DATE_USER_CACHE_STATE_TIMEZONE "timezone" -#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_TIMEZONE) - 1) -#define PHP_DATE_USER_CACHE_STATE_CTIME "ctime" -#define PHP_DATE_USER_CACHE_STATE_CTIME_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_CTIME) - 1) - -static void php_date_user_cache_add_assoc_int64(zval *arr, const char *name, int64_t value) +/* {{{ */ +PHP_METHOD(DateInterval, __set_state) { -#if PHP_DATE_SIZEOF_LONG == 8 - add_assoc_long(arr, name, (zend_long) value); -#else - add_assoc_str(arr, name, zend_strpprintf(0, "%lld", (long long) value)); -#endif -} + php_interval_obj *intobj; + HashTable *myht; -static void php_date_user_cache_copy_time_snapshot(timelib_time *dst, const timelib_time *src) -{ - memset(dst, 0, sizeof(*dst)); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(myht) + ZEND_PARSE_PARAMETERS_END(); - dst->y = src->y; - dst->m = src->m; - dst->d = src->d; - dst->h = src->h; - dst->i = src->i; - dst->s = src->s; - dst->us = src->us; - dst->z = src->z; - dst->dst = src->dst; - dst->relative = src->relative; - dst->sse = src->sse; - dst->have_time = src->have_time; - dst->have_date = src->have_date; - dst->have_zone = src->have_zone; - dst->have_relative = src->have_relative; - dst->have_weeknr_day = src->have_weeknr_day; - dst->sse_uptodate = src->sse_uptodate; - dst->tim_uptodate = src->tim_uptodate; - dst->is_localtime = src->is_localtime; - dst->zone_type = src->zone_type; + php_date_instantiate(date_ce_interval, return_value); + intobj = Z_PHPINTERVAL_P(return_value); + php_date_interval_initialize_from_hash(intobj, myht); } +/* }}} */ -static bool php_date_copy_user_cache_state( - void *ctx, - zend_object *new_object, - zend_object *old_object, - php_user_cache_safe_direct_clone_value_func_t clone_value) +/* {{{ */ +PHP_METHOD(DateInterval, __serialize) { - php_date_obj *new_obj, *old_obj; - php_timezone_obj *new_tzobj, *old_tzobj; - php_interval_obj *new_intervalobj, *old_intervalobj; - - (void) ctx; + zval *object = ZEND_THIS; + php_interval_obj *intervalobj; + HashTable *myht; - if (clone_value == NULL) { - return false; - } + ZEND_PARSE_PARAMETERS_NONE(); - if (instanceof_function(old_object->ce, date_ce_date) || - instanceof_function(old_object->ce, date_ce_immutable) - ) { - old_obj = php_date_obj_from_obj(old_object); - new_obj = php_date_obj_from_obj(new_object); + intervalobj = Z_PHPINTERVAL_P(object); + DATE_CHECK_INITIALIZED(intervalobj->initialized, Z_OBJCE_P(object)); - if (old_obj->time == NULL) { - return true; - } + array_init(return_value); + myht = Z_ARRVAL_P(return_value); + date_interval_object_to_hash(intervalobj, myht); - new_obj->time = timelib_time_clone(old_obj->time); + add_common_properties(myht, &intervalobj->std); +} +/* }}} */ - return new_obj->time != NULL; +static bool date_interval_is_internal_property(const zend_string *name) +{ + if ( + zend_string_equals_literal(name, "date_string") || + zend_string_equals_literal(name, "from_string") || + zend_string_equals_literal(name, "y") || + zend_string_equals_literal(name, "m") || + zend_string_equals_literal(name, "d") || + zend_string_equals_literal(name, "h") || + zend_string_equals_literal(name, "i") || + zend_string_equals_literal(name, "s") || + zend_string_equals_literal(name, "f") || + zend_string_equals_literal(name, "invert") || + zend_string_equals_literal(name, "days") + ) { + return true; } + return false; +} - if (instanceof_function(old_object->ce, date_ce_timezone)) { - old_tzobj = php_timezone_obj_from_obj(old_object); - new_tzobj = php_timezone_obj_from_obj(new_object); +static void restore_custom_dateinterval_properties(zval *object, const HashTable *myht) +{ + zend_string *prop_name; + zval *prop_val; - if (!old_tzobj->initialized) { - return true; + ZEND_HASH_FOREACH_STR_KEY_VAL(myht, prop_name, prop_val) { + if (!prop_name || (Z_TYPE_P(prop_val) == IS_REFERENCE) || date_interval_is_internal_property(prop_name)) { + continue; } + update_property(Z_OBJ_P(object), prop_name, prop_val); + } ZEND_HASH_FOREACH_END(); +} - new_tzobj->type = old_tzobj->type; - new_tzobj->initialized = true; - switch (new_tzobj->type) { - case TIMELIB_ZONETYPE_ID: - new_tzobj->tzi.tz = old_tzobj->tzi.tz; - - return true; - case TIMELIB_ZONETYPE_OFFSET: - new_tzobj->tzi.utc_offset = old_tzobj->tzi.utc_offset; - - return true; - case TIMELIB_ZONETYPE_ABBR: - new_tzobj->tzi.z.utc_offset = old_tzobj->tzi.z.utc_offset; - new_tzobj->tzi.z.dst = old_tzobj->tzi.z.dst; - new_tzobj->tzi.z.abbr = old_tzobj->tzi.z.abbr != NULL - ? timelib_strdup(old_tzobj->tzi.z.abbr) - : NULL - ; - return old_tzobj->tzi.z.abbr == NULL || new_tzobj->tzi.z.abbr != NULL; - default: - return false; - } - } +/* {{{ */ +PHP_METHOD(DateInterval, __unserialize) +{ + zval *object = ZEND_THIS; + php_interval_obj *intervalobj; + HashTable *myht; - if (instanceof_function(old_object->ce, date_ce_interval)) { - old_intervalobj = php_interval_obj_from_obj(old_object); - new_intervalobj = php_interval_obj_from_obj(new_object); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(myht) + ZEND_PARSE_PARAMETERS_END(); - new_intervalobj->civil_or_wall = old_intervalobj->civil_or_wall; - new_intervalobj->from_string = old_intervalobj->from_string; + intervalobj = Z_PHPINTERVAL_P(object); - if (old_intervalobj->date_string != NULL) { - new_intervalobj->date_string = zend_string_copy(old_intervalobj->date_string); - } + php_date_interval_initialize_from_hash(intervalobj, myht); + restore_custom_dateinterval_properties(object, myht); +} +/* }}} */ - new_intervalobj->initialized = old_intervalobj->initialized; +/* {{{ */ +PHP_METHOD(DateInterval, __wakeup) +{ + zval *object = ZEND_THIS; + php_interval_obj *intobj; + const HashTable *myht; - if (old_intervalobj->diff != NULL) { - new_intervalobj->diff = timelib_rel_time_clone(old_intervalobj->diff); + ZEND_PARSE_PARAMETERS_NONE(); - return new_intervalobj->diff != NULL; - } + intobj = Z_PHPINTERVAL_P(object); - return true; - } + myht = Z_OBJPROP_P(object); - return false; + php_date_interval_initialize_from_hash(intobj, myht); } +/* }}} */ -static bool php_date_user_cache_timezone_pair_to_hash(php_date_obj *dateobj, HashTable *props) +static void date_interval_instantiate_from_time(zval *return_value, timelib_time *time, zend_string *time_str) { - zval zv; - - ZVAL_LONG(&zv, dateobj->time->zone_type); - zend_hash_str_update(props, "timezone_type", sizeof("timezone_type")-1, &zv); - - switch (dateobj->time->zone_type) { - case TIMELIB_ZONETYPE_ID: - ZVAL_STRING(&zv, dateobj->time->tz_info->name); - break; - case TIMELIB_ZONETYPE_OFFSET: - ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z)); - break; - case TIMELIB_ZONETYPE_ABBR: - ZVAL_STRING(&zv, dateobj->time->tz_abbr); - break; - default: - return false; - } - zend_hash_str_update(props, "timezone", sizeof("timezone")-1, &zv); + php_interval_obj *diobj; - return true; + php_date_instantiate(date_ce_interval, return_value); + diobj = Z_PHPINTERVAL_P(return_value); + diobj->diff = timelib_rel_time_clone(&time->relative); + diobj->initialized = true; + diobj->civil_or_wall = PHP_DATE_CIVIL; + diobj->from_string = true; + diobj->date_string = zend_string_copy(time_str); } -static bool php_date_serialize_datetime_user_cache_state(php_date_obj *dateobj, zval *state) +/* {{{ Uses the normal date parsers and sets up a DateInterval from the relative parts of the parsed string */ +PHP_FUNCTION(date_interval_create_from_date_string) { - timelib_time snapshot; - zval zv; - - if (dateobj->time == NULL || !dateobj->time->is_localtime) { - return false; - } + zend_string *time_str = NULL; + timelib_time *time; + timelib_error_container *err = NULL; - /* ctime (binary timelib_time snapshot) plus timezone_type/timezone. The - * "date" string of the regular __serialize is omitted; unserialize rebuilds - * the time fields from the snapshot but still reads the timezone pair. */ - array_init_size(state, 3); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(time_str) + ZEND_PARSE_PARAMETERS_END(); - php_date_user_cache_copy_time_snapshot(&snapshot, dateobj->time); + time = timelib_strtotime(ZSTR_VAL(time_str), ZSTR_LEN(time_str), &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper); - if (!php_date_user_cache_timezone_pair_to_hash(dateobj, Z_ARRVAL_P(state))) { - zval_ptr_dtor(state); - ZVAL_UNDEF(state); + if (err->error_count > 0) { + php_error_docref(NULL, E_WARNING, "Unknown or bad format (%s) at position %d (%c): %s", ZSTR_VAL(time_str), + err->error_messages[0].position, err->error_messages[0].character ? err->error_messages[0].character : ' ', err->error_messages[0].message); + RETVAL_FALSE; + goto cleanup; + } - return false; + if (time->have_date || time->have_time || time->have_zone) { + php_error_docref(NULL, E_WARNING, "String '%s' contains non-relative elements", ZSTR_VAL(time_str)); + RETVAL_FALSE; + goto cleanup; } - ZVAL_STRINGL(&zv, (const char *) &snapshot, sizeof(snapshot)); - zend_hash_str_update(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN, &zv); + date_interval_instantiate_from_time(return_value, time, time_str); - return true; +cleanup: + timelib_time_dtor(time); + timelib_error_container_dtor(err); } +/* }}} */ -static bool php_date_serialize_user_cache_state(zval *state, const zval *object) +/* {{{ Uses the normal date parsers and sets up a DateInterval from the relative parts of the parsed string */ +PHP_METHOD(DateInterval, createFromDateString) { - php_timezone_obj *tzobj; - php_interval_obj *intervalobj; - - ZVAL_UNDEF(state); + zend_string *time_str = NULL; + timelib_time *time; + timelib_error_container *err = NULL; - if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || - instanceof_function(Z_OBJCE_P(object), date_ce_immutable) - ) { - return php_date_serialize_datetime_user_cache_state(Z_PHPDATE_P((zval *) object), state); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(time_str) + ZEND_PARSE_PARAMETERS_END(); + + time = timelib_strtotime(ZSTR_VAL(time_str), ZSTR_LEN(time_str), &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper); + + if (err->error_count > 0) { + zend_throw_error(date_ce_date_malformed_interval_string_exception, "Unknown or bad format (%s) at position %d (%c): %s", ZSTR_VAL(time_str), + err->error_messages[0].position, err->error_messages[0].character ? err->error_messages[0].character : ' ', err->error_messages[0].message); + goto cleanup; } - if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { - tzobj = Z_PHPTIMEZONE_P((zval *) object); + if (time->have_date || time->have_time || time->have_zone) { + zend_throw_error(date_ce_date_malformed_interval_string_exception, "String '%s' contains non-relative elements", ZSTR_VAL(time_str)); + goto cleanup; + } - if (!tzobj->initialized) { - return false; - } + date_interval_instantiate_from_time(return_value, time, time_str); - array_init_size(state, 2); - date_timezone_object_to_hash(tzobj, Z_ARRVAL_P(state)); +cleanup: + timelib_time_dtor(time); + timelib_error_container_dtor(err); +} +/* }}} */ - return true; +/* {{{ date_interval_format - */ +static zend_string *date_interval_format(const char *format, size_t format_len, timelib_rel_time *t) +{ + smart_str string = {0}; + size_t i; + int length, have_format_spec = 0; + char buffer[33]; + + if (!format_len) { + return ZSTR_EMPTY_ALLOC(); } - if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { - intervalobj = Z_PHPINTERVAL_P((zval *) object); + for (i = 0; i < format_len; i++) { + if (have_format_spec) { + switch (format[i]) { + case 'Y': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->y); break; + case 'y': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->y); break; - if (!intervalobj->initialized || intervalobj->diff == NULL) { - return false; - } + case 'M': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->m); break; + case 'm': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->m); break; - if (intervalobj->from_string && intervalobj->date_string == NULL) { - return false; - } + case 'D': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->d); break; + case 'd': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->d); break; - array_init_size(state, intervalobj->from_string ? 2 : 18); - date_interval_object_to_hash(intervalobj, Z_ARRVAL_P(state)); + case 'H': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->h); break; + case 'h': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->h); break; - if (!intervalobj->from_string) { - add_assoc_long(state, "weekday", intervalobj->diff->weekday); - add_assoc_long(state, "weekday_behavior", intervalobj->diff->weekday_behavior); - add_assoc_long(state, "first_last_day_of", intervalobj->diff->first_last_day_of); - add_assoc_long(state, "special_type", intervalobj->diff->special.type); - php_date_user_cache_add_assoc_int64(state, "special_amount", intervalobj->diff->special.amount); - add_assoc_long(state, "have_weekday_relative", intervalobj->diff->have_weekday_relative); - add_assoc_long(state, "have_special_relative", intervalobj->diff->have_special_relative); - add_assoc_long(state, "civil_or_wall", intervalobj->civil_or_wall); - } + case 'I': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->i); break; + case 'i': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->i); break; - return true; - } + case 'S': length = slprintf(buffer, sizeof(buffer), "%02" ZEND_LONG_FMT_SPEC, (zend_long) t->s); break; + case 's': length = slprintf(buffer, sizeof(buffer), ZEND_LONG_FMT, (zend_long) t->s); break; - return false; -} + case 'F': length = slprintf(buffer, sizeof(buffer), "%06" ZEND_LONG_FMT_SPEC, (zend_long) t->us); break; + case 'f': length = slprintf(buffer, sizeof(buffer), ZEND_LONG_FMT, (zend_long) t->us); break; -static bool php_date_unserialize_datetime_user_cache_state(zval *object, zval *state) -{ - php_date_obj *dateobj; - timelib_time *time; - timelib_tzinfo *tzi; - zval *z_ctime, *z_timezone; + case 'a': { + if ((int) t->days != TIMELIB_UNSET) { + length = slprintf(buffer, sizeof(buffer), "%d", (int) t->days); + } else { + length = slprintf(buffer, sizeof(buffer), "(unknown)"); + } + } break; + case 'r': length = slprintf(buffer, sizeof(buffer), "%s", t->invert ? "-" : ""); break; + case 'R': length = slprintf(buffer, sizeof(buffer), "%c", t->invert ? '-' : '+'); break; - z_ctime = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN); - if (z_ctime == NULL) { - return false; + case '%': length = slprintf(buffer, sizeof(buffer), "%%"); break; + default: buffer[0] = '%'; buffer[1] = format[i]; buffer[2] = '\0'; length = 2; break; + } + smart_str_appendl(&string, buffer, length); + have_format_spec = 0; + } else { + if (format[i] == '%') { + have_format_spec = 1; + } else { + smart_str_appendc(&string, format[i]); + } + } } - if (Z_TYPE_P(z_ctime) != IS_STRING || Z_STRLEN_P(z_ctime) != sizeof(timelib_time)) { - return false; - } + smart_str_0(&string); - time = timelib_time_ctor(); - if (time == NULL) { - return false; + if (string.s == NULL) { + return ZSTR_EMPTY_ALLOC(); } - memcpy(time, Z_STRVAL_P(z_ctime), sizeof(timelib_time)); - time->tz_abbr = NULL; - time->tz_info = NULL; + return string.s; +} +/* }}} */ - switch (time->zone_type) { - case TIMELIB_ZONETYPE_ID: - z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); - if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || - UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) - ) { - timelib_time_dtor(time); +/* {{{ Formats the interval. */ +PHP_FUNCTION(date_interval_format) +{ + zval *object; + php_interval_obj *diobj; + const char *format; + size_t format_len; - return false; - } + if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "Os", &object, date_ce_interval, &format, &format_len) == FAILURE) { + RETURN_THROWS(); + } + diobj = Z_PHPINTERVAL_P(object); + DATE_CHECK_INITIALIZED(diobj->initialized, Z_OBJCE_P(object)); - tzi = php_date_parse_tzfile(Z_STRVAL_P(z_timezone), DATE_TIMEZONEDB); - if (tzi == NULL) { - timelib_time_dtor(time); + RETURN_STR(date_interval_format(format, format_len, diobj->diff)); +} +/* }}} */ - return false; - } - time->tz_info = tzi; - break; - case TIMELIB_ZONETYPE_OFFSET: - break; - case TIMELIB_ZONETYPE_ABBR: - z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); - if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || - UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) - ) { - timelib_time_dtor(time); +static bool date_period_initialize(timelib_time **st, timelib_time **et, timelib_rel_time **d, zend_long *recurrences, const char *format, size_t format_length) /* {{{ */ +{ + timelib_time *b = NULL, *e = NULL; + timelib_rel_time *p = NULL; + int r = 0; + timelib_error_container *errors; + bool retval = false; - return false; - } + timelib_strtointerval(format, format_length, &b, &e, &p, &r, &errors); - time->tz_abbr = timelib_strdup(Z_STRVAL_P(z_timezone)); - if (time->tz_abbr == NULL) { - timelib_time_dtor(time); + if (errors->error_count > 0) { + retval = false; + zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "Unknown or bad format (%s)", format); + if (b) { + timelib_time_dtor(b); + } + if (e) { + timelib_time_dtor(e); + } + if (p) { + timelib_rel_time_dtor(p); + } + } else { + *st = b; + *et = e; + *d = p; + *recurrences = r; + retval = true; + } + timelib_error_container_dtor(errors); + return retval; +} /* }}} */ - return false; - } - break; - default: - timelib_time_dtor(time); +static bool date_period_init_iso8601_string(php_period_obj *dpobj, zend_class_entry* base_ce, const char *isostr, size_t isostr_len, zend_long *recurrences) +{ + if (!date_period_initialize(&(dpobj->start), &(dpobj->end), &(dpobj->interval), recurrences, isostr, isostr_len)) { + return false; + } - return false; + if (dpobj->start == NULL) { + zend_string *func = get_active_function_or_method_name(); + zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): ISO interval must contain a start date, \"%s\" given", ZSTR_VAL(func), isostr); + zend_string_release(func); + return false; + } + if (dpobj->interval == NULL) { + zend_string *func = get_active_function_or_method_name(); + zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): ISO interval must contain an interval, \"%s\" given", ZSTR_VAL(func), isostr); + zend_string_release(func); + return false; + } + if (dpobj->end == NULL && *recurrences == 0) { + zend_string *func = get_active_function_or_method_name(); + zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): ISO interval must contain an end date or a recurrence count, \"%s\" given", ZSTR_VAL(func), isostr); + zend_string_release(func); + return false; } - dateobj = Z_PHPDATE_P(object); - if (dateobj->time != NULL) { - timelib_time_dtor(dateobj->time); + if (dpobj->start) { + timelib_update_ts(dpobj->start, NULL); } - dateobj->time = time; + if (dpobj->end) { + timelib_update_ts(dpobj->end, NULL); + } + dpobj->start_ce = base_ce; return true; } -static bool php_date_unserialize_user_cache_state(zval *object, zval *state) +static bool date_period_init_finish(php_period_obj *dpobj, zend_long options, zend_long recurrences) { - php_timezone_obj *tzobj; + const zend_long max_recurrences = (INT_MAX - 8); - if (Z_TYPE_P(state) != IS_ARRAY) { + if (dpobj->end == NULL && (recurrences < 1 || recurrences > max_recurrences)) { + zend_string *func = get_active_function_or_method_name(); + zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): Recurrence count must be greater or equal to 1 and lower than " ZEND_LONG_FMT, ZSTR_VAL(func), max_recurrences + 1); + zend_string_release(func); return false; } - if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || - instanceof_function(Z_OBJCE_P(object), date_ce_immutable) - ) { - return php_date_unserialize_datetime_user_cache_state(object, state); - } - - if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { - tzobj = Z_PHPTIMEZONE_P(object); - - return php_date_timezone_initialize_from_hash(&tzobj, Z_ARRVAL_P(state)); - } + /* options */ + dpobj->include_start_date = !(options & PHP_DATE_PERIOD_EXCLUDE_START_DATE); + dpobj->include_end_date = options & PHP_DATE_PERIOD_INCLUDE_END_DATE; - if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { - php_date_interval_initialize_from_hash(Z_PHPINTERVAL_P(object), Z_ARRVAL_P(state)); + /* recurrences */ + recurrences += dpobj->include_start_date + dpobj->include_end_date; - return !EG(exception); + if (UNEXPECTED(recurrences > max_recurrences)) { + zend_string *func = get_active_function_or_method_name(); + zend_throw_exception_ex(date_ce_date_malformed_string_exception, 0, "%s(): Recurrence count must be greater or equal to 1 and lower than " ZEND_LONG_FMT " (including options)", ZSTR_VAL(func), max_recurrences + 1); + zend_string_release(func); + return false; } - return false; -} + dpobj->recurrences = (int)recurrences; -static const php_user_cache_safe_direct_handlers php_date_user_cache_handlers = { - .prefer_request_local_prototype = true, - .copy = php_date_copy_user_cache_state, - .state_serialize = php_date_serialize_user_cache_state, - .state_unserialize = php_date_unserialize_user_cache_state, -}; + dpobj->initialized = true; -static void php_date_register_user_cache_handlers(void) -{ - php_user_cache_safe_direct_register_class(date_ce_date, &php_date_user_cache_handlers); - php_user_cache_safe_direct_register_class(date_ce_immutable, &php_date_user_cache_handlers); - php_user_cache_safe_direct_register_class(date_ce_timezone, &php_date_user_cache_handlers); - php_user_cache_safe_direct_register_class(date_ce_interval, &php_date_user_cache_handlers); + return true; } -/* {{{ */ -PHP_METHOD(DateInterval, __set_state) +PHP_METHOD(DatePeriod, createFromISO8601String) { - php_interval_obj *intobj; - HashTable *myht; + php_period_obj *dpobj; + zend_long recurrences = 0, options = 0; + char *isostr = NULL; + size_t isostr_len = 0; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_ARRAY_HT(myht) + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STRING(isostr, isostr_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(options) ZEND_PARSE_PARAMETERS_END(); - php_date_instantiate(date_ce_interval, return_value); - intobj = Z_PHPINTERVAL_P(return_value); - php_date_interval_initialize_from_hash(intobj, myht); + if (object_init_ex(return_value, execute_data->This.value.ce ? execute_data->This.value.ce : date_ce_period) != SUCCESS) { + RETURN_THROWS(); + } + dpobj = Z_PHPPERIOD_P(return_value); + + dpobj->current = NULL; + + if (!date_period_init_iso8601_string(dpobj, date_ce_immutable, isostr, isostr_len, &recurrences)) { + RETURN_THROWS(); + } + + if (!date_period_init_finish(dpobj, options, recurrences)) { + RETURN_THROWS(); + } } -/* }}} */ -/* {{{ */ -PHP_METHOD(DateInterval, __serialize) +/* {{{ Creates new DatePeriod object. */ +PHP_METHOD(DatePeriod, __construct) { - zval *object = ZEND_THIS; - php_interval_obj *intervalobj; - HashTable *myht; + php_period_obj *dpobj; + php_date_obj *dateobj; + zval *start, *end = NULL, *interval; + zend_long recurrences = 0, options = 0; + char *isostr = NULL; + size_t isostr_len = 0; + timelib_time *clone; - ZEND_PARSE_PARAMETERS_NONE(); + if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "OOl|l", &start, date_ce_interface, &interval, date_ce_interval, &recurrences, &options) == FAILURE) { + if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "OOO|l", &start, date_ce_interface, &interval, date_ce_interval, &end, date_ce_interface, &options) == FAILURE) { + if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "s|l", &isostr, &isostr_len, &options) == FAILURE) { + zend_type_error("DatePeriod::__construct() accepts (DateTimeInterface, DateInterval, int [, int]), or (DateTimeInterface, DateInterval, DateTime [, int]), or (string [, int]) as arguments"); + RETURN_THROWS(); + } + } + } - intervalobj = Z_PHPINTERVAL_P(object); - DATE_CHECK_INITIALIZED(intervalobj->initialized, Z_OBJCE_P(object)); + dpobj = Z_PHPPERIOD_P(ZEND_THIS); + dpobj->current = NULL; - array_init(return_value); - myht = Z_ARRVAL_P(return_value); - date_interval_object_to_hash(intervalobj, myht); + if (isostr) { + zend_error(E_DEPRECATED, "Calling DatePeriod::__construct(string $isostr, int $options = 0) is deprecated, " + "use DatePeriod::createFromISO8601String() instead"); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } - add_common_properties(myht, &intervalobj->std); -} -/* }}} */ + if (!date_period_init_iso8601_string(dpobj, date_ce_date, isostr, isostr_len, &recurrences)) { + RETURN_THROWS(); + } + } else { + /* check initialisation */ + DATE_CHECK_INITIALIZED(Z_PHPDATE_P(start)->time, date_ce_interface); + if (end) { + DATE_CHECK_INITIALIZED(Z_PHPDATE_P(end)->time, date_ce_interface); + } -static bool date_interval_is_internal_property(const zend_string *name) -{ - if ( - zend_string_equals_literal(name, "date_string") || - zend_string_equals_literal(name, "from_string") || - zend_string_equals_literal(name, "y") || - zend_string_equals_literal(name, "m") || - zend_string_equals_literal(name, "d") || - zend_string_equals_literal(name, "h") || - zend_string_equals_literal(name, "i") || - zend_string_equals_literal(name, "s") || - zend_string_equals_literal(name, "f") || - zend_string_equals_literal(name, "invert") || - zend_string_equals_literal(name, "days") - ) { - return true; - } - return false; -} + /* init */ + php_interval_obj *intobj = Z_PHPINTERVAL_P(interval); -static void restore_custom_dateinterval_properties(zval *object, const HashTable *myht) -{ - zend_string *prop_name; - zval *prop_val; + /* start date */ + dateobj = Z_PHPDATE_P(start); + clone = timelib_time_ctor(); + memcpy(clone, dateobj->time, sizeof(timelib_time)); + if (dateobj->time->tz_abbr) { + clone->tz_abbr = timelib_strdup(dateobj->time->tz_abbr); + } + if (dateobj->time->tz_info) { + clone->tz_info = dateobj->time->tz_info; + } + dpobj->start = clone; + dpobj->start_ce = Z_OBJCE_P(start); - ZEND_HASH_FOREACH_STR_KEY_VAL(myht, prop_name, prop_val) { - if (!prop_name || (Z_TYPE_P(prop_val) == IS_REFERENCE) || date_interval_is_internal_property(prop_name)) { - continue; + /* interval */ + dpobj->interval = timelib_rel_time_clone(intobj->diff); + + /* end date */ + if (end) { + dateobj = Z_PHPDATE_P(end); + clone = timelib_time_clone(dateobj->time); + dpobj->end = clone; } - update_property(Z_OBJ_P(object), prop_name, prop_val); - } ZEND_HASH_FOREACH_END(); -} + } + if (!date_period_init_finish(dpobj, options, recurrences)) { + RETURN_THROWS(); + } +} +/* }}} */ -/* {{{ */ -PHP_METHOD(DateInterval, __unserialize) +/* {{{ Get start date. */ +PHP_METHOD(DatePeriod, getStartDate) { - zval *object = ZEND_THIS; - php_interval_obj *intervalobj; - HashTable *myht; + php_period_obj *dpobj; + php_date_obj *dateobj; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_ARRAY_HT(myht) - ZEND_PARSE_PARAMETERS_END(); + ZEND_PARSE_PARAMETERS_NONE(); - intervalobj = Z_PHPINTERVAL_P(object); + dpobj = Z_PHPPERIOD_P(ZEND_THIS); + DATE_CHECK_INITIALIZED(dpobj->start, Z_OBJCE_P(ZEND_THIS)); - php_date_interval_initialize_from_hash(intervalobj, myht); - restore_custom_dateinterval_properties(object, myht); + php_date_instantiate(dpobj->start_ce, return_value); + dateobj = Z_PHPDATE_P(return_value); + dateobj->time = timelib_time_ctor(); + *dateobj->time = *dpobj->start; + if (dpobj->start->tz_abbr) { + dateobj->time->tz_abbr = timelib_strdup(dpobj->start->tz_abbr); + } + if (dpobj->start->tz_info) { + dateobj->time->tz_info = dpobj->start->tz_info; + } } /* }}} */ -/* {{{ */ -PHP_METHOD(DateInterval, __wakeup) +/* {{{ Get end date. */ +PHP_METHOD(DatePeriod, getEndDate) { - zval *object = ZEND_THIS; - php_interval_obj *intobj; - const HashTable *myht; + php_period_obj *dpobj; + php_date_obj *dateobj; ZEND_PARSE_PARAMETERS_NONE(); - intobj = Z_PHPINTERVAL_P(object); + dpobj = Z_PHPPERIOD_P(ZEND_THIS); - myht = Z_OBJPROP_P(object); + if (!dpobj->end) { + return; + } - php_date_interval_initialize_from_hash(intobj, myht); + php_date_instantiate(dpobj->start_ce, return_value); + dateobj = Z_PHPDATE_P(return_value); + dateobj->time = timelib_time_ctor(); + *dateobj->time = *dpobj->end; + if (dpobj->end->tz_abbr) { + dateobj->time->tz_abbr = timelib_strdup(dpobj->end->tz_abbr); + } + if (dpobj->end->tz_info) { + dateobj->time->tz_info = dpobj->end->tz_info; + } } /* }}} */ -static void date_interval_instantiate_from_time(zval *return_value, timelib_time *time, zend_string *time_str) +/* {{{ Get date interval. */ +PHP_METHOD(DatePeriod, getDateInterval) { + php_period_obj *dpobj; php_interval_obj *diobj; + ZEND_PARSE_PARAMETERS_NONE(); + + dpobj = Z_PHPPERIOD_P(ZEND_THIS); + DATE_CHECK_INITIALIZED(dpobj->interval, Z_OBJCE_P(ZEND_THIS)); + php_date_instantiate(date_ce_interval, return_value); diobj = Z_PHPINTERVAL_P(return_value); - diobj->diff = timelib_rel_time_clone(&time->relative); + diobj->diff = timelib_rel_time_clone(dpobj->interval); diobj->initialized = true; - diobj->civil_or_wall = PHP_DATE_CIVIL; - diobj->from_string = true; - diobj->date_string = zend_string_copy(time_str); } +/* }}} */ -/* {{{ Uses the normal date parsers and sets up a DateInterval from the relative parts of the parsed string */ -PHP_FUNCTION(date_interval_create_from_date_string) +/* {{{ Get recurrences. */ +PHP_METHOD(DatePeriod, getRecurrences) { - zend_string *time_str = NULL; - timelib_time *time; - timelib_error_container *err = NULL; + php_period_obj *dpobj; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STR(time_str) - ZEND_PARSE_PARAMETERS_END(); + ZEND_PARSE_PARAMETERS_NONE(); - time = timelib_strtotime(ZSTR_VAL(time_str), ZSTR_LEN(time_str), &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper); + dpobj = Z_PHPPERIOD_P(ZEND_THIS); - if (err->error_count > 0) { - php_error_docref(NULL, E_WARNING, "Unknown or bad format (%s) at position %d (%c): %s", ZSTR_VAL(time_str), - err->error_messages[0].position, err->error_messages[0].character ? err->error_messages[0].character : ' ', err->error_messages[0].message); - RETVAL_FALSE; - goto cleanup; + if (0 == dpobj->recurrences - dpobj->include_start_date - dpobj->include_end_date) { + return; } - if (time->have_date || time->have_time || time->have_zone) { - php_error_docref(NULL, E_WARNING, "String '%s' contains non-relative elements", ZSTR_VAL(time_str)); - RETVAL_FALSE; - goto cleanup; - } + RETURN_LONG(dpobj->recurrences - dpobj->include_start_date - dpobj->include_end_date); +} +/* }}} */ - date_interval_instantiate_from_time(return_value, time, time_str); +PHP_METHOD(DatePeriod, getIterator) +{ + ZEND_PARSE_PARAMETERS_NONE(); -cleanup: - timelib_time_dtor(time); - timelib_error_container_dtor(err); + zend_create_internal_iterator_zval(return_value, ZEND_THIS); } -/* }}} */ -/* {{{ Uses the normal date parsers and sets up a DateInterval from the relative parts of the parsed string */ -PHP_METHOD(DateInterval, createFromDateString) +static bool check_id_allowed(const char *id, zend_long what) /* {{{ */ { - zend_string *time_str = NULL; - timelib_time *time; - timelib_error_container *err = NULL; + if ((what & PHP_DATE_TIMEZONE_GROUP_AFRICA) && strncasecmp(id, "Africa/", 7) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_AMERICA) && strncasecmp(id, "America/", 8) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_ANTARCTICA) && strncasecmp(id, "Antarctica/", 11) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_ARCTIC) && strncasecmp(id, "Arctic/", 7) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_ASIA) && strncasecmp(id, "Asia/", 5) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_ATLANTIC) && strncasecmp(id, "Atlantic/", 9) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_AUSTRALIA) && strncasecmp(id, "Australia/", 10) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_EUROPE) && strncasecmp(id, "Europe/", 7) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_INDIAN) && strncasecmp(id, "Indian/", 7) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_PACIFIC) && strncasecmp(id, "Pacific/", 8) == 0) return true; + if ((what & PHP_DATE_TIMEZONE_GROUP_UTC) && strncasecmp(id, "UTC", 3) == 0) return true; + return false; +} /* }}} */ - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STR(time_str) - ZEND_PARSE_PARAMETERS_END(); +/* {{{ Returns numerically index array with all timezone identifiers. */ +PHP_FUNCTION(timezone_identifiers_list) +{ + const timelib_tzdb *tzdb; + const timelib_tzdb_index_entry *table; + int i, item_count; + zend_long what = PHP_DATE_TIMEZONE_GROUP_ALL; + char *option = NULL; + size_t option_len = 0; - time = timelib_strtotime(ZSTR_VAL(time_str), ZSTR_LEN(time_str), &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper); + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(what) + Z_PARAM_STRING_OR_NULL(option, option_len) + ZEND_PARSE_PARAMETERS_END(); - if (err->error_count > 0) { - zend_throw_error(date_ce_date_malformed_interval_string_exception, "Unknown or bad format (%s) at position %d (%c): %s", ZSTR_VAL(time_str), - err->error_messages[0].position, err->error_messages[0].character ? err->error_messages[0].character : ' ', err->error_messages[0].message); - goto cleanup; + /* Extra validation */ + if (what == PHP_DATE_TIMEZONE_PER_COUNTRY && option_len != 2) { + zend_argument_value_error(2, "must be a two-letter ISO 3166-1 compatible country code " + "when argument #1 ($timezoneGroup) is DateTimeZone::PER_COUNTRY"); + RETURN_THROWS(); } - if (time->have_date || time->have_time || time->have_zone) { - zend_throw_error(date_ce_date_malformed_interval_string_exception, "String '%s' contains non-relative elements", ZSTR_VAL(time_str)); - goto cleanup; - } + tzdb = DATE_TIMEZONEDB; + table = timelib_timezone_identifiers_list((timelib_tzdb*) tzdb, &item_count); - date_interval_instantiate_from_time(return_value, time, time_str); + array_init(return_value); + zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); -cleanup: - timelib_time_dtor(time); - timelib_error_container_dtor(err); + for (i = 0; i < item_count; ++i) { + if (what == PHP_DATE_TIMEZONE_PER_COUNTRY) { + if (tzdb->data[table[i].pos + 5] == option[0] && tzdb->data[table[i].pos + 6] == option[1]) { + add_next_index_string(return_value, table[i].id); + } + } else if (what == PHP_DATE_TIMEZONE_GROUP_ALL_W_BC || (check_id_allowed(table[i].id, what) && (tzdb->data[table[i].pos + 4] == '\1'))) { + add_next_index_string(return_value, table[i].id); + } + }; } /* }}} */ -/* {{{ date_interval_format - */ -static zend_string *date_interval_format(const char *format, size_t format_len, timelib_rel_time *t) +/* {{{ Returns the Olson database version number. */ +PHP_FUNCTION(timezone_version_get) { - smart_str string = {0}; - size_t i; - int length, have_format_spec = 0; - char buffer[33]; - - if (!format_len) { - return ZSTR_EMPTY_ALLOC(); - } - - for (i = 0; i < format_len; i++) { - if (have_format_spec) { - switch (format[i]) { - case 'Y': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->y); break; - case 'y': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->y); break; - - case 'M': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->m); break; - case 'm': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->m); break; + const timelib_tzdb *tzdb; - case 'D': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->d); break; - case 'd': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->d); break; + ZEND_PARSE_PARAMETERS_NONE(); - case 'H': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->h); break; - case 'h': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->h); break; + tzdb = DATE_TIMEZONEDB; + RETURN_STRING(tzdb->version); +} +/* }}} */ - case 'I': length = slprintf(buffer, sizeof(buffer), "%02d", (int) t->i); break; - case 'i': length = slprintf(buffer, sizeof(buffer), "%d", (int) t->i); break; +/* {{{ Returns associative array containing dst, offset and the timezone name */ +PHP_FUNCTION(timezone_abbreviations_list) +{ + const timelib_tz_lookup_table *table, *entry; + zval element, *abbr_array_p, abbr_array; - case 'S': length = slprintf(buffer, sizeof(buffer), "%02" ZEND_LONG_FMT_SPEC, (zend_long) t->s); break; - case 's': length = slprintf(buffer, sizeof(buffer), ZEND_LONG_FMT, (zend_long) t->s); break; + ZEND_PARSE_PARAMETERS_NONE(); - case 'F': length = slprintf(buffer, sizeof(buffer), "%06" ZEND_LONG_FMT_SPEC, (zend_long) t->us); break; - case 'f': length = slprintf(buffer, sizeof(buffer), ZEND_LONG_FMT, (zend_long) t->us); break; + table = timelib_timezone_abbreviations_list(); + array_init(return_value); + entry = table; - case 'a': { - if ((int) t->days != TIMELIB_UNSET) { - length = slprintf(buffer, sizeof(buffer), "%d", (int) t->days); - } else { - length = slprintf(buffer, sizeof(buffer), "(unknown)"); - } - } break; - case 'r': length = slprintf(buffer, sizeof(buffer), "%s", t->invert ? "-" : ""); break; - case 'R': length = slprintf(buffer, sizeof(buffer), "%c", t->invert ? '-' : '+'); break; + do { + array_init(&element); + add_assoc_bool_ex(&element, "dst", sizeof("dst") -1, entry->type); + add_assoc_long_ex(&element, "offset", sizeof("offset") - 1, entry->gmtoffset); + if (entry->full_tz_name) { + add_assoc_string_ex(&element, "timezone_id", sizeof("timezone_id") - 1, entry->full_tz_name); + } else { + add_assoc_null_ex(&element, "timezone_id", sizeof("timezone_id") - 1); + } - case '%': length = slprintf(buffer, sizeof(buffer), "%%"); break; - default: buffer[0] = '%'; buffer[1] = format[i]; buffer[2] = '\0'; length = 2; break; - } - smart_str_appendl(&string, buffer, length); - have_format_spec = 0; + abbr_array_p = zend_hash_str_find(Z_ARRVAL_P(return_value), entry->name, strlen(entry->name)); + if (!abbr_array_p) { + array_init(&abbr_array); + add_assoc_zval(return_value, entry->name, &abbr_array); } else { - if (format[i] == '%') { - have_format_spec = 1; - } else { - smart_str_appendc(&string, format[i]); - } + ZVAL_COPY_VALUE(&abbr_array, abbr_array_p); } - } + add_next_index_zval(&abbr_array, &element); + entry++; + } while (entry->name); +} +/* }}} */ - smart_str_0(&string); +/* {{{ Sets the default timezone used by all date/time functions in a script */ +PHP_FUNCTION(date_default_timezone_set) +{ + char *zone; + size_t zone_len; - if (string.s == NULL) { - return ZSTR_EMPTY_ALLOC(); - } + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(zone, zone_len) + ZEND_PARSE_PARAMETERS_END(); - return string.s; + if (!timelib_timezone_id_is_valid(zone, DATE_TIMEZONEDB)) { + php_error_docref(NULL, E_NOTICE, "Timezone ID '%s' is invalid", zone); + RETURN_FALSE; + } + if (DATEG(timezone)) { + efree(DATEG(timezone)); + DATEG(timezone) = NULL; + } + DATEG(timezone) = estrndup(zone, zone_len); + RETURN_TRUE; } /* }}} */ -/* {{{ Formats the interval. */ -PHP_FUNCTION(date_interval_format) +/* {{{ Gets the default timezone used by all date/time functions in a script */ +PHP_FUNCTION(date_default_timezone_get) { - zval *object; - php_interval_obj *diobj; - const char *format; - size_t format_len; + timelib_tzinfo *default_tz; + ZEND_PARSE_PARAMETERS_NONE(); - if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "Os", &object, date_ce_interval, &format, &format_len) == FAILURE) { + default_tz = get_timezone_info(); + if (!default_tz) { RETURN_THROWS(); } - diobj = Z_PHPINTERVAL_P(object); - DATE_CHECK_INITIALIZED(diobj->initialized, Z_OBJCE_P(object)); - - RETURN_STR(date_interval_format(format, format_len, diobj->diff)); + RETVAL_STRING(default_tz->name); } /* }}} */ -static bool date_period_initialize(timelib_time **st, timelib_time **et, timelib_rel_time **d, zend_long *recurrences, const char *format, size_t format_length) /* {{{ */ +/* {{{ php_do_date_sunrise_sunset + * Common for date_sunrise() and date_sunset() functions + */ +static void php_do_date_sunrise_sunset(INTERNAL_FUNCTION_PARAMETERS, bool calc_sunset) { - timelib_time *b = NULL, *e = NULL; - timelib_rel_time *p = NULL; - int r = 0; - timelib_error_container *errors; - bool retval = false; + double latitude, longitude, zenith, gmt_offset, altitude; + bool latitude_is_null = 1, longitude_is_null = 1, zenith_is_null = 1, gmt_offset_is_null = 1; + double h_rise, h_set, N; + timelib_sll rise, set, transit; + zend_long time, retformat = SUNFUNCS_RET_STRING; + int rs; + timelib_time *t; + timelib_tzinfo *tzi; + zend_string *retstr; - timelib_strtointerval(format, format_length, &b, &e, &p, &r, &errors); + ZEND_PARSE_PARAMETERS_START(1, 6) + Z_PARAM_LONG(time) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(retformat) + Z_PARAM_DOUBLE_OR_NULL(latitude, latitude_is_null) + Z_PARAM_DOUBLE_OR_NULL(longitude, longitude_is_null) + Z_PARAM_DOUBLE_OR_NULL(zenith, zenith_is_null) + Z_PARAM_DOUBLE_OR_NULL(gmt_offset, gmt_offset_is_null) + ZEND_PARSE_PARAMETERS_END(); - if (errors->error_count > 0) { - retval = false; - zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "Unknown or bad format (%s)", format); - if (b) { - timelib_time_dtor(b); - } - if (e) { - timelib_time_dtor(e); - } - if (p) { - timelib_rel_time_dtor(p); - } - } else { - *st = b; - *et = e; - *d = p; - *recurrences = r; - retval = true; + if (latitude_is_null) { + latitude = zend_ini_double_literal("date.default_latitude"); } - timelib_error_container_dtor(errors); - return retval; -} /* }}} */ -static bool date_period_init_iso8601_string(php_period_obj *dpobj, zend_class_entry* base_ce, const char *isostr, size_t isostr_len, zend_long *recurrences) -{ - if (!date_period_initialize(&(dpobj->start), &(dpobj->end), &(dpobj->interval), recurrences, isostr, isostr_len)) { - return false; + if (longitude_is_null) { + longitude = zend_ini_double_literal("date.default_longitude"); } - if (dpobj->start == NULL) { - zend_string *func = get_active_function_or_method_name(); - zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): ISO interval must contain a start date, \"%s\" given", ZSTR_VAL(func), isostr); - zend_string_release(func); - return false; - } - if (dpobj->interval == NULL) { - zend_string *func = get_active_function_or_method_name(); - zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): ISO interval must contain an interval, \"%s\" given", ZSTR_VAL(func), isostr); - zend_string_release(func); - return false; + if (zenith_is_null) { + if (calc_sunset) { + zenith = zend_ini_double_literal("date.sunset_zenith"); + } else { + zenith = zend_ini_double_literal("date.sunrise_zenith"); + } } - if (dpobj->end == NULL && *recurrences == 0) { - zend_string *func = get_active_function_or_method_name(); - zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): ISO interval must contain an end date or a recurrence count, \"%s\" given", ZSTR_VAL(func), isostr); - zend_string_release(func); - return false; + + if (retformat != SUNFUNCS_RET_TIMESTAMP && + retformat != SUNFUNCS_RET_STRING && + retformat != SUNFUNCS_RET_DOUBLE) + { + zend_argument_value_error(2, "must be one of SUNFUNCS_RET_TIMESTAMP, SUNFUNCS_RET_STRING, or SUNFUNCS_RET_DOUBLE"); + RETURN_THROWS(); } + altitude = 90 - zenith; - if (dpobj->start) { - timelib_update_ts(dpobj->start, NULL); + if (!zend_finite(latitude) || !zend_finite(longitude)) { + RETURN_FALSE; } - if (dpobj->end) { - timelib_update_ts(dpobj->end, NULL); + + /* Initialize time struct */ + tzi = get_timezone_info(); + if (!tzi) { + RETURN_THROWS(); } - dpobj->start_ce = base_ce; + t = timelib_time_ctor(); + t->tz_info = tzi; + t->zone_type = TIMELIB_ZONETYPE_ID; - return true; -} + if (gmt_offset_is_null) { + gmt_offset = timelib_get_current_offset(t) / 3600.0; + } -static bool date_period_init_finish(php_period_obj *dpobj, zend_long options, zend_long recurrences) -{ - const zend_long max_recurrences = (INT_MAX - 8); + timelib_unixtime2local(t, time); + rs = timelib_astro_rise_set_altitude(t, longitude, latitude, altitude, 1, &h_rise, &h_set, &rise, &set, &transit); + timelib_time_dtor(t); - if (dpobj->end == NULL && (recurrences < 1 || recurrences > max_recurrences)) { - zend_string *func = get_active_function_or_method_name(); - zend_throw_exception_ex(date_ce_date_malformed_period_string_exception, 0, "%s(): Recurrence count must be greater or equal to 1 and lower than " ZEND_LONG_FMT, ZSTR_VAL(func), max_recurrences + 1); - zend_string_release(func); - return false; + if (rs != 0) { + RETURN_FALSE; } - /* options */ - dpobj->include_start_date = !(options & PHP_DATE_PERIOD_EXCLUDE_START_DATE); - dpobj->include_end_date = options & PHP_DATE_PERIOD_INCLUDE_END_DATE; - - /* recurrences */ - recurrences += dpobj->include_start_date + dpobj->include_end_date; + if (retformat == SUNFUNCS_RET_TIMESTAMP) { + RETURN_LONG(calc_sunset ? set : rise); + } + N = (calc_sunset ? h_set : h_rise) + gmt_offset; - if (UNEXPECTED(recurrences > max_recurrences)) { - zend_string *func = get_active_function_or_method_name(); - zend_throw_exception_ex(date_ce_date_malformed_string_exception, 0, "%s(): Recurrence count must be greater or equal to 1 and lower than " ZEND_LONG_FMT " (including options)", ZSTR_VAL(func), max_recurrences + 1); - zend_string_release(func); - return false; + if (N > 24 || N < 0) { + N -= floor(N / 24) * 24; + } + if (!(N <= 24 && N >= 0)) { + RETURN_FALSE; } - dpobj->recurrences = (int)recurrences; + switch (retformat) { + case SUNFUNCS_RET_STRING: + retstr = strpprintf(0, "%02d:%02d", (int) N, (int) (60 * (N - (int) N))); + RETURN_NEW_STR(retstr); + case SUNFUNCS_RET_DOUBLE: + RETURN_DOUBLE(N); + } +} +/* }}} */ - dpobj->initialized = true; +/* {{{ Returns time of sunrise for a given day and location */ +PHP_FUNCTION(date_sunrise) +{ + php_do_date_sunrise_sunset(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); +} +/* }}} */ - return true; +/* {{{ Returns time of sunset for a given day and location */ +PHP_FUNCTION(date_sunset) +{ + php_do_date_sunrise_sunset(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1); } +/* }}} */ -PHP_METHOD(DatePeriod, createFromISO8601String) +/* {{{ Returns an array with information about sun set/rise and twilight begin/end */ +PHP_FUNCTION(date_sun_info) { - php_period_obj *dpobj; - zend_long recurrences = 0, options = 0; - char *isostr = NULL; - size_t isostr_len = 0; + zend_long time; + double latitude, longitude; + timelib_time *t, *t2; + timelib_tzinfo *tzi; + int rs; + timelib_sll rise, set, transit; + int dummy; + double ddummy; - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_STRING(isostr, isostr_len) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(options) + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_LONG(time) + Z_PARAM_DOUBLE(latitude) + Z_PARAM_DOUBLE(longitude) ZEND_PARSE_PARAMETERS_END(); - if (object_init_ex(return_value, execute_data->This.value.ce ? execute_data->This.value.ce : date_ce_period) != SUCCESS) { + if (!zend_finite(latitude)) { + zend_argument_value_error(2, "must be finite"); RETURN_THROWS(); } - dpobj = Z_PHPPERIOD_P(return_value); - - dpobj->current = NULL; - - if (!date_period_init_iso8601_string(dpobj, date_ce_immutable, isostr, isostr_len, &recurrences)) { + if (!zend_finite(longitude)) { + zend_argument_value_error(3, "must be finite"); RETURN_THROWS(); } - if (!date_period_init_finish(dpobj, options, recurrences)) { + /* Initialize time struct */ + tzi = get_timezone_info(); + if (!tzi) { RETURN_THROWS(); } -} + t = timelib_time_ctor(); + t->tz_info = tzi; + t->zone_type = TIMELIB_ZONETYPE_ID; + timelib_unixtime2local(t, time); -/* {{{ Creates new DatePeriod object. */ -PHP_METHOD(DatePeriod, __construct) -{ - php_period_obj *dpobj; - php_date_obj *dateobj; - zval *start, *end = NULL, *interval; - zend_long recurrences = 0, options = 0; - char *isostr = NULL; - size_t isostr_len = 0; - timelib_time *clone; + /* Setup */ + t2 = timelib_time_ctor(); + array_init(return_value); - if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "OOl|l", &start, date_ce_interface, &interval, date_ce_interval, &recurrences, &options) == FAILURE) { - if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "OOO|l", &start, date_ce_interface, &interval, date_ce_interval, &end, date_ce_interface, &options) == FAILURE) { - if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "s|l", &isostr, &isostr_len, &options) == FAILURE) { - zend_type_error("DatePeriod::__construct() accepts (DateTimeInterface, DateInterval, int [, int]), or (DateTimeInterface, DateInterval, DateTime [, int]), or (string [, int]) as arguments"); - RETURN_THROWS(); - } - } + /* Get sun up/down and transit */ + rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -35.0/60, 1, &ddummy, &ddummy, &rise, &set, &transit); + switch (rs) { + case -1: /* always below */ + add_assoc_bool(return_value, "sunrise", 0); + add_assoc_bool(return_value, "sunset", 0); + break; + case 1: /* always above */ + add_assoc_bool(return_value, "sunrise", 1); + add_assoc_bool(return_value, "sunset", 1); + break; + default: + t2->sse = rise; + add_assoc_long(return_value, "sunrise", timelib_date_to_int(t2, &dummy)); + t2->sse = set; + add_assoc_long(return_value, "sunset", timelib_date_to_int(t2, &dummy)); } + t2->sse = transit; + add_assoc_long(return_value, "transit", timelib_date_to_int(t2, &dummy)); - dpobj = Z_PHPPERIOD_P(ZEND_THIS); - dpobj->current = NULL; + /* Get civil twilight */ + rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -6.0, 0, &ddummy, &ddummy, &rise, &set, &transit); + switch (rs) { + case -1: /* always below */ + add_assoc_bool(return_value, "civil_twilight_begin", 0); + add_assoc_bool(return_value, "civil_twilight_end", 0); + break; + case 1: /* always above */ + add_assoc_bool(return_value, "civil_twilight_begin", 1); + add_assoc_bool(return_value, "civil_twilight_end", 1); + break; + default: + t2->sse = rise; + add_assoc_long(return_value, "civil_twilight_begin", timelib_date_to_int(t2, &dummy)); + t2->sse = set; + add_assoc_long(return_value, "civil_twilight_end", timelib_date_to_int(t2, &dummy)); + } - if (isostr) { - zend_error(E_DEPRECATED, "Calling DatePeriod::__construct(string $isostr, int $options = 0) is deprecated, " - "use DatePeriod::createFromISO8601String() instead"); - if (UNEXPECTED(EG(exception))) { - RETURN_THROWS(); - } + /* Get nautical twilight */ + rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -12.0, 0, &ddummy, &ddummy, &rise, &set, &transit); + switch (rs) { + case -1: /* always below */ + add_assoc_bool(return_value, "nautical_twilight_begin", 0); + add_assoc_bool(return_value, "nautical_twilight_end", 0); + break; + case 1: /* always above */ + add_assoc_bool(return_value, "nautical_twilight_begin", 1); + add_assoc_bool(return_value, "nautical_twilight_end", 1); + break; + default: + t2->sse = rise; + add_assoc_long(return_value, "nautical_twilight_begin", timelib_date_to_int(t2, &dummy)); + t2->sse = set; + add_assoc_long(return_value, "nautical_twilight_end", timelib_date_to_int(t2, &dummy)); + } - if (!date_period_init_iso8601_string(dpobj, date_ce_date, isostr, isostr_len, &recurrences)) { - RETURN_THROWS(); - } - } else { - /* check initialisation */ - DATE_CHECK_INITIALIZED(Z_PHPDATE_P(start)->time, date_ce_interface); - if (end) { - DATE_CHECK_INITIALIZED(Z_PHPDATE_P(end)->time, date_ce_interface); - } - - /* init */ - php_interval_obj *intobj = Z_PHPINTERVAL_P(interval); - - /* start date */ - dateobj = Z_PHPDATE_P(start); - clone = timelib_time_ctor(); - memcpy(clone, dateobj->time, sizeof(timelib_time)); - if (dateobj->time->tz_abbr) { - clone->tz_abbr = timelib_strdup(dateobj->time->tz_abbr); - } - if (dateobj->time->tz_info) { - clone->tz_info = dateobj->time->tz_info; - } - dpobj->start = clone; - dpobj->start_ce = Z_OBJCE_P(start); - - /* interval */ - dpobj->interval = timelib_rel_time_clone(intobj->diff); - - /* end date */ - if (end) { - dateobj = Z_PHPDATE_P(end); - clone = timelib_time_clone(dateobj->time); - dpobj->end = clone; - } - } - - if (!date_period_init_finish(dpobj, options, recurrences)) { - RETURN_THROWS(); + /* Get astronomical twilight */ + rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -18.0, 0, &ddummy, &ddummy, &rise, &set, &transit); + switch (rs) { + case -1: /* always below */ + add_assoc_bool(return_value, "astronomical_twilight_begin", 0); + add_assoc_bool(return_value, "astronomical_twilight_end", 0); + break; + case 1: /* always above */ + add_assoc_bool(return_value, "astronomical_twilight_begin", 1); + add_assoc_bool(return_value, "astronomical_twilight_end", 1); + break; + default: + t2->sse = rise; + add_assoc_long(return_value, "astronomical_twilight_begin", timelib_date_to_int(t2, &dummy)); + t2->sse = set; + add_assoc_long(return_value, "astronomical_twilight_end", timelib_date_to_int(t2, &dummy)); } + timelib_time_dtor(t); + timelib_time_dtor(t2); } /* }}} */ -/* {{{ Get start date. */ -PHP_METHOD(DatePeriod, getStartDate) +static HashTable *date_object_get_gc_period(zend_object *object, zval **table, int *n) /* {{{ */ { - php_period_obj *dpobj; - php_date_obj *dateobj; + *table = NULL; + *n = 0; + return zend_std_get_properties(object); +} /* }}} */ - ZEND_PARSE_PARAMETERS_NONE(); +static void date_period_object_to_hash(php_period_obj *period_obj, HashTable *props) +{ + zval zv; - dpobj = Z_PHPPERIOD_P(ZEND_THIS); - DATE_CHECK_INITIALIZED(dpobj->start, Z_OBJCE_P(ZEND_THIS)); + create_date_period_datetime(period_obj->start, period_obj->start_ce, &zv); + zend_hash_str_update(props, "start", sizeof("start")-1, &zv); - php_date_instantiate(dpobj->start_ce, return_value); - dateobj = Z_PHPDATE_P(return_value); - dateobj->time = timelib_time_ctor(); - *dateobj->time = *dpobj->start; - if (dpobj->start->tz_abbr) { - dateobj->time->tz_abbr = timelib_strdup(dpobj->start->tz_abbr); - } - if (dpobj->start->tz_info) { - dateobj->time->tz_info = dpobj->start->tz_info; - } -} -/* }}} */ + create_date_period_datetime(period_obj->current, period_obj->start_ce, &zv); + zend_hash_str_update(props, "current", sizeof("current")-1, &zv); -/* {{{ Get end date. */ -PHP_METHOD(DatePeriod, getEndDate) -{ - php_period_obj *dpobj; - php_date_obj *dateobj; + create_date_period_datetime(period_obj->end, period_obj->start_ce, &zv); + zend_hash_str_update(props, "end", sizeof("end")-1, &zv); - ZEND_PARSE_PARAMETERS_NONE(); + create_date_period_interval(period_obj->interval, &zv); + zend_hash_str_update(props, "interval", sizeof("interval")-1, &zv); - dpobj = Z_PHPPERIOD_P(ZEND_THIS); + /* converted to larger type (int->long); must check when unserializing */ + ZVAL_LONG(&zv, (zend_long) period_obj->recurrences); + zend_hash_str_update(props, "recurrences", sizeof("recurrences")-1, &zv); - if (!dpobj->end) { - return; - } + ZVAL_BOOL(&zv, period_obj->include_start_date); + zend_hash_str_update(props, "include_start_date", sizeof("include_start_date")-1, &zv); - php_date_instantiate(dpobj->start_ce, return_value); - dateobj = Z_PHPDATE_P(return_value); - dateobj->time = timelib_time_ctor(); - *dateobj->time = *dpobj->end; - if (dpobj->end->tz_abbr) { - dateobj->time->tz_abbr = timelib_strdup(dpobj->end->tz_abbr); - } - if (dpobj->end->tz_info) { - dateobj->time->tz_info = dpobj->end->tz_info; - } + ZVAL_BOOL(&zv, period_obj->include_end_date); + zend_hash_str_update(props, "include_end_date", sizeof("include_end_date")-1, &zv); } -/* }}} */ -/* {{{ Get date interval. */ -PHP_METHOD(DatePeriod, getDateInterval) +static bool php_date_period_initialize_from_hash(php_period_obj *period_obj, const HashTable *myht) /* {{{ */ { - php_period_obj *dpobj; - php_interval_obj *diobj; + zval *ht_entry; - ZEND_PARSE_PARAMETERS_NONE(); + /* this function does no rollback on error */ - dpobj = Z_PHPPERIOD_P(ZEND_THIS); - DATE_CHECK_INITIALIZED(dpobj->interval, Z_OBJCE_P(ZEND_THIS)); + ht_entry = zend_hash_str_find(myht, "start", sizeof("start")-1); + if (ht_entry) { + if (Z_TYPE_P(ht_entry) == IS_OBJECT && instanceof_function(Z_OBJCE_P(ht_entry), date_ce_interface)) { + php_date_obj *date_obj; + date_obj = Z_PHPDATE_P(ht_entry); - php_date_instantiate(date_ce_interval, return_value); - diobj = Z_PHPINTERVAL_P(return_value); - diobj->diff = timelib_rel_time_clone(dpobj->interval); - diobj->initialized = true; -} -/* }}} */ + if (!date_obj->time) { + return false; + } -/* {{{ Get recurrences. */ -PHP_METHOD(DatePeriod, getRecurrences) -{ - php_period_obj *dpobj; + if (period_obj->start != NULL) { + timelib_time_dtor(period_obj->start); + } + period_obj->start = timelib_time_clone(date_obj->time); + period_obj->start_ce = Z_OBJCE_P(ht_entry); + } else if (Z_TYPE_P(ht_entry) != IS_NULL) { + return false; + } + } else { + return false; + } - ZEND_PARSE_PARAMETERS_NONE(); + ht_entry = zend_hash_str_find(myht, "end", sizeof("end")-1); + if (ht_entry) { + if (Z_TYPE_P(ht_entry) == IS_OBJECT && instanceof_function(Z_OBJCE_P(ht_entry), date_ce_interface)) { + php_date_obj *date_obj; + date_obj = Z_PHPDATE_P(ht_entry); - dpobj = Z_PHPPERIOD_P(ZEND_THIS); + if (!date_obj->time || !period_obj->start_ce) { + return false; + } - if (0 == dpobj->recurrences - dpobj->include_start_date - dpobj->include_end_date) { - return; + if (period_obj->end != NULL) { + timelib_time_dtor(period_obj->end); + } + period_obj->end = timelib_time_clone(date_obj->time); + } else if (Z_TYPE_P(ht_entry) != IS_NULL) { + return false; + } + } else { + return false; } - RETURN_LONG(dpobj->recurrences - dpobj->include_start_date - dpobj->include_end_date); -} -/* }}} */ + ht_entry = zend_hash_str_find(myht, "current", sizeof("current")-1); + if (ht_entry) { + if (Z_TYPE_P(ht_entry) == IS_OBJECT && instanceof_function(Z_OBJCE_P(ht_entry), date_ce_interface)) { + php_date_obj *date_obj; + date_obj = Z_PHPDATE_P(ht_entry); -PHP_METHOD(DatePeriod, getIterator) -{ - ZEND_PARSE_PARAMETERS_NONE(); + if (!date_obj->time || !period_obj->start_ce) { + return false; + } - zend_create_internal_iterator_zval(return_value, ZEND_THIS); -} + if (period_obj->current != NULL) { + timelib_time_dtor(period_obj->current); + } + period_obj->current = timelib_time_clone(date_obj->time); + } else if (Z_TYPE_P(ht_entry) != IS_NULL) { + return false; + } + } else { + return false; + } -static bool check_id_allowed(const char *id, zend_long what) /* {{{ */ -{ - if ((what & PHP_DATE_TIMEZONE_GROUP_AFRICA) && strncasecmp(id, "Africa/", 7) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_AMERICA) && strncasecmp(id, "America/", 8) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_ANTARCTICA) && strncasecmp(id, "Antarctica/", 11) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_ARCTIC) && strncasecmp(id, "Arctic/", 7) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_ASIA) && strncasecmp(id, "Asia/", 5) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_ATLANTIC) && strncasecmp(id, "Atlantic/", 9) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_AUSTRALIA) && strncasecmp(id, "Australia/", 10) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_EUROPE) && strncasecmp(id, "Europe/", 7) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_INDIAN) && strncasecmp(id, "Indian/", 7) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_PACIFIC) && strncasecmp(id, "Pacific/", 8) == 0) return true; - if ((what & PHP_DATE_TIMEZONE_GROUP_UTC) && strncasecmp(id, "UTC", 3) == 0) return true; - return false; -} /* }}} */ + ht_entry = zend_hash_str_find(myht, "interval", sizeof("interval")-1); + if (ht_entry) { + if (Z_TYPE_P(ht_entry) == IS_OBJECT && Z_OBJCE_P(ht_entry) == date_ce_interval) { + php_interval_obj *interval_obj; + interval_obj = Z_PHPINTERVAL_P(ht_entry); -/* {{{ Returns numerically index array with all timezone identifiers. */ -PHP_FUNCTION(timezone_identifiers_list) -{ - const timelib_tzdb *tzdb; - const timelib_tzdb_index_entry *table; - int i, item_count; - zend_long what = PHP_DATE_TIMEZONE_GROUP_ALL; - char *option = NULL; - size_t option_len = 0; + if (!interval_obj->initialized) { + return false; + } - ZEND_PARSE_PARAMETERS_START(0, 2) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(what) - Z_PARAM_STRING_OR_NULL(option, option_len) - ZEND_PARSE_PARAMETERS_END(); + if (period_obj->interval != NULL) { + timelib_rel_time_dtor(period_obj->interval); + } + period_obj->interval = timelib_rel_time_clone(interval_obj->diff); + } else { /* interval is required */ + return false; + } + } else { + return false; + } - /* Extra validation */ - if (what == PHP_DATE_TIMEZONE_PER_COUNTRY && option_len != 2) { - zend_argument_value_error(2, "must be a two-letter ISO 3166-1 compatible country code " - "when argument #1 ($timezoneGroup) is DateTimeZone::PER_COUNTRY"); - RETURN_THROWS(); + ht_entry = zend_hash_str_find(myht, "recurrences", sizeof("recurrences")-1); + if (ht_entry && + Z_TYPE_P(ht_entry) == IS_LONG && Z_LVAL_P(ht_entry) >= 0 && Z_LVAL_P(ht_entry) <= INT_MAX) { + period_obj->recurrences = Z_LVAL_P(ht_entry); + } else { + return false; } - tzdb = DATE_TIMEZONEDB; - table = timelib_timezone_identifiers_list((timelib_tzdb*) tzdb, &item_count); + ht_entry = zend_hash_str_find(myht, "include_start_date", sizeof("include_start_date")-1); + if (ht_entry && + (Z_TYPE_P(ht_entry) == IS_FALSE || Z_TYPE_P(ht_entry) == IS_TRUE)) { + period_obj->include_start_date = (Z_TYPE_P(ht_entry) == IS_TRUE); + } else { + return false; + } - array_init(return_value); - zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); + ht_entry = zend_hash_str_find(myht, "include_end_date", sizeof("include_end_date")-1); + if (ht_entry && + (Z_TYPE_P(ht_entry) == IS_FALSE || Z_TYPE_P(ht_entry) == IS_TRUE)) { + period_obj->include_end_date = (Z_TYPE_P(ht_entry) == IS_TRUE); + } else { + return false; + } - for (i = 0; i < item_count; ++i) { - if (what == PHP_DATE_TIMEZONE_PER_COUNTRY) { - if (tzdb->data[table[i].pos + 5] == option[0] && tzdb->data[table[i].pos + 6] == option[1]) { - add_next_index_string(return_value, table[i].id); - } - } else if (what == PHP_DATE_TIMEZONE_GROUP_ALL_W_BC || (check_id_allowed(table[i].id, what) && (tzdb->data[table[i].pos + 4] == '\1'))) { - add_next_index_string(return_value, table[i].id); - } - }; -} -/* }}} */ + period_obj->initialized = true; -/* {{{ Returns the Olson database version number. */ -PHP_FUNCTION(timezone_version_get) -{ - const timelib_tzdb *tzdb; + return true; +} /* }}} */ - ZEND_PARSE_PARAMETERS_NONE(); +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE "timezone" +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_TIMEZONE) - 1) +#define PHP_DATE_USER_CACHE_STATE_CTIME "ctime" +#define PHP_DATE_USER_CACHE_STATE_CTIME_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_CTIME) - 1) - tzdb = DATE_TIMEZONEDB; - RETURN_STRING(tzdb->version); +static void php_date_user_cache_add_assoc_int64(zval *arr, const char *name, int64_t value) +{ +#if PHP_DATE_SIZEOF_LONG == 8 + add_assoc_long(arr, name, (zend_long) value); +#else + add_assoc_str(arr, name, zend_strpprintf(0, "%lld", (long long) value)); +#endif } -/* }}} */ -/* {{{ Returns associative array containing dst, offset and the timezone name */ -PHP_FUNCTION(timezone_abbreviations_list) +static void php_date_user_cache_copy_time_snapshot(timelib_time *dst, const timelib_time *src) { - const timelib_tz_lookup_table *table, *entry; - zval element, *abbr_array_p, abbr_array; - - ZEND_PARSE_PARAMETERS_NONE(); - - table = timelib_timezone_abbreviations_list(); - array_init(return_value); - entry = table; - - do { - array_init(&element); - add_assoc_bool_ex(&element, "dst", sizeof("dst") -1, entry->type); - add_assoc_long_ex(&element, "offset", sizeof("offset") - 1, entry->gmtoffset); - if (entry->full_tz_name) { - add_assoc_string_ex(&element, "timezone_id", sizeof("timezone_id") - 1, entry->full_tz_name); - } else { - add_assoc_null_ex(&element, "timezone_id", sizeof("timezone_id") - 1); - } + memset(dst, 0, sizeof(*dst)); - abbr_array_p = zend_hash_str_find(Z_ARRVAL_P(return_value), entry->name, strlen(entry->name)); - if (!abbr_array_p) { - array_init(&abbr_array); - add_assoc_zval(return_value, entry->name, &abbr_array); - } else { - ZVAL_COPY_VALUE(&abbr_array, abbr_array_p); - } - add_next_index_zval(&abbr_array, &element); - entry++; - } while (entry->name); + dst->y = src->y; + dst->m = src->m; + dst->d = src->d; + dst->h = src->h; + dst->i = src->i; + dst->s = src->s; + dst->us = src->us; + dst->z = src->z; + dst->dst = src->dst; + dst->relative = src->relative; + dst->sse = src->sse; + dst->have_time = src->have_time; + dst->have_date = src->have_date; + dst->have_zone = src->have_zone; + dst->have_relative = src->have_relative; + dst->have_weeknr_day = src->have_weeknr_day; + dst->sse_uptodate = src->sse_uptodate; + dst->tim_uptodate = src->tim_uptodate; + dst->is_localtime = src->is_localtime; + dst->zone_type = src->zone_type; } -/* }}} */ -/* {{{ Sets the default timezone used by all date/time functions in a script */ -PHP_FUNCTION(date_default_timezone_set) +static bool php_date_copy_user_cache_state( + void *ctx, + zend_object *new_object, + zend_object *old_object, + php_user_cache_safe_direct_clone_value_func_t clone_value) { - char *zone; - size_t zone_len; + php_date_obj *new_obj, *old_obj; + php_timezone_obj *new_tzobj, *old_tzobj; + php_interval_obj *new_intervalobj, *old_intervalobj; + php_period_obj *new_periodobj, *old_periodobj; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STRING(zone, zone_len) - ZEND_PARSE_PARAMETERS_END(); + (void) ctx; - if (!timelib_timezone_id_is_valid(zone, DATE_TIMEZONEDB)) { - php_error_docref(NULL, E_NOTICE, "Timezone ID '%s' is invalid", zone); - RETURN_FALSE; - } - if (DATEG(timezone)) { - efree(DATEG(timezone)); - DATEG(timezone) = NULL; + if (clone_value == NULL) { + return false; } - DATEG(timezone) = estrndup(zone, zone_len); - RETURN_TRUE; -} -/* }}} */ -/* {{{ Gets the default timezone used by all date/time functions in a script */ -PHP_FUNCTION(date_default_timezone_get) -{ - timelib_tzinfo *default_tz; - ZEND_PARSE_PARAMETERS_NONE(); + if (instanceof_function(old_object->ce, date_ce_date) || + instanceof_function(old_object->ce, date_ce_immutable) + ) { + old_obj = php_date_obj_from_obj(old_object); + new_obj = php_date_obj_from_obj(new_object); - default_tz = get_timezone_info(); - if (!default_tz) { - RETURN_THROWS(); + if (old_obj->time == NULL) { + return true; + } + + new_obj->time = timelib_time_clone(old_obj->time); + + return new_obj->time != NULL; } - RETVAL_STRING(default_tz->name); -} -/* }}} */ -/* {{{ php_do_date_sunrise_sunset - * Common for date_sunrise() and date_sunset() functions - */ -static void php_do_date_sunrise_sunset(INTERNAL_FUNCTION_PARAMETERS, bool calc_sunset) -{ - double latitude, longitude, zenith, gmt_offset, altitude; - bool latitude_is_null = 1, longitude_is_null = 1, zenith_is_null = 1, gmt_offset_is_null = 1; - double h_rise, h_set, N; - timelib_sll rise, set, transit; - zend_long time, retformat = SUNFUNCS_RET_STRING; - int rs; - timelib_time *t; - timelib_tzinfo *tzi; - zend_string *retstr; + if (instanceof_function(old_object->ce, date_ce_timezone)) { + old_tzobj = php_timezone_obj_from_obj(old_object); + new_tzobj = php_timezone_obj_from_obj(new_object); - ZEND_PARSE_PARAMETERS_START(1, 6) - Z_PARAM_LONG(time) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(retformat) - Z_PARAM_DOUBLE_OR_NULL(latitude, latitude_is_null) - Z_PARAM_DOUBLE_OR_NULL(longitude, longitude_is_null) - Z_PARAM_DOUBLE_OR_NULL(zenith, zenith_is_null) - Z_PARAM_DOUBLE_OR_NULL(gmt_offset, gmt_offset_is_null) - ZEND_PARSE_PARAMETERS_END(); + if (!old_tzobj->initialized) { + return true; + } - if (latitude_is_null) { - latitude = zend_ini_double_literal("date.default_latitude"); - } + new_tzobj->type = old_tzobj->type; + new_tzobj->initialized = true; + switch (new_tzobj->type) { + case TIMELIB_ZONETYPE_ID: + new_tzobj->tzi.tz = old_tzobj->tzi.tz; - if (longitude_is_null) { - longitude = zend_ini_double_literal("date.default_longitude"); - } + return true; + case TIMELIB_ZONETYPE_OFFSET: + new_tzobj->tzi.utc_offset = old_tzobj->tzi.utc_offset; - if (zenith_is_null) { - if (calc_sunset) { - zenith = zend_ini_double_literal("date.sunset_zenith"); - } else { - zenith = zend_ini_double_literal("date.sunrise_zenith"); + return true; + case TIMELIB_ZONETYPE_ABBR: + new_tzobj->tzi.z.utc_offset = old_tzobj->tzi.z.utc_offset; + new_tzobj->tzi.z.dst = old_tzobj->tzi.z.dst; + new_tzobj->tzi.z.abbr = old_tzobj->tzi.z.abbr != NULL + ? timelib_strdup(old_tzobj->tzi.z.abbr) + : NULL + ; + + return old_tzobj->tzi.z.abbr == NULL || new_tzobj->tzi.z.abbr != NULL; + default: + return false; } } - if (retformat != SUNFUNCS_RET_TIMESTAMP && - retformat != SUNFUNCS_RET_STRING && - retformat != SUNFUNCS_RET_DOUBLE) - { - zend_argument_value_error(2, "must be one of SUNFUNCS_RET_TIMESTAMP, SUNFUNCS_RET_STRING, or SUNFUNCS_RET_DOUBLE"); - RETURN_THROWS(); - } - altitude = 90 - zenith; + if (instanceof_function(old_object->ce, date_ce_interval)) { + old_intervalobj = php_interval_obj_from_obj(old_object); + new_intervalobj = php_interval_obj_from_obj(new_object); - if (!zend_finite(latitude) || !zend_finite(longitude)) { - RETURN_FALSE; - } + new_intervalobj->civil_or_wall = old_intervalobj->civil_or_wall; + new_intervalobj->from_string = old_intervalobj->from_string; - /* Initialize time struct */ - tzi = get_timezone_info(); - if (!tzi) { - RETURN_THROWS(); - } - t = timelib_time_ctor(); - t->tz_info = tzi; - t->zone_type = TIMELIB_ZONETYPE_ID; + if (old_intervalobj->date_string != NULL) { + new_intervalobj->date_string = zend_string_copy(old_intervalobj->date_string); + } - if (gmt_offset_is_null) { - gmt_offset = timelib_get_current_offset(t) / 3600.0; - } + new_intervalobj->initialized = old_intervalobj->initialized; - timelib_unixtime2local(t, time); - rs = timelib_astro_rise_set_altitude(t, longitude, latitude, altitude, 1, &h_rise, &h_set, &rise, &set, &transit); - timelib_time_dtor(t); + if (old_intervalobj->diff != NULL) { + new_intervalobj->diff = timelib_rel_time_clone(old_intervalobj->diff); - if (rs != 0) { - RETURN_FALSE; - } + return new_intervalobj->diff != NULL; + } - if (retformat == SUNFUNCS_RET_TIMESTAMP) { - RETURN_LONG(calc_sunset ? set : rise); + return true; } - N = (calc_sunset ? h_set : h_rise) + gmt_offset; - if (N > 24 || N < 0) { - N -= floor(N / 24) * 24; - } - if (!(N <= 24 && N >= 0)) { - RETURN_FALSE; - } + if (instanceof_function(old_object->ce, date_ce_period)) { + old_periodobj = php_period_obj_from_obj(old_object); + new_periodobj = php_period_obj_from_obj(new_object); - switch (retformat) { - case SUNFUNCS_RET_STRING: - retstr = strpprintf(0, "%02d:%02d", (int) N, (int) (60 * (N - (int) N))); - RETURN_NEW_STR(retstr); - case SUNFUNCS_RET_DOUBLE: - RETURN_DOUBLE(N); - } -} -/* }}} */ + new_periodobj->start_ce = old_periodobj->start_ce; + new_periodobj->recurrences = old_periodobj->recurrences; + new_periodobj->initialized = old_periodobj->initialized; + new_periodobj->include_start_date = old_periodobj->include_start_date; + new_periodobj->include_end_date = old_periodobj->include_end_date; -/* {{{ Returns time of sunrise for a given day and location */ -PHP_FUNCTION(date_sunrise) -{ - php_do_date_sunrise_sunset(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); -} -/* }}} */ + if (old_periodobj->start != NULL) { + new_periodobj->start = timelib_time_clone(old_periodobj->start); + if (new_periodobj->start == NULL) { + return false; + } + } -/* {{{ Returns time of sunset for a given day and location */ -PHP_FUNCTION(date_sunset) -{ - php_do_date_sunrise_sunset(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1); -} -/* }}} */ + if (old_periodobj->current != NULL) { + new_periodobj->current = timelib_time_clone(old_periodobj->current); + if (new_periodobj->current == NULL) { + return false; + } + } -/* {{{ Returns an array with information about sun set/rise and twilight begin/end */ -PHP_FUNCTION(date_sun_info) -{ - zend_long time; - double latitude, longitude; - timelib_time *t, *t2; - timelib_tzinfo *tzi; - int rs; - timelib_sll rise, set, transit; - int dummy; - double ddummy; + if (old_periodobj->end != NULL) { + new_periodobj->end = timelib_time_clone(old_periodobj->end); + if (new_periodobj->end == NULL) { + return false; + } + } - ZEND_PARSE_PARAMETERS_START(3, 3) - Z_PARAM_LONG(time) - Z_PARAM_DOUBLE(latitude) - Z_PARAM_DOUBLE(longitude) - ZEND_PARSE_PARAMETERS_END(); + if (old_periodobj->interval != NULL) { + new_periodobj->interval = timelib_rel_time_clone(old_periodobj->interval); + if (new_periodobj->interval == NULL) { + return false; + } + } - if (!zend_finite(latitude)) { - zend_argument_value_error(2, "must be finite"); - RETURN_THROWS(); - } - if (!zend_finite(longitude)) { - zend_argument_value_error(3, "must be finite"); - RETURN_THROWS(); + return true; } - /* Initialize time struct */ - tzi = get_timezone_info(); - if (!tzi) { - RETURN_THROWS(); - } - t = timelib_time_ctor(); - t->tz_info = tzi; - t->zone_type = TIMELIB_ZONETYPE_ID; - timelib_unixtime2local(t, time); + return false; +} - /* Setup */ - t2 = timelib_time_ctor(); - array_init(return_value); +static bool php_date_user_cache_timezone_pair_to_hash(php_date_obj *dateobj, HashTable *props) +{ + zval zv; - /* Get sun up/down and transit */ - rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -35.0/60, 1, &ddummy, &ddummy, &rise, &set, &transit); - switch (rs) { - case -1: /* always below */ - add_assoc_bool(return_value, "sunrise", 0); - add_assoc_bool(return_value, "sunset", 0); - break; - case 1: /* always above */ - add_assoc_bool(return_value, "sunrise", 1); - add_assoc_bool(return_value, "sunset", 1); - break; - default: - t2->sse = rise; - add_assoc_long(return_value, "sunrise", timelib_date_to_int(t2, &dummy)); - t2->sse = set; - add_assoc_long(return_value, "sunset", timelib_date_to_int(t2, &dummy)); - } - t2->sse = transit; - add_assoc_long(return_value, "transit", timelib_date_to_int(t2, &dummy)); + ZVAL_LONG(&zv, dateobj->time->zone_type); + zend_hash_str_update(props, "timezone_type", sizeof("timezone_type")-1, &zv); - /* Get civil twilight */ - rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -6.0, 0, &ddummy, &ddummy, &rise, &set, &transit); - switch (rs) { - case -1: /* always below */ - add_assoc_bool(return_value, "civil_twilight_begin", 0); - add_assoc_bool(return_value, "civil_twilight_end", 0); + switch (dateobj->time->zone_type) { + case TIMELIB_ZONETYPE_ID: + ZVAL_STRING(&zv, dateobj->time->tz_info->name); break; - case 1: /* always above */ - add_assoc_bool(return_value, "civil_twilight_begin", 1); - add_assoc_bool(return_value, "civil_twilight_end", 1); + case TIMELIB_ZONETYPE_OFFSET: + ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z)); + break; + case TIMELIB_ZONETYPE_ABBR: + ZVAL_STRING(&zv, dateobj->time->tz_abbr); break; default: - t2->sse = rise; - add_assoc_long(return_value, "civil_twilight_begin", timelib_date_to_int(t2, &dummy)); - t2->sse = set; - add_assoc_long(return_value, "civil_twilight_end", timelib_date_to_int(t2, &dummy)); + return false; } + zend_hash_str_update(props, "timezone", sizeof("timezone")-1, &zv); - /* Get nautical twilight */ - rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -12.0, 0, &ddummy, &ddummy, &rise, &set, &transit); - switch (rs) { - case -1: /* always below */ - add_assoc_bool(return_value, "nautical_twilight_begin", 0); - add_assoc_bool(return_value, "nautical_twilight_end", 0); - break; - case 1: /* always above */ - add_assoc_bool(return_value, "nautical_twilight_begin", 1); - add_assoc_bool(return_value, "nautical_twilight_end", 1); - break; - default: - t2->sse = rise; - add_assoc_long(return_value, "nautical_twilight_begin", timelib_date_to_int(t2, &dummy)); - t2->sse = set; - add_assoc_long(return_value, "nautical_twilight_end", timelib_date_to_int(t2, &dummy)); + return true; +} + +static bool php_date_serialize_datetime_user_cache_state(php_date_obj *dateobj, zval *state) +{ + timelib_time snapshot; + zval zv; + + if (dateobj->time == NULL || !dateobj->time->is_localtime) { + return false; } - /* Get astronomical twilight */ - rs = timelib_astro_rise_set_altitude(t, longitude, latitude, -18.0, 0, &ddummy, &ddummy, &rise, &set, &transit); - switch (rs) { - case -1: /* always below */ - add_assoc_bool(return_value, "astronomical_twilight_begin", 0); - add_assoc_bool(return_value, "astronomical_twilight_end", 0); - break; - case 1: /* always above */ - add_assoc_bool(return_value, "astronomical_twilight_begin", 1); - add_assoc_bool(return_value, "astronomical_twilight_end", 1); - break; - default: - t2->sse = rise; - add_assoc_long(return_value, "astronomical_twilight_begin", timelib_date_to_int(t2, &dummy)); - t2->sse = set; - add_assoc_long(return_value, "astronomical_twilight_end", timelib_date_to_int(t2, &dummy)); + array_init_size(state, 3); + + php_date_user_cache_copy_time_snapshot(&snapshot, dateobj->time); + + if (!php_date_user_cache_timezone_pair_to_hash(dateobj, Z_ARRVAL_P(state))) { + zval_ptr_dtor(state); + ZVAL_UNDEF(state); + + return false; } - timelib_time_dtor(t); - timelib_time_dtor(t2); + + ZVAL_STRINGL(&zv, (const char *) &snapshot, sizeof(snapshot)); + zend_hash_str_update(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN, &zv); + + return true; } -/* }}} */ -static HashTable *date_object_get_gc_period(zend_object *object, zval **table, int *n) /* {{{ */ +static bool php_date_serialize_user_cache_state(zval *state, const zval *object) { - *table = NULL; - *n = 0; - return zend_std_get_properties(object); -} /* }}} */ + php_timezone_obj *tzobj; + php_interval_obj *intervalobj; -static void date_period_object_to_hash(php_period_obj *period_obj, HashTable *props) -{ - zval zv; + ZVAL_UNDEF(state); - create_date_period_datetime(period_obj->start, period_obj->start_ce, &zv); - zend_hash_str_update(props, "start", sizeof("start")-1, &zv); + if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || + instanceof_function(Z_OBJCE_P(object), date_ce_immutable) + ) { + return php_date_serialize_datetime_user_cache_state(Z_PHPDATE_P((zval *) object), state); + } - create_date_period_datetime(period_obj->current, period_obj->start_ce, &zv); - zend_hash_str_update(props, "current", sizeof("current")-1, &zv); + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + tzobj = Z_PHPTIMEZONE_P((zval *) object); - create_date_period_datetime(period_obj->end, period_obj->start_ce, &zv); - zend_hash_str_update(props, "end", sizeof("end")-1, &zv); + if (!tzobj->initialized) { + return false; + } - create_date_period_interval(period_obj->interval, &zv); - zend_hash_str_update(props, "interval", sizeof("interval")-1, &zv); + array_init_size(state, 2); + date_timezone_object_to_hash(tzobj, Z_ARRVAL_P(state)); - /* converted to larger type (int->long); must check when unserializing */ - ZVAL_LONG(&zv, (zend_long) period_obj->recurrences); - zend_hash_str_update(props, "recurrences", sizeof("recurrences")-1, &zv); + return true; + } - ZVAL_BOOL(&zv, period_obj->include_start_date); - zend_hash_str_update(props, "include_start_date", sizeof("include_start_date")-1, &zv); + if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { + intervalobj = Z_PHPINTERVAL_P((zval *) object); - ZVAL_BOOL(&zv, period_obj->include_end_date); - zend_hash_str_update(props, "include_end_date", sizeof("include_end_date")-1, &zv); -} + if (!intervalobj->initialized || intervalobj->diff == NULL) { + return false; + } -static bool php_date_period_initialize_from_hash(php_period_obj *period_obj, const HashTable *myht) /* {{{ */ -{ - zval *ht_entry; + if (intervalobj->from_string && intervalobj->date_string == NULL) { + return false; + } - /* this function does no rollback on error */ + array_init_size(state, intervalobj->from_string ? 2 : 18); + date_interval_object_to_hash(intervalobj, Z_ARRVAL_P(state)); - ht_entry = zend_hash_str_find(myht, "start", sizeof("start")-1); - if (ht_entry) { - if (Z_TYPE_P(ht_entry) == IS_OBJECT && instanceof_function(Z_OBJCE_P(ht_entry), date_ce_interface)) { - php_date_obj *date_obj; - date_obj = Z_PHPDATE_P(ht_entry); + if (!intervalobj->from_string) { + add_assoc_long(state, "weekday", intervalobj->diff->weekday); + add_assoc_long(state, "weekday_behavior", intervalobj->diff->weekday_behavior); + add_assoc_long(state, "first_last_day_of", intervalobj->diff->first_last_day_of); + add_assoc_long(state, "special_type", intervalobj->diff->special.type); + php_date_user_cache_add_assoc_int64(state, "special_amount", intervalobj->diff->special.amount); + add_assoc_long(state, "have_weekday_relative", intervalobj->diff->have_weekday_relative); + add_assoc_long(state, "have_special_relative", intervalobj->diff->have_special_relative); + add_assoc_long(state, "civil_or_wall", intervalobj->civil_or_wall); + } - if (!date_obj->time) { - return false; - } + return true; + } - if (period_obj->start != NULL) { - timelib_time_dtor(period_obj->start); - } - period_obj->start = timelib_time_clone(date_obj->time); - period_obj->start_ce = Z_OBJCE_P(ht_entry); - } else if (Z_TYPE_P(ht_entry) != IS_NULL) { + if (instanceof_function(Z_OBJCE_P(object), date_ce_period)) { + php_period_obj *periodobj = Z_PHPPERIOD_P((zval *) object); + + if (!periodobj->initialized || periodobj->start == NULL) { return false; } - } else { - return false; + + array_init_size(state, 7); + date_period_object_to_hash(periodobj, Z_ARRVAL_P(state)); + + return true; } - ht_entry = zend_hash_str_find(myht, "end", sizeof("end")-1); - if (ht_entry) { - if (Z_TYPE_P(ht_entry) == IS_OBJECT && instanceof_function(Z_OBJCE_P(ht_entry), date_ce_interface)) { - php_date_obj *date_obj; - date_obj = Z_PHPDATE_P(ht_entry); + return false; +} - if (!date_obj->time || !period_obj->start_ce) { - return false; - } +static bool php_date_unserialize_datetime_user_cache_state(zval *object, zval *state) +{ + php_date_obj *dateobj; + timelib_time *time; + timelib_tzinfo *tzi; + zval *z_ctime, *z_timezone; - if (period_obj->end != NULL) { - timelib_time_dtor(period_obj->end); - } - period_obj->end = timelib_time_clone(date_obj->time); - } else if (Z_TYPE_P(ht_entry) != IS_NULL) { - return false; - } - } else { + z_ctime = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN); + if (z_ctime == NULL) { return false; } - ht_entry = zend_hash_str_find(myht, "current", sizeof("current")-1); - if (ht_entry) { - if (Z_TYPE_P(ht_entry) == IS_OBJECT && instanceof_function(Z_OBJCE_P(ht_entry), date_ce_interface)) { - php_date_obj *date_obj; - date_obj = Z_PHPDATE_P(ht_entry); + if (Z_TYPE_P(z_ctime) != IS_STRING || Z_STRLEN_P(z_ctime) != sizeof(timelib_time)) { + return false; + } + + time = timelib_time_ctor(); + if (time == NULL) { + return false; + } + + memcpy(time, Z_STRVAL_P(z_ctime), sizeof(timelib_time)); + time->tz_abbr = NULL; + time->tz_info = NULL; + + switch (time->zone_type) { + case TIMELIB_ZONETYPE_ID: + z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); + if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || + UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) + ) { + timelib_time_dtor(time); - if (!date_obj->time || !period_obj->start_ce) { return false; } - if (period_obj->current != NULL) { - timelib_time_dtor(period_obj->current); - } - period_obj->current = timelib_time_clone(date_obj->time); - } else if (Z_TYPE_P(ht_entry) != IS_NULL) { - return false; - } - } else { - return false; - } + tzi = php_date_parse_tzfile(Z_STRVAL_P(z_timezone), DATE_TIMEZONEDB); + if (tzi == NULL) { + timelib_time_dtor(time); - ht_entry = zend_hash_str_find(myht, "interval", sizeof("interval")-1); - if (ht_entry) { - if (Z_TYPE_P(ht_entry) == IS_OBJECT && Z_OBJCE_P(ht_entry) == date_ce_interval) { - php_interval_obj *interval_obj; - interval_obj = Z_PHPINTERVAL_P(ht_entry); + return false; + } + time->tz_info = tzi; + break; + case TIMELIB_ZONETYPE_OFFSET: + break; + case TIMELIB_ZONETYPE_ABBR: + z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); + if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || + UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) + ) { + timelib_time_dtor(time); - if (!interval_obj->initialized) { return false; } - if (period_obj->interval != NULL) { - timelib_rel_time_dtor(period_obj->interval); + time->tz_abbr = timelib_strdup(Z_STRVAL_P(z_timezone)); + if (time->tz_abbr == NULL) { + timelib_time_dtor(time); + + return false; } - period_obj->interval = timelib_rel_time_clone(interval_obj->diff); - } else { /* interval is required */ + break; + default: + timelib_time_dtor(time); + return false; - } - } else { - return false; } - ht_entry = zend_hash_str_find(myht, "recurrences", sizeof("recurrences")-1); - if (ht_entry && - Z_TYPE_P(ht_entry) == IS_LONG && Z_LVAL_P(ht_entry) >= 0 && Z_LVAL_P(ht_entry) <= INT_MAX) { - period_obj->recurrences = Z_LVAL_P(ht_entry); - } else { - return false; + dateobj = Z_PHPDATE_P(object); + if (dateobj->time != NULL) { + timelib_time_dtor(dateobj->time); } + dateobj->time = time; - ht_entry = zend_hash_str_find(myht, "include_start_date", sizeof("include_start_date")-1); - if (ht_entry && - (Z_TYPE_P(ht_entry) == IS_FALSE || Z_TYPE_P(ht_entry) == IS_TRUE)) { - period_obj->include_start_date = (Z_TYPE_P(ht_entry) == IS_TRUE); - } else { + return true; +} + +static bool php_date_unserialize_user_cache_state(zval *object, zval *state) +{ + php_timezone_obj *tzobj; + + if (Z_TYPE_P(state) != IS_ARRAY) { return false; } - ht_entry = zend_hash_str_find(myht, "include_end_date", sizeof("include_end_date")-1); - if (ht_entry && - (Z_TYPE_P(ht_entry) == IS_FALSE || Z_TYPE_P(ht_entry) == IS_TRUE)) { - period_obj->include_end_date = (Z_TYPE_P(ht_entry) == IS_TRUE); - } else { - return false; + if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || + instanceof_function(Z_OBJCE_P(object), date_ce_immutable) + ) { + return php_date_unserialize_datetime_user_cache_state(object, state); } - period_obj->initialized = true; + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + tzobj = Z_PHPTIMEZONE_P(object); - return true; -} /* }}} */ + return php_date_timezone_initialize_from_hash(&tzobj, Z_ARRVAL_P(state)); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { + php_date_interval_initialize_from_hash(Z_PHPINTERVAL_P(object), Z_ARRVAL_P(state)); + + return !EG(exception); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_period)) { + return php_date_period_initialize_from_hash(Z_PHPPERIOD_P(object), Z_ARRVAL_P(state)) + && !EG(exception); + } + + return false; +} + +static const php_user_cache_safe_direct_handlers php_date_user_cache_handlers = { + .prefer_request_local_prototype = true, + .copy = php_date_copy_user_cache_state, + .state_serialize = php_date_serialize_user_cache_state, + .state_unserialize = php_date_unserialize_user_cache_state, +}; + +static void php_date_register_user_cache_handlers(void) +{ + php_user_cache_safe_direct_register_class(date_ce_date, &php_date_user_cache_handlers); + php_user_cache_safe_direct_register_class(date_ce_immutable, &php_date_user_cache_handlers); + php_user_cache_safe_direct_register_class(date_ce_timezone, &php_date_user_cache_handlers); + php_user_cache_safe_direct_register_class(date_ce_interval, &php_date_user_cache_handlers); + php_user_cache_safe_direct_register_class(date_ce_period, &php_date_user_cache_handlers); +} /* {{{ */ PHP_METHOD(DatePeriod, __set_state) diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index 691a5eae99cb..7b104f3f3760 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -18,7 +18,7 @@ #include "php.h" #include "ext/standard/php_var.h" -#include "ext/user_cache/php_user_cache.h" #include "ext/user_cache/php_user_cache.h" /* For user_cache safe direct path */ +#include "ext/user_cache/php_user_cache.h" /* For user_cache safe direct path */ #include "zend_smart_str.h" #include "zend_interfaces.h" #include "zend_exceptions.h" diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_dateperiod.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_dateperiod.phpt new file mode 100644 index 000000000000..29a71418fbfa --- /dev/null +++ b/ext/user_cache/tests/fpm/user_cache_fpm_dateperiod.phpt @@ -0,0 +1,112 @@ +--TEST-- +FPM: UserCache\Cache safe-direct DatePeriod state survives across requests +--EXTENSIONS-- +user_cache +--SKIPIF-- + +--FILE-- +label = $label; + } +} + +function period_dates(DatePeriod $period): string +{ + $dates = []; + foreach ($period as $date) { + $dates[] = $date->format('Y-m-d'); + } + return implode(',', $dates); +} + +$cache = UserCache\Cache::getPool('default'); +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + $cache->clear(); + + $byRecurrences = new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 3); + $byEnd = new DatePeriod( + new DateTimeImmutable('2026-03-01'), + new DateInterval('P1M'), + new DateTimeImmutable('2026-05-15'), + ); + $tagged = new TaggedDatePeriod(new DateTimeImmutable('2026-02-01'), new DateInterval('P1D'), 2); + $tagged->tag('window'); + + var_dump($cache->store('period_payload', [ + 'recurrences' => $byRecurrences, + 'end' => $byEnd, + 'tagged' => $tagged, + ])); + echo "seed\n"; + return; +} + +$payload = $cache->fetch('period_payload'); +$recurrences = $payload['recurrences']; +$end = $payload['end']; +$tagged = $payload['tagged']; + +echo ($recurrences instanceof DatePeriod ? 'DatePeriod' : get_debug_type($recurrences)), ',', period_dates($recurrences), "\n"; +echo period_dates($end), "\n"; +echo ($tagged instanceof TaggedDatePeriod ? 'TaggedDatePeriod' : get_debug_type($tagged)), ',', $tagged->label, ',', period_dates($tagged), "\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'user_cache.shm_size' => '32M', + 'date.timezone' => 'UTC', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "DatePeriod,2026-01-01,2026-01-02,2026-01-03,2026-01-04\n" . + "2026-03-01,2026-04-01,2026-05-01\n" . + "TaggedDatePeriod,window,2026-02-01,2026-02-02,2026-02-03" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +unset($tester); +gc_collect_cycles(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/user_cache/tests/user_cache_dateinterval_safe_direct.phpt b/ext/user_cache/tests/user_cache_dateinterval_safe_direct.phpt new file mode 100644 index 000000000000..20da01f9e1b9 --- /dev/null +++ b/ext/user_cache/tests/user_cache_dateinterval_safe_direct.phpt @@ -0,0 +1,87 @@ +--TEST-- +UserCache\Cache: DateInterval safe-direct state round-trips (incl. subclasses) +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +--FILE-- +label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +$cache = UserCache\Cache::getPool('dateinterval-safe-direct'); +$cache->clear(); + +/* --- spec interval: non-from_string, full field set --- */ +$spec = new DateInterval('P1Y2M3DT4H5M6S'); +$cache->store('spec', $spec); +$s = $cache->fetch('spec'); +var_dump($s instanceof DateInterval); +var_dump(serialize($s) === serialize($spec)); +var_dump($s->format('%y-%m-%d %h:%i:%s')); + +/* --- diff interval: carries computed days + invert --- */ +$diff = (new DateTimeImmutable('2026-01-01 00:00:00'))->diff(new DateTimeImmutable('2026-03-15 10:30:00')); +$cache->store('diff', $diff); +$d = $cache->fetch('diff'); +var_dump(serialize($d) === serialize($diff)); +var_dump($d->days); +var_dump($d->invert); +var_dump($d->format('%a days %h:%i')); + +/* --- inverted diff --- */ +$inv = (new DateTimeImmutable('2026-03-15'))->diff(new DateTimeImmutable('2026-01-01')); +$cache->store('inv', $inv); +var_dump($cache->fetch('inv')->invert); + +/* --- from_string interval --- */ +$fs = DateInterval::createFromDateString('2 days 4 hours'); +$cache->store('fs', $fs); +$f = $cache->fetch('fs'); +var_dump(serialize($f) === serialize($fs)); +var_dump($f->format('%d %h')); + +/* --- subclass with extra state, no __serialize override => safe-direct --- */ +$tagged = new TaggedInterval('P10DT12H'); +$tagged->tag('window', 9); +$cache->store('tagged', $tagged); +$t = $cache->fetch('tagged'); +var_dump($t instanceof TaggedInterval); +var_dump($t->label); +var_dump($t->describe()); +var_dump($t->format('%d %h')); +?> +--EXPECT-- +bool(true) +bool(true) +string(11) "1-2-3 4:5:6" +bool(true) +int(73) +int(0) +string(13) "73 days 10:30" +int(1) +bool(true) +string(3) "2 4" +bool(true) +string(6) "window" +string(8) "window:9" +string(5) "10 12" diff --git a/ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt b/ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt new file mode 100644 index 000000000000..e2c341499c23 --- /dev/null +++ b/ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt @@ -0,0 +1,156 @@ +--TEST-- +UserCache\Cache: DatePeriod safe-direct state round-trips (incl. subclasses) +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +date.timezone=UTC +--FILE-- +label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +class MagicDatePeriod extends DatePeriod +{ + public static int $serializeCount = 0; + public static int $unserializeCount = 0; + + public string $note = 'none'; + + public function __serialize(): array + { + self::$serializeCount++; + + return parent::__serialize() + ['note' => 'serialized-' . $this->note]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->note = $data['note']; + } +} + +function period_dates(DatePeriod $period): array +{ + $dates = []; + foreach ($period as $date) { + $dates[] = $date->format('Y-m-d'); + } + return $dates; +} + +$cache = UserCache\Cache::getPool('dateperiod-safe-direct'); +$cache->clear(); + +/* --- recurrences-based --- */ +$byRecurrences = new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 3); +$cache->store('recurrences', $byRecurrences); +$r = $cache->fetch('recurrences'); +var_dump($r instanceof DatePeriod); +var_dump(serialize($r) === serialize($byRecurrences)); +var_dump(period_dates($r) === ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']); + +/* --- end-based --- */ +$byEnd = new DatePeriod( + new DateTimeImmutable('2026-03-01'), + new DateInterval('P1M'), + new DateTimeImmutable('2026-05-15'), +); +$cache->store('end', $byEnd); +var_dump(serialize($cache->fetch('end')) === serialize($byEnd)); + +/* --- EXCLUDE_START_DATE --- */ +$excludeStart = new DatePeriod( + new DateTimeImmutable('2026-01-01'), + new DateInterval('P1D'), + 2, + DatePeriod::EXCLUDE_START_DATE, +); +$cache->store('exclude-start', $excludeStart); +var_dump(period_dates($cache->fetch('exclude-start')) === ['2026-01-02', '2026-01-03']); + +/* --- INCLUDE_END_DATE --- */ +$includeEnd = new DatePeriod( + new DateTimeImmutable('2026-01-01'), + new DateInterval('P1D'), + new DateTimeImmutable('2026-01-03'), + DatePeriod::INCLUDE_END_DATE, +); +$cache->store('include-end', $includeEnd); +var_dump(serialize($cache->fetch('include-end')) === serialize($includeEnd)); +var_dump(period_dates($cache->fetch('include-end')) === ['2026-01-01', '2026-01-02', '2026-01-03']); + +/* --- subclass with extra state, no __serialize override => safe-direct --- */ +$tagged = new TaggedDatePeriod(new DateTimeImmutable('2026-02-01'), new DateInterval('P1D'), 2); +$tagged->tag('window', 9); +$cache->store('tagged', $tagged); +$t = $cache->fetch('tagged'); +var_dump($t instanceof TaggedDatePeriod); +var_dump($t->label); +var_dump($t->describe()); +var_dump(period_dates($t) === ['2026-02-01', '2026-02-02', '2026-02-03']); + +/* --- subclass overriding __serialize => magic hooks route, still round-trips --- */ +$magic = new MagicDatePeriod(new DateTimeImmutable('2026-04-01'), new DateInterval('P1D'), 1); +$magic->note = 'hello'; +$cache->store('magic', $magic); +$m = $cache->fetch('magic'); +var_dump($m instanceof MagicDatePeriod); +var_dump($m->note); +var_dump(period_dates($m) === ['2026-04-01', '2026-04-02']); +var_dump(MagicDatePeriod::$serializeCount > 0); +var_dump(MagicDatePeriod::$unserializeCount > 0); + +/* --- shared identity: interval reused across the graph --- */ +$interval = new DateInterval('P2D'); +$graph = [ + 'period' => new DatePeriod(new DateTimeImmutable('2026-06-01'), $interval, 2), + 'interval' => $interval, + 'zone' => new DateTimeZone('Asia/Tokyo'), +]; +$cache->store('graph', $graph); +$g = $cache->fetch('graph'); +var_dump(serialize($g) === serialize($graph)); +var_dump($g['interval'] instanceof DateInterval && $g['zone']->getName() === 'Asia/Tokyo'); +var_dump(period_dates($g['period']) === ['2026-06-01', '2026-06-03', '2026-06-05']); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(6) "window" +string(8) "window:9" +bool(true) +bool(true) +string(5) "hello" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) From 6fb4975c040dca86e7fa18b9a331eb58255734c3 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Wed, 15 Jul 2026 06:18:34 +0000 Subject: [PATCH 11/12] fix: UAF on DatePeriod safe-direct path --- ext/user_cache/user_cache_shared_graph.c | 27 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/ext/user_cache/user_cache_shared_graph.c b/ext/user_cache/user_cache_shared_graph.c index 3d8c4f654821..81f1e4b3a71f 100644 --- a/ext/user_cache/user_cache_shared_graph.c +++ b/ext/user_cache/user_cache_shared_graph.c @@ -445,12 +445,19 @@ static zend_always_inline bool user_cache_shared_graph_is_unmangled_property_nam return ZSTR_LEN(prop_name) != 0 && ZSTR_VAL(prop_name)[0] != '\0'; } +/* + * The state table pointer is captured before recursing into the state graph: + * the memoized state zval lives in a HashTable whose bucket array is + * reallocated when nested objects add their own memo entries, so a pointer + * into that bucket must not be dereferenced afterwards. The referenced state + * array itself is stable, so its HashTable is safe to hold across recursion. + */ static zend_always_inline bool user_cache_shared_graph_safe_direct_property_shadows_state( - zend_string *prop_name, zval *state) + zend_string *prop_name, const HashTable *state_ht) { - return user_cache_shared_graph_is_unmangled_property_name(prop_name) && - Z_TYPE_P(state) == IS_ARRAY && - zend_hash_exists(Z_ARRVAL_P(state), prop_name) + return state_ht != NULL && + user_cache_shared_graph_is_unmangled_property_name(prop_name) && + zend_hash_exists(state_ht, prop_name) ; } @@ -1526,6 +1533,7 @@ static bool user_cache_shared_graph_calc_safe_direct_object( { zend_string *prop_name; zval *prop_val, *src_val, *state_ptr, state; + const HashTable *state_ht; HashTable *props; uint32_t prop_count; bool result; @@ -1540,6 +1548,9 @@ static bool user_cache_shared_graph_calc_safe_direct_object( return false; } + /* state_ptr may dangle once recursion resizes the memo; keep the array. */ + state_ht = Z_TYPE_P(state_ptr) == IS_ARRAY ? Z_ARRVAL_P(state_ptr) : NULL; + /* Reserve shared objects only once. */ if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) { result = true; @@ -1586,7 +1597,7 @@ static bool user_cache_shared_graph_calc_safe_direct_object( if (user_cache_shared_graph_safe_direct_property_shadows_state( prop_name, - state_ptr + state_ht ) ) { continue; @@ -2769,6 +2780,7 @@ static bool user_cache_shared_graph_copy_safe_direct_object( zend_object *obj, php_user_cache_shared_graph_value *dst) { + const HashTable *sd_state_ht; php_user_cache_shared_graph_safe_direct_object *graph_safe_direct; php_user_cache_shared_graph_property *graph_properties; php_user_cache_copy_object_ref_result ref_result; @@ -2800,6 +2812,9 @@ static bool user_cache_shared_graph_copy_safe_direct_object( return false; } + /* sd_state_ptr may dangle once recursion resizes the memo; keep the array. */ + sd_state_ht = Z_TYPE_P(sd_state_ptr) == IS_ARRAY ? Z_ARRVAL_P(sd_state_ptr) : NULL; + if (!user_cache_shared_graph_copy_alloc( ctx, sizeof(php_user_cache_shared_graph_safe_direct_object), @@ -2877,7 +2892,7 @@ static bool user_cache_shared_graph_copy_safe_direct_object( if (user_cache_shared_graph_safe_direct_property_shadows_state( prop_name, - sd_state_ptr + sd_state_ht ) ) { ++prop_idx; From 3910708d8be4246e84128168ac58c4563a3d1415 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Wed, 15 Jul 2026 09:07:16 +0000 Subject: [PATCH 12/12] fix: cant drop entry if __unserialize throwed Exception --- .../user_cache_fpm_decode_failure_delete.phpt | 39 ++++++---- .../user_cache_fetch_restore_exception.phpt | 77 +++++++++++++++++++ ...uest_local_slot_unserialize_exception.phpt | 39 +++++----- ext/user_cache/user_cache.c | 74 +++++++++++++++--- 4 files changed, 186 insertions(+), 43 deletions(-) create mode 100644 ext/user_cache/tests/user_cache_fetch_restore_exception.phpt diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt index d14f9be26237..e834e35f5fd2 100644 --- a/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt +++ b/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt @@ -1,5 +1,5 @@ --TEST-- -FPM: UserCache\Cache deletes entries that fail to decode +FPM: UserCache\Cache drops silently-corrupt entries but propagates decode exceptions --EXTENSIONS-- user_cache --SKIPIF-- @@ -55,11 +55,29 @@ if ($action === 'fetch') { } }); + /* A missing class with no autoloader hit fails to decode without an + * exception, so the entry is corrupt and dropped. */ var_dump($cache->fetch('single', 'DEFAULT')); var_dump($cache->has('single')); - var_dump($cache->fetch('autoload-single', 'DEFAULT')); + + /* A throwing autoloader is a genuine userland exception: it propagates and + * the entry is kept, matching native unserialize(). */ + try { + $cache->fetch('autoload-single', 'DEFAULT'); + echo "no exception\n"; + } catch (Exception $e) { + echo "caught: ", $e->getMessage(), "\n"; + } var_dump($cache->has('autoload-single')); - var_dump($cache->fetchMultiple(['multiple', 'missing', 'autoload-multiple'], 'DEFAULT')); + + /* In a batch, the corrupt entry is dropped and the miss yields the default + * before the throwing autoloader aborts the whole fetch. */ + try { + $cache->fetchMultiple(['multiple', 'missing', 'autoload-multiple'], 'DEFAULT'); + echo "no exception\n"; + } catch (Exception $e) { + echo "caught: ", $e->getMessage(), "\n"; + } var_dump($cache->has('multiple')); var_dump($cache->has('autoload-multiple')); return; @@ -87,18 +105,11 @@ $tester->request(query: 'action=seed')->expectBody( $tester->request(query: 'action=fetch')->expectBody( "string(7) \"DEFAULT\"\n" . "bool(false)\n" . - "string(7) \"DEFAULT\"\n" . - "bool(false)\n" . - "array(3) {\n" . - " [\"multiple\"]=>\n" . - " string(7) \"DEFAULT\"\n" . - " [\"missing\"]=>\n" . - " string(7) \"DEFAULT\"\n" . - " [\"autoload-multiple\"]=>\n" . - " string(7) \"DEFAULT\"\n" . - "}\n" . + "caught: autoload failed\n" . + "bool(true)\n" . + "caught: autoload failed\n" . "bool(false)\n" . - "bool(false)" + "bool(true)" ); $tester->terminate(); diff --git a/ext/user_cache/tests/user_cache_fetch_restore_exception.phpt b/ext/user_cache/tests/user_cache_fetch_restore_exception.phpt new file mode 100644 index 000000000000..5857ff684c39 --- /dev/null +++ b/ext/user_cache/tests/user_cache_fetch_restore_exception.phpt @@ -0,0 +1,77 @@ +--TEST-- +UserCache\Cache: fetch/remember/fetchMultiple propagate restore-hook exceptions and keep the entry +--EXTENSIONS-- +user_cache +--INI-- +user_cache.enable=1 +user_cache.enable_cli=1 +opcache.file_cache_only=0 +user_cache.shm_size=16M +--FILE-- +clear(); + +class RestoreWakeupThrows +{ + public static bool $failNext = false; + public int $value = 0; + + public function __wakeup(): void + { + if (self::$failNext) { + self::$failNext = false; + throw new RuntimeException('wakeup failure'); + } + } +} + +RestoreWakeupThrows::$failNext = false; + +var_dump($cache->store('k', new RestoreWakeupThrows())); + +/* fetch(): a throwing __wakeup propagates like native unserialize() and the + * entry is retained because it is not corrupt. */ +RestoreWakeupThrows::$failNext = true; +try { + $cache->fetch('k', 'DEFAULT'); + echo "no exception\n"; +} catch (RuntimeException $e) { + echo "fetch caught: ", $e->getMessage(), "\n"; +} +var_dump($cache->has('k')); + +/* remember(): the entry exists, so the exception propagates instead of + * recomputing via the callback. */ +RestoreWakeupThrows::$failNext = true; +try { + $cache->remember('k', fn (): string => 'computed'); + echo "no exception\n"; +} catch (RuntimeException $e) { + echo "remember caught: ", $e->getMessage(), "\n"; +} +var_dump($cache->has('k')); + +/* fetchMultiple(): a throwing restore aborts the whole batch. */ +RestoreWakeupThrows::$failNext = true; +try { + $cache->fetchMultiple(['k'], 'DEFAULT'); + echo "no exception\n"; +} catch (RuntimeException $e) { + echo "fetchMultiple caught: ", $e->getMessage(), "\n"; +} +var_dump($cache->has('k')); + +/* Once the hook no longer throws, the retained entry restores normally. */ +$restored = $cache->fetch('k', 'DEFAULT'); +var_dump($restored instanceof RestoreWakeupThrows); +?> +--EXPECT-- +bool(true) +fetch caught: wakeup failure +bool(true) +remember caught: wakeup failure +bool(true) +fetchMultiple caught: wakeup failure +bool(true) +bool(true) diff --git a/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt b/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt index 46eab8d5415d..05c71da30e4f 100644 --- a/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt +++ b/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt @@ -1,5 +1,5 @@ --TEST-- -UserCache\Cache: throwing __unserialize drops the entry and never seeds a request-local slot +UserCache\Cache: throwing __unserialize propagates and keeps the entry --EXTENSIONS-- user_cache --INI-- @@ -38,29 +38,32 @@ class SlotThrowingUnserialize } } +SlotThrowingUnserialize::$unserializeCalls = 0; + var_dump($cache->store('key', new SlotThrowingUnserialize(7))); -/* A failed restore drops the entry without seeding the local slot. */ +/* A throwing restore hook propagates like native unserialize(), and the entry + * is kept because it is not corrupt. No request-local slot is seeded. */ SlotThrowingUnserialize::$failNext = true; -var_dump($cache->fetch('key')); +try { + $cache->fetch('key'); + echo "no exception\n"; +} catch (RuntimeException $e) { + echo "caught: ", $e->getMessage(), "\n"; +} echo "calls after throwing fetch: ", SlotThrowingUnserialize::$unserializeCalls, "\n"; -echo "entry dropped: "; -var_dump($cache->has('key') === false); -var_dump($cache->fetch('key')); +echo "entry kept: "; +var_dump($cache->has('key')); -var_dump($cache->store('key', new SlotThrowingUnserialize(8))); -$cache->fetch('key'); -$cache->fetch('key'); -$cloned = $cache->fetch('key'); -echo "calls after re-store and three fetches: ", SlotThrowingUnserialize::$unserializeCalls, "\n"; -echo "restored value: ", $cloned->value, "\n"; +/* The next fetch no longer throws and restores the value, seeding the slot. */ +$restored = $cache->fetch('key'); +echo "restored value: ", $restored->value, "\n"; +echo "calls after successful fetch: ", SlotThrowingUnserialize::$unserializeCalls, "\n"; ?> --EXPECT-- bool(true) -NULL +caught: unserialize failure calls after throwing fetch: 1 -entry dropped: bool(true) -NULL -bool(true) -calls after re-store and three fetches: 4 -restored value: 8 +entry kept: bool(true) +restored value: 7 +calls after successful fetch: 2 diff --git a/ext/user_cache/user_cache.c b/ext/user_cache/user_cache.c index 8bab95562ad8..1b93a3a7c5ae 100644 --- a/ext/user_cache/user_cache.c +++ b/ext/user_cache/user_cache.c @@ -607,7 +607,7 @@ static bool user_cache_fetch_if_present_api( zval *return_value) { php_user_cache_fetch_pending_seed pending_seed; - bool fetched, stack_overflowed, entry_found = false; + bool fetched, entry_found = false; if (!user_cache_can_read()) { return false; @@ -624,12 +624,13 @@ static bool user_cache_fetch_if_present_api( } if (EG(exception)) { - stack_overflowed = UC_G(stack_overflowed); + /* A stack overflow in the lock-free path is not a real error and would + * recur under the read lock, so surface a miss and drop the exception. */ + if (UC_G(stack_overflowed)) { + UC_G(stack_overflowed) = false; + zend_clear_exception(); - UC_G(stack_overflowed) = false; - zend_clear_exception(); - if (!stack_overflowed) { - user_cache_delete_corrupt_entry_prevalidated(key); + return false; } return false; @@ -667,9 +668,10 @@ static bool user_cache_fetch_if_present_api( } if (EG(exception)) { - zend_clear_exception(); + return false; } + /* Decode failed without an exception: the entry is corrupt, drop it. */ user_cache_delete_corrupt_entry_prevalidated(key); return false; @@ -737,11 +739,14 @@ static void user_cache_fetch_multiple_fetch_one( } ZVAL_COPY(out, default_value); - } else { - if (EG(exception)) { - zend_clear_exception(); - } + } else if (EG(exception)) { + /* A restore hook or autoloader threw: propagate the userland exception + * and keep the entry. Drop the batch lock so the caller can unwind. */ + php_user_cache_unlock(); + *rlock_held = false; + ZVAL_UNDEF(out); + } else { php_user_cache_unlock(); *rlock_held = false; user_cache_delete_corrupt_entry_prevalidated(storage_key); @@ -844,6 +849,14 @@ static zend_result user_cache_fetch_multiple_api( &pending_seeds[i] ); + /* A restore hook or autoloader threw: abort the batch and let the + * userland exception propagate, matching native unserialize(). */ + if (EG(exception)) { + zval_ptr_dtor(&val); + + break; + } + zend_symtable_update(Z_ARRVAL_P(return_value), key, &val); } } zend_catch { @@ -855,6 +868,27 @@ static zend_result user_cache_fetch_multiple_api( php_user_cache_unlock(); } + if (EG(exception)) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + + for (i = 0; i < count; i++) { + zend_string_release(storage_keys[i]); + } + + if (storage_keys != NULL) { + efree(storage_keys); + } + + if (pending_seeds != NULL) { + efree(pending_seeds); + } + + user_cache_release_key_list(prepared_keys, count); + + return FAILURE; + } + /* Clone outside the shared-memory lock. */ user_cache_finish_fetch_multiple(prepared_keys, storage_keys, pending_seeds, count, return_value); @@ -3201,6 +3235,14 @@ ZEND_METHOD(UserCache_Cache, remember) return; } + /* A restore hook or autoloader threw while materializing the entry: the + * entry exists, so propagate the exception instead of recomputing. */ + if (EG(exception)) { + zend_string_release(storage_key); + + RETURN_THROWS(); + } + locked = user_cache_lock_api(storage_key, 0); /* Another request may have populated the entry while the lock was acquired. */ @@ -3215,6 +3257,16 @@ ZEND_METHOD(UserCache_Cache, remember) return; } + if (EG(exception)) { + if (locked) { + locked = user_cache_unlock_api(storage_key); + } + + zend_string_release(storage_key); + + RETURN_THROWS(); + } + /* Do not return before zend_end_try() restores the bailout chain. */ zend_try { callback_failed =