8000 [String] allow passing a string of custom characters to ByteString::fromRandom by azjezz · Pull Request #36471 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[String] allow passing a string of custom characters to ByteString::fromRandom #36471

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions src/Symfony/Component/String/ByteString.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,64 @@
*/
class ByteString extends AbstractString
{
private const ALPHABET_ALPHANUMERIC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

public function __construct(string $string = '')
{
$this->string = $string;
}

public static function fromRandom(int $length = 16): self
/*
* The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03)
*
* https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16
*
* Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE).
*
* Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/)
*/

public static function fromRandom(int $length = 16, string $alphabet = null): self
{
$string = '';
if ($length <= 0) {
throw new InvalidArgumentException(sprintf('Expected positive length value, got "%d".', $length));
}

$alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC;
$alphabetSize = \strlen($alphabet);
$bits = (int) ceil(log($alphabetSize, 2.0));
if ($bits <= 0 || $bits > 56) {
throw new InvalidArgumentException('Expected $alphabet\'s length to be in [2^1, 2^56].');
}

do {
$string .= str_replace(['/', '+', '='], '', base64_encode(random_bytes($length)));
} while (\strlen($string) < $length);
$ret = '';
while ($length > 0) {
$urandomLength = (int) ceil(2 * $length * $bits / 8.0);
$data = random_bytes($urandomLength);
$unpackedData = 0;
$unpackedBits = 0;
for ($i = 0; $i < $urandomLength && $length > 0; ++$i) {
// Unpack 8 bits
$unpackedData = ($unpackedData << 8) | \ord($data[$i]);
$unpackedBits += 8;

// While we have enough bits to select a character from the alphabet, keep
// consuming the random data
for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) {
$index = ($unpackedData & ((1 << $bits) - 1));
$unpackedData >>= $bits;
// Unfortunately, the alphabet size is not necessarily a power of two.
// Worst case, it is 2^k + 1, which means we need (k+1) bits and we
// have around a 50% chance of missing as k gets larger
if ($index < $alphabetSize) {
$ret .= $alphabet[$index];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funny idea: move the function on the base class and allow "characters" to be either code points and/or grapheme clusters, depending on what chunk() returns

--$length;
}
}
}
}

return new static(substr($string, 0, $length));
return new static($ret);
}

public function bytesAt(int $offset): array
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/String/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ CHANGELOG
depending of the input string UTF-8 compliancy
* added `$cut` parameter to `Symfony\Component\String\AbstractString::truncate()`
* added `AbstractString::containsAny()`
* allow passing a string of custom characters to `ByteString::fromRandom()`

5.0.0
-----
Expand Down
42 changes: 42 additions & 0 deletions src/Symfony/Component/String/Tests/ByteStringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\String\Tests;

use Symfony\Component\String\AbstractString;
use function Symfony\Component\String\b;
use Symfony\Component\String\ByteString;

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

public function testFromRandom(): void
{
$random = ByteString::fromRandom(32);

self::assertSame(32, $random->length());
foreach ($random->chunk() as $char) {
self::assertNotNull(b('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')->indexOf($char));
}
}

public function testFromRandomWithSpecificChars(): void
{
$random = ByteString::fromRandom(32, 'abc');

self::assertSame(32, $random->length());
foreach ($random->chunk() as $char) {
self::assertNotNull(b('abc')->indexOf($char));
}
}

public function testFromRandomEarlyReturnForZeroLength(): void
{
self::assertSame('', ByteString::fromRandom(0));
}

public function testFromRandomThrowsForNegativeLength(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Expected positive length value, got -1');

ByteString::fromRandom(-1);
}

public function testFromRandomAlphabetMin(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Expected $alphabet\'s length to be in [2^1, 2^56]');

ByteString::fromRandom(32, 'a');
}

public static function provideBytesAt(): array
{
return array_merge(
Expand Down
0