UnixTimeGenerator.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. <?php
  2. /**
  3. * This file is part of the ramsey/uuid library
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. *
  8. * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
  9. * @license http://opensource.org/licenses/MIT MIT
  10. */
  11. declare(strict_types=1);
  12. namespace Ramsey\Uuid\Generator;
  13. use Brick\Math\BigInteger;
  14. use DateTimeImmutable;
  15. use DateTimeInterface;
  16. use Ramsey\Uuid\Type\Hexadecimal;
  17. use function hash;
  18. use function pack;
  19. use function str_pad;
  20. use function strlen;
  21. use function substr;
  22. use function substr_replace;
  23. use function unpack;
  24. use const PHP_INT_SIZE;
  25. use const STR_PAD_LEFT;
  26. /**
  27. * UnixTimeGenerator generates bytes that combine a 48-bit timestamp in
  28. * milliseconds since the Unix Epoch with 80 random bits
  29. *
  30. * Code and concepts within this class are borrowed from the symfony/uid package
  31. * and are used under the terms of the MIT license distributed with symfony/uid.
  32. *
  33. * symfony/uid is copyright (c) Fabien Potencier.
  34. *
  35. * @link https://symfony.com/components/Uid Symfony Uid component
  36. * @link https://github.com/symfony/uid/blob/4f9f537e57261519808a7ce1d941490736522bbc/UuidV7.php Symfony UuidV7 class
  37. * @link https://github.com/symfony/uid/blob/6.2/LICENSE MIT License
  38. */
  39. class UnixTimeGenerator implements TimeGeneratorInterface
  40. {
  41. private static string $time = '';
  42. private static ?string $seed = null;
  43. private static int $seedIndex = 0;
  44. /** @var int[] */
  45. private static array $rand = [];
  46. /** @var int[] */
  47. private static array $seedParts;
  48. public function __construct(
  49. private RandomGeneratorInterface $randomGenerator,
  50. private int $intSize = PHP_INT_SIZE
  51. ) {
  52. }
  53. /**
  54. * @param Hexadecimal|int|string|null $node Unused in this generator
  55. * @param int|null $clockSeq Unused in this generator
  56. * @param DateTimeInterface $dateTime A date-time instance to use when
  57. * generating bytes
  58. *
  59. * @inheritDoc
  60. */
  61. public function generate($node = null, ?int $clockSeq = null, ?DateTimeInterface $dateTime = null): string
  62. {
  63. $time = ($dateTime ?? new DateTimeImmutable('now'))->format('Uv');
  64. if ($time > self::$time || ($dateTime !== null && $time !== self::$time)) {
  65. $this->randomize($time);
  66. } else {
  67. $time = $this->increment();
  68. }
  69. if ($this->intSize >= 8) {
  70. $time = substr(pack('J', (int) $time), -6);
  71. } else {
  72. $time = str_pad(BigInteger::of($time)->toBytes(false), 6, "\x00", STR_PAD_LEFT);
  73. }
  74. /** @var non-empty-string */
  75. return $time . pack('n*', self::$rand[1], self::$rand[2], self::$rand[3], self::$rand[4], self::$rand[5]);
  76. }
  77. private function randomize(string $time): void
  78. {
  79. if (self::$seed === null) {
  80. $seed = $this->randomGenerator->generate(16);
  81. self::$seed = $seed;
  82. } else {
  83. $seed = $this->randomGenerator->generate(10);
  84. }
  85. /** @var int[] $rand */
  86. $rand = unpack('n*', $seed);
  87. $rand[1] &= 0x03ff;
  88. self::$rand = $rand;
  89. self::$time = $time;
  90. }
  91. /**
  92. * Special thanks to Nicolas Grekas for sharing the following information:
  93. *
  94. * Within the same ms, we increment the rand part by a random 24-bit number.
  95. *
  96. * Instead of getting this number from random_bytes(), which is slow, we get
  97. * it by sha512-hashing self::$seed. This produces 64 bytes of entropy,
  98. * which we need to split in a list of 24-bit numbers. unpack() first splits
  99. * them into 16 x 32-bit numbers; we take the first byte of each of these
  100. * numbers to get 5 extra 24-bit numbers. Then, we consume those numbers
  101. * one-by-one and run this logic every 21 iterations.
  102. *
  103. * self::$rand holds the random part of the UUID, split into 5 x 16-bit
  104. * numbers for x86 portability. We increment this random part by the next
  105. * 24-bit number in the self::$seedParts list and decrement
  106. * self::$seedIndex.
  107. *
  108. * @link https://twitter.com/nicolasgrekas/status/1583356938825261061 Tweet from Nicolas Grekas
  109. */
  110. private function increment(): string
  111. {
  112. if (self::$seedIndex === 0 && self::$seed !== null) {
  113. self::$seed = hash('sha512', self::$seed, true);
  114. /** @var int[] $s */
  115. $s = unpack('l*', self::$seed);
  116. $s[] = ($s[1] >> 8 & 0xff0000) | ($s[2] >> 16 & 0xff00) | ($s[3] >> 24 & 0xff);
  117. $s[] = ($s[4] >> 8 & 0xff0000) | ($s[5] >> 16 & 0xff00) | ($s[6] >> 24 & 0xff);
  118. $s[] = ($s[7] >> 8 & 0xff0000) | ($s[8] >> 16 & 0xff00) | ($s[9] >> 24 & 0xff);
  119. $s[] = ($s[10] >> 8 & 0xff0000) | ($s[11] >> 16 & 0xff00) | ($s[12] >> 24 & 0xff);
  120. $s[] = ($s[13] >> 8 & 0xff0000) | ($s[14] >> 16 & 0xff00) | ($s[15] >> 24 & 0xff);
  121. self::$seedParts = $s;
  122. self::$seedIndex = 21;
  123. }
  124. self::$rand[5] = 0xffff & $carry = self::$rand[5] + 1 + (self::$seedParts[self::$seedIndex--] & 0xffffff);
  125. self::$rand[4] = 0xffff & $carry = self::$rand[4] + ($carry >> 16);
  126. self::$rand[3] = 0xffff & $carry = self::$rand[3] + ($carry >> 16);
  127. self::$rand[2] = 0xffff & $carry = self::$rand[2] + ($carry >> 16);
  128. self::$rand[1] += $carry >> 16;
  129. if (0xfc00 & self::$rand[1]) {
  130. $time = self::$time;
  131. $mtime = (int) substr($time, -9);
  132. if ($this->intSize >= 8 || strlen($time) < 10) {
  133. $time = (string) ((int) $time + 1);
  134. } elseif ($mtime === 999999999) {
  135. $time = (1 + (int) substr($time, 0, -9)) . '000000000';
  136. } else {
  137. $mtime++;
  138. $time = substr_replace($time, str_pad((string) $mtime, 9, '0', STR_PAD_LEFT), -9);
  139. }
  140. $this->randomize($time);
  141. }
  142. return self::$time;
  143. }
  144. }