BinaryUtil.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Uid;
  11. use Symfony\Component\Uid\Exception\InvalidArgumentException;
  12. /**
  13. * @internal
  14. *
  15. * @author Nicolas Grekas <p@tchwork.com>
  16. */
  17. class BinaryUtil
  18. {
  19. public const BASE10 = [
  20. '' => '0123456789',
  21. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
  22. ];
  23. public const BASE58 = [
  24. '' => '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
  25. 1 => 0, 1, 2, 3, 4, 5, 6, 7, 8, 'A' => 9,
  26. 'B' => 10, 'C' => 11, 'D' => 12, 'E' => 13, 'F' => 14, 'G' => 15,
  27. 'H' => 16, 'J' => 17, 'K' => 18, 'L' => 19, 'M' => 20, 'N' => 21,
  28. 'P' => 22, 'Q' => 23, 'R' => 24, 'S' => 25, 'T' => 26, 'U' => 27,
  29. 'V' => 28, 'W' => 29, 'X' => 30, 'Y' => 31, 'Z' => 32, 'a' => 33,
  30. 'b' => 34, 'c' => 35, 'd' => 36, 'e' => 37, 'f' => 38, 'g' => 39,
  31. 'h' => 40, 'i' => 41, 'j' => 42, 'k' => 43, 'm' => 44, 'n' => 45,
  32. 'o' => 46, 'p' => 47, 'q' => 48, 'r' => 49, 's' => 50, 't' => 51,
  33. 'u' => 52, 'v' => 53, 'w' => 54, 'x' => 55, 'y' => 56, 'z' => 57,
  34. ];
  35. // https://datatracker.ietf.org/doc/html/rfc9562#section-5.1
  36. // 0x01b21dd213814000 is the number of 100-ns intervals between the
  37. // UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
  38. private const TIME_OFFSET_INT = 0x01B21DD213814000;
  39. private const TIME_OFFSET_BIN = "\x01\xb2\x1d\xd2\x13\x81\x40\x00";
  40. private const TIME_OFFSET_COM1 = "\xfe\x4d\xe2\x2d\xec\x7e\xbf\xff";
  41. private const TIME_OFFSET_COM2 = "\xfe\x4d\xe2\x2d\xec\x7e\xc0\x00";
  42. public static function toBase(string $bytes, array $map): string
  43. {
  44. $base = \strlen($alphabet = $map['']);
  45. $bytes = array_values(unpack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', $bytes));
  46. $digits = '';
  47. while ($count = \count($bytes)) {
  48. $quotient = [];
  49. $remainder = 0;
  50. for ($i = 0; $i !== $count; ++$i) {
  51. $carry = $bytes[$i] + ($remainder << (\PHP_INT_SIZE >= 8 ? 16 : 8));
  52. $digit = intdiv($carry, $base);
  53. $remainder = $carry % $base;
  54. if ($digit || $quotient) {
  55. $quotient[] = $digit;
  56. }
  57. }
  58. $digits = $alphabet[$remainder].$digits;
  59. $bytes = $quotient;
  60. }
  61. return $digits;
  62. }
  63. public static function fromBase(string $digits, array $map): string
  64. {
  65. $base = \strlen($map['']);
  66. $count = \strlen($digits);
  67. $bytes = [];
  68. while ($count) {
  69. $quotient = [];
  70. $remainder = 0;
  71. for ($i = 0; $i !== $count; ++$i) {
  72. $carry = ($bytes ? $digits[$i] : $map[$digits[$i]]) + $remainder * $base;
  73. if (\PHP_INT_SIZE >= 8) {
  74. $digit = $carry >> 16;
  75. $remainder = $carry & 0xFFFF;
  76. } else {
  77. $digit = $carry >> 8;
  78. $remainder = $carry & 0xFF;
  79. }
  80. if ($digit || $quotient) {
  81. $quotient[] = $digit;
  82. }
  83. }
  84. $bytes[] = $remainder;
  85. $count = \count($digits = $quotient);
  86. }
  87. return pack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', ...array_reverse($bytes));
  88. }
  89. public static function add(string $a, string $b): string
  90. {
  91. $carry = 0;
  92. for ($i = 7; 0 <= $i; --$i) {
  93. $carry += \ord($a[$i]) + \ord($b[$i]);
  94. $a[$i] = \chr($carry & 0xFF);
  95. $carry >>= 8;
  96. }
  97. return $a;
  98. }
  99. /**
  100. * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
  101. *
  102. * @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 as a numeric string
  103. */
  104. public static function hexToNumericString(string $time): string
  105. {
  106. if (\PHP_INT_SIZE >= 8) {
  107. $time = (string) (hexdec($time) - self::TIME_OFFSET_INT);
  108. } else {
  109. $time = str_pad(hex2bin($time), 8, "\0", \STR_PAD_LEFT);
  110. if (self::TIME_OFFSET_BIN <= $time) {
  111. $time = self::add($time, self::TIME_OFFSET_COM2);
  112. $time[0] = $time[0] & "\x7F";
  113. $time = self::toBase($time, self::BASE10);
  114. } else {
  115. $time = self::add($time, self::TIME_OFFSET_COM1);
  116. $time = '-'.self::toBase($time ^ "\xff\xff\xff\xff\xff\xff\xff\xff", self::BASE10);
  117. }
  118. }
  119. if (9 > \strlen($time)) {
  120. $time = '-' === $time[0] ? '-'.str_pad(substr($time, 1), 8, '0', \STR_PAD_LEFT) : str_pad($time, 8, '0', \STR_PAD_LEFT);
  121. }
  122. return $time;
  123. }
  124. /**
  125. * Sub-microseconds are lost since they are not handled by \DateTimeImmutable.
  126. *
  127. * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
  128. */
  129. public static function hexToDateTime(string $time): \DateTimeImmutable
  130. {
  131. return \DateTimeImmutable::createFromFormat('U.u?', substr_replace(self::hexToNumericString($time), '.', -7, 0));
  132. }
  133. /**
  134. * @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
  135. */
  136. public static function dateTimeToHex(\DateTimeInterface $time): string
  137. {
  138. if (\PHP_INT_SIZE >= 8) {
  139. if (-self::TIME_OFFSET_INT > $time = (int) $time->format('Uu0')) {
  140. throw new InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.');
  141. }
  142. return str_pad(dechex(self::TIME_OFFSET_INT + $time), 16, '0', \STR_PAD_LEFT);
  143. }
  144. $time = $time->format('Uu0');
  145. $negative = '-' === $time[0];
  146. if ($negative && self::TIME_OFFSET_INT < $time = substr($time, 1)) {
  147. throw new InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.');
  148. }
  149. $time = self::fromBase($time, self::BASE10);
  150. $time = str_pad($time, 8, "\0", \STR_PAD_LEFT);
  151. if ($negative) {
  152. $time = self::add($time, self::TIME_OFFSET_COM1) ^ "\xff\xff\xff\xff\xff\xff\xff\xff";
  153. } else {
  154. $time = self::add($time, self::TIME_OFFSET_BIN);
  155. }
  156. return bin2hex($time);
  157. }
  158. }