From d8479adc49538e2981bbebb021cf1f13153ba263 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 14 Mar 2020 14:04:34 +0100 Subject: [PATCH] [Uid] add AbstractUid and interop with base-58/32/RFC4122 encodings --- src/Symfony/Component/Uid/AbstractUid.php | 106 +++++++++++++++++++ src/Symfony/Component/Uid/BinaryUtil.php | 13 +++ src/Symfony/Component/Uid/NilUuid.php | 2 +- src/Symfony/Component/Uid/Tests/UlidTest.php | 23 ++++ src/Symfony/Component/Uid/Tests/UuidTest.php | 21 ++++ src/Symfony/Component/Uid/Ulid.php | 51 ++++----- src/Symfony/Component/Uid/Uuid.php | 62 +++++------ src/Symfony/Component/Uid/UuidV1.php | 8 +- src/Symfony/Component/Uid/UuidV4.php | 2 +- src/Symfony/Component/Uid/UuidV6.php | 8 +- 10 files changed, 219 insertions(+), 77 deletions(-) create mode 100644 src/Symfony/Component/Uid/AbstractUid.php diff --git a/src/Symfony/Component/Uid/AbstractUid.php b/src/Symfony/Component/Uid/AbstractUid.php new file mode 100644 index 0000000000..765fc3b05f --- /dev/null +++ b/src/Symfony/Component/Uid/AbstractUid.php @@ -0,0 +1,106 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Uid/BinaryUtil.php b/src/Symfony/Component/Uid/BinaryUtil.php index 5e3925df8a..812dd4f665 100644 --- a/src/Symfony/Component/Uid/BinaryUtil.php +++ b/src/Symfony/Component/Uid/BinaryUtil.php @@ -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['']); diff --git a/src/Symfony/Component/Uid/NilUuid.php b/src/Symfony/Component/Uid/NilUuid.php index c7614e46c9..c3b5db16d4 100644 --- a/src/Symfony/Component/Uid/NilUuid.php +++ b/src/Symfony/Component/Uid/NilUuid.php @@ -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'; } } diff --git a/src/Symfony/Component/Uid/Tests/UlidTest.php b/src/Symfony/Component/Uid/Tests/UlidTest.php index 4558ec85e9..5a3d076c06 100644 --- a/src/Symfony/Component/Uid/Tests/UlidTest.php +++ b/src/Symfony/Component/Uid/Tests/UlidTest.php @@ -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 */ diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index f6e8bf1bb0..addb9dfa4b 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -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')); diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php index 6bd2565dfd..aa1d70601c 100644 --- a/src/Symfony/Component/Uid/Ulid.php +++ b/src/Symfony/Component/Uid/Ulid.php @@ -20,17 +20,15 @@ namespace Symfony\Component\Uid; * * @author Nicolas Grekas */ -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); diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index f96a0e4d94..50d2993cdb 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -16,7 +16,7 @@ namespace Symfony\Component\Uid; * * @author Grégoire Pineau */ -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); } } diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php index 30bdfc7043..3694e0c907 100644 --- a/src/Symfony/Component/Uid/UuidV1.php +++ b/src/Symfony/Component/Uid/UuidV1.php @@ -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); } } diff --git a/src/Symfony/Component/Uid/UuidV4.php b/src/Symfony/Component/Uid/UuidV4.php index 30212a7e85..97ed1acf78 100644 --- a/src/Symfony/Component/Uid/UuidV4.php +++ b/src/Symfony/Component/Uid/UuidV4.php @@ -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); } diff --git a/src/Symfony/Component/Uid/UuidV6.php b/src/Symfony/Component/Uid/UuidV6.php index 947ed68db4..d479b8b0f8 100644 --- a/src/Symfony/Component/Uid/UuidV6.php +++ b/src/Symfony/Component/Uid/UuidV6.php @@ -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); } }