From cd98e0aaa036720cec3ca0bfb46252d51ebe27ab Mon Sep 17 00:00:00 2001 From: lemon-mint Date: Tue, 26 May 2026 02:50:16 +0000 Subject: [PATCH 1/3] Align PHP Randflake parity with original implementation Use the upstream official test vectors to verify encrypted IDs, base32hex strings, and inspect output against the Go implementation. Switch SPARX-64 packing to little-endian and replace RFC4648 byte Base32Hex with Randflake's mathematical base32hex conversion. --- composer.json | 3 +- composer.lock | 74 +---- .../RandflakeIdDecodingErrorException.php | 2 +- .../RandflakeIdDecryptionErrorException.php | 2 +- .../RandflakeIdEncodingErrorException.php | 2 +- .../RandflakeIdEncryptionErrorException.php | 2 +- .../RandflakeIdInvalidIdOverflowException.php | 2 +- ...RandflakeIdUnsupportedIntSizeException.php | 2 +- src/Generator.php | 91 +++--- tests/GeneratorTest.php | 271 +++++++++++++++++- 10 files changed, 331 insertions(+), 120 deletions(-) diff --git a/composer.json b/composer.json index 3f21d01..59099cb 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,7 @@ "sparx64" ], "require": { - "php": "^8.1", - "paragonie/constant_time_encoding": "^3.1" + "php": "^8.1" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.93", diff --git a/composer.lock b/composer.lock index 831f00d..08f7014 100644 --- a/composer.lock +++ b/composer.lock @@ -4,78 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9f2882d3c6c1fb13e9b8d82f70b1afe7", - "packages": [ - { - "name": "paragonie/constant_time_encoding", - "version": "v3.1.3", - "source": { - "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", - "shasum": "" - }, - "require": { - "php": "^8" - }, - "require-dev": { - "infection/infection": "^0", - "nikic/php-fuzzer": "^0", - "phpunit/phpunit": "^9|^10|^11", - "vimeo/psalm": "^4|^5|^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "ParagonIE\\ConstantTime\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com", - "role": "Maintainer" - }, - { - "name": "Steve 'Sc00bz' Thomas", - "email": "steve@tobtu.com", - "homepage": "https://www.tobtu.com", - "role": "Original Developer" - } - ], - "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", - "keywords": [ - "base16", - "base32", - "base32_decode", - "base32_encode", - "base64", - "base64_decode", - "base64_encode", - "bin2hex", - "encoding", - "hex", - "hex2bin", - "rfc4648" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/constant_time_encoding/issues", - "source": "https://github.com/paragonie/constant_time_encoding" - }, - "time": "2025-09-24T15:06:41+00:00" - } - ], + "content-hash": "5d2f635b2349956f84f36d75a936c8ec", + "packages": [], "packages-dev": [ { "name": "clue/ndjson-react", diff --git a/src/Exception/RandflakeIdDecodingErrorException.php b/src/Exception/RandflakeIdDecodingErrorException.php index 18102ed..fa17b8f 100644 --- a/src/Exception/RandflakeIdDecodingErrorException.php +++ b/src/Exception/RandflakeIdDecodingErrorException.php @@ -9,7 +9,7 @@ final class RandflakeIdDecodingErrorException extends RandflakeIdException public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null) { parent::__construct( - "" !== $message ? $message : "Decoding error occurred.", + "" !== $message ? $message : "Failed to decode ID.", $code, $previous ); diff --git a/src/Exception/RandflakeIdDecryptionErrorException.php b/src/Exception/RandflakeIdDecryptionErrorException.php index 7c77fef..916399e 100644 --- a/src/Exception/RandflakeIdDecryptionErrorException.php +++ b/src/Exception/RandflakeIdDecryptionErrorException.php @@ -9,7 +9,7 @@ final class RandflakeIdDecryptionErrorException extends RandflakeIdException public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null) { parent::__construct( - "" !== $message ? $message : "Decryption error occurred.", + "" !== $message ? $message : "Failed to decrypt ID.", $code, $previous ); diff --git a/src/Exception/RandflakeIdEncodingErrorException.php b/src/Exception/RandflakeIdEncodingErrorException.php index f5989db..9dbf182 100644 --- a/src/Exception/RandflakeIdEncodingErrorException.php +++ b/src/Exception/RandflakeIdEncodingErrorException.php @@ -9,7 +9,7 @@ final class RandflakeIdEncodingErrorException extends RandflakeIdException public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null) { parent::__construct( - "" !== $message ? $message : "Encoding error occurred.", + "" !== $message ? $message : "Failed to encode ID.", $code, $previous ); diff --git a/src/Exception/RandflakeIdEncryptionErrorException.php b/src/Exception/RandflakeIdEncryptionErrorException.php index 9006b55..8f9d9df 100644 --- a/src/Exception/RandflakeIdEncryptionErrorException.php +++ b/src/Exception/RandflakeIdEncryptionErrorException.php @@ -9,7 +9,7 @@ final class RandflakeIdEncryptionErrorException extends RandflakeIdException public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null) { parent::__construct( - "" !== $message ? $message : "Encryption error occurred.", + "" !== $message ? $message : "Failed to encrypt ID.", $code, $previous ); diff --git a/src/Exception/RandflakeIdInvalidIdOverflowException.php b/src/Exception/RandflakeIdInvalidIdOverflowException.php index 4a35601..d41d72b 100644 --- a/src/Exception/RandflakeIdInvalidIdOverflowException.php +++ b/src/Exception/RandflakeIdInvalidIdOverflowException.php @@ -9,7 +9,7 @@ final class RandflakeIdInvalidIdOverflowException extends RandflakeIdException public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null) { parent::__construct( - "" !== $message ? $message : "ID is greater than the maximum possible value.", + "" !== $message ? $message : "ID is greater than maximum possible value.", $code, $previous ); diff --git a/src/Exception/RandflakeIdUnsupportedIntSizeException.php b/src/Exception/RandflakeIdUnsupportedIntSizeException.php index 7aad745..49a548c 100644 --- a/src/Exception/RandflakeIdUnsupportedIntSizeException.php +++ b/src/Exception/RandflakeIdUnsupportedIntSizeException.php @@ -9,7 +9,7 @@ final class RandflakeIdUnsupportedIntSizeException extends RandflakeIdException public function __construct(string $message = "", int $code = 0, \Throwable|null $previous = null) { parent::__construct( - "" !== $message ? $message : "This library requires a 64-bit PHP installation to function.", + "" !== $message ? $message : "Unsupported integer size. Randflake ID generator requires 64-bit integer support.", $code, $previous ); diff --git a/src/Generator.php b/src/Generator.php index 329526e..79678f6 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -17,7 +17,6 @@ use Adambean\RandflakeId\Exception\RandflakeIdInvalidSecretException; use Adambean\RandflakeId\Exception\RandflakeIdResourceExhaustedException; use Adambean\RandflakeId\Exception\RandflakeIdUnsupportedIntSizeException; -use ParagonIE\ConstantTime\Base32Hex; /** * Randflake ID generator. @@ -37,6 +36,7 @@ */ final class Generator { + private const B32HEX_CHARS = "0123456789abcdefghijklmnopqrstuv"; /* * ------------------------------------------------------------------------- * Variables @@ -149,7 +149,7 @@ public function __construct( throw new RandflakeIdInvalidNodeException(); } - if ("" === ($secret = trim($secret)) || strlen($secret) !== RandflakeId::SECRET_LENGTH) { + if (strlen($secret) !== RandflakeId::SECRET_LENGTH) { throw new RandflakeIdInvalidSecretException(); } @@ -266,6 +266,40 @@ private function makeNewRawId(): string return $this->intToString($id); } + /** + * Decode a mathematical Base32Hex string into an unsigned numeric string. + * + * @param non-empty-string $idEncoded + * + * @return numeric-string + */ + private function decodeBase32HexToNumericString(string $idEncoded): string + { + RandflakeId::assertBase32HexStringId($idEncoded); + + $idEncoded = strtolower(trim($idEncoded)); + $idDecodedNum = "0"; + $length = strlen($idEncoded); + for ($i = 0; $i < $length; $i++) { + $charCode = ord($idEncoded[$i]); + if ($charCode >= 48 && $charCode <= 57) { + $value = $charCode - 48; + } elseif ($charCode >= 97 && $charCode <= 118) { + $value = $charCode - 87; + } else { + throw new RandflakeIdDecodingErrorException("Invalid base32hex character."); + } + + $idDecodedNum = bcadd(bcmul($idDecodedNum, "32", 0), strval($value), 0); + } + + if (bccomp($idDecodedNum, $this->numMaxMinus1, 0) > 0) { + throw new RandflakeIdDecodingErrorException("Decoded ID is out of valid range."); + } + + return $idDecodedNum; + } + /* @@ -412,14 +446,7 @@ public function isNumericStringIdValid(string $id): void */ public function isEncodedStringIdValid(string $id): void { - RandflakeId::assertBase32HexStringId($id); - - $idDecoded = Base32Hex::decode(strtolower($id)); - if ("" === ($idDecoded = trim($idDecoded))) { - throw new RandflakeIdDecodingErrorException("Failed to decode ID."); - } - - RandflakeId::addNullPaddingToPackedId($idDecoded); + $this->decodeBase32HexToNumericString($id); } /** @@ -487,14 +514,14 @@ public function encryptId(string $idRaw): string { $this->isNumericStringIdValid($idRaw); - $idRawBytes = pack("J", $this->stringToInt($idRaw)); + $idRawBytes = pack("P", $this->stringToInt($idRaw)); $idEncrypted = $this->secretBox?->encrypt($idRawBytes); if (null === $idEncrypted) { throw new RandflakeIdEncryptionErrorException("Failed to encrypt ID."); } - $idEncryptedBytes = unpack("J", $idEncrypted); + $idEncryptedBytes = unpack("P", $idEncrypted); if (!is_array($idEncryptedBytes) || !isset($idEncryptedBytes[1]) || !is_int($idEncryptedBytes[1])) { throw new RandflakeIdEncryptionErrorException("Failed to unpack encrypted ID."); } @@ -524,14 +551,14 @@ public function decryptId(string $idEncrypted): string { $this->isNumericStringIdValid($idEncrypted); - $idEncryptedBytes = pack("J", $this->stringToInt($idEncrypted)); + $idEncryptedBytes = pack("P", $this->stringToInt($idEncrypted)); $idDecrypted = $this->secretBox?->decrypt($idEncryptedBytes); if (null === $idDecrypted) { throw new RandflakeIdDecryptionErrorException("Failed to decrypt ID."); } - $idDecryptedBytes = unpack("J", $idDecrypted); + $idDecryptedBytes = unpack("P", $idDecrypted); if (!is_array($idDecryptedBytes) || !isset($idDecryptedBytes[1]) || !is_int($idDecryptedBytes[1])) { throw new RandflakeIdDecryptionErrorException("Failed to unpack decrypted ID."); } @@ -557,14 +584,19 @@ public function encodeId(string $idPlain): string { $this->isNumericStringIdValid($idPlain); - if (bccomp($idPlain, $this->numMaxMinus1, 0) === 1) { - throw new RandflakeIdEncodingErrorException("ID is too large."); + if (bccomp($idPlain, "0", 0) === 0) { + return "0"; } - $idPlainBytes = pack("J", $this->stringToInt($idPlain)); + $idEncoded = ""; + $idRemaining = $idPlain; + while (bccomp($idRemaining, "0", 0) > 0) { + $remainder = bcmod($idRemaining, "32", 0); + $idEncoded = self::B32HEX_CHARS[(int) $remainder] . $idEncoded; + $idRemaining = bcdiv($idRemaining, "32", 0); + } - $idEncoded = Base32Hex::encodeUnpadded($idPlainBytes); - if ("" === ($idEncoded = strtolower(trim($idEncoded)))) { + if ("" === $idEncoded) { throw new RandflakeIdEncodingErrorException("Failed to encode ID."); } @@ -584,26 +616,7 @@ public function encodeId(string $idPlain): string */ public function decodeId(string $idEncoded): string { - $this->isEncodedStringIdValid($idEncoded); - - $idDecoded = Base32Hex::decode(strtolower($idEncoded)); - if ("" === ($idDecoded = trim($idDecoded))) { - throw new RandflakeIdDecodingErrorException("Failed to decode ID."); - } - - $idDecoded = RandflakeId::addNullPaddingToPackedId($idDecoded); - - $idDecodedBytes = unpack("J", $idDecoded); - if (!is_array($idDecodedBytes) || !isset($idDecodedBytes[1]) || !is_int($idDecodedBytes[1])) { - throw new RandflakeIdDecodingErrorException("Failed to unpack decoded ID."); - } - - $idDecodedNum = $this->intToString($idDecodedBytes[1]); - if (bccomp($idDecodedNum, "0", 0) < 0 || bccomp($idDecodedNum, $this->numMaxMinus1, 0) > 0) { - throw new RandflakeIdDecodingErrorException("Decoded ID is out of valid range."); - } - - return $idDecodedNum; + return $this->decodeBase32HexToNumericString($idEncoded); } /** diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 1275dc9..0b00d32 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -20,6 +20,274 @@ private static function makeGenerator(): Generator ); } + /** + * @return list + */ + private static function officialTestVectors(): array + { + return [ + [ + "secret" => "dffd6021bb2bd5b0af676290809ec3a5", + "node_id" => 42, + "lease_start" => 1730000000, + "lease_end" => 1735000000, + "timestamp" => 1733706297, + "sequence" => 1, + "raw_id" => "63673697622556673", + "encrypted_id" => "4594531474933654033", + "encoded_id" => "3vgoe12ccb8gh", + ], + [ + "secret" => "00000000000000000000000000000000", + "node_id" => 0, + "lease_start" => 1730000000, + "lease_end" => 1730000010, + "timestamp" => 1730000001, + "sequence" => 0, + "raw_id" => "17179869184", + "encrypted_id" => "2111581968557607991", + "encoded_id" => "1qjeojjevu31n", + ], + [ + "secret" => "ffffffffffffffffffffffffffffffff", + "node_id" => 131071, + "lease_start" => 1730000000, + "lease_end" => 1730000010, + "timestamp" => 1730000002, + "sequence" => 131071, + "raw_id" => "51539607551", + "encrypted_id" => "1072887061578045911", + "encoded_id" => "tot934enhmen", + ], + [ + "secret" => "000102030405060708090a0b0c0d0e0f", + "node_id" => 1, + "lease_start" => 1730000000, + "lease_end" => 1730001000, + "timestamp" => 1730000123, + "sequence" => 1, + "raw_id" => "2113124040705", + "encrypted_id" => "-232447010193727000", + "encoded_id" => "fphhelk04q8f8", + ], + [ + "secret" => "0f0e0d0c0b0a09080706050403020100", + "node_id" => 131070, + "lease_start" => 1730000001, + "lease_end" => 1730005000, + "timestamp" => 1730004567, + "sequence" => 131070, + "raw_id" => "78477642301438", + "encrypted_id" => "-2085243871051999270", + "encoded_id" => "e63tpnt9u55uq", + ], + [ + "secret" => "73757065722d7365637265742d6b6579", + "node_id" => 7, + "lease_start" => 1730500000, + "lease_end" => 1732500000, + "timestamp" => 1731234567, + "sequence" => 42, + "raw_id" => "21209699559800874", + "encrypted_id" => "-2990835006926556165", + "encoded_id" => "dcvjbd135dsvr", + ], + [ + "secret" => "00112233445566778899aabbccddeeff", + "node_id" => 65535, + "lease_start" => 1731000000, + "lease_end" => 1739000000, + "timestamp" => 1737654321, + "sequence" => 65535, + "raw_id" => "131500242062213119", + "encrypted_id" => "8136495406619906497", + "encoded_id" => "71ql3eqe2qke1", + ], + [ + "secret" => "ffeeddccbbaa99887766554433221100", + "node_id" => 65536, + "lease_start" => 1732000000, + "lease_end" => 1740000000, + "timestamp" => 1738888888, + "sequence" => 65536, + "raw_id" => "152709941621227520", + "encrypted_id" => "2668698934995960496", + "encoded_id" => "2a28vhb0ebglg", + ], + [ + "secret" => "0123456789abcdeffedcba9876543210", + "node_id" => 12345, + "lease_start" => 1740000000, + "lease_end" => 1748000000, + "timestamp" => 1745678901, + "sequence" => 12345, + "raw_id" => "269361469746982969", + "encrypted_id" => "-4589356659636265124", + "encoded_id" => "c0jqkdrtct7qs", + ], + [ + "secret" => "89abcdef0123456776543210fedcba98", + "node_id" => 98765, + "lease_start" => 1750000000, + "lease_end" => 1758000000, + "timestamp" => 1753456789, + "sequence" => 98765, + "raw_id" => "402984579442115021", + "encrypted_id" => "7890279631303626821", + "encoded_id" => "6qvv7gkl5kk25", + ], + [ + "secret" => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "node_id" => 2, + "lease_start" => 1760000000, + "lease_end" => 1765000000, + "timestamp" => 1762345678, + "sequence" => 2, + "raw_id" => "555694516708048898", + "encrypted_id" => "1271837609463808827", + "encoded_id" => "139jpi4u150pr", + ], + [ + "secret" => "55555555555555555555555555555555", + "node_id" => 131071, + "lease_start" => 1770000000, + "lease_end" => 1779000000, + "timestamp" => 1777777777, + "sequence" => 0, + "raw_id" => "820815975942062080", + "encrypted_id" => "-2472022342516504789", + "encoded_id" => "drccsorenj1pb", + ], + [ + "secret" => "31415926535897932384626433832795", + "node_id" => 31415, + "lease_start" => 1800000000, + "lease_end" => 1810000000, + "timestamp" => 1803141592, + "sequence" => 65358, + "raw_id" => "1256562986587193166", + "encrypted_id" => "-4403670233087647781", + "encoded_id" => "c5oo584odinur", + ], + [ + "secret" => "27182818284590452353602874713526", + "node_id" => 27182, + "lease_start" => 1850000000, + "lease_end" => 1860000000, + "timestamp" => 1852718281, + "sequence" => 84590, + "raw_id" => "2108284017628236398", + "encrypted_id" => "-2703274410196994664", + "encoded_id" => "dkv0a7scsrtco", + ], + [ + "secret" => "11235813213455891442333776109871", + "node_id" => 1098, + "lease_start" => 1900000000, + "lease_end" => 1910000000, + "timestamp" => 1901123581, + "sequence" => 33776, + "raw_id" => "2939880736021578736", + "encrypted_id" => "8857717212368356482", + "encoded_id" => "7lr7erve18h42", + ], + [ + "secret" => "fedcba98765432100123456789abcdef", + "node_id" => 131071, + "lease_start" => 2266870901, + "lease_end" => 2266870911, + "timestamp" => 2266870911, + "sequence" => 131071, + "raw_id" => "9223372036854775807", + "encrypted_id" => "-6720148258825199276", + "encoded_id" => "a5f9sodpc78ak", + ], + ]; + } + + /** + * @param numeric-string $id + * @return numeric-string + */ + private static function signedVectorIdToUnsigned(string $id): string + { + if (!str_starts_with($id, "-")) { + if (!ctype_digit($id)) { + self::fail("Positive vector ID must be an unsigned integer string."); + } + + return $id; + } + + $unsigned = bcadd($id, bcpow("2", "64", 0), 0); + if (!ctype_digit($unsigned)) { + self::fail("Signed vector ID could not be converted to an unsigned integer string."); + } + + return $unsigned; + } + + private static function setGeneratorState(Generator $generator, int $sequence, int $rollover): void + { + $sequenceProperty = new \ReflectionProperty($generator, "sequence"); + $sequenceProperty->setAccessible(true); + $sequenceProperty->setValue($generator, $sequence); + + $rolloverProperty = new \ReflectionProperty($generator, "rollover"); + $rolloverProperty->setAccessible(true); + $rolloverProperty->setValue($generator, $rollover); + } + + public function testOfficialGoTestVectors(): void + { + foreach (self::officialTestVectors() as $vector) { + $secret = hex2bin($vector["secret"]); + if (false === $secret || "" === $secret) { + self::fail("Official test vector secret must decode to a non-empty binary string."); + } + + $generator = new Generator( + $vector["node_id"], + $secret, + $vector["lease_start"], + $vector["lease_end"], + $vector["timestamp"] + ); + + self::setGeneratorState( + $generator, + $vector["sequence"] === 0 ? RandflakeId::MAX_SEQUENCE : $vector["sequence"] - 1, + $vector["sequence"] === 0 ? $vector["timestamp"] - 1 : $vector["lease_start"] + ); + + $encryptedUnsigned = self::signedVectorIdToUnsigned($vector["encrypted_id"]); + $generatedEncrypted = $generator->generate(true, false); + + self::assertSame($encryptedUnsigned, $generator->encryptId($vector["raw_id"])); + self::assertSame($encryptedUnsigned, $generatedEncrypted); + self::assertSame((int) $vector["encrypted_id"], $generator->stringToInt($generatedEncrypted)); + self::assertSame($vector["encoded_id"], $generator->encodeId($generatedEncrypted)); + self::assertSame($encryptedUnsigned, $generator->decodeId($vector["encoded_id"])); + self::assertSame($vector["raw_id"], $generator->decryptId($generatedEncrypted)); + + $details = $generator->inspect($vector["encoded_id"], true); + self::assertSame(strval($vector["timestamp"] - RandflakeId::EPOCH_OFFSET), $details["timestamp"]); + self::assertSame(strval($vector["timestamp"]), $details["timestampUtc"]); + self::assertSame(strval($vector["node_id"]), $details["nodeId"]); + self::assertSame(strval($vector["sequence"]), $details["sequence"]); + } + } + public function testGenerate(): void { $generator = self::makeGenerator(); @@ -85,6 +353,7 @@ public function testAssertions(): void RandflakeId::assertValidId($idRawEncoded, true); RandflakeId::assertValidId($idEncrypted, false); RandflakeId::assertValidId($idEncryptedEncoded, true); + self::addToAssertionCount(8); } public function testStringIntegers(): void @@ -96,7 +365,7 @@ public function testStringIntegers(): void "0" => 0, "1234567890" => 1234567890, "9223372036854775807" => 9223372036854775807, - "9223372036854775808" => -9223372036854775808, + "9223372036854775808" => PHP_INT_MIN, "17293822564152854774" => -1152921509556696842, "18446597525811658164" => -146547897893452, "18446744068759701750" => -4949849866, From d12e9cfe111939d174e25a37886628ba1b466657 Mon Sep 17 00:00:00 2001 From: Adam Reece <1108717+Adambean@users.noreply.github.com> Date: Wed, 27 May 2026 20:18:28 +0100 Subject: [PATCH 2/3] Generator: - Add constants section header. - Add comment to B32HEX_CHARS constant. - Add `@throws` documentation to `decodeBase32HexToNumericString()`. --- src/Generator.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Generator.php b/src/Generator.php index 79678f6..428727f 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -36,7 +36,17 @@ */ final class Generator { + /* + * ------------------------------------------------------------------------- + * Constants + * ------------------------------------------------------------------------- + */ + + /** @var string Character set used in Base32Hex strings. */ private const B32HEX_CHARS = "0123456789abcdefghijklmnopqrstuv"; + + + /* * ------------------------------------------------------------------------- * Variables @@ -272,6 +282,9 @@ private function makeNewRawId(): string * @param non-empty-string $idEncoded * * @return numeric-string + * + * @throws RandflakeIdDecodingErrorException + * If decoding fails or the decoded ID is out of valid range. */ private function decodeBase32HexToNumericString(string $idEncoded): string { From d3557034907b744bf9df100896d94e279412e84c Mon Sep 17 00:00:00 2001 From: Adam Reece <1108717+Adambean@users.noreply.github.com> Date: Wed, 27 May 2026 20:24:06 +0100 Subject: [PATCH 3/3] Generator test: Tweak to `@return` documentation for `officialTestVectors()`. --- tests/GeneratorTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index 0b00d32..1d6c324 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -23,14 +23,14 @@ private static function makeGenerator(): Generator /** * @return list */ private static function officialTestVectors(): array @@ -217,6 +217,7 @@ private static function officialTestVectors(): array /** * @param numeric-string $id + * * @return numeric-string */ private static function signedVectorIdToUnsigned(string $id): string