UnsignedIntegerMath.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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. return array_pad($bytes, -$numBytes, 0);
  66. }
  67. /**
  68. * @param string $value may be zero-length, never extra leading zeroes
  69. * @param int $numBytes
  70. *
  71. * @return int[]|null
  72. */
  73. private function getBytesFromOctal($value, $numBytes)
  74. {
  75. if ($value === '') {
  76. return array_fill(0, $numBytes, 0);
  77. }
  78. $bits = implode(
  79. '',
  80. array_map(
  81. function ($octalDigit) {
  82. return str_pad(decbin(octdec($octalDigit)), 3, '0', STR_PAD_LEFT);
  83. },
  84. str_split($value, 1)
  85. )
  86. );
  87. $bits = ltrim($bits, '0');
  88. return $bits === '' ? array_fill(0, $numBytes, 0) : static::getBytesFromBits($bits, $numBytes);
  89. }
  90. /**
  91. * @param string $value never zero-length, never extra leading zeroes
  92. * @param int $numBytes
  93. *
  94. * @return int[]|null
  95. */
  96. private function getBytesFromDecimal($value, $numBytes)
  97. {
  98. $valueLength = strlen($value);
  99. $maxSignedIntLength = strlen((string) $this->getMaxSignedInt());
  100. if ($valueLength < $maxSignedIntLength) {
  101. return $this->getBytesFromBits(decbin((int) $value), $numBytes);
  102. }
  103. // Divide by two, so that we have 1 less bit
  104. $carry = 0;
  105. $halfValue = ltrim(
  106. implode(
  107. '',
  108. array_map(
  109. function ($digit) use (&$carry) {
  110. $number = $carry + (int) $digit;
  111. $carry = ($number % 2) * 10;
  112. return (string) $number >> 1;
  113. },
  114. str_split($value, 1)
  115. )
  116. ),
  117. '0'
  118. );
  119. $halfValueBytes = $this->getBytesFromDecimal($halfValue, $numBytes);
  120. if ($halfValueBytes === null) {
  121. return null;
  122. }
  123. $carry = $carry === 0 ? 0 : 1;
  124. $result = array_fill(0, $numBytes, 0);
  125. for ($index = $numBytes - 1; $index >= 0; $index--) {
  126. $byte = $carry + ($halfValueBytes[$index] << 1);
  127. if ($byte <= 0xFF) {
  128. $carry = 0;
  129. } else {
  130. $carry = ($byte & ~0xFF) >> 8;
  131. $byte -= 0x100;
  132. }
  133. $result[$index] = $byte;
  134. }
  135. if ($carry !== 0) {
  136. // Overflow
  137. return null;
  138. }
  139. return $result;
  140. }
  141. /**
  142. * @param string $value never zero-length, never extra leading zeroes
  143. * @param int $numBytes
  144. *
  145. * @return int[]|null
  146. */
  147. private function getBytesFromHexadecimal($value, $numBytes)
  148. {
  149. $valueLength = strlen($value);
  150. if ($valueLength > $numBytes << 1) {
  151. // overflow
  152. return null;
  153. }
  154. $value = str_pad($value, $valueLength + $valueLength % 2, '0', STR_PAD_LEFT);
  155. $bytes = array_map('hexdec', str_split($value, 2));
  156. return array_pad($bytes, -$numBytes, 0);
  157. }
  158. }