diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ea4f0..74308c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Security +## [4.0.0-alpha3] - 2020-02-21 + +### Fixed + +* Fix microsecond rounding error on 32-bit systems. + + ## [4.0.0-alpha2] - 2020-02-21 ### Added @@ -823,7 +830,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/4.0.0-alpha2...HEAD +[unreleased]: https://github.com/ramsey/uuid/compare/4.0.0-alpha3...HEAD +[4.0.0-alpha3]: https://github.com/ramsey/uuid/compare/4.0.0-alpha2...4.0.0-alpha3 [4.0.0-alpha2]: https://github.com/ramsey/uuid/compare/4.0.0-alpha1...4.0.0-alpha2 [4.0.0-alpha1]: https://github.com/ramsey/uuid/compare/3.9.3...4.0.0-alpha1 [3.9.3]: https://github.com/ramsey/uuid/compare/3.9.2...3.9.3 diff --git a/src/Converter/Time/GenericTimeConverter.php b/src/Converter/Time/GenericTimeConverter.php index 6fab086..d5ce5f7 100644 --- a/src/Converter/Time/GenericTimeConverter.php +++ b/src/Converter/Time/GenericTimeConverter.php @@ -92,10 +92,8 @@ class GenericTimeConverter implements TimeConverterInterface new IntegerValue('122192928000000000') ); - // Round down so that the microseconds do not shift the timestamp - // into the next second, giving us the wrong Unix timestamp. $unixTimestamp = $this->calculator->divide( - RoundingMode::DOWN, + RoundingMode::HALF_UP, 6, $epochNanoseconds, new IntegerValue('10000000') diff --git a/src/Converter/Time/PhpTimeConverter.php b/src/Converter/Time/PhpTimeConverter.php index 4fa0191..607102b 100644 --- a/src/Converter/Time/PhpTimeConverter.php +++ b/src/Converter/Time/PhpTimeConverter.php @@ -154,9 +154,22 @@ class PhpTimeConverter implements TimeConverterInterface return []; } + $microseconds = $split[1]; + + // Ensure the microseconds are no longer than 6 digits. If they are, + // truncate the number to the first 6 digits and round up, if needed. + if (strlen($microseconds) > 6) { + $roundingDigit = (int) substr($microseconds, 6, 1); + $microseconds = (int) substr($microseconds, 0, 6); + + if ($roundingDigit >= 5) { + $microseconds++; + } + } + return [ 'sec' => $split[0], - 'usec' => str_pad($split[1], 6, '0', STR_PAD_RIGHT), + 'usec' => str_pad((string) $microseconds, 6, '0', STR_PAD_RIGHT), ]; } } diff --git a/src/Rfc4122/UuidV1.php b/src/Rfc4122/UuidV1.php index aa3d8c7..4989915 100644 --- a/src/Rfc4122/UuidV1.php +++ b/src/Rfc4122/UuidV1.php @@ -78,23 +78,12 @@ final class UuidV1 extends Uuid implements UuidInterface { $time = $this->timeConverter->convertTime($this->fields->getTimestamp()); - $microseconds = $time->getMicroSeconds()->toString(); - - if (strlen($microseconds) > 6) { - $roundingDigit = (int) substr($microseconds, 6, 1); - $microseconds = (int) substr($microseconds, 0, 6); - - if ($roundingDigit >= 5) { - $microseconds++; - } - } - try { return new DateTimeImmutable( '@' . $time->getSeconds()->toString() . '.' - . str_pad((string) $microseconds, 6, '0', STR_PAD_LEFT) + . str_pad($time->getMicroSeconds()->toString(), 6, '0', STR_PAD_LEFT) ); } catch (Throwable $e) { throw new DateTimeException($e->getMessage(), (int) $e->getCode(), $e); diff --git a/tests/UuidTest.php b/tests/UuidTest.php index d3347f4..24cdd88 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -7,9 +7,15 @@ namespace Ramsey\Uuid\Test; use Brick\Math\BigDecimal; use Brick\Math\RoundingMode; use DateTimeInterface; +use Mockery; use PHPUnit\Framework\MockObject\MockObject; +use Ramsey\Uuid\Builder\DefaultUuidBuilder; +use Ramsey\Uuid\Codec\StringCodec; use Ramsey\Uuid\Codec\TimestampFirstCombCodec; use Ramsey\Uuid\Codec\TimestampLastCombCodec; +use Ramsey\Uuid\Converter\Number\BigNumberConverter; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Exception\DateTimeException; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Exception\InvalidUuidStringException; use Ramsey\Uuid\Exception\UnsupportedOperationException; @@ -1464,6 +1470,33 @@ class UuidTest extends TestCase $this->assertEquals($uuid->getVersion(), Uuid::UUID_TYPE_HASH_SHA1); } + public function testGetDateTimeThrowsExceptionWhenDateTimeCannotParseDate(): void + { + $numberConverter = new BigNumberConverter(); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + + $timeConverter + ->shouldReceive('convertTime') + ->once() + ->andReturn(new Time(1234567890, '1234567')); + + $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); + $codec = new StringCodec($builder); + + $factory = new UuidFactory(); + $factory->setCodec($codec); + + $uuid = $factory->fromString('b1484596-25dc-11ea-978f-2e728ce88125'); + + $this->expectException(DateTimeException::class); + $this->expectExceptionMessage( + 'DateTimeImmutable::__construct(): Failed to parse time string ' + . '(@1234567890.1234567) at position 18 (7): Unexpected character' + ); + + $uuid->getDateTime(); + } + /** * @param class-string $expectedClass * @param mixed[] $args