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

This commit is contained in:
azjezz 2020-05-03 09:03:42 +01:00
parent 4d8a4b6ede
commit 5d15c0be60
3 changed files with 93 additions and 6 deletions

View File

@ -25,20 +25,64 @@ use Symfony\Component\String\Exception\RuntimeException;
*/
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));
}
do {
$string .= str_replace(['/', '+', '='], '', base64_encode(random_bytes($length)));
} while (\strlen($string) < $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].');
}
return new static(substr($string, 0, $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];
--$length;
}
}
}
}
return new static($ret);
}
public function bytesAt(int $offset): array

View File

@ -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
-----

View File

@ -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
@ -21,6 +22,47 @@ class ByteStringTest extends AbstractAsciiTestCase
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(