8000 feature #36471 [String] allow passing a string of custom characters t… · symfony/symfony@5a2aef1 · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 5a2aef1

Browse files
committed
feature #36471 [String] allow passing a string of custom characters to ByteString::fromRandom (azjezz)
This PR was merged into the 5.1-dev branch. Discussion ---------- [String] allow passing a string of custom characters to ByteString::fromRandom | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes/ | Deprecations? | no | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> Commits ------- 5d15c0b [String] allow passing a string of custom characters to ByteString::fromRandom
2 parents ad01068 + 5d15c0b commit 5a2aef1

File tree

3 files changed

+93
-6
lines changed

3 files changed

+93
-6
lines changed

src/Symfony/Component/String/ByteString.php

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,64 @@
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+
/*
36+
* The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03)
37+
*
38+
* https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16
39+
*
40+
* Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE).
41+
*
42+
* Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/)
43+
*/
44+
45+
public static function fromRandom(int $length = 16, string $alphabet = null): self
3446
{
35-
$string = '';
47+
if ($length <= 0) {
48+
throw new InvalidArgumentException(sprintf('Expected positive length value, got "%d".', $length));
49+
}
50+
51+
$alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC;
52+
$alphabetSize = \strlen($alphabet);
53+
$bits = (int) ceil(log($alphabetSize, 2.0));
54+
if ($bits <= 0 || $bits > 56) {
55+
throw new InvalidArgumentException('Expected $alphabet\'s length to be in [2^1, 2^56].');
56+
}
3657

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

41-
return new static(substr($string, 0, $length));
85+
return new static($ret);
4286
}
4387

4488
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()`
< 6D47 /code>
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