
PHP Internals Book says: > The ZVAL_DUP macro is similar to ZVAL_COPY, but will duplicate arrays, rather > than just incrementing their refcount. If you are using this macro, you are > almost certainly doing something very wrong. Replace this by an explicit call to `zend_array_dup()`, as done in `php_array_diff()`. Besides being more explicit in what is happening, this likely also results in better assembly.
563 lines
15 KiB
C
563 lines
15 KiB
C
/*
|
||
+----------------------------------------------------------------------+
|
||
| 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: Go Kudo <zeriyoshi@php.net> |
|
||
+----------------------------------------------------------------------+
|
||
*/
|
||
|
||
#ifdef HAVE_CONFIG_H
|
||
# include "config.h"
|
||
#endif
|
||
|
||
#include "php.h"
|
||
#include "php_random.h"
|
||
|
||
#include "ext/standard/php_array.h"
|
||
#include "ext/standard/php_string.h"
|
||
|
||
#include "Zend/zend_enum.h"
|
||
#include "Zend/zend_exceptions.h"
|
||
#include "zend_portability.h"
|
||
|
||
static inline void randomizer_common_init(php_random_randomizer *randomizer, zend_object *engine_object) {
|
||
if (engine_object->ce->type == ZEND_INTERNAL_CLASS) {
|
||
/* Internal classes always php_random_engine struct */
|
||
php_random_engine *engine = php_random_engine_from_obj(engine_object);
|
||
|
||
/* Copy engine pointers */
|
||
randomizer->engine = engine->engine;
|
||
} else {
|
||
/* Self allocation */
|
||
php_random_status_state_user *state = php_random_status_alloc(&php_random_algo_user, false);
|
||
randomizer->engine = (php_random_algo_with_state){
|
||
.algo = &php_random_algo_user,
|
||
.state = state,
|
||
};
|
||
|
||
zend_string *mname;
|
||
zend_function *generate_method;
|
||
|
||
mname = ZSTR_INIT_LITERAL("generate", 0);
|
||
generate_method = zend_hash_find_ptr(&engine_object->ce->function_table, mname);
|
||
zend_string_release(mname);
|
||
|
||
/* Create compatible state */
|
||
state->object = engine_object;
|
||
state->generate_method = generate_method;
|
||
|
||
/* Mark self-allocated for memory management */
|
||
randomizer->is_userland_algo = true;
|
||
}
|
||
}
|
||
|
||
/* {{{ Random\Randomizer::__construct() */
|
||
PHP_METHOD(Random_Randomizer, __construct)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
zval engine;
|
||
zval *param_engine = NULL;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(0, 1)
|
||
Z_PARAM_OPTIONAL
|
||
Z_PARAM_OBJECT_OF_CLASS_OR_NULL(param_engine, random_ce_Random_Engine);
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
if (param_engine != NULL) {
|
||
ZVAL_COPY(&engine, param_engine);
|
||
} else {
|
||
/* Create default RNG instance */
|
||
object_init_ex(&engine, random_ce_Random_Engine_Secure);
|
||
}
|
||
|
||
zend_update_property(random_ce_Random_Randomizer, Z_OBJ_P(ZEND_THIS), "engine", strlen("engine"), &engine);
|
||
|
||
OBJ_RELEASE(Z_OBJ_P(&engine));
|
||
|
||
if (EG(exception)) {
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
randomizer_common_init(randomizer, Z_OBJ_P(&engine));
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Generate a float in [0, 1) */
|
||
PHP_METHOD(Random_Randomizer, nextFloat)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
php_random_algo_with_state engine = randomizer->engine;
|
||
|
||
uint64_t result;
|
||
size_t total_size;
|
||
|
||
ZEND_PARSE_PARAMETERS_NONE();
|
||
|
||
result = 0;
|
||
total_size = 0;
|
||
do {
|
||
php_random_result r = engine.algo->generate(engine.state);
|
||
result = result | (r.result << (total_size * 8));
|
||
total_size += r.size;
|
||
if (EG(exception)) {
|
||
RETURN_THROWS();
|
||
}
|
||
} while (total_size < sizeof(uint64_t));
|
||
|
||
/* A double has 53 bits of precision, thus we must not
|
||
* use the full 64 bits of the uint64_t, because we would
|
||
* introduce a bias / rounding error.
|
||
*/
|
||
#if DBL_MANT_DIG != 53
|
||
# error "Random_Randomizer::nextFloat(): Requires DBL_MANT_DIG == 53 to work."
|
||
#endif
|
||
const double step_size = 1.0 / (1ULL << 53);
|
||
|
||
/* Use the upper 53 bits, because some engine's lower bits
|
||
* are of lower quality.
|
||
*/
|
||
result = (result >> 11);
|
||
|
||
RETURN_DOUBLE(step_size * result);
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Generates a random float within a configurable interval.
|
||
*
|
||
* This method uses the γ-section algorithm by Frédéric Goualard.
|
||
*/
|
||
PHP_METHOD(Random_Randomizer, getFloat)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
double min, max;
|
||
zend_object *bounds = NULL;
|
||
int bounds_type = 'C' + sizeof("ClosedOpen") - 1;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(2, 3)
|
||
Z_PARAM_DOUBLE(min)
|
||
Z_PARAM_DOUBLE(max)
|
||
Z_PARAM_OPTIONAL
|
||
Z_PARAM_OBJ_OF_CLASS(bounds, random_ce_Random_IntervalBoundary);
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
if (!zend_finite(min)) {
|
||
zend_argument_value_error(1, "must be finite");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
if (!zend_finite(max)) {
|
||
zend_argument_value_error(2, "must be finite");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
if (bounds) {
|
||
zval *case_name = zend_enum_fetch_case_name(bounds);
|
||
zend_string *bounds_name = Z_STR_P(case_name);
|
||
|
||
bounds_type = ZSTR_VAL(bounds_name)[0] + ZSTR_LEN(bounds_name);
|
||
}
|
||
|
||
switch (bounds_type) {
|
||
case 'C' + sizeof("ClosedOpen") - 1:
|
||
if (UNEXPECTED(max <= min)) {
|
||
zend_argument_value_error(2, "must be greater than argument #1 ($min)");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
RETURN_DOUBLE(php_random_gammasection_closed_open(randomizer->engine, min, max));
|
||
case 'C' + sizeof("ClosedClosed") - 1:
|
||
if (UNEXPECTED(max < min)) {
|
||
zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
RETURN_DOUBLE(php_random_gammasection_closed_closed(randomizer->engine, min, max));
|
||
case 'O' + sizeof("OpenClosed") - 1:
|
||
if (UNEXPECTED(max <= min)) {
|
||
zend_argument_value_error(2, "must be greater than argument #1 ($min)");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
RETURN_DOUBLE(php_random_gammasection_open_closed(randomizer->engine, min, max));
|
||
case 'O' + sizeof("OpenOpen") - 1:
|
||
if (UNEXPECTED(max <= min)) {
|
||
zend_argument_value_error(2, "must be greater than argument #1 ($min)");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
RETVAL_DOUBLE(php_random_gammasection_open_open(randomizer->engine, min, max));
|
||
|
||
if (UNEXPECTED(isnan(Z_DVAL_P(return_value)))) {
|
||
zend_value_error("The given interval is empty, there are no floats between argument #1 ($min) and argument #2 ($max).");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
return;
|
||
default:
|
||
ZEND_UNREACHABLE();
|
||
}
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Generate positive random number */
|
||
PHP_METHOD(Random_Randomizer, nextInt)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
php_random_algo_with_state engine = randomizer->engine;
|
||
|
||
ZEND_PARSE_PARAMETERS_NONE();
|
||
|
||
php_random_result result = engine.algo->generate(engine.state);
|
||
if (EG(exception)) {
|
||
RETURN_THROWS();
|
||
}
|
||
if (result.size > sizeof(zend_long)) {
|
||
zend_throw_exception(random_ce_Random_RandomException, "Generated value exceeds size of int", 0);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
RETURN_LONG((zend_long) (result.result >> 1));
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Generate random number in range */
|
||
PHP_METHOD(Random_Randomizer, getInt)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
php_random_algo_with_state engine = randomizer->engine;
|
||
|
||
uint64_t result;
|
||
zend_long min, max;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(2, 2)
|
||
Z_PARAM_LONG(min)
|
||
Z_PARAM_LONG(max)
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
if (UNEXPECTED(max < min)) {
|
||
zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
if (UNEXPECTED(
|
||
engine.algo->range == php_random_algo_mt19937.range
|
||
&& ((php_random_status_state_mt19937 *) engine.state)->mode != MT_RAND_MT19937
|
||
)) {
|
||
uint64_t r = php_random_algo_mt19937.generate(engine.state).result >> 1;
|
||
|
||
/* This is an inlined version of the RAND_RANGE_BADSCALING macro that does not invoke UB when encountering
|
||
* (max - min) > ZEND_LONG_MAX.
|
||
*/
|
||
zend_ulong offset = (double) ( (double) max - min + 1.0) * (r / (PHP_MT_RAND_MAX + 1.0));
|
||
|
||
result = (zend_long) (offset + min);
|
||
} else {
|
||
result = engine.algo->range(engine.state, min, max);
|
||
}
|
||
|
||
if (EG(exception)) {
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
RETURN_LONG((zend_long) result);
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Generate random bytes string in ordered length */
|
||
PHP_METHOD(Random_Randomizer, getBytes)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
php_random_algo_with_state engine = randomizer->engine;
|
||
|
||
zend_string *retval;
|
||
zend_long user_length;
|
||
size_t total_size = 0;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(1, 1)
|
||
Z_PARAM_LONG(user_length)
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
if (user_length < 1) {
|
||
zend_argument_value_error(1, "must be greater than 0");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
size_t length = (size_t)user_length;
|
||
retval = zend_string_alloc(length, 0);
|
||
|
||
php_random_result result;
|
||
while (total_size + 8 <= length) {
|
||
result = engine.algo->generate(engine.state);
|
||
if (EG(exception)) {
|
||
zend_string_free(retval);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
/* If the result is not 64 bits, we can't use the fast path and
|
||
* we don't attempt to use it in the future, because we don't
|
||
* expect engines to change their output size.
|
||
*
|
||
* While it would be possible to always memcpy() the entire output,
|
||
* using result.size as the length that would result in much worse
|
||
* assembly, because it will actually emit a call to memcpy()
|
||
* instead of just storing the 64 bit value at a memory offset.
|
||
*/
|
||
if (result.size != 8) {
|
||
goto non_64;
|
||
}
|
||
|
||
#ifdef WORDS_BIGENDIAN
|
||
uint64_t swapped = ZEND_BYTES_SWAP64(result.result);
|
||
memcpy(ZSTR_VAL(retval) + total_size, &swapped, 8);
|
||
#else
|
||
memcpy(ZSTR_VAL(retval) + total_size, &result.result, 8);
|
||
#endif
|
||
total_size += 8;
|
||
}
|
||
|
||
while (total_size < length) {
|
||
result = engine.algo->generate(engine.state);
|
||
if (EG(exception)) {
|
||
zend_string_free(retval);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
non_64:
|
||
|
||
for (size_t i = 0; i < result.size; i++) {
|
||
ZSTR_VAL(retval)[total_size++] = result.result & 0xff;
|
||
result.result >>= 8;
|
||
if (total_size >= length) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
ZSTR_VAL(retval)[length] = '\0';
|
||
RETURN_STR(retval);
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Shuffling array */
|
||
PHP_METHOD(Random_Randomizer, shuffleArray)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
zval *array;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(1, 1)
|
||
Z_PARAM_ARRAY(array)
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
RETVAL_ARR(zend_array_dup(Z_ARRVAL_P(array)));
|
||
if (!php_array_data_shuffle(randomizer->engine, return_value)) {
|
||
RETURN_THROWS();
|
||
}
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Shuffling binary */
|
||
PHP_METHOD(Random_Randomizer, shuffleBytes)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
zend_string *bytes;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(1, 1)
|
||
Z_PARAM_STR(bytes)
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
if (ZSTR_LEN(bytes) < 2) {
|
||
RETURN_STR_COPY(bytes);
|
||
}
|
||
|
||
RETVAL_STRINGL(ZSTR_VAL(bytes), ZSTR_LEN(bytes));
|
||
if (!php_binary_string_shuffle(randomizer->engine, Z_STRVAL_P(return_value), (zend_long) Z_STRLEN_P(return_value))) {
|
||
RETURN_THROWS();
|
||
}
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Pick keys */
|
||
PHP_METHOD(Random_Randomizer, pickArrayKeys)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
zval *input, t;
|
||
zend_long num_req;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(2, 2);
|
||
Z_PARAM_ARRAY(input)
|
||
Z_PARAM_LONG(num_req)
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
if (!php_array_pick_keys(
|
||
randomizer->engine,
|
||
input,
|
||
num_req,
|
||
return_value,
|
||
false)
|
||
) {
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
/* Keep compatibility, But the result is always an array */
|
||
if (Z_TYPE_P(return_value) != IS_ARRAY) {
|
||
ZVAL_COPY_VALUE(&t, return_value);
|
||
array_init(return_value);
|
||
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &t);
|
||
}
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Get Random Bytes for String */
|
||
PHP_METHOD(Random_Randomizer, getBytesFromString)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
php_random_algo_with_state engine = randomizer->engine;
|
||
|
||
zend_long user_length;
|
||
zend_string *source, *retval;
|
||
size_t total_size = 0;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(2, 2);
|
||
Z_PARAM_STR(source)
|
||
Z_PARAM_LONG(user_length)
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
const size_t source_length = ZSTR_LEN(source);
|
||
const size_t max_offset = source_length - 1;
|
||
|
||
if (source_length < 1) {
|
||
zend_argument_must_not_be_empty_error(1);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
if (user_length < 1) {
|
||
zend_argument_value_error(2, "must be greater than 0");
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
size_t length = (size_t)user_length;
|
||
retval = zend_string_alloc(length, 0);
|
||
|
||
if (max_offset > 0xff) {
|
||
while (total_size < length) {
|
||
uint64_t offset = engine.algo->range(engine.state, 0, max_offset);
|
||
|
||
if (EG(exception)) {
|
||
zend_string_free(retval);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
|
||
}
|
||
} else {
|
||
uint64_t mask = max_offset;
|
||
// Copy the top-most bit into all lower bits.
|
||
// Shifting by 4 is sufficient, because max_offset
|
||
// is guaranteed to fit in an 8-bit integer at this
|
||
// point.
|
||
mask |= mask >> 1;
|
||
mask |= mask >> 2;
|
||
mask |= mask >> 4;
|
||
// Expand the lowest byte into all bytes.
|
||
mask *= 0x0101010101010101;
|
||
|
||
int failures = 0;
|
||
while (total_size < length) {
|
||
php_random_result result = engine.algo->generate(engine.state);
|
||
if (EG(exception)) {
|
||
zend_string_free(retval);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
uint64_t offsets = result.result & mask;
|
||
for (size_t i = 0; i < result.size; i++) {
|
||
uint64_t offset = offsets & 0xff;
|
||
offsets >>= 8;
|
||
|
||
if (offset > max_offset) {
|
||
if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) {
|
||
zend_string_free(retval);
|
||
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
failures = 0;
|
||
|
||
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
|
||
if (total_size >= length) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
ZSTR_VAL(retval)[length] = '\0';
|
||
RETURN_STR(retval);
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Random\Randomizer::__serialize() */
|
||
PHP_METHOD(Random_Randomizer, __serialize)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
zval t;
|
||
|
||
ZEND_PARSE_PARAMETERS_NONE();
|
||
|
||
array_init(return_value);
|
||
ZVAL_ARR(&t, zend_std_get_properties(&randomizer->std));
|
||
Z_TRY_ADDREF(t);
|
||
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &t);
|
||
}
|
||
/* }}} */
|
||
|
||
/* {{{ Random\Randomizer::__unserialize() */
|
||
PHP_METHOD(Random_Randomizer, __unserialize)
|
||
{
|
||
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
|
||
HashTable *d;
|
||
zval *members_zv;
|
||
zval *zengine;
|
||
|
||
ZEND_PARSE_PARAMETERS_START(1, 1)
|
||
Z_PARAM_ARRAY_HT(d);
|
||
ZEND_PARSE_PARAMETERS_END();
|
||
|
||
/* Verify the expected number of elements, this implicitly ensures that no additional elements are present. */
|
||
if (zend_hash_num_elements(d) != 1) {
|
||
zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
members_zv = zend_hash_index_find(d, 0);
|
||
if (!members_zv || Z_TYPE_P(members_zv) != IS_ARRAY) {
|
||
zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
|
||
RETURN_THROWS();
|
||
}
|
||
object_properties_load(&randomizer->std, Z_ARRVAL_P(members_zv));
|
||
if (EG(exception)) {
|
||
zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
zengine = zend_read_property(randomizer->std.ce, &randomizer->std, "engine", strlen("engine"), 1, NULL);
|
||
if (Z_TYPE_P(zengine) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(zengine), random_ce_Random_Engine)) {
|
||
zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
|
||
RETURN_THROWS();
|
||
}
|
||
|
||
randomizer_common_init(randomizer, Z_OBJ_P(zengine));
|
||
}
|
||
/* }}} */
|