[Uid] add AbstractUid and interop with base-58/32/RFC4122 encodings

This commit is contained in:
Nicolas Grekas 2020-03-14 14:04:34 +01:00
parent 1d955cc1f6
commit d8479adc49
10 changed files with 219 additions and 77 deletions

View File

@ -0,0 +1,106 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Uid;
/**
* @experimental in 5.1
*
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractUid implements \JsonSerializable
{
/**
* The identifier in its canonic representation.
*/
protected $uid;
/**
* Whether the passed value is valid for the constructor of the current class.
*/
abstract public static function isValid(string $uid): bool;
/**
* Creates an AbstractUid from an identifier represented in any of the supported formats.
*
* @return static
*
* @throws \InvalidArgumentException When the passed value is not valid
*/
abstract public static function fromString(string $uid): self;
/**
* Returns the identifier as a raw binary string.
*/
abstract public function toBinary(): string;
/**
* Returns the identifier as a base-58 case sensitive string.
*/
public function toBase58(): string
{
return strtr(sprintf('%022s', BinaryUtil::toBase($this->toBinary(), BinaryUtil::BASE58)), '0', '1');
}
/**
* Returns the identifier as a base-32 case insensitive string.
*/
public function toBase32(): string
{
$uid = bin2hex($this->toBinary());
$uid = sprintf('%02s%04s%04s%04s%04s%04s%04s',
base_convert(substr($uid, 0, 2), 16, 32),
base_convert(substr($uid, 2, 5), 16, 32),
base_convert(substr($uid, 7, 5), 16, 32),
base_convert(substr($uid, 12, 5), 16, 32),
base_convert(substr($uid, 17, 5), 16, 32),
base_convert(substr($uid, 22, 5), 16, 32),
base_convert(substr($uid, 27, 5), 16, 32)
);
return strtr($uid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ');
}
/**
* Returns the identifier as a RFC4122 case insensitive string.
*/
public function toRfc4122(): string
{
return uuid_unparse($this->toBinary());
}
/**
* Returns whether the argument is an AbstractUid and contains the same value as the current instance.
*/
public function equals($other): bool
{
if (!$other instanceof self) {
return false;
}
return $this->uid === $other->uid;
}
public function compare(self $other): int
{
return (\strlen($this->uid) - \strlen($other->uid)) ?: ($this->uid <=> $other->uid);
}
public function __toString(): string
{
return $this->uid;
}
public function jsonSerialize(): string
{
return $this->uid;
}
}

View File

@ -23,6 +23,19 @@ class BinaryUtil
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
];
public const BASE58 = [
'' => '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
1 => 0, 1, 2, 3, 4, 5, 6, 7, 8, 'A' => 9,
'B' => 10, 'C' => 11, 'D' => 12, 'E' => 13, 'F' => 14, 'G' => 15,
'H' => 16, 'J' => 17, 'K' => 18, 'L' => 19, 'M' => 20, 'N' => 21,
'P' => 22, 'Q' => 23, 'R' => 24, 'S' => 25, 'T' => 26, 'U' => 27,
'V' => 28, 'W' => 29, 'X' => 30, 'Y' => 31, 'Z' => 32, 'a' => 33,
'b' => 34, 'c' => 35, 'd' => 36, 'e' => 37, 'f' => 38, 'g' => 39,
'h' => 40, 'i' => 41, 'j' => 42, 'k' => 43, 'm' => 44, 'n' => 45,
'o' => 46, 'p' => 47, 'q' => 48, 'r' => 49, 's' => 50, 't' => 51,
'u' => 52, 'v' => 53, 'w' => 54, 'x' => 55, 'y' => 56, 'z' => 57,
];
public static function toBase(string $bytes, array $map): string
{
$base = \strlen($alphabet = $map['']);

View File

@ -22,6 +22,6 @@ class NilUuid extends Uuid
public function __construct()
{
$this->uuid = '00000000-0000-0000-0000-000000000000';
$this->uid = '00000000-0000-0000-0000-000000000000';
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Tests\Component\Uid;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Ulid;
use Symfony\Component\Uid\UuidV4;
class UlidTest extends TestCase
{
@ -49,6 +50,28 @@ class UlidTest extends TestCase
$this->assertTrue($ulid->equals(Ulid::fromString(hex2bin('7fffffffffffffffffffffffffffffff'))));
}
public function testFromUuid()
{
$uuid = new UuidV4();
$ulid = Ulid::fromString($uuid);
$this->assertSame($uuid->toBase32(), (string) $ulid);
$this->assertSame($ulid->toBase32(), (string) $ulid);
$this->assertSame((string) $uuid, $ulid->toRfc4122());
$this->assertTrue($ulid->equals(Ulid::fromString($uuid)));
}
public function testBase58()
{
$ulid = new Ulid('00000000000000000000000000');
$this->assertSame('1111111111111111111111', $ulid->toBase58());
$ulid = Ulid::fromString("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF");
$this->assertSame('YcVfxkQb6JRzqk5kF2tNLv', $ulid->toBase58());
$this->assertTrue($ulid->equals(Ulid::fromString('YcVfxkQb6JRzqk5kF2tNLv')));
}
/**
* @group time-sensitive
*/

View File

@ -13,6 +13,7 @@ namespace Symfony\Tests\Component\Uid;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\NilUuid;
use Symfony\Component\Uid\Ulid;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV3;
@ -95,6 +96,26 @@ class UuidTest extends TestCase
$this->assertSame(self::A_UUID_V4, (string) $uuid);
}
public function testFromUlid()
{
$ulid = new Ulid();
$uuid = Uuid::fromString($ulid);
$this->assertSame((string) $ulid, $uuid->toBase32());
$this->assertSame((string) $uuid, $uuid->toRfc4122());
$this->assertTrue($uuid->equals(Uuid::fromString($ulid)));
}
public function testBase58()
{
$uuid = new NilUuid();
$this->assertSame('1111111111111111111111', $uuid->toBase58());
$uuid = Uuid::fromString("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF");
$this->assertSame('YcVfxkQb6JRzqk5kF2tNLv', $uuid->toBase58());
$this->assertTrue($uuid->equals(Uuid::fromString('YcVfxkQb6JRzqk5kF2tNLv')));
}
public function testIsValid()
{
$this->assertFalse(Uuid::isValid('not a uuid'));

View File

@ -20,17 +20,15 @@ namespace Symfony\Component\Uid;
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class Ulid implements \JsonSerializable
class Ulid extends AbstractUid
{
private static $time = -1;
private static $rand = [];
private $ulid;
public function __construct(string $ulid = null)
{
if (null === $ulid) {
$this->ulid = self::generate();
$this->uid = self::generate();
return;
}
@ -39,7 +37,7 @@ class Ulid implements \JsonSerializable
throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid));
}
$this->ulid = strtr($ulid, 'abcdefghjkmnpqrstvwxyz', 'ABCDEFGHJKMNPQRSTVWXYZ');
$this->uid = strtr($ulid, 'abcdefghjkmnpqrstvwxyz', 'ABCDEFGHJKMNPQRSTVWXYZ');
}
public static function isValid(string $ulid): bool
@ -55,8 +53,17 @@ class Ulid implements \JsonSerializable
return $ulid[0] <= '7';
}
public static function fromString(string $ulid): self
/**
* {@inheritdoc}
*/
public static function fromString(string $ulid): parent
{
if (36 === \strlen($ulid) && Uuid::isValid($ulid)) {
$ulid = Uuid::fromString($ulid)->toBinary();
} elseif (22 === \strlen($ulid) && 22 === strspn($ulid, BinaryUtil::BASE58[''])) {
$ulid = BinaryUtil::fromBase($ulid, BinaryUtil::BASE58);
}
if (16 !== \strlen($ulid)) {
return new static($ulid);
}
@ -75,9 +82,9 @@ class Ulid implements \JsonSerializable
return new self(strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'));
}
public function toBinary()
public function toBinary(): string
{
$ulid = strtr($this->ulid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
$ulid = strtr($this->uid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
$ulid = sprintf('%02s%05s%05s%05s%05s%05s%05s',
base_convert(substr($ulid, 0, 2), 32, 16),
@ -92,26 +99,14 @@ class Ulid implements \JsonSerializable
return hex2bin($ulid);
}
/**
* Returns whether the argument is of class Ulid and contains the same value as the current instance.
*/
public function equals($other): bool
public function toBase32(): string
{
if (!$other instanceof self) {
return false;
}
return $this->ulid === $other->ulid;
}
public function compare(self $other): int
{
return $this->ulid <=> $other->ulid;
return $this->uid;
}
public function getTime(): float
{
$time = strtr(substr($this->ulid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
$time = strtr(substr($this->uid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
if (\PHP_INT_SIZE >= 8) {
return hexdec(base_convert($time, 32, 16)) / 1000;
@ -126,16 +121,6 @@ class Ulid implements \JsonSerializable
return BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10) / 1000;
}
public function __toString(): string
{
return $this->ulid;
}
public function jsonSerialize(): string
{
return $this->ulid;
}
private static function generate(): string
{
$time = microtime(false);

View File

@ -16,7 +16,7 @@ namespace Symfony\Component\Uid;
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Uuid implements \JsonSerializable
class Uuid extends AbstractUid
{
protected const TYPE = UUID_TYPE_DEFAULT;
@ -24,23 +24,31 @@ class Uuid implements \JsonSerializable
public function __construct(string $uuid)
{
if (static::TYPE !== uuid_type($uuid)) {
$type = uuid_type($uuid);
if (false === $type || UUID_TYPE_INVALID === $type || (static::TYPE ?: $type) !== $type) {
throw new \InvalidArgumentException(sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid));
}
$this->uuid = strtr($uuid, 'ABCDEF', 'abcdef');
$this->uid = strtr($uuid, 'ABCDEF', 'abcdef');
}
/**
* @return static
*/
public static function fromString(string $uuid): self
public static function fromString(string $uuid): parent
{
if (16 === \strlen($uuid)) {
$uuid = uuid_unparse($uuid);
if (22 === \strlen($uuid) && 22 === strspn($uuid, BinaryUtil::BASE58[''])) {
$uuid = BinaryUtil::fromBase($uuid, BinaryUtil::BASE58);
}
if (__CLASS__ !== static::class) {
if (16 === \strlen($uuid)) {
$uuid = uuid_unparse($uuid);
} elseif (26 === \strlen($uuid) && Ulid::isValid($uuid)) {
$uuid = (new Ulid($uuid))->toRfc4122();
}
if (__CLASS__ !== static::class || 36 !== \strlen($uuid)) {
return new static($uuid);
}
@ -51,10 +59,9 @@ class Uuid implements \JsonSerializable
case UuidV5::TYPE: return new UuidV5($uuid);
case UuidV6::TYPE: return new UuidV6($uuid);
case NilUuid::TYPE: return new NilUuid();
case self::TYPE: return new self($uuid);
}
throw new \InvalidArgumentException(sprintf('Invalid UUID: "%s".', $uuid));
return new self($uuid);
}
final public static function v1(): UuidV1
@ -64,7 +71,7 @@ class Uuid implements \JsonSerializable
final public static function v3(self $namespace, string $name): UuidV3
{
return new UuidV3(uuid_generate_md5($namespace->uuid, $name));
return new UuidV3(uuid_generate_md5($namespace->uid, $name));
}
final public static function v4(): UuidV4
@ -74,7 +81,7 @@ class Uuid implements \JsonSerializable
final public static function v5(self $namespace, string $name): UuidV5
{
return new UuidV5(uuid_generate_sha1($namespace->uuid, $name));
return new UuidV5(uuid_generate_sha1($namespace->uid, $name));
}
final public static function v6(): UuidV6
@ -93,33 +100,20 @@ class Uuid implements \JsonSerializable
public function toBinary(): string
{
return uuid_parse($this->uuid);
return uuid_parse($this->uid);
}
/**
* Returns whether the argument is of class Uuid and contains the same value as the current instance.
*/
public function equals($other): bool
public function toRfc4122(): string
{
if (!$other instanceof self) {
return false;
return $this->uid;
}
public function compare(parent $other): int
{
if (false !== $cmp = uuid_compare($this->uid, $other->uid)) {
return $cmp;
}
return 0 === uuid_compare($this->uuid, $other->uuid);
}
public function compare(self $other): int
{
return uuid_compare($this->uuid, $other->uuid);
}
public function __toString(): string
{
return $this->uuid;
}
public function jsonSerialize(): string
{
return $this->uuid;
return parent::compare($other);
}
}

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Uid;
/**
* A v1 UUID contains a 60-bit timestamp and 63 extra unique bits.
* A v1 UUID contains a 60-bit timestamp and 62 extra unique bits.
*
* @experimental in 5.1
*
@ -31,7 +31,7 @@ class UuidV1 extends Uuid
public function __construct(string $uuid = null)
{
if (null === $uuid) {
$this->uuid = uuid_create(static::TYPE);
$this->uid = uuid_create(static::TYPE);
} else {
parent::__construct($uuid);
}
@ -39,7 +39,7 @@ class UuidV1 extends Uuid
public function getTime(): float
{
$time = '0'.substr($this->uuid, 15, 3).substr($this->uuid, 9, 4).substr($this->uuid, 0, 8);
$time = '0'.substr($this->uid, 15, 3).substr($this->uid, 9, 4).substr($this->uid, 0, 8);
if (\PHP_INT_SIZE >= 8) {
return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000;
@ -54,6 +54,6 @@ class UuidV1 extends Uuid
public function getNode(): string
{
return uuid_mac($this->uuid);
return uuid_mac($this->uid);
}
}

View File

@ -30,7 +30,7 @@ class UuidV4 extends Uuid
$uuid[8] = $uuid[8] & "\x3F" | "\x80";
$uuid = bin2hex($uuid);
$this->uuid = substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12);
$this->uid = substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12);
} else {
parent::__construct($uuid);
}

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Uid;
/**
* A v6 UUID is lexicographically sortable and contains a 60-bit timestamp and 63 extra unique bits.
* A v6 UUID is lexicographically sortable and contains a 60-bit timestamp and 62 extra unique bits.
*
* @experimental in 5.1
*
@ -32,7 +32,7 @@ class UuidV6 extends Uuid
{
if (null === $uuid) {
$uuid = uuid_create(UUID_TYPE_TIME);
$this->uuid = substr($uuid, 15, 3).substr($uuid, 9, 4).$uuid[0].'-'.substr($uuid, 1, 4).'-6'.substr($uuid, 5, 3).substr($uuid, 18);
$this->uid = substr($uuid, 15, 3).substr($uuid, 9, 4).$uuid[0].'-'.substr($uuid, 1, 4).'-6'.substr($uuid, 5, 3).substr($uuid, 18);
} else {
parent::__construct($uuid);
}
@ -40,7 +40,7 @@ class UuidV6 extends Uuid
public function getTime(): float
{
$time = '0'.substr($this->uuid, 0, 8).substr($this->uuid, 9, 4).substr($this->uuid, 15, 3);
$time = '0'.substr($this->uid, 0, 8).substr($this->uid, 9, 4).substr($this->uid, 15, 3);
if (\PHP_INT_SIZE >= 8) {
return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000;
@ -55,6 +55,6 @@ class UuidV6 extends Uuid
public function getNode(): string
{
return substr($this->uuid, 24);
return substr($this->uid, 24);
}
}