From e9012383f81110564c2c3741d5f108084c37eac8 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Thu, 20 Feb 2020 22:11:28 -0600 Subject: [PATCH 1/2] Fix rounding bug in timestamp for v1 UUIDs --- CHANGELOG.md | 20 ++++++- src/DegradedUuid.php | 2 +- src/Uuid.php | 4 +- tests/Converter/Time/PhpTimeConverterTest.php | 58 ++++++++++++++++++- tests/UuidTest.php | 17 ++++-- 5 files changed, 91 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2f1548..57b7f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Security +## [3.9.3] - 2020-02-20 + +### Fixed + +* For v1 UUIDs, round down for timestamps so that microseconds do not bump the + timestamp to the next second. + + As an example, consider the case of timestamp `1` with `600000` microseconds + (`1.600000`). This is the first second after midnight on January 1, 1970, UTC. + Previous versions of this library had a bug that would round this to `2`, so + the rendered time was `1970-01-01 00:00:02`. This was incorrect. Despite + having `600000` microseconds, the time should not round up to the next second. + Rather, the time should be `1970-01-01 00:00:01.600000`. Since this version of + ramsey/uuid does not support microseconds, the microseconds are dropped, and + the time is `1970-01-01 00:00:01`. No rounding should occur. + + ## [3.9.2] - 2019-12-17 ### Fixed @@ -591,7 +608,8 @@ versions leading up to this release.* [ramsey/uuid-doctrine]: https://github.com/ramsey/uuid-doctrine [ramsey/uuid-console]: https://github.com/ramsey/uuid-console -[unreleased]: https://github.com/ramsey/uuid/compare/3.9.2...HEAD +[unreleased]: https://github.com/ramsey/uuid/compare/3.9.3...HEAD +[3.9.3]: https://github.com/ramsey/uuid/compare/3.9.2...3.9.3 [3.9.2]: https://github.com/ramsey/uuid/compare/3.9.1...3.9.2 [3.9.1]: https://github.com/ramsey/uuid/compare/3.9.0...3.9.1 [3.9.0]: https://github.com/ramsey/uuid/compare/3.8.0...3.9.0 diff --git a/src/DegradedUuid.php b/src/DegradedUuid.php index 2669761..4e11272 100644 --- a/src/DegradedUuid.php +++ b/src/DegradedUuid.php @@ -40,7 +40,7 @@ class DegradedUuid extends Uuid $ts = new BigNumber($time, 20); $ts->subtract('122192928000000000'); $ts->divide('10000000.0'); - $ts->round(); + $ts->floor(); $unixTime = $ts->getValue(); return new DateTime("@{$unixTime}"); diff --git a/src/Uuid.php b/src/Uuid.php index 7ffcec4..f2912b4 100644 --- a/src/Uuid.php +++ b/src/Uuid.php @@ -350,8 +350,8 @@ class Uuid implements UuidInterface throw new UnsupportedOperationException('Not a time-based UUID'); } - $unixTime = ($this->getTimestamp() - 0x01b21dd213814000) / 1e7; - $unixTime = number_format($unixTime, 0, '', ''); + $unixTimeNanoseconds = $this->getTimestamp() - 0x01b21dd213814000; + $unixTime = ($unixTimeNanoseconds - $unixTimeNanoseconds % 1e7) / 1e7; return new DateTime("@{$unixTime}"); } diff --git a/tests/Converter/Time/PhpTimeConverterTest.php b/tests/Converter/Time/PhpTimeConverterTest.php index 47a3a17..1c9b120 100644 --- a/tests/Converter/Time/PhpTimeConverterTest.php +++ b/tests/Converter/Time/PhpTimeConverterTest.php @@ -12,7 +12,6 @@ use Ramsey\Uuid\Converter\Time\PhpTimeConverter; */ class PhpTimeConverterTest extends PHPUnit_Framework_TestCase { - public function testCalculateTimeReturnsArrayOfTimeSegments() { $seconds = 5; @@ -28,4 +27,61 @@ class PhpTimeConverterTest extends PHPUnit_Framework_TestCase $returned = $converter->calculateTime($seconds, $microSeconds); $this->assertEquals($expectedArray, $returned); } + + /** + * @dataProvider provideCalculateTime + */ + public function testCalculateTime($seconds, $microSeconds, $expected) + { + $converter = new PhpTimeConverter(); + + $result = $converter->calculateTime($seconds, $microSeconds); + + $this->assertSame($expected, $result); + } + + public function provideCalculateTime() + { + return [ + [ + 'seconds' => '-12219146756', + 'microSeconds' => '0', + 'expected' => [ + 'low' => '0901e600', + 'mid' => '0154', + 'hi' => '0000', + ], + ], + [ + 'seconds' => '103072857659', + 'microseconds' => '999999', + 'expected' => [ + 'low' => 'ff9785f6', + 'mid' => 'ffff', + 'hi' => '0fff', + ], + ], + [ + 'seconds' => '1578612359', + 'microseconds' => '521023', + 'expected' => [ + 'low' => '64c71df6', + 'mid' => '3337', + 'hi' => '01ea', + ], + ], + + // This is the earliest possible date supported by v1 UUIDs: + // 1582-10-15 00:00:00.000000 + [ + 'seconds' => '-12219292800', + 'microSeconds' => '0', + 'expected' => [ + 'low' => '00000000', + 'mid' => '0000', + 'hi' => '0000', + ], + ], + ]; + } } diff --git a/tests/UuidTest.php b/tests/UuidTest.php index 6ea5414..4299fbc 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -172,21 +172,25 @@ class UuidTest extends TestCase $uuid = Uuid::fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); $this->assertEquals('2012-07-04T02:14:34+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('1341368074', $uuid->getDateTime()->format('U')); // Check an old date $uuid = Uuid::fromString('0901e600-0154-1000-9b21-0800200c9a66'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); $this->assertEquals('1582-10-16T16:34:04+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('-12219146756', $uuid->getDateTime()->format('U')); // Check a future date $uuid = Uuid::fromString('ff9785f6-ffff-1fff-9669-00007ffffffe'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); - $this->assertEquals('5236-03-31T21:21:00+00:00', $uuid->getDateTime()->format('c')); + $this->assertEquals('5236-03-31T21:20:59+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('103072857659', $uuid->getDateTime()->format('U')); // Check the oldest date $uuid = Uuid::fromString('00000000-0000-1000-9669-00007ffffffe'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); $this->assertEquals('1582-10-15T00:00:00+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('-12219292800', $uuid->getDateTime()->format('U')); } /** @@ -200,21 +204,25 @@ class UuidTest extends TestCase $uuid = Uuid::fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); $this->assertEquals('2012-07-04T02:14:34+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('1341368074', $uuid->getDateTime()->format('U')); // Check an old date $uuid = Uuid::fromString('0901e600-0154-1000-9b21-0800200c9a66'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); $this->assertEquals('1582-10-16T16:34:04+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('-12219146756', $uuid->getDateTime()->format('U')); // Check a future date $uuid = Uuid::fromString('ff9785f6-ffff-1fff-9669-00007ffffffe'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); - $this->assertEquals('5236-03-31T21:21:00+00:00', $uuid->getDateTime()->format('c')); + $this->assertEquals('5236-03-31T21:20:59+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('103072857659', $uuid->getDateTime()->format('U')); // Check the oldest date $uuid = Uuid::fromString('00000000-0000-1000-9669-00007ffffffe'); $this->assertInstanceOf('\DateTime', $uuid->getDateTime()); $this->assertEquals('1582-10-15T00:00:00+00:00', $uuid->getDateTime()->format('c')); + $this->assertSame('-12219292800', $uuid->getDateTime()->format('U')); } /** @@ -1333,9 +1341,8 @@ class UuidTest extends TestCase ); // Assert that the time matches - $testTime = round($currentTime + ($usec / 1000000)); - $this->assertEquals($testTime, $uuid64->getDateTime()->getTimestamp()); - $this->assertEquals($testTime, $uuid32->getDateTime()->getTimestamp()); + $this->assertEquals($currentTime, $uuid64->getDateTime()->getTimestamp()); + $this->assertEquals($currentTime, $uuid32->getDateTime()->getTimestamp()); } $currentTime++; From 7e1633a6964b48589b142d60542f9ed31bd37a92 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Thu, 20 Feb 2020 22:36:14 -0600 Subject: [PATCH 2/2] Skip tests on 32-bit systems --- tests/Converter/Time/PhpTimeConverterTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/Converter/Time/PhpTimeConverterTest.php b/tests/Converter/Time/PhpTimeConverterTest.php index 1c9b120..f9478cf 100644 --- a/tests/Converter/Time/PhpTimeConverterTest.php +++ b/tests/Converter/Time/PhpTimeConverterTest.php @@ -2,18 +2,20 @@ namespace Ramsey\Uuid\Test\Converter; -use PHPUnit_Framework_TestCase; use Ramsey\Uuid\Converter\Time\PhpTimeConverter; +use Ramsey\Uuid\Test\TestCase; /** * Class PhpTimeConverterTest * @package Ramsey\Uuid\Test\Converter * @covers Ramsey\Uuid\Converter\Time\PhpTimeConverter */ -class PhpTimeConverterTest extends PHPUnit_Framework_TestCase +class PhpTimeConverterTest extends TestCase { public function testCalculateTimeReturnsArrayOfTimeSegments() { + $this->skip64BitTest(); + $seconds = 5; $microSeconds = 3; $calculatedTime = ($seconds * 10000000) + ($microSeconds * 10) + 0x01b21dd213814000; @@ -33,6 +35,8 @@ class PhpTimeConverterTest extends PHPUnit_Framework_TestCase */ public function testCalculateTime($seconds, $microSeconds, $expected) { + $this->skip64BitTest(); + $converter = new PhpTimeConverter(); $result = $converter->calculateTime($seconds, $microSeconds);