Skip to content

Commit 397050c

Browse files
committed
Add Randomizer::getBytesFromAlphabet() method
1 parent 2ba4747 commit 397050c

File tree

9 files changed

+219
-7
lines changed

9 files changed

+219
-7
lines changed

ext/random/php_random.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ PHPAPI double php_combined_lcg(void);
7474

7575
# define MT_N (624)
7676

77+
#define PHP_RANDOM_RANGE_ATTEMPTS (50)
78+
7779
PHPAPI void php_mt_srand(uint32_t seed);
7880
PHPAPI uint32_t php_mt_rand(void);
7981
PHPAPI zend_long php_mt_rand_range(zend_long min, zend_long max);

ext/random/random.c

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,6 @@ static zend_object_handlers random_engine_xoshiro256starstar_object_handlers;
8686
static zend_object_handlers random_engine_secure_object_handlers;
8787
static zend_object_handlers random_randomizer_object_handlers;
8888

89-
#define RANDOM_RANGE_ATTEMPTS (50)
90-
9189
static inline uint32_t rand_range32(const php_random_algo *algo, php_random_status *status, uint32_t umax)
9290
{
9391
uint32_t result, limit, r;
@@ -124,8 +122,8 @@ static inline uint32_t rand_range32(const php_random_algo *algo, php_random_stat
124122
/* Discard numbers over the limit to avoid modulo bias */
125123
while (UNEXPECTED(result > limit)) {
126124
/* If the requirements cannot be met in a cycles, return fail */
127-
if (++count > RANDOM_RANGE_ATTEMPTS) {
128-
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", RANDOM_RANGE_ATTEMPTS);
125+
if (++count > PHP_RANDOM_RANGE_ATTEMPTS) {
126+
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
129127
return 0;
130128
}
131129

@@ -180,8 +178,8 @@ static inline uint64_t rand_range64(const php_random_algo *algo, php_random_stat
180178
/* Discard numbers over the limit to avoid modulo bias */
181179
while (UNEXPECTED(result > limit)) {
182180
/* If the requirements cannot be met in a cycles, return fail */
183-
if (++count > RANDOM_RANGE_ATTEMPTS) {
184-
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", RANDOM_RANGE_ATTEMPTS);
181+
if (++count > PHP_RANDOM_RANGE_ATTEMPTS) {
182+
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
185183
return 0;
186184
}
187185

ext/random/random.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ public function getInt(int $min, int $max): int {}
137137

138138
public function getBytes(int $length): string {}
139139

140+
public function getBytesFromAlphabet(string $alphabet, int $length): string {}
141+
140142
public function shuffleArray(array $array): array {}
141143

142144
public function shuffleBytes(string $bytes): string {}

ext/random/random_arginfo.h

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/random/randomizer.c

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,90 @@ PHP_METHOD(Random_Randomizer, pickArrayKeys)
243243
}
244244
/* }}} */
245245

246+
/* {{{ Get Random Bytes for Alphabet */
247+
PHP_METHOD(Random_Randomizer, getBytesFromAlphabet)
248+
{
249+
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
250+
zend_long length;
251+
zend_string *alphabet, *retval;
252+
size_t total_size = 0;
253+
254+
ZEND_PARSE_PARAMETERS_START(2, 2);
255+
Z_PARAM_STR(alphabet)
256+
Z_PARAM_LONG(length)
257+
ZEND_PARSE_PARAMETERS_END();
258+
259+
const size_t alphabet_length = ZSTR_LEN(alphabet);
260+
261+
if (alphabet_length < 1) {
262+
zend_argument_value_error(1, "cannot be empty");
263+
RETURN_THROWS();
264+
}
265+
266+
if (length < 1) {
267+
zend_argument_value_error(2, "must be greater than 0");
268+
RETURN_THROWS();
269+
}
270+
271+
retval = zend_string_alloc(length, 0);
272+
273+
if (alphabet_length > 0xFF) {
274+
while (total_size < length) {
275+
uint64_t offset = randomizer->algo->range(randomizer->status, 0, alphabet_length - 1);
276+
277+
if (EG(exception)) {
278+
zend_string_free(retval);
279+
RETURN_THROWS();
280+
}
281+
282+
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(alphabet)[offset];
283+
}
284+
} else {
285+
uint64_t mask = 0xFF;
286+
if (alphabet_length <= 0x7F) mask = 0x7F;
287+
if (alphabet_length <= 0x3F) mask = 0x3F;
288+
if (alphabet_length <= 0x1F) mask = 0x1F;
289+
if (alphabet_length <= 0xF) mask = 0xF;
290+
if (alphabet_length <= 0x7) mask = 0x7;
291+
if (alphabet_length <= 0x3) mask = 0x3;
292+
if (alphabet_length <= 0x1) mask = 0x1;
293+
294+
int failures = 0;
295+
while (total_size < length) {
296+
uint64_t result = randomizer->algo->generate(randomizer->status);
297+
if (EG(exception)) {
298+
zend_string_free(retval);
299+
RETURN_THROWS();
300+
}
301+
302+
for (size_t i = 0; i < randomizer->status->last_generated_size; i++) {
303+
uint64_t offset = (result >> (i * 8)) & mask;
304+
305+
if (offset >= alphabet_length) {
306+
if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) {
307+
zend_string_free(retval);
308+
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
309+
RETURN_THROWS();
310+
}
311+
312+
continue;
313+
}
314+
315+
failures = 0;
316+
317+
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(alphabet)[offset];
318+
if (total_size >= length) {
319+
break;
320+
}
321+
}
322+
}
323+
}
324+
325+
ZSTR_VAL(retval)[length] = '\0';
326+
RETURN_STR(retval);
327+
}
328+
/* }}} */
329+
246330
/* {{{ Random\Randomizer::__serialize() */
247331
PHP_METHOD(Random_Randomizer, __serialize)
248332
{

ext/random/tests/03_randomizer/engine_unsafe_biased.phpt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,24 @@ try {
4949
echo $e->getMessage(), PHP_EOL;
5050
}
5151

52+
try {
53+
var_dump(randomizer()->getBytesFromAlphabet('123', 10));
54+
} catch (Random\BrokenRandomEngineError $e) {
55+
echo $e->getMessage(), PHP_EOL;
56+
}
57+
58+
try {
59+
var_dump(randomizer()->getBytesFromAlphabet(str_repeat('a', 500), 10));
60+
} catch (Random\BrokenRandomEngineError $e) {
61+
echo $e->getMessage(), PHP_EOL;
62+
}
63+
5264
?>
5365
--EXPECTF--
5466
Failed to generate an acceptable random number in 50 attempts
5567
int(%d)
5668
string(2) "ff"
5769
Failed to generate an acceptable random number in 50 attempts
5870
Failed to generate an acceptable random number in 50 attempts
71+
Failed to generate an acceptable random number in 50 attempts
72+
Failed to generate an acceptable random number in 50 attempts

ext/random/tests/03_randomizer/engine_unsafe_empty_string.phpt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,24 @@ try {
4949
echo $e->getMessage(), PHP_EOL;
5050
}
5151

52+
try {
53+
var_dump(randomizer()->getBytesFromAlphabet('123', 10));
54+
} catch (Random\BrokenRandomEngineError $e) {
55+
echo $e->getMessage(), PHP_EOL;
56+
}
57+
58+
try {
59+
var_dump(randomizer()->getBytesFromAlphabet(str_repeat('a', 500), 10));
60+
} catch (Random\BrokenRandomEngineError $e) {
61+
echo $e->getMessage(), PHP_EOL;
62+
}
63+
5264
?>
5365
--EXPECT--
5466
A random engine must return a non-empty string
5567
A random engine must return a non-empty string
5668
A random engine must return a non-empty string
5769
A random engine must return a non-empty string
5870
A random engine must return a non-empty string
71+
A random engine must return a non-empty string
72+
A random engine must return a non-empty string
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--TEST--
2+
Random: Randomizer: getBytesFromAlphabet(): Basic functionality
3+
--FILE--
4+
<?php
5+
6+
use Random\Engine;
7+
use Random\Engine\Mt19937;
8+
use Random\Engine\PcgOneseq128XslRr64;
9+
use Random\Engine\Secure;
10+
use Random\Engine\Test\TestShaEngine;
11+
use Random\Engine\Xoshiro256StarStar;
12+
use Random\Randomizer;
13+
14+
require __DIR__ . "/../../engines.inc";
15+
16+
$engines = [];
17+
$engines[] = new Mt19937(null, MT_RAND_MT19937);
18+
$engines[] = new Mt19937(null, MT_RAND_PHP);
19+
$engines[] = new PcgOneseq128XslRr64();
20+
$engines[] = new Xoshiro256StarStar();
21+
$engines[] = new Secure();
22+
$engines[] = new TestShaEngine();
23+
24+
foreach ($engines as $engine) {
25+
echo $engine::class, PHP_EOL;
26+
27+
$randomizer = new Randomizer($engine);
28+
var_dump($randomizer->getBytesFromAlphabet('a', 10));
29+
var_dump($randomizer->getBytesFromAlphabet(str_repeat('a', 256), 5));
30+
31+
for ($i = 1; $i < 250; $i++) {
32+
$output = $randomizer->getBytesFromAlphabet(str_repeat('ab', $i), 500);
33+
34+
// This check can theoretically fail with a chance of 0.5**500.
35+
if (!str_contains($output, 'a') || !str_contains($output, 'b')) {
36+
die("failure: didn't see both a and b at {$i}");
37+
}
38+
}
39+
}
40+
41+
die('success');
42+
43+
?>
44+
--EXPECT--
45+
Random\Engine\Mt19937
46+
string(10) "aaaaaaaaaa"
47+
string(5) "aaaaa"
48+
Random\Engine\Mt19937
49+
string(10) "aaaaaaaaaa"
50+
string(5) "aaaaa"
51+
Random\Engine\PcgOneseq128XslRr64
52+
string(10) "aaaaaaaaaa"
53+
string(5) "aaaaa"
54+
Random\Engine\Xoshiro256StarStar
55+
string(10) "aaaaaaaaaa"
56+
string(5) "aaaaa"
57+
Random\Engine\Secure
58+
string(10) "aaaaaaaaaa"
59+
string(5) "aaaaa"
60+
Random\Engine\Test\TestShaEngine
61+
string(10) "aaaaaaaaaa"
62+
string(5) "aaaaa"
63+
success
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Random: Randomizer: getBytesFromAlphabet(): Parameters are correctly validated
3+
--FILE--
4+
<?php
5+
6+
use Random\Randomizer;
7+
8+
function randomizer(): Randomizer
9+
{
10+
return new Randomizer();
11+
}
12+
13+
try {
14+
var_dump(randomizer()->getBytesFromAlphabet("", 2));
15+
} catch (ValueError $e) {
16+
echo $e->getMessage(), PHP_EOL;
17+
}
18+
19+
try {
20+
var_dump(randomizer()->getBytesFromAlphabet("abc", 0));
21+
} catch (ValueError $e) {
22+
echo $e->getMessage(), PHP_EOL;
23+
}
24+
25+
?>
26+
--EXPECTF--
27+
Random\Randomizer::getBytesFromAlphabet(): Argument #1 ($alphabet) cannot be empty
28+
Random\Randomizer::getBytesFromAlphabet(): Argument #2 ($length) must be greater than 0

0 commit comments

Comments
 (0)