Skip to content

Commit ac3ecd0

Browse files
Add Randomizer::getBytesFromString() method (#9664)
* Add `Randomizer::getBytesFromAlphabet()` method * Rename `getBytesFromAlphabet` to `getBytesFromString` * [ci skip] Add NEWS/UPGRADING for Randomizer::getBytesFromString() Co-authored-by: Tim Düsterhus <tim@bastelstu.be>
1 parent 1b503a1 commit ac3ecd0

File tree

11 files changed

+238
-7
lines changed

11 files changed

+238
-7
lines changed

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ PHP NEWS
5555
- Posix:
5656
. Added posix_sysconf. (David Carlier)
5757

58+
- Random:
59+
. Added Randomizer::getBytesFromString(). (Joshua Rüsweg)
60+
5861
- Reflection:
5962
. Fix GH-9470 (ReflectionMethod constructor should not find private parent
6063
method). (ilutov)

UPGRADING

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ PHP 8.3 UPGRADE NOTES
6262
- Posix:
6363
. Added posix_sysconf call to get runtime informations.
6464

65+
- Random:
66+
. Added Randomizer::getBytesFromString().
67+
RFC: https://wiki.php.net/rfc/randomizer_additions
68+
6569
- Sockets:
6670
. Added socket_atmark to checks if the socket is OOB marked.
6771

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;
@@ -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 getBytesFromString(string $string, 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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,102 @@ PHP_METHOD(Random_Randomizer, pickArrayKeys)
258258
}
259259
/* }}} */
260260

261+
/* {{{ Get Random Bytes for String */
262+
PHP_METHOD(Random_Randomizer, getBytesFromString)
263+
{
264+
php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
265+
zend_long length;
266+
zend_string *source, *retval;
267+
size_t total_size = 0;
268+
269+
ZEND_PARSE_PARAMETERS_START(2, 2);
270+
Z_PARAM_STR(source)
271+
Z_PARAM_LONG(length)
272+
ZEND_PARSE_PARAMETERS_END();
273+
274+
const size_t source_length = ZSTR_LEN(source);
275+
276+
if (source_length < 1) {
277+
zend_argument_value_error(1, "cannot be empty");
278+
RETURN_THROWS();
279+
}
280+
281+
if (length < 1) {
282+
zend_argument_value_error(2, "must be greater than 0");
283+
RETURN_THROWS();
284+
}
285+
286+
retval = zend_string_alloc(length, 0);
287+
288+
if (source_length > 0xFF) {
289+
while (total_size < length) {
290+
uint64_t offset = randomizer->algo->range(randomizer->status, 0, source_length - 1);
291+
292+
if (EG(exception)) {
293+
zend_string_free(retval);
294+
RETURN_THROWS();
295+
}
296+
297+
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
298+
}
299+
} else {
300+
uint64_t mask;
301+
if (source_length <= 0x1) {
302+
mask = 0x0;
303+
} else if (source_length <= 0x2) {
304+
mask = 0x1;
305+
} else if (source_length <= 0x4) {
306+
mask = 0x3;
307+
} else if (source_length <= 0x8) {
308+
mask = 0x7;
309+
} else if (source_length <= 0x10) {
310+
mask = 0xF;
311+
} else if (source_length <= 0x20) {
312+
mask = 0x1F;
313+
} else if (source_length <= 0x40) {
314+
mask = 0x3F;
315+
} else if (source_length <= 0x80) {
316+
mask = 0x7F;
317+
} else {
318+
mask = 0xFF;
319+
}
320+
321+
int failures = 0;
322+
while (total_size < length) {
323+
uint64_t result = randomizer->algo->generate(randomizer->status);
324+
if (EG(exception)) {
325+
zend_string_free(retval);
326+
RETURN_THROWS();
327+
}
328+
329+
for (size_t i = 0; i < randomizer->status->last_generated_size; i++) {
330+
uint64_t offset = (result >> (i * 8)) & mask;
331+
332+
if (offset >= source_length) {
333+
if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) {
334+
zend_string_free(retval);
335+
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
336+
RETURN_THROWS();
337+
}
338+
339+
continue;
340+
}
341+
342+
failures = 0;
343+
344+
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
345+
if (total_size >= length) {
346+
break;
347+
}
348+
}
349+
}
350+
}
351+
352+
ZSTR_VAL(retval)[length] = '\0';
353+
RETURN_STR(retval);
354+
}
355+
/* }}} */
356+
261357
/* {{{ Random\Randomizer::__serialize() */
262358
PHP_METHOD(Random_Randomizer, __serialize)
263359
{

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()->getBytesFromString('123', 10));
54+
} catch (Random\BrokenRandomEngineError $e) {
55+
echo $e->getMessage(), PHP_EOL;
56+
}
57+
58+
try {
59+
var_dump(randomizer()->getBytesFromString(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()->getBytesFromString('123', 10));
54+
} catch (Random\BrokenRandomEngineError $e) {
55+
echo $e->getMessage(), PHP_EOL;
56+
}
57+
58+
try {
59+
var_dump(randomizer()->getBytesFromString(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: getBytesFromString(): 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->getBytesFromString('a', 10));
29+
var_dump($randomizer->getBytesFromString(str_repeat('a', 256), 5));
30+
31+
for ($i = 1; $i < 250; $i++) {
32+
$output = $randomizer->getBytesFromString(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: getBytesFromString(): 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()->getBytesFromString("", 2));
15+
} catch (ValueError $e) {
16+
echo $e->getMessage(), PHP_EOL;
17+
}
18+
19+
try {
20+
var_dump(randomizer()->getBytesFromString("abc", 0));
21+
} catch (ValueError $e) {
22+
echo $e->getMessage(), PHP_EOL;
23+
}
24+
25+
?>
26+
--EXPECTF--
27+
Random\Randomizer::getBytesFromString(): Argument #1 ($string) cannot be empty
28+
Random\Randomizer::getBytesFromString(): Argument #2 ($length) must be greater than 0

0 commit comments

Comments
 (0)