8000 [String] allow passing a string of custom characters to ByteString::f… · symfony/symfony@9f8878f · GitHub
[go: up one dir, main page]

Skip to content

Commit 9f8878f

Browse files
committed
[String] allow passing a string of custom characters to ByteString::fromRandom
1 parent 4d8a4b6 commit 9f8878f

File tree

3 files changed

+87
-6
lines changed

3 files changed

+87
-6
lines changed

src/Symfony/Component/String/ByteString.php

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,58 @@
2525
*/
2626
class ByteString extends AbstractString
2727
{
28+
private const ALPHABET_ALPHANUMERIC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
29+
2830
public function __construct(string $string = '')
2931
{
3032
$this->string = $string;
3133
}
3234

33-
public static function fromRandom(int $length = 16): self
35+
public static function fromRandom(int $length = 16, string $alphabet = null): self
3436
{
35-
$string = '';
37+
if (0 === $length) {
38+
return new static('');
39+
}
40+
41+
if ($length < 0) {
42+
throw new \RuntimeException(sprintf('Expected positive length value, got "%d".', $length));
43+
}
44+
45+
$alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC;
46+
$alphabet_size = \strlen($alphabet);
47+
$bits = (int) ceil(log($alphabet_size, 2.0));
48+
if ($bits <= 0 || $bits > 56) {
49+
throw new \RuntimeException('Expected $alphabet\'s length to be in [2^1, 2^56].');
50+
}
3651

37-
do {
38-
$string .= str_replace(['/', '+', '='], '', base64_encode(random_bytes($length)));
39-
} while (\strlen($string) < $length);
52+
$ret = '';
53+
while ($length > 0) {
54+
$urandom_length = (int) ceil(2 * $length * $bits / 8.0);
55+
$data = random_bytes($urandom_length);
56+
$unpacked_data = 0;
57+
$unpacked_bits = 0;
58+
for ($i = 0; $i < $urandom_length && $length > 0; ++$i) {
59+
// Unpack 8 bits
60+
$unpacked_data = ($unpacked_data << 8) | \ord($data[$i]);
61+
$unpacked_bits += 8;
62+
63+
// While we have enough bits to select a character from the alphabet, keep
64+
// consuming the random data
65+
for (; $unpacked_bits >= $bits && $length > 0; $unpacked_bits -= $bits) {
66+
$index = ($unpacked_data & ((1 << $bits) - 1));
67+
$unpacked_data >>= $bits;
68+
// Unfortunately, the alphabet size is not necessarily a power of two.
69+
// Worst case, it is 2^k + 1, which means we need (k+1) bits and we
70+
// have around a 50% chance of missing as k gets larger
71+
if ($index < $alphabet_size) {
72+
$ret .= $alphabet[$index];
73+
--$length;
74+
}
75+
}
76+
}
77+
}
4078

41-
return new static(substr($string, 0, $length));
79+
return new static($ret);
4280
}
4381

4482
public function bytesAt(int $offset): array

src/Symfony/Component/String/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CHANGELOG
1212
depending of the input string UTF-8 compliancy
1313
* added `$cut` parameter to `Symfony\Component\String\AbstractString::truncate()`
1414
* added `AbstractString::containsAny()`
15+
* allow passing a string of custom characters to ByteString::fromRandom
1516

1617
5.0.0
1718
-----

src/Symfony/Component/String/Tests/ByteStringTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\String\Tests;
1313

1414
use Symfony\Component\String\AbstractString;
15+
use function Symfony\Component\String\b;
1516
use Symfony\Component\String\ByteString;
1617

1718
class ByteStringTest extends AbstractAsciiTestCase
@@ -21,6 +22,47 @@ protected static function createFromString(string $string): AbstractString
2122
return new ByteString($string);
2223
}
2324

25+
public function testFromRandom(): void
26+
{
27+
$random = ByteString::fromRandom(32);
28+
29+
self::assertSame(32, $random->length());
30+
foreach ($random->chunk() as $char) {
31+
self::assertNotNull(b('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')->indexOf($char));
32+
}
33+
}
34+
35+
public function testFromRandomWithSpecificChars(): void
36+
{
37+
$random = ByteString::fromRandom(32, 'abc');
38+
39+
self::assertSame(32, $random->length());
40+
foreach ($random->chunk() as $char) {
41+
self::assertNotNull(b('abc')->indexOf($char));
42+
}
43+
}
44+
45+
public function testFromRandomEarlyReturnForZeroLength(): void
46+
{
47+
self::assertSame('', ByteString::fromRandom(0));
48+
}
49+
50+
public function testFromRandomThrowsForNegativeLength(): void
51+
{
52+
$this->expectException(\RuntimeException::class);
53+
$this->expectExceptionMessage('Expected positive length value, got -1');
54+
55+
ByteString::fromRandom(-1);
56+
}
57+
58+
public function testFromRandomAlphabetMin(): void
59+
{
60+
$this->expectException(\RuntimeException::class);
61+
$this->expectExceptionMessage('Expected $alphabet\'s length to be in [2^1, 2^56]');
62+
63+
ByteString::fromRandom(32, 'a');
64+
}
65+
2466
public static function provideBytesAt(): array
2567
{
2668
return array_merge(

0 commit comments

Comments
 (0)
0