[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, 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 public static function toBase(string $bytes, array $map): string
{ {
$base = \strlen($alphabet = $map['']); $base = \strlen($alphabet = $map['']);

View File

@ -22,6 +22,6 @@ class NilUuid extends Uuid
public function __construct() 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 PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Ulid;
use Symfony\Component\Uid\UuidV4;
class UlidTest extends TestCase class UlidTest extends TestCase
{ {
@ -49,6 +50,28 @@ class UlidTest extends TestCase
$this->assertTrue($ulid->equals(Ulid::fromString(hex2bin('7fffffffffffffffffffffffffffffff')))); $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 * @group time-sensitive
*/ */

View File

@ -13,6 +13,7 @@ namespace Symfony\Tests\Component\Uid;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\NilUuid; use Symfony\Component\Uid\NilUuid;
use Symfony\Component\Uid\Ulid;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV1; use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV3; use Symfony\Component\Uid\UuidV3;
@ -95,6 +96,26 @@ class UuidTest extends TestCase
$this->assertSame(self::A_UUID_V4, (string) $uuid); $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() public function testIsValid()
{ {
$this->assertFalse(Uuid::isValid('not a uuid')); $this->assertFalse(Uuid::isValid('not a uuid'));

View File

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

View File

@ -16,7 +16,7 @@ namespace Symfony\Component\Uid;
* *
* @author Grégoire Pineau <lyrixx@lyrixx.info> * @author Grégoire Pineau <lyrixx@lyrixx.info>
*/ */
class Uuid implements \JsonSerializable class Uuid extends AbstractUid
{ {
protected const TYPE = UUID_TYPE_DEFAULT; protected const TYPE = UUID_TYPE_DEFAULT;
@ -24,23 +24,31 @@ class Uuid implements \JsonSerializable
public function __construct(string $uuid) 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)); 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 * @return static
*/ */
public static function fromString(string $uuid): self public static function fromString(string $uuid): parent
{ {
if (16 === \strlen($uuid)) { if (22 === \strlen($uuid) && 22 === strspn($uuid, BinaryUtil::BASE58[''])) {
$uuid = uuid_unparse($uuid); $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); return new static($uuid);
} }
@ -51,10 +59,9 @@ class Uuid implements \JsonSerializable
case UuidV5::TYPE: return new UuidV5($uuid); case UuidV5::TYPE: return new UuidV5($uuid);
case UuidV6::TYPE: return new UuidV6($uuid); case UuidV6::TYPE: return new UuidV6($uuid);
case NilUuid::TYPE: return new NilUuid(); 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 final public static function v1(): UuidV1
@ -64,7 +71,7 @@ class Uuid implements \JsonSerializable
final public static function v3(self $namespace, string $name): UuidV3 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 final public static function v4(): UuidV4
@ -74,7 +81,7 @@ class Uuid implements \JsonSerializable
final public static function v5(self $namespace, string $name): UuidV5 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 final public static function v6(): UuidV6
@ -93,33 +100,20 @@ class Uuid implements \JsonSerializable
public function toBinary(): string public function toBinary(): string
{ {
return uuid_parse($this->uuid); return uuid_parse($this->uid);
} }
/** public function toRfc4122(): string
* Returns whether the argument is of class Uuid and contains the same value as the current instance.
*/
public function equals($other): bool
{ {
if (!$other instanceof self) { return $this->uid;
return false; }
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); return parent::compare($other);
}
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;
} }
} }

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Uid; 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 * @experimental in 5.1
* *
@ -31,7 +31,7 @@ class UuidV1 extends Uuid
public function __construct(string $uuid = null) public function __construct(string $uuid = null)
{ {
if (null === $uuid) { if (null === $uuid) {
$this->uuid = uuid_create(static::TYPE); $this->uid = uuid_create(static::TYPE);
} else { } else {
parent::__construct($uuid); parent::__construct($uuid);
} }
@ -39,7 +39,7 @@ class UuidV1 extends Uuid
public function getTime(): float 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) { if (\PHP_INT_SIZE >= 8) {
return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000; return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000;
@ -54,6 +54,6 @@ class UuidV1 extends Uuid
public function getNode(): string 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[8] = $uuid[8] & "\x3F" | "\x80";
$uuid = bin2hex($uuid); $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 { } else {
parent::__construct($uuid); parent::__construct($uuid);
} }

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Uid; 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 * @experimental in 5.1
* *
@ -32,7 +32,7 @@ class UuidV6 extends Uuid
{ {
if (null === $uuid) { if (null === $uuid) {
$uuid = uuid_create(UUID_TYPE_TIME); $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 { } else {
parent::__construct($uuid); parent::__construct($uuid);
} }
@ -40,7 +40,7 @@ class UuidV6 extends Uuid
public function getTime(): float 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) { if (\PHP_INT_SIZE >= 8) {
return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000; return (hexdec($time) - self::TIME_OFFSET_INT) / 10000000;
@ -55,6 +55,6 @@ class UuidV6 extends Uuid
public function getNode(): string public function getNode(): string
{ {
return substr($this->uuid, 24); return substr($this->uid, 24);
} }
} }