From 0448f0df4492828dff01d7298496d323cf7862fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Wed, 8 Apr 2026 16:29:00 +0200 Subject: [PATCH 1/2] Introduce new time retrieval Zend-API --- UPGRADING.INTERNALS | 1 + Zend/zend_time.c | 55 +++++++++++++++++++++ Zend/zend_time.h | 95 +++++++++++++++++++++++++++++++++++++ configure.ac | 3 ++ win32/build/config.w32 | 2 +- win32/build/config.w32.h.in | 1 + 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 Zend/zend_time.c create mode 100644 Zend/zend_time.h diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 5da8d205be74..c7aeec71b72e 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -88,6 +88,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES ZEND_AST_TRAIT_METHOD_REFERENCE. . The EMPTY_SWITCH_DEFAULT_CASE() macro has been removed. Use default: ZEND_UNREACHABLE(); instead. + . Introduced a new time-retrieval API zend_time_*. ======================== 2. Build system changes diff --git a/Zend/zend_time.c b/Zend/zend_time.c new file mode 100644 index 000000000000..161252d36499 --- /dev/null +++ b/Zend/zend_time.c @@ -0,0 +1,55 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Marc Bennewitz | + +----------------------------------------------------------------------+ +*/ + +#include "zend_time.h" + +/* Current real/wall-time in seconds */ +ZEND_API time_t zend_time_real_sec(void) { + return time(NULL); +} + +ZEND_API void zend_time_real_spec(struct timespec *ts) { +#if defined(HAVE_CLOCK_GETTIME) + + (void) clock_gettime(CLOCK_REALTIME, ts); + +#elif defined(HAVE_TIMESPEC_GET) + + (void) timespec_get(ts, TIME_UTC); + +#elif defined(HAVE_GETTIMEOFDAY) + + struct timeval tv; + (void) gettimeofday(&tv, NULL); + zend_time_val2spec(tv, ts); + +#else + + ts->tv_sec = zend_time_real_get(); + ts->tv_nsec = 0; + +#endif +} + +ZEND_API uint64_t zend_time_mono_fallback_nsec(void) { +#if ZEND_HRTIME_AVAILABLE + return (uint64_t)zend_hrtime(); +#else + struct timespec ts; + zend_time_real_spec(&ts); + return ((uint64_t) ts.tv_sec * ZEND_NANO_IN_SEC) + ts.tv_nsec; +#endif +} diff --git a/Zend/zend_time.h b/Zend/zend_time.h new file mode 100644 index 000000000000..4da6cd9e7e49 --- /dev/null +++ b/Zend/zend_time.h @@ -0,0 +1,95 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Marc Bennewitz | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_TIME_H +#define ZEND_TIME_H + +#include "zend_portability.h" + +#ifdef PHP_WIN32 +# include "win32/time.h" +#endif +#ifdef HAVE_SYS_TIME_H +# include +#endif +#include + +#include "zend_hrtime.h" + +#ifndef PHP_WIN32 +# define tv_sec_t time_t +# define tv_usec_t suseconds_t +#else +# define tv_sec_t long +# define tv_usec_t long +#endif + +#define ZEND_MILLI_IN_SEC 1000U +#define ZEND_MICRO_IN_SEC 1000000U + +BEGIN_EXTERN_C() + +/* Assign seconds to timeval */ +static zend_always_inline void zend_time_sec2val(time_t s, struct timeval *tv) { + tv->tv_sec = (tv_sec_t) s; + tv->tv_usec = 0; +} + +/* Assign microseconds to timeval */ +static zend_always_inline void zend_time_usec2val(int64_t usec, struct timeval *tv) { + tv->tv_sec = (tv_sec_t) (usec / ZEND_MICRO_IN_SEC); + tv->tv_usec = (tv_usec_t) (usec % ZEND_MICRO_IN_SEC); + + if (UNEXPECTED(tv->tv_usec < 0)) { + tv->tv_usec += ZEND_MICRO_IN_SEC; + tv->tv_sec -= 1; + } +} + +/* Assign double (seconds) to timeval */ +static zend_always_inline void zend_time_dbl2val(double s, struct timeval *tv) { + tv->tv_sec = (tv_sec_t) s; + tv->tv_usec = (tv_usec_t) ((s - tv->tv_sec) * ZEND_MICRO_IN_SEC); + + if (UNEXPECTED(tv->tv_usec < 0)) { + tv->tv_usec += ZEND_MICRO_IN_SEC; + tv->tv_sec -= 1; + } else if (UNEXPECTED(tv->tv_usec >= ZEND_MICRO_IN_SEC)) { + // rare, but protects against rounding up to exactly 1 second + tv->tv_usec -= ZEND_MICRO_IN_SEC; + tv->tv_sec += 1; + } +} + +/* Assign timeval to timespec */ +static zend_always_inline void zend_time_val2spec(struct timeval tv, struct timespec *ts) { + ts->tv_sec = (time_t) tv.tv_sec; + ts->tv_nsec = (long) (tv.tv_usec * 1000); +} + +/* Current real/wall-time in seconds */ +ZEND_API time_t zend_time_real_sec(void); + +/* Current real/wall-time in up-to nano seconds */ +ZEND_API void zend_time_real_spec(struct timespec *ts); + +/* Monotonic time in nanoseconds with a fallback to real/wall-time + if no monotonic timer is available */ +ZEND_API uint64_t zend_time_mono_fallback_nsec(void); + +END_EXTERN_C() + +#endif // ZEND_TIME_H diff --git a/configure.ac b/configure.ac index 214c0ab91b2a..0cf89e667b97 100644 --- a/configure.ac +++ b/configure.ac @@ -547,6 +547,7 @@ AC_CHECK_FUNCS(m4_normalize([ asctime_r asprintf chroot + clock_gettime ctime_r explicit_memset fdatasync @@ -598,6 +599,7 @@ AC_CHECK_FUNCS(m4_normalize([ strptime strtok_r symlink + timespec_get tzset unsetenv usleep @@ -1754,6 +1756,7 @@ PHP_ADD_SOURCES([Zend], m4_normalize([ zend_generators.c zend_hash.c zend_highlight.c + zend_time.c zend_hrtime.c zend_inheritance.c zend_ini_parser.c diff --git a/win32/build/config.w32 b/win32/build/config.w32 index aefcfb5f8247..33acc3eea7bf 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -240,7 +240,7 @@ ADD_SOURCES("Zend", "zend_language_parser.c zend_language_scanner.c \ zend_default_classes.c zend_execute.c zend_strtod.c zend_gc.c zend_closures.c zend_weakrefs.c \ zend_float.c zend_string.c zend_generators.c zend_virtual_cwd.c zend_ast.c \ zend_inheritance.c zend_smart_str.c zend_cpuinfo.c zend_observer.c zend_system_id.c \ - zend_enum.c zend_fibers.c zend_atomic.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c \ + zend_enum.c zend_fibers.c zend_atomic.c zend_time.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c \ zend_lazy_objects.c zend_autoload.c"); ADD_SOURCES("Zend\\Optimizer", "zend_optimizer.c pass1.c pass3.c optimize_func_calls.c block_pass.c optimize_temp_vars_5.c nop_removal.c compact_literals.c zend_cfg.c zend_dfg.c dfa_pass.c zend_ssa.c zend_inference.c zend_func_info.c zend_call_graph.c zend_dump.c escape_analysis.c compact_vars.c dce.c sccp.c scdf.c"); diff --git a/win32/build/config.w32.h.in b/win32/build/config.w32.h.in index 7b1bb4d932d3..fff0206db1c0 100644 --- a/win32/build/config.w32.h.in +++ b/win32/build/config.w32.h.in @@ -48,6 +48,7 @@ #undef HAVE_SETITIMER #undef HAVE_IODBC #define HAVE_LIBDL 1 +#define HAVE_TIMESPEC_GET 1 #define HAVE_GETTIMEOFDAY 1 #define HAVE_PUTENV 1 #define HAVE_TZSET 1 From 866da2fa5ebf016a33fface977e8178bc8f028f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Thu, 9 Apr 2026 22:56:55 +0200 Subject: [PATCH 2/2] Add UUID extension --- EXTENSIONS | 6 + ext/standard/credits_ext.h | 1 + ext/uuid/CREDITS | 2 + ext/uuid/benchmark/7_php_uuid.expectation | 7 + ext/uuid/benchmark/7_php_uuid.ini | 8 + ext/uuid/benchmark/7_php_uuid.php | 7 + ext/uuid/benchmark/8_symfony_uuid.expectation | 7 + ext/uuid/benchmark/8_symfony_uuid.ini | 8 + ext/uuid/benchmark/8_symfony_uuid.php | 9 + ext/uuid/benchmark/8_symfony_uuid_install.sh | 22 + ext/uuid/benchmark/9_ramsey_uuid.expectation | 9 + ext/uuid/benchmark/9_ramsey_uuid.ini | 8 + ext/uuid/benchmark/9_ramsey_uuid.php | 9 + ext/uuid/benchmark/9_ramsey_uuid_install.sh | 22 + ext/uuid/config.m4 | 11 + ext/uuid/config.w32 | 7 + ext/uuid/php_uuid.c | 385 ++++++++++++++++++ ext/uuid/php_uuid.h | 31 ++ ext/uuid/php_uuid.stub.php | 26 ++ ext/uuid/php_uuid_arginfo.h | 65 +++ ext/uuid/tests/v7_generate_basic.phpt | 18 + ext/uuid/tests/v7_generate_seed.phpt | 15 + ext/uuid/tests/v7_parse.phpt | 15 + ext/uuid/tests/v7_to_string.phpt | 12 + ext/uuid/uuidv7-h/uuidv7.h | 307 ++++++++++++++ 25 files changed, 1017 insertions(+) create mode 100644 ext/uuid/CREDITS create mode 100755 ext/uuid/benchmark/7_php_uuid.expectation create mode 100755 ext/uuid/benchmark/7_php_uuid.ini create mode 100644 ext/uuid/benchmark/7_php_uuid.php create mode 100755 ext/uuid/benchmark/8_symfony_uuid.expectation create mode 100755 ext/uuid/benchmark/8_symfony_uuid.ini create mode 100644 ext/uuid/benchmark/8_symfony_uuid.php create mode 100755 ext/uuid/benchmark/8_symfony_uuid_install.sh create mode 100755 ext/uuid/benchmark/9_ramsey_uuid.expectation create mode 100755 ext/uuid/benchmark/9_ramsey_uuid.ini create mode 100644 ext/uuid/benchmark/9_ramsey_uuid.php create mode 100755 ext/uuid/benchmark/9_ramsey_uuid_install.sh create mode 100644 ext/uuid/config.m4 create mode 100644 ext/uuid/config.w32 create mode 100644 ext/uuid/php_uuid.c create mode 100644 ext/uuid/php_uuid.h create mode 100644 ext/uuid/php_uuid.stub.php create mode 100644 ext/uuid/php_uuid_arginfo.h create mode 100644 ext/uuid/tests/v7_generate_basic.phpt create mode 100644 ext/uuid/tests/v7_generate_seed.phpt create mode 100644 ext/uuid/tests/v7_parse.phpt create mode 100644 ext/uuid/tests/v7_to_string.phpt create mode 100644 ext/uuid/uuidv7-h/uuidv7.h diff --git a/EXTENSIONS b/EXTENSIONS index 8da09aed5392..04482a269c6f 100644 --- a/EXTENSIONS +++ b/EXTENSIONS @@ -512,6 +512,12 @@ MAINTENANCE: Maintained STATUS: Working SINCE: 8.5.0 ------------------------------------------------------------------------------- +EXTENSION: uuid +PRIMARY MAINTAINER Máté Kocsis (2026 - 2026) +MAINTENANCE: Maintained +STATUS: Working +SINCE: 8.6.0 +------------------------------------------------------------------------------- EXTENSION: zip PRIMARY MAINTAINER: Pierre-Alain Joye (2006 - 2011) Remi Collet (2013-2020) diff --git a/ext/standard/credits_ext.h b/ext/standard/credits_ext.h index 509135a27e3e..dcf8c825f495 100644 --- a/ext/standard/credits_ext.h +++ b/ext/standard/credits_ext.h @@ -70,6 +70,7 @@ CREDIT_LINE("System V Shared Memory", "Christian Cartus"); CREDIT_LINE("tidy", "John Coggeshall, Ilia Alshanetsky"); CREDIT_LINE("tokenizer", "Andrei Zmievski, Johannes Schlueter"); CREDIT_LINE("uri", "Máté Kocsis, Tim Düsterhus, Ignace Nyamagana Butera, Arnaud Le Blanc, Dennis Snell, Niels Dossche, Nicolas Grekas"); +CREDIT_LINE("uuid", "Máté Kocsis"); CREDIT_LINE("XML", "Stig Bakken, Thies C. Arntzen, Sterling Hughes"); CREDIT_LINE("XMLReader", "Rob Richards"); CREDIT_LINE("XMLWriter", "Rob Richards, Pierre-Alain Joye"); diff --git a/ext/uuid/CREDITS b/ext/uuid/CREDITS new file mode 100644 index 000000000000..d36351d4230f --- /dev/null +++ b/ext/uuid/CREDITS @@ -0,0 +1,2 @@ +uuid +Máté Kocsis diff --git a/ext/uuid/benchmark/7_php_uuid.expectation b/ext/uuid/benchmark/7_php_uuid.expectation new file mode 100755 index 000000000000..8f2c4a9c3e67 --- /dev/null +++ b/ext/uuid/benchmark/7_php_uuid.expectation @@ -0,0 +1,7 @@ +X-Powered-By: PHP/%v +Content-type: text/html; charset=UTF-8 + +object(Uuid\UuidV7)#%d (%d) { + ["uuid"]=> + string(36) "%s-%s-%s-%s-%s" +} diff --git a/ext/uuid/benchmark/7_php_uuid.ini b/ext/uuid/benchmark/7_php_uuid.ini new file mode 100755 index 000000000000..d5e75f5649ae --- /dev/null +++ b/ext/uuid/benchmark/7_php_uuid.ini @@ -0,0 +1,8 @@ +TEST_NAME="PHP UUID" +TEST_ID=php_uuid +TEST_WARMUP=20 +TEST_ITERATIONS=50 +TEST_REQUESTS=10 + +TEST_TYPE=micro +TEST_FILE=config/test/7_php_uuid.php diff --git a/ext/uuid/benchmark/7_php_uuid.php b/ext/uuid/benchmark/7_php_uuid.php new file mode 100644 index 000000000000..e9e98f2c13fb --- /dev/null +++ b/ext/uuid/benchmark/7_php_uuid.php @@ -0,0 +1,7 @@ + + string(36) "%s-%s-%s-%s-%s" +} diff --git a/ext/uuid/benchmark/8_symfony_uuid.ini b/ext/uuid/benchmark/8_symfony_uuid.ini new file mode 100755 index 000000000000..04e6d037abc4 --- /dev/null +++ b/ext/uuid/benchmark/8_symfony_uuid.ini @@ -0,0 +1,8 @@ +TEST_NAME="Symfony UUID" +TEST_ID=symfony_uuid +TEST_WARMUP=20 +TEST_ITERATIONS=50 +TEST_REQUESTS=10 + +TEST_TYPE=micro +TEST_FILE=config/test/8_symfony_uuid.php diff --git a/ext/uuid/benchmark/8_symfony_uuid.php b/ext/uuid/benchmark/8_symfony_uuid.php new file mode 100644 index 000000000000..2e4873c1af70 --- /dev/null +++ b/ext/uuid/benchmark/8_symfony_uuid.php @@ -0,0 +1,9 @@ + + NULL + ["uuid":"Ramsey\Uuid\Lazy\LazyUuidFromString":private]=> + string(36) "%s-%s-%s-%s-%s" +} diff --git a/ext/uuid/benchmark/9_ramsey_uuid.ini b/ext/uuid/benchmark/9_ramsey_uuid.ini new file mode 100755 index 000000000000..572ec6bf01cb --- /dev/null +++ b/ext/uuid/benchmark/9_ramsey_uuid.ini @@ -0,0 +1,8 @@ +TEST_NAME="Ramsey UUID" +TEST_ID=ramsey_uuid +TEST_WARMUP=20 +TEST_ITERATIONS=50 +TEST_REQUESTS=10 + +TEST_TYPE=micro +TEST_FILE=config/test/9_ramsey_uuid.php diff --git a/ext/uuid/benchmark/9_ramsey_uuid.php b/ext/uuid/benchmark/9_ramsey_uuid.php new file mode 100644 index 000000000000..4c372187a56c --- /dev/null +++ b/ext/uuid/benchmark/9_ramsey_uuid.php @@ -0,0 +1,9 @@ + | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "php.h" +#include "Zend/zend_API.h" +#include "Zend/zend_exceptions.h" +#include "Zend/zend_interfaces.h" +#include "Zend/zend_objects.h" +#include "Zend/zend_time.h" +#include "ext/date/php_date.h" +#include "ext/random/php_random.h" +#include "ext/standard/info.h" + +#include "php_uuid.h" +#include "php_uuid_arginfo.h" +#include "uuidv7-h/uuidv7.h" + +#define PHP_UUID_SERIALIZE_UUID_FIELD_NAME "uuid" + +zend_class_entry *php_uuid_ce_uuidv7; + +static zend_object_handlers object_handlers_uuidv7; + +static const zend_module_dep uuid_deps[] = { + ZEND_MOD_REQUIRED("date") + ZEND_MOD_REQUIRED("random") + ZEND_MOD_END +}; + +static inline php_uuid_v7_object *php_uuid_v7_object_from_obj(zend_object *object) { + return (php_uuid_v7_object*)((char*)(object) - XtOffsetOf(php_uuid_v7_object, std)); +} + +#define Z_UUID_V7_OBJECT_P(zv) php_uuid_v7_object_from_obj(Z_OBJ_P((zv))) + +ZEND_ATTRIBUTE_NONNULL_ARGS(1) PHPAPI zend_result php_uuid_v7_parse(const zend_string *uuid_str, php_uuid_v7 uuid) +{ + int result = uuidv7_from_string(ZSTR_VAL(uuid_str), uuid); + + return result == 0 ? SUCCESS : FAILURE; +} + +ZEND_ATTRIBUTE_NONNULL PHPAPI zend_string *php_uuid_v7_to_string(const uint8_t *uuid) +{ + zend_string *uuid_string = zend_string_alloc(36, false); + + uuidv7_to_string(uuid, ZSTR_VAL(uuid_string)); + + return uuid_string; +} + +ZEND_ATTRIBUTE_NONNULL static HashTable *uuid_get_debug_properties(php_uuid_v7_object *uuid_object) +{ + const HashTable *std_properties = zend_std_get_properties(&uuid_object->std); + HashTable *result = zend_array_dup(std_properties); + + zval tmp; + ZVAL_STR_COPY(&tmp, php_uuid_v7_to_string(uuid_object->uuid)); + zend_hash_str_add(result, "uuid", sizeof("uuid") - 1, &tmp); + zval_ptr_dtor(&tmp); + + return result; +} + +PHP_METHOD(Uuid_UuidV7, parse) +{ + zend_string *uuid_str; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(uuid_str) + ZEND_PARSE_PARAMETERS_END(); + + object_init_ex(return_value, Z_CE_P(ZEND_THIS)); + php_uuid_v7_object *uuid_object = Z_UUID_V7_OBJECT_P(return_value); + + if (uuidv7_from_string(ZSTR_VAL(uuid_str), uuid_object->uuid) == FAILURE) { + zend_throw_exception(NULL, "The specified UUID v7 is malformed", 0); + RETURN_THROWS(); + } + + uuid_object->is_initialized = false; +} + +PHP_METHOD(Uuid_UuidV7, generate) +{ + zend_object *datetime_object = NULL, *random_engine_object = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_OBJ_OF_CLASS_OR_NULL(datetime_object, php_date_get_immutable_ce()) + Z_PARAM_OBJ_OF_CLASS_OR_NULL(random_engine_object, random_ce_Random_Engine) + ZEND_PARSE_PARAMETERS_END(); + + object_init_ex(return_value, Z_CE_P(ZEND_THIS)); + php_uuid_v7_object *uuid_object = Z_UUID_V7_OBJECT_P(return_value); + + uint64_t unix_time_ms; + if (datetime_object == NULL) { + unix_time_ms = zend_time_mono_fallback_nsec() / 1000000; + } else { + php_date_obj *datetime = php_date_obj_from_obj(datetime_object); + if (!datetime->time) { + zend_argument_error(NULL, 1, "is an unconstructed object"); + RETURN_THROWS(); + } + + zval zv_timestamp; + zend_call_method_with_0_params(datetime_object, php_date_get_immutable_ce(), NULL, "gettimestamp", &zv_timestamp); + if (Z_TYPE(zv_timestamp) != IS_LONG) { + zend_throw_error(NULL, "Call to DateTimeImmutable::getTimestamp() failed"); + zval_ptr_dtor(&zv_timestamp); + RETURN_THROWS(); + } + + unix_time_ms = (uint64_t) Z_LVAL(zv_timestamp) * 1000; + } + + php_random_algo_with_state random_algo; + if (random_engine_object == NULL) { + random_algo = php_random_default_engine(); + } else { + php_random_engine *random_engine = php_random_engine_from_obj(random_engine_object); + random_algo = random_engine->engine; + } + + uint8_t random_bytes[10]; + for (int i = 0; i < 10; i++) { + random_bytes[i] = php_random_range(random_algo, 0, 127); + } + + int8_t result = uuidv7_generate(uuid_object->uuid, unix_time_ms, random_bytes, NULL); + switch (result) { + case UUIDV7_STATUS_UNPRECEDENTED: + ZEND_FALLTHROUGH; + case UUIDV7_STATUS_NEW_TIMESTAMP: + ZEND_FALLTHROUGH; + case UUIDV7_STATUS_COUNTER_INC: + ZEND_FALLTHROUGH; + case UUIDV7_STATUS_TIMESTAMP_INC: + ZEND_FALLTHROUGH; + case UUIDV7_STATUS_CLOCK_ROLLBACK: + ZEND_FALLTHROUGH; + case UUIDV7_STATUS_ERR_TIMESTAMP: + ZEND_FALLTHROUGH; + case UUIDV7_STATUS_ERR_TIMESTAMP_OVERFLOW: + break; + default: ZEND_UNREACHABLE(); + } + + uuid_object->is_initialized = true; +} + +PHP_METHOD(Uuid_UuidV7, __construct) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + zend_throw_error(NULL, "Cannot directly construct %s, use the static factory methods instead", ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name)); +} + +PHP_METHOD(Uuid_UuidV7, equals) +{ + zend_object *that_object; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJ_OF_CLASS(that_object, php_uuid_ce_uuidv7) + ZEND_PARSE_PARAMETERS_END(); + + php_uuid_v7_object *this_uuid_object = Z_UUID_V7_OBJECT_P(ZEND_THIS); + php_uuid_v7_object *that_uuid_object = php_uuid_v7_object_from_obj(that_object); + ZEND_ASSERT(this_uuid_object->uuid != NULL); + ZEND_ASSERT(that_uuid_object->uuid != NULL); + + if (this_uuid_object->std.ce != that_uuid_object->std.ce && + !instanceof_function(this_uuid_object->std.ce, that_uuid_object->std.ce) && + !instanceof_function(that_uuid_object->std.ce, this_uuid_object->std.ce) + ) { + RETURN_FALSE; + } + + for (int i = 0; i < 16; i++) { + if (this_uuid_object->uuid[i] != that_uuid_object->uuid[i]) { + RETURN_FALSE; + } + } + + RETURN_TRUE; +} + +PHP_METHOD(Uuid_UuidV7, toBytes) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_uuid_v7_object *uuid_object = Z_UUID_V7_OBJECT_P(ZEND_THIS); + + RETURN_STR(php_uuid_v7_to_string(uuid_object->uuid)); +} + +PHP_METHOD(Uuid_UuidV7, toString) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_uuid_v7_object *uuid_object = Z_UUID_V7_OBJECT_P(ZEND_THIS); + + RETURN_STR(php_uuid_v7_to_string(uuid_object->uuid)); +} + +PHP_METHOD(Uuid_UuidV7, __serialize) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_uuid_v7_object *uuid_object = Z_UUID_V7_OBJECT_P(ZEND_THIS); + ZEND_ASSERT(uuid_object->uuid != NULL); + + /* Serialize state: "uuid" key in the first array */ + zval tmp; + ZVAL_STR(&tmp, php_uuid_v7_to_string(uuid_object->uuid)); + + array_init(return_value); + + zval arr; + array_init(&arr); + zend_hash_str_add_new(Z_ARRVAL(arr), PHP_UUID_SERIALIZE_UUID_FIELD_NAME, sizeof(PHP_UUID_SERIALIZE_UUID_FIELD_NAME) - 1, &tmp); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &arr); + + /* Serialize regular properties: second array */ + ZVAL_EMPTY_ARRAY(&arr); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &arr); +} + +PHP_METHOD(Uuid_UuidV7, __unserialize) +{ + HashTable *data; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(data) + ZEND_PARSE_PARAMETERS_END(); + + php_uuid_v7_object *uuid_object = php_uuid_v7_object_from_obj(Z_OBJ_P(ZEND_THIS)); + if (uuid_object->is_initialized) { + /* Intentionally throw two exceptions for proper chaining. */ + zend_throw_error(NULL, "Cannot modify readonly object of class %s", ZSTR_VAL(uuid_object->std.ce->name)); + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } + + /* Verify the expected number of elements, this implicitly ensures that no additional elements are present. */ + if (zend_hash_num_elements(data) != 2) { + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } + + /* Unserialize state: "uuid" key in the first array */ + zval *arr = zend_hash_index_find(data, 0); + if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) { + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } + + /* Verify the expected number of elements inside the first array, this implicitly ensures that no additional elements are present. */ + if (zend_hash_num_elements(Z_ARRVAL_P(arr)) != 1) { + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } + + zval *uuid_zv = zend_hash_str_find(Z_ARRVAL_P(arr), ZEND_STRL(PHP_UUID_SERIALIZE_UUID_FIELD_NAME)); + if (uuid_zv == NULL || Z_TYPE_P(uuid_zv) != IS_STRING) { + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } + + if (php_uuid_v7_parse(Z_STR_P(uuid_zv), uuid_object->uuid) == FAILURE) { + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } + + /* Unserialize regular properties: second array */ + arr = zend_hash_index_find(data, 1); + if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) { + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } + + /* Verify that there is no regular property in the second array, because the UUID classes have no properties and they are final. */ + if (zend_hash_num_elements(Z_ARRVAL_P(arr)) > 0) { + zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(uuid_object->std.ce->name)); + RETURN_THROWS(); + } +} + +PHP_METHOD(Uuid_UuidV7, __debugInfo) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_uuid_v7_object *uuid_object = Z_UUID_V7_OBJECT_P(ZEND_THIS); + + RETURN_ARR(uuid_get_debug_properties(uuid_object)); +} + +ZEND_ATTRIBUTE_NONNULL zend_object *php_uuid_v7_object_create_uuid_v7(zend_class_entry *class_type) +{ + php_uuid_v7_object *uuid_object = zend_object_alloc(sizeof(*uuid_object), class_type); + + zend_object_std_init(&uuid_object->std, class_type); + object_properties_init(&uuid_object->std, class_type); + + uuid_object->is_initialized = false; + + return &uuid_object->std; +} + +ZEND_ATTRIBUTE_NONNULL void php_uuid_v7_object_handler_free(zend_object *object) +{ + php_uuid_v7_object *uuid_object = php_uuid_v7_object_from_obj(object); + + uuid_object->is_initialized = false; + + zend_object_std_dtor(&uuid_object->std); +} + +ZEND_ATTRIBUTE_NONNULL zend_object *php_uuid_v7_object_handler_clone(zend_object *object) +{ + php_uuid_v7_object *uuid_object = php_uuid_v7_object_from_obj(object); + + ZEND_ASSERT(uuid_object->uuid != NULL); + + php_uuid_v7_object *new_uuid_object = php_uuid_v7_object_from_obj(object->ce->create_object(object->ce)); + + memcpy(new_uuid_object->uuid, uuid_object->uuid, sizeof(php_uuid_v7)); + zend_objects_clone_members(&new_uuid_object->std, &uuid_object->std); + + return &new_uuid_object->std; +} + +static PHP_MINIT_FUNCTION(uuid) +{ + php_uuid_ce_uuidv7 = register_class_Uuid_UuidV7(); + php_uuid_ce_uuidv7->create_object = php_uuid_v7_object_create_uuid_v7; + php_uuid_ce_uuidv7->default_object_handlers = &object_handlers_uuidv7; + memcpy(&object_handlers_uuidv7, zend_get_std_object_handlers(), sizeof(zend_object_handlers)); + object_handlers_uuidv7.offset = XtOffsetOf(php_uuid_v7_object, std); + object_handlers_uuidv7.free_obj = php_uuid_v7_object_handler_free; + object_handlers_uuidv7.clone_obj = php_uuid_v7_object_handler_clone; + + return SUCCESS; +} + +static PHP_MINFO_FUNCTION(uuid) +{ + php_info_print_table_start(); + php_info_print_table_row(2, "UUID support", "active"); + php_info_print_table_end(); +} + +zend_module_entry uuid_module_entry = { + STANDARD_MODULE_HEADER_EX, NULL, + uuid_deps, + "uuid", /* Extension name */ + NULL, /* zend_function_entry */ + PHP_MINIT(uuid), /* PHP_MINIT - Module initialization */ + NULL, /* PHP_MSHUTDOWN - Module shutdown */ + NULL, /* PHP_RINIT - Request initialization */ + NULL, /* PHP_RSHUTDOWN - Request shutdown */ + PHP_MINFO(uuid), /* PHP_MINFO - Module info */ + PHP_VERSION, /* Version */ + NO_MODULE_GLOBALS, + NULL, + STANDARD_MODULE_PROPERTIES_EX +}; diff --git a/ext/uuid/php_uuid.h b/ext/uuid/php_uuid.h new file mode 100644 index 000000000000..2d879dd59672 --- /dev/null +++ b/ext/uuid/php_uuid.h @@ -0,0 +1,31 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Máté Kocsis | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_UUID_H +#define PHP_UUID_H + +extern zend_module_entry uuid_module_entry; +#define phpext_uuid_ptr &uuid_module_entry + +typedef uint8_t php_uuid_v7[16]; + +typedef struct php_uuid_v7_object { + php_uuid_v7 uuid; + bool is_initialized; + zend_object std; +} php_uuid_v7_object; + +#endif diff --git a/ext/uuid/php_uuid.stub.php b/ext/uuid/php_uuid.stub.php new file mode 100644 index 000000000000..0fab56a340a6 --- /dev/null +++ b/ext/uuid/php_uuid.stub.php @@ -0,0 +1,26 @@ + +--EXPECTF-- +object(Uuid\UuidV7)#%s (%s) { + ["uuid"]=> + string(36) "019b76da-a800-7819-8e76-39652553744d" +} diff --git a/ext/uuid/tests/v7_generate_seed.phpt b/ext/uuid/tests/v7_generate_seed.phpt new file mode 100644 index 000000000000..08955496ac1b --- /dev/null +++ b/ext/uuid/tests/v7_generate_seed.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generate UUIDv7 +--FILE-- + +--EXPECTF-- +object(Uuid\UuidV7)#%s (%s) { + ["uuid"]=> + string(36) "%s-%s-%s-%s-%s" +} diff --git a/ext/uuid/tests/v7_parse.phpt b/ext/uuid/tests/v7_parse.phpt new file mode 100644 index 000000000000..86ce5c518075 --- /dev/null +++ b/ext/uuid/tests/v7_parse.phpt @@ -0,0 +1,15 @@ +--TEST-- +Parse UUID v7 string +--FILE-- + +--EXPECTF-- +object(Uuid\UuidV7)#%s (%s) { + ["uuid"]=> + string(36) "00000bff-896a-782f-a96a-1b5f4f502630" +} diff --git a/ext/uuid/tests/v7_to_string.phpt b/ext/uuid/tests/v7_to_string.phpt new file mode 100644 index 000000000000..6859cbd6a1de --- /dev/null +++ b/ext/uuid/tests/v7_to_string.phpt @@ -0,0 +1,12 @@ +--TEST-- +Convert a UUID v7 to a string +--FILE-- +toString()); + +?> +--EXPECT-- +string(36) "00000bff-896a-782f-a96a-1b5f4f502630" diff --git a/ext/uuid/uuidv7-h/uuidv7.h b/ext/uuid/uuidv7-h/uuidv7.h new file mode 100644 index 000000000000..07e1772d0a87 --- /dev/null +++ b/ext/uuid/uuidv7-h/uuidv7.h @@ -0,0 +1,307 @@ +/** + * @file + * + * uuidv7.h - Single-file C/C++ UUIDv7 Library + * + * @version v0.1.6 + * @author LiosK + * @copyright Licensed under the Apache License, Version 2.0 + * @see https://github.com/LiosK/uuidv7-h + */ +/* + * Copyright 2022 LiosK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef UUIDV7_H_BAEDKYFQ +#define UUIDV7_H_BAEDKYFQ + +#include +#include + +/** + * @name Status codes returned by uuidv7_generate() + * + * @{ + */ + +/** + * Indicates that the `unix_ts_ms` passed was used because no preceding UUID was + * specified. + */ +#define UUIDV7_STATUS_UNPRECEDENTED (0) + +/** + * Indicates that the `unix_ts_ms` passed was used because it was greater than + * the previous one. + */ +#define UUIDV7_STATUS_NEW_TIMESTAMP (1) + +/** + * Indicates that the counter was incremented because the `unix_ts_ms` passed + * was no greater than the previous one. + */ +#define UUIDV7_STATUS_COUNTER_INC (2) + +/** + * Indicates that the previous `unix_ts_ms` was incremented because the counter + * reached its maximum value. + */ +#define UUIDV7_STATUS_TIMESTAMP_INC (3) + +/** + * Indicates that the monotonic order of generated UUIDs was broken because the + * `unix_ts_ms` passed was less than the previous one by more than ten seconds. + */ +#define UUIDV7_STATUS_CLOCK_ROLLBACK (4) + +/** Indicates that an invalid `unix_ts_ms` is passed. */ +#define UUIDV7_STATUS_ERR_TIMESTAMP (-1) + +/** + * Indicates that the attempt to increment the previous `unix_ts_ms` failed + * because it had reached its maximum value. + */ +#define UUIDV7_STATUS_ERR_TIMESTAMP_OVERFLOW (-2) + +/** @} */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Low-level primitives + * + * @{ + */ + +/** + * Generates a new UUIDv7 from the given Unix time, random bytes, and previous + * UUID. + * + * @param uuid_out 16-byte byte array where the generated UUID is stored. + * @param unix_ts_ms Current Unix time in milliseconds. + * @param rand_bytes At least 10-byte byte array filled with random bytes. This + * function consumes the leading 4 bytes or the whole 10 + * bytes per call depending on the conditions. + * `uuidv7_status_n_rand_consumed()` maps the return value of + * this function to the number of random bytes consumed. + * @param uuid_prev 16-byte byte array representing the immediately preceding + * UUID, from which the previous timestamp and counter are + * extracted. This may be NULL if the caller does not care + * the ascending order of UUIDs within the same timestamp. + * This may point to the same location as `uuid_out`; this + * function reads the value before writing. + * @return One of the `UUIDV7_STATUS_*` codes that describe the + * characteristics of generated UUIDs. Callers can usually + * ignore the status unless they need to guarantee the + * monotonic order of UUIDs or fine-tune the generation + * process. + */ +static inline int8_t uuidv7_generate(uint8_t *uuid_out, uint64_t unix_ts_ms, + const uint8_t *rand_bytes, + const uint8_t *uuid_prev) { + static const uint64_t MAX_TIMESTAMP = ((uint64_t)1 << 48) - 1; + static const uint64_t MAX_COUNTER = ((uint64_t)1 << 42) - 1; + + if (unix_ts_ms > MAX_TIMESTAMP) { + return UUIDV7_STATUS_ERR_TIMESTAMP; + } + + int8_t status; + uint64_t timestamp = 0; + if (uuid_prev == NULL) { + status = UUIDV7_STATUS_UNPRECEDENTED; + timestamp = unix_ts_ms; + } else { + for (int i = 0; i < 6; i++) { + timestamp = (timestamp << 8) | uuid_prev[i]; + } + + if (unix_ts_ms > timestamp) { + status = UUIDV7_STATUS_NEW_TIMESTAMP; + timestamp = unix_ts_ms; + } else if (unix_ts_ms + 10000 < timestamp) { + // ignore prev if clock moves back by more than ten seconds + status = UUIDV7_STATUS_CLOCK_ROLLBACK; + timestamp = unix_ts_ms; + } else { + // increment prev counter + uint64_t counter = uuid_prev[6] & 0x0f; // skip ver + counter = (counter << 8) | uuid_prev[7]; + counter = (counter << 6) | (uuid_prev[8] & 0x3f); // skip var + counter = (counter << 8) | uuid_prev[9]; + counter = (counter << 8) | uuid_prev[10]; + counter = (counter << 8) | uuid_prev[11]; + + if (counter++ < MAX_COUNTER) { + status = UUIDV7_STATUS_COUNTER_INC; + uuid_out[6] = counter >> 38; // ver + bits 0-3 + uuid_out[7] = counter >> 30; // bits 4-11 + uuid_out[8] = counter >> 24; // var + bits 12-17 + uuid_out[9] = counter >> 16; // bits 18-25 + uuid_out[10] = counter >> 8; // bits 26-33 + uuid_out[11] = counter; // bits 34-41 + } else { + // increment prev timestamp at counter overflow + status = UUIDV7_STATUS_TIMESTAMP_INC; + timestamp++; + if (timestamp > MAX_TIMESTAMP) { + return UUIDV7_STATUS_ERR_TIMESTAMP_OVERFLOW; + } + } + } + } + + uuid_out[0] = timestamp >> 40; + uuid_out[1] = timestamp >> 32; + uuid_out[2] = timestamp >> 24; + uuid_out[3] = timestamp >> 16; + uuid_out[4] = timestamp >> 8; + uuid_out[5] = timestamp; + + for (int i = (status == UUIDV7_STATUS_COUNTER_INC) ? 12 : 6; i < 16; i++) { + uuid_out[i] = *rand_bytes++; + } + + uuid_out[6] = 0x70 | (uuid_out[6] & 0x0f); // set ver + uuid_out[8] = 0x80 | (uuid_out[8] & 0x3f); // set var + + return status; +} + +/** + * Determines the number of random bytes consumsed by `uuidv7_generate()` from + * the `UUIDV7_STATUS_*` code returned. + * + * @param status `UUIDV7_STATUS_*` code returned by `uuidv7_generate()`. + * @return `4` if `status` is `UUIDV7_STATUS_COUNTER_INC` or `10` + * otherwise. + */ +static inline int uuidv7_status_n_rand_consumed(int8_t status) { + return status == UUIDV7_STATUS_COUNTER_INC ? 4 : 10; +} + +/** + * Encodes a UUID in the 8-4-4-4-12 hexadecimal string representation. + * + * @param uuid 16-byte byte array representing the UUID to encode. + * @param string_out Character array where the encoded string is stored. Its + * length must be 37 (36 digits + NUL) or longer. + */ +static inline void uuidv7_to_string(const uint8_t *uuid, char *string_out) { + static const char DIGITS[] = "0123456789abcdef"; + for (int i = 0; i < 16; i++) { + uint_fast8_t e = uuid[i]; + *string_out++ = DIGITS[e >> 4]; + *string_out++ = DIGITS[e & 15]; + if (i == 3 || i == 5 || i == 7 || i == 9) { + *string_out++ = '-'; + } + } + *string_out = '\0'; +} + +/** + * Decodes the 8-4-4-4-12 hexadecimal string representation of a UUID. + * + * @param string 37-byte (36 digits + NUL) character array representing the + * 8-4-4-4-12 hexadecimal string representation. + * @param uuid_out 16-byte byte array where the decoded UUID is stored. + * @return Zero on success or non-zero integer on failure. + */ +static inline int uuidv7_from_string(const char *string, uint8_t *uuid_out) { + for (int i = 0; i < 32; i++) { + char c = *string++; + // clang-format off + uint8_t x = c == '0' ? 0 : c == '1' ? 1 : c == '2' ? 2 : c == '3' ? 3 + : c == '4' ? 4 : c == '5' ? 5 : c == '6' ? 6 : c == '7' ? 7 + : c == '8' ? 8 : c == '9' ? 9 : c == 'a' ? 10 : c == 'b' ? 11 + : c == 'c' ? 12 : c == 'd' ? 13 : c == 'e' ? 14 : c == 'f' ? 15 + : c == 'A' ? 10 : c == 'B' ? 11 : c == 'C' ? 12 : c == 'D' ? 13 + : c == 'E' ? 14 : c == 'F' ? 15 : 0xff; + // clang-format on + if (x == 0xff) { + return -1; // invalid digit + } + + if ((i & 1) == 0) { + uuid_out[i >> 1] = x << 4; // even i => hi 4 bits + } else { + uuid_out[i >> 1] |= x; // odd i => lo 4 bits + } + + if ((i == 7 || i == 11 || i == 15 || i == 19) && (*string++ != '-')) { + return -1; // invalid format + } + } + if (*string != '\0') { + return -1; // invalid length + } + return 0; // success +} + +/** @} */ + +/** + * @name High-level APIs that require platform integration + * + * @{ + */ + +/** + * Generates a new UUIDv7 with the current Unix time. + * + * This declaration defines the interface to generate a new UUIDv7 with the + * current time, default random number generator, and global shared state + * holding the previously generated UUID. Since this single-file library does + * not provide platform-specific implementations, users need to prepare a + * concrete implementation (if necessary) by integrating a real-time clock, + * cryptographically strong random number generator, and shared state storage + * available in the target platform. + * + * @param uuid_out 16-byte byte array where the generated UUID is stored. + * @return One of the `UUIDV7_STATUS_*` codes that describe the + * characteristics of generated UUIDs or an + * implementation-dependent code. Callers can usually ignore + * the `UUIDV7_STATUS_*` code unless they need to guarantee the + * monotonic order of UUIDs or fine-tune the generation + * process. The implementation-dependent code must be out of + * the range of `int8_t` and negative if it reports an error. + */ +int uuidv7_new(uint8_t *uuid_out); + +/** + * Generates an 8-4-4-4-12 hexadecimal string representation of new UUIDv7. + * + * @param string_out Character array where the encoded string is stored. Its + * length must be 37 (36 digits + NUL) or longer. + * @return Return value of `uuidv7_new()`. + * @note Provide a concrete `uuidv7_new()` implementation to enable + * this function. + */ +static inline int uuidv7_new_string(char *string_out) { + uint8_t uuid[16]; + int result = uuidv7_new(uuid); + uuidv7_to_string(uuid, string_out); + return result; +} + +/** @} */ + +#ifdef __cplusplus +} /* extern "C" { */ +#endif + +#endif /* #ifndef UUIDV7_H_BAEDKYFQ */