UnsignedIntegerMath.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <?php
  2. namespace IPLib\Service;
  3. /**
  4. * Helper class to work with unsigned integers.
  5. *
  6. * @internal
  7. */
  8. class UnsignedIntegerMath
  9. {
  10. /**
  11. * Convert a string containing a decimal, octal or hexadecimal number into its bytes.
  12. *
  13. * @param string $value
  14. * @param int $numBytes the wanted number of bytes
  15. * @param bool $onlyDecimal Only parse decimal numbers
  16. *
  17. * @return int[]|null
  18. */
  19. public function getBytes($value, $numBytes, $onlyDecimal = false)
  20. {
  21. $m = null;
  22. if ($onlyDecimal) {
  23. if (preg_match('/^0*(\d+)$/', $value, $m)) {
  24. return $this->getBytesFromDecimal($m[1], $numBytes);
  25. }
  26. } else {
  27. if (preg_match('/^0[Xx]0*([0-9A-Fa-f]+)$/', $value, $m)) {
  28. return $this->getBytesFromHexadecimal($m[1], $numBytes);
  29. }
  30. if (preg_match('/^0+([0-7]*)$/', $value, $m)) {
  31. return $this->getBytesFromOctal($m[1], $numBytes);
  32. }
  33. if (preg_match('/^[1-9][0-9]*$/', $value)) {
  34. return $this->getBytesFromDecimal($value, $numBytes);
  35. }
  36. }
  37. // Not a valid number
  38. return null;
  39. }
  40. /**
  41. * @return int
  42. */
  43. protected function getMaxSignedInt()
  44. {
  45. return PHP_INT_MAX;
  46. }
  47. /**
  48. * @param string $value never zero-length, never extra leading zeroes
  49. * @param int $numBytes
  50. *
  51. * @return int[]|null
  52. */
  53. private function getBytesFromBits($value, $numBytes)
  54. {
  55. $valueLength = strlen($value);
  56. if ($valueLength > $numBytes << 3) {
  57. // overflow
  58. return null;
  59. }
  60. $remainderBits = $valueLength % 8;
  61. if ($remainderBits !== 0) {
  62. $value = str_pad($value, $valueLength + 8 - $remainderBits, '0', STR_PAD_LEFT);
  63. }
  64. $bytes = array_map('bindec', str_split($value, 8));
  65. /** @var int[] $bytes */
  66. return array_pad($bytes, -$numBytes, 0);
  67. }
  68. /**
  69. * @param string $value may be zero-length, never extra leading zeroes
  70. * @param int $numBytes
  71. *
  72. * @return int[]|null
  73. */
  74. private function getBytesFromOctal($value, $numBytes)
  75. {
  76. if ($value === '') {
  77. return array_fill(0, $numBytes, 0);
  78. }
  79. $bits = implode(
  80. '',
  81. array_map(
  82. function ($octalDigit) {
  83. return str_pad(decbin((int) octdec($octalDigit)), 3, '0', STR_PAD_LEFT);
  84. },
  85. str_split($value, 1)
  86. )
  87. );
  88. $bits = ltrim($bits, '0');
  89. return $bits === '' ? array_fill(0, $numBytes, 0) : self::getBytesFromBits($bits, $numBytes);
  90. }
  91. /**
  92. * @param string $value never zero-length, never extra leading zeroes
  93. * @param int $numBytes
  94. *
  95. * @return int[]|null
  96. */
  97. private function getBytesFromDecimal($value, $numBytes)
  98. {
  99. $valueLength = strlen($value);
  100. $maxSignedIntLength = strlen((string) $this->getMaxSignedInt());
  101. if ($valueLength < $maxSignedIntLength) {
  102. return $this->getBytesFromBits(decbin((int) $value), $numBytes);
  103. }
  104. // Divide by two, so that we have 1 less bit
  105. $carry = 0;
  106. $halfValue = ltrim(
  107. implode(
  108. '',
  109. array_map(
  110. function ($digit) use (&$carry) {
  111. $number = $carry + (int) $digit;
  112. $carry = ($number % 2) * 10;
  113. return (string) $number >> 1;
  114. },
  115. str_split($value, 1)
  116. )
  117. ),
  118. '0'
  119. );
  120. $halfValueBytes = $this->getBytesFromDecimal($halfValue, $numBytes);
  121. if ($halfValueBytes === null) {
  122. return null;
  123. }
  124. $carry = $carry === 0 ? 0 : 1;
  125. $result = array_fill(0, $numBytes, 0);
  126. for ($index = $numBytes - 1; $index >= 0; $index--) {
  127. $byte = $carry + ($halfValueBytes[$index] << 1);
  128. if ($byte <= 0xFF) {
  129. $carry = 0;
  130. } else {
  131. $carry = ($byte & ~0xFF) >> 8;
  132. $byte -= 0x100;
  133. }
  134. $result[$index] = $byte;
  135. }
  136. if ($carry !== 0) {
  137. // Overflow
  138. return null;
  139. }
  140. return $result;
  141. }
  142. /**
  143. * @param string $value never zero-length, never extra leading zeroes
  144. * @param int $numBytes
  145. *
  146. * @return int[]|null
  147. */
  148. private function getBytesFromHexadecimal($value, $numBytes)
  149. {
  150. $valueLength = strlen($value);
  151. if ($valueLength > $numBytes << 1) {
  152. // overflow
  153. return null;
  154. }
  155. $value = str_pad($value, $valueLength + $valueLength % 2, '0', STR_PAD_LEFT);
  156. $bytes = array_map('hexdec', str_split($value, 2));
  157. /** @var int[] $bytes */
  158. return array_pad($bytes, -$numBytes, 0);
  159. }
  160. }