diff --git a/src/Uuid.php b/src/Uuid.php index 2d54183..8a9b3b9 100644 --- a/src/Uuid.php +++ b/src/Uuid.php @@ -17,12 +17,14 @@ * @category Xmf\Uuid * @package Xmf * @author Richard Griffith - * @copyright 2017 XOOPS Project (https://xoops.org) - * @license GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html) - * @link https://xoops.org + * @copyright 2017-2019 XOOPS Project (https://xoops.org) + * @license GNU GPL 2 or later (https://www.gnu.org/licenses/gpl-2.0.html) */ class Uuid { + // match spec for version 4 UUID as per rfc4122 + protected const UUID_REGEX = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/'; + /** * generate - generate a version 4 (random) UUID * @@ -41,4 +43,48 @@ public static function generate() return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } + + /** + * Pack a UUID into a binary string + * + * @param string $uuid a valid UUID + * + * @return string packed UUID as a binary string + * + * @throws \InvalidArgumentException + * @throws \UnexpectedValueException + */ + public static function packAsBinary($uuid) + { + if (!preg_match(static::UUID_REGEX, $uuid)) { + throw new \InvalidArgumentException('Invalid UUID'); + } + $return = pack("H*", str_replace('-', '', $uuid)); + if (false === $return) { + throw new \UnexpectedValueException('Packing UUID Failed'); + } + return $return; + } + + /** + * Unpack a UUID stored as a binary string + * + * @param string $packedUuid a packed UUID as returned by packAsBinary() + * + * @return string unpacked UUID + * + * @throws \InvalidArgumentException + * @throws \UnexpectedValueException + */ + public static function unpackBinary($packedUuid) + { + if (16 !== strlen($packedUuid)) { + throw new \InvalidArgumentException('Invalid packed UUID'); + } + $return = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($packedUuid), 4)); + if (!preg_match(static::UUID_REGEX, $return)) { + throw new \UnexpectedValueException('Unpacking UUID Failed'); + } + return $return; + } } diff --git a/tests/unit/UuidTest.php b/tests/unit/UuidTest.php index 114b6c9..2ff18b0 100644 --- a/tests/unit/UuidTest.php +++ b/tests/unit/UuidTest.php @@ -40,5 +40,48 @@ public function testGenerate() $this->assertNotEquals($result, $anotherResult); } + + public function testPackUnpack() + { + $uuid = Uuid::generate(); + $binUuid = Uuid::packAsBinary($uuid); + $strUuid = Uuid::unpackBinary($binUuid); + $this->assertEquals($uuid, $strUuid); + } + + public function testInvalidPack() + { + $this->expectException('\InvalidArgumentException'); + $binUuid = Uuid::packAsBinary('garbage-data'); + } + + public function testInvalidUnpack() + { + $this->expectException('\InvalidArgumentException'); + $binUuid = Uuid::unpackBinary('123456789012345'); + } + + public function testInvalidUnpack2() + { + $this->expectException('\UnexpectedValueException'); + $binUuid = Uuid::unpackBinary('0000000000000000'); + } + + /* verify natural sort order is the same for readable and binary formats */ + public function testSortOrder() + { + $auuid = []; + $buuid = []; + for ($i=1; $i<10; ++$i) { + $uuid = Uuid::generate(); + $auuid[] = $uuid; + $buuid[] = Uuid::packAsBinary($uuid); + } + sort($auuid); + sort($buuid); + foreach ($auuid as $key => $uuid) { + $this->assertEquals($auuid[$key], Uuid::unpackBinary($buuid[$key])); + } + } }