From 64aa0062e2f0d912c5c86adbc60d174c78e16618 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 7 Aug 2024 14:35:05 +0200 Subject: [PATCH] [Uid] Add support for binary, base-32 and base-58 representations in `Uuid::isValid()` --- src/Symfony/Component/Uid/CHANGELOG.md | 1 + src/Symfony/Component/Uid/Tests/UuidTest.php | 32 +++++++++ src/Symfony/Component/Uid/Uuid.php | 68 +++++++++++++++----- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index 73cde81e3413b..6e5fca9981e1d 100644 --- a/src/Symfony/Component/Uid/CHANGELOG.md +++ b/src/Symfony/Component/Uid/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Make `AbstractUid` implement `Ds\Hashable` if available + * Add support for binary, base-32 and base-58 representations in `Uuid::isValid()` 7.1 --- diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index ab1761aac1830..efedb0db37c48 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -204,6 +204,38 @@ public function testIsValid() $this->assertTrue(UuidV4::isValid(self::A_UUID_V4)); } + public function testIsValidWithVariousFormat() + { + $uuid = Uuid::v4(); + + $this->assertTrue(Uuid::isValid($uuid->toBase32(), Uuid::FORMAT_BASE_32)); + $this->assertFalse(Uuid::isValid($uuid->toBase58(), Uuid::FORMAT_BASE_32)); + $this->assertFalse(Uuid::isValid($uuid->toBinary(), Uuid::FORMAT_BASE_32)); + $this->assertFalse(Uuid::isValid($uuid->toRfc4122(), Uuid::FORMAT_BASE_32)); + + $this->assertFalse(Uuid::isValid($uuid->toBase32(), Uuid::FORMAT_BASE_58)); + $this->assertTrue(Uuid::isValid($uuid->toBase58(), Uuid::FORMAT_BASE_58)); + $this->assertFalse(Uuid::isValid($uuid->toBinary(), Uuid::FORMAT_BASE_58)); + $this->assertFalse(Uuid::isValid($uuid->toRfc4122(), Uuid::FORMAT_BASE_58)); + + $this->assertFalse(Uuid::isValid($uuid->toBase32(), Uuid::FORMAT_BINARY)); + $this->assertFalse(Uuid::isValid($uuid->toBase58(), Uuid::FORMAT_BINARY)); + $this->assertTrue(Uuid::isValid($uuid->toBinary(), Uuid::FORMAT_BINARY)); + $this->assertFalse(Uuid::isValid($uuid->toRfc4122(), Uuid::FORMAT_BINARY)); + + $this->assertFalse(Uuid::isValid($uuid->toBase32(), Uuid::FORMAT_RFC_4122)); + $this->assertFalse(Uuid::isValid($uuid->toBase58(), Uuid::FORMAT_RFC_4122)); + $this->assertFalse(Uuid::isValid($uuid->toBinary(), Uuid::FORMAT_RFC_4122)); + $this->assertTrue(Uuid::isValid($uuid->toRfc4122(), Uuid::FORMAT_RFC_4122)); + + $this->assertTrue(Uuid::isValid($uuid->toBase32(), Uuid::FORMAT_ALL)); + $this->assertTrue(Uuid::isValid($uuid->toBase58(), Uuid::FORMAT_ALL)); + $this->assertTrue(Uuid::isValid($uuid->toBinary(), Uuid::FORMAT_ALL)); + $this->assertTrue(Uuid::isValid($uuid->toRfc4122(), Uuid::FORMAT_ALL)); + + $this->assertFalse(Uuid::isValid('30J7CNpDMfXPZrCsn4Cgey', Uuid::FORMAT_BASE_58), 'Fake base-58 string with the "O" forbidden char is not valid'); + } + public function testIsValidWithNilUuid() { $this->assertTrue(Uuid::isValid('00000000-0000-0000-0000-000000000000')); diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index 2820cdf0762d5..9d8f243d0f558 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -23,6 +23,12 @@ class Uuid extends AbstractUid public const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; public const NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'; + public const FORMAT_BINARY = 1; + public const FORMAT_BASE_32 = 1 << 1; + public const FORMAT_BASE_58 = 1 << 2; + public const FORMAT_RFC_4122 = 1 << 3; + public const FORMAT_ALL = -1; + protected const TYPE = 0; protected const NIL = '00000000-0000-0000-0000-000000000000'; protected const MAX = 'ffffffff-ffff-ffff-ffff-ffffffffffff'; @@ -44,22 +50,7 @@ public function __construct(string $uuid, bool $checkVariant = false) public static function fromString(string $uuid): static { - if (22 === \strlen($uuid) && 22 === strspn($uuid, BinaryUtil::BASE58[''])) { - $uuid = str_pad(BinaryUtil::fromBase($uuid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); - } - - if (16 === \strlen($uuid)) { - // don't use uuid_unparse(), it's slower - $uuid = bin2hex($uuid); - $uuid = substr_replace($uuid, '-', 8, 0); - $uuid = substr_replace($uuid, '-', 13, 0); - $uuid = substr_replace($uuid, '-', 18, 0); - $uuid = substr_replace($uuid, '-', 23, 0); - } elseif (26 === \strlen($uuid) && Ulid::isValid($uuid)) { - $ulid = new NilUlid(); - $ulid->uid = strtoupper($uuid); - $uuid = $ulid->toRfc4122(); - } + $uuid = self::transformToRfc4122($uuid, self::FORMAT_ALL); if (__CLASS__ !== static::class || 36 !== \strlen($uuid)) { return new static($uuid); @@ -130,8 +121,19 @@ final public static function v8(string $uuid): UuidV8 return new UuidV8($uuid); } - public static function isValid(string $uuid): bool + /** + * @param int-mask-of $format + */ + public static function isValid(string $uuid /*, int $format = self::FORMAT_RFC_4122 */): bool { + $format = 1 < \func_num_args() ? func_get_arg(1) : self::FORMAT_RFC_4122; + + if (36 === \strlen($uuid) && !($format & self::FORMAT_RFC_4122)) { + return false; + } + + $uuid = self::transformToRfc4122($uuid, $format); + if (self::NIL === $uuid && \in_array(static::class, [__CLASS__, NilUuid::class], true)) { return true; } @@ -182,4 +184,36 @@ private static function format(string $uuid, string $version): string return substr_replace($uuid, '-', 23, 0); } + + /** + * Transforms a binary string, a base-32 string or a base-58 string to a RFC4122 string. + * + * @param int-mask-of $format + * + * @return non-empty-string + */ + private static function transformToRfc4122(string $uuid, int $format): string + { + $fromBase58 = false; + if (22 === \strlen($uuid) && 22 === strspn($uuid, BinaryUtil::BASE58['']) && $format & self::FORMAT_BASE_58) { + $uuid = str_pad(BinaryUtil::fromBase($uuid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); + $fromBase58 = true; + } + + // base-58 are always transformed to binary string, but they must only be valid when the format is FORMAT_BASE_58 + if (16 === \strlen($uuid) && $format & self::FORMAT_BINARY || $fromBase58 && $format & self::FORMAT_BASE_58) { + // don't use uuid_unparse(), it's slower + $uuid = bin2hex($uuid); + $uuid = substr_replace($uuid, '-', 8, 0); + $uuid = substr_replace($uuid, '-', 13, 0); + $uuid = substr_replace($uuid, '-', 18, 0); + $uuid = substr_replace($uuid, '-', 23, 0); + } elseif (26 === \strlen($uuid) && Ulid::isValid($uuid) && $format & self::FORMAT_BASE_32) { + $ulid = new NilUlid(); + $ulid->uid = strtoupper($uuid); + $uuid = $ulid->toRfc4122(); + } + + return $uuid; + } }