Rev 846 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
827 | daniel-mar | 1 | <?php |
2 | |||
3 | /** |
||
4 | * Common String Functions |
||
5 | * |
||
6 | * PHP version 5 |
||
7 | * |
||
874 | daniel-mar | 8 | * @category Common |
9 | * @package Functions\Strings |
||
827 | daniel-mar | 10 | * @author Jim Wigginton <terrafrost@php.net> |
11 | * @copyright 2016 Jim Wigginton |
||
12 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
||
13 | * @link http://phpseclib.sourceforge.net |
||
14 | */ |
||
15 | |||
16 | namespace phpseclib3\Common\Functions; |
||
17 | |||
18 | use phpseclib3\Math\BigInteger; |
||
19 | use phpseclib3\Math\Common\FiniteField; |
||
20 | |||
21 | /** |
||
22 | * Common String Functions |
||
23 | * |
||
874 | daniel-mar | 24 | * @package Functions\Strings |
827 | daniel-mar | 25 | * @author Jim Wigginton <terrafrost@php.net> |
26 | */ |
||
27 | abstract class Strings |
||
28 | { |
||
29 | /** |
||
30 | * String Shift |
||
31 | * |
||
32 | * Inspired by array_shift |
||
33 | * |
||
34 | * @param string $string |
||
35 | * @param int $index |
||
874 | daniel-mar | 36 | * @access public |
827 | daniel-mar | 37 | * @return string |
38 | */ |
||
39 | public static function shift(&$string, $index = 1) |
||
40 | { |
||
41 | $substr = substr($string, 0, $index); |
||
42 | $string = substr($string, $index); |
||
43 | return $substr; |
||
44 | } |
||
45 | |||
46 | /** |
||
47 | * String Pop |
||
48 | * |
||
49 | * Inspired by array_pop |
||
50 | * |
||
51 | * @param string $string |
||
52 | * @param int $index |
||
874 | daniel-mar | 53 | * @access public |
827 | daniel-mar | 54 | * @return string |
55 | */ |
||
56 | public static function pop(&$string, $index = 1) |
||
57 | { |
||
58 | $substr = substr($string, -$index); |
||
59 | $string = substr($string, 0, -$index); |
||
60 | return $substr; |
||
61 | } |
||
62 | |||
63 | /** |
||
64 | * Parse SSH2-style string |
||
65 | * |
||
66 | * Returns either an array or a boolean if $data is malformed. |
||
67 | * |
||
68 | * Valid characters for $format are as follows: |
||
69 | * |
||
70 | * C = byte |
||
71 | * b = boolean (true/false) |
||
72 | * N = uint32 |
||
73 | * Q = uint64 |
||
74 | * s = string |
||
75 | * i = mpint |
||
76 | * L = name-list |
||
77 | * |
||
78 | * uint64 is not supported. |
||
79 | * |
||
80 | * @param string $format |
||
81 | * @param string $data |
||
82 | * @return mixed |
||
83 | */ |
||
84 | public static function unpackSSH2($format, &$data) |
||
85 | { |
||
86 | $format = self::formatPack($format); |
||
87 | $result = []; |
||
88 | for ($i = 0; $i < strlen($format); $i++) { |
||
89 | switch ($format[$i]) { |
||
90 | case 'C': |
||
91 | case 'b': |
||
92 | if (!strlen($data)) { |
||
93 | throw new \LengthException('At least one byte needs to be present for successful C / b decodes'); |
||
94 | } |
||
95 | break; |
||
96 | case 'N': |
||
97 | case 'i': |
||
98 | case 's': |
||
99 | case 'L': |
||
100 | if (strlen($data) < 4) { |
||
101 | throw new \LengthException('At least four byte needs to be present for successful N / i / s / L decodes'); |
||
102 | } |
||
103 | break; |
||
104 | case 'Q': |
||
105 | if (strlen($data) < 8) { |
||
106 | throw new \LengthException('At least eight byte needs to be present for successful N / i / s / L decodes'); |
||
107 | } |
||
108 | break; |
||
109 | |||
110 | default: |
||
111 | throw new \InvalidArgumentException('$format contains an invalid character'); |
||
112 | } |
||
113 | switch ($format[$i]) { |
||
114 | case 'C': |
||
115 | $result[] = ord(self::shift($data)); |
||
116 | continue 2; |
||
117 | case 'b': |
||
118 | $result[] = ord(self::shift($data)) != 0; |
||
119 | continue 2; |
||
120 | case 'N': |
||
121 | list(, $temp) = unpack('N', self::shift($data, 4)); |
||
122 | $result[] = $temp; |
||
123 | continue 2; |
||
124 | case 'Q': |
||
125 | // pack() added support for Q in PHP 5.6.3 and PHP 5.6 is phpseclib 3's minimum version |
||
126 | // so in theory we could support this BUT, "64-bit format codes are not available for |
||
127 | // 32-bit versions" and phpseclib works on 32-bit installs. on 32-bit installs |
||
128 | // 64-bit floats can be used to get larger numbers then 32-bit signed ints would allow |
||
129 | // for. sure, you're not gonna get the full precision of 64-bit numbers but just because |
||
130 | // you need > 32-bit precision doesn't mean you need the full 64-bit precision |
||
131 | extract(unpack('Nupper/Nlower', self::shift($data, 8))); |
||
132 | $temp = $upper ? 4294967296 * $upper : 0; |
||
133 | $temp += $lower < 0 ? ($lower & 0x7FFFFFFFF) + 0x80000000 : $lower; |
||
134 | // $temp = hexdec(bin2hex(self::shift($data, 8))); |
||
135 | $result[] = $temp; |
||
136 | continue 2; |
||
137 | } |
||
138 | list(, $length) = unpack('N', self::shift($data, 4)); |
||
139 | if (strlen($data) < $length) { |
||
140 | throw new \LengthException("$length bytes needed; " . strlen($data) . ' bytes available'); |
||
141 | } |
||
142 | $temp = self::shift($data, $length); |
||
143 | switch ($format[$i]) { |
||
144 | case 'i': |
||
145 | $result[] = new BigInteger($temp, -256); |
||
146 | break; |
||
147 | case 's': |
||
148 | $result[] = $temp; |
||
149 | break; |
||
150 | case 'L': |
||
151 | $result[] = explode(',', $temp); |
||
152 | } |
||
153 | } |
||
154 | |||
155 | return $result; |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | * Create SSH2-style string |
||
160 | * |
||
161 | * @param string $format |
||
162 | * @param string|int|float|array|bool ...$elements |
||
874 | daniel-mar | 163 | * @access public |
827 | daniel-mar | 164 | * @return string |
165 | */ |
||
166 | public static function packSSH2($format, ...$elements) |
||
167 | { |
||
168 | $format = self::formatPack($format); |
||
169 | if (strlen($format) != count($elements)) { |
||
170 | throw new \InvalidArgumentException('There must be as many arguments as there are characters in the $format string'); |
||
171 | } |
||
172 | $result = ''; |
||
173 | for ($i = 0; $i < strlen($format); $i++) { |
||
174 | $element = $elements[$i]; |
||
175 | switch ($format[$i]) { |
||
176 | case 'C': |
||
177 | if (!is_int($element)) { |
||
178 | throw new \InvalidArgumentException('Bytes must be represented as an integer between 0 and 255, inclusive.'); |
||
179 | } |
||
180 | $result .= pack('C', $element); |
||
181 | break; |
||
182 | case 'b': |
||
183 | if (!is_bool($element)) { |
||
184 | throw new \InvalidArgumentException('A boolean parameter was expected.'); |
||
185 | } |
||
186 | $result .= $element ? "\1" : "\0"; |
||
187 | break; |
||
188 | case 'Q': |
||
189 | if (!is_int($element) && !is_float($element)) { |
||
190 | throw new \InvalidArgumentException('An integer was expected.'); |
||
191 | } |
||
192 | // 4294967296 == 1 << 32 |
||
193 | $result .= pack('NN', $element / 4294967296, $element); |
||
194 | break; |
||
195 | case 'N': |
||
196 | if (is_float($element)) { |
||
197 | $element = (int) $element; |
||
198 | } |
||
199 | if (!is_int($element)) { |
||
200 | throw new \InvalidArgumentException('An integer was expected.'); |
||
201 | } |
||
202 | $result .= pack('N', $element); |
||
203 | break; |
||
204 | case 's': |
||
205 | if (!self::is_stringable($element)) { |
||
206 | throw new \InvalidArgumentException('A string was expected.'); |
||
207 | } |
||
208 | $result .= pack('Na*', strlen($element), $element); |
||
209 | break; |
||
210 | case 'i': |
||
211 | if (!$element instanceof BigInteger && !$element instanceof FiniteField\Integer) { |
||
212 | throw new \InvalidArgumentException('A phpseclib3\Math\BigInteger or phpseclib3\Math\Common\FiniteField\Integer object was expected.'); |
||
213 | } |
||
214 | $element = $element->toBytes(true); |
||
215 | $result .= pack('Na*', strlen($element), $element); |
||
216 | break; |
||
217 | case 'L': |
||
218 | if (!is_array($element)) { |
||
219 | throw new \InvalidArgumentException('An array was expected.'); |
||
220 | } |
||
221 | $element = implode(',', $element); |
||
222 | $result .= pack('Na*', strlen($element), $element); |
||
223 | break; |
||
224 | default: |
||
225 | throw new \InvalidArgumentException('$format contains an invalid character'); |
||
226 | } |
||
227 | } |
||
228 | return $result; |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * Expand a pack string |
||
233 | * |
||
234 | * Converts C5 to CCCCC, for example. |
||
235 | * |
||
874 | daniel-mar | 236 | * @access private |
827 | daniel-mar | 237 | * @param string $format |
238 | * @return string |
||
239 | */ |
||
240 | private static function formatPack($format) |
||
241 | { |
||
242 | $parts = preg_split('#(\d+)#', $format, -1, PREG_SPLIT_DELIM_CAPTURE); |
||
243 | $format = ''; |
||
244 | for ($i = 1; $i < count($parts); $i += 2) { |
||
245 | $format .= substr($parts[$i - 1], 0, -1) . str_repeat(substr($parts[$i - 1], -1), $parts[$i]); |
||
246 | } |
||
247 | $format .= $parts[$i - 1]; |
||
248 | |||
249 | return $format; |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * Convert binary data into bits |
||
254 | * |
||
255 | * bin2hex / hex2bin refer to base-256 encoded data as binary, whilst |
||
256 | * decbin / bindec refer to base-2 encoded data as binary. For the purposes |
||
257 | * of this function, bin refers to base-256 encoded data whilst bits refers |
||
258 | * to base-2 encoded data |
||
259 | * |
||
874 | daniel-mar | 260 | * @access public |
827 | daniel-mar | 261 | * @param string $x |
262 | * @return string |
||
263 | */ |
||
264 | public static function bits2bin($x) |
||
265 | { |
||
266 | /* |
||
267 | // the pure-PHP approach is faster than the GMP approach |
||
268 | if (function_exists('gmp_export')) { |
||
269 | return strlen($x) ? gmp_export(gmp_init($x, 2)) : gmp_init(0); |
||
270 | } |
||
271 | */ |
||
272 | |||
273 | if (preg_match('#[^01]#', $x)) { |
||
274 | throw new \RuntimeException('The only valid characters are 0 and 1'); |
||
275 | } |
||
276 | |||
277 | if (!defined('PHP_INT_MIN')) { |
||
278 | define('PHP_INT_MIN', ~PHP_INT_MAX); |
||
279 | } |
||
280 | |||
281 | $length = strlen($x); |
||
282 | if (!$length) { |
||
283 | return ''; |
||
284 | } |
||
285 | $block_size = PHP_INT_SIZE << 3; |
||
286 | $pad = $block_size - ($length % $block_size); |
||
287 | if ($pad != $block_size) { |
||
288 | $x = str_repeat('0', $pad) . $x; |
||
289 | } |
||
290 | |||
291 | $parts = str_split($x, $block_size); |
||
292 | $str = ''; |
||
293 | foreach ($parts as $part) { |
||
294 | $xor = $part[0] == '1' ? PHP_INT_MIN : 0; |
||
295 | $part[0] = '0'; |
||
296 | $str .= pack( |
||
297 | PHP_INT_SIZE == 4 ? 'N' : 'J', |
||
298 | $xor ^ eval('return 0b' . $part . ';') |
||
299 | ); |
||
300 | } |
||
301 | return ltrim($str, "\0"); |
||
302 | } |
||
303 | |||
304 | /** |
||
305 | * Convert bits to binary data |
||
306 | * |
||
874 | daniel-mar | 307 | * @access public |
827 | daniel-mar | 308 | * @param string $x |
309 | * @return string |
||
310 | */ |
||
311 | public static function bin2bits($x, $trim = true) |
||
312 | { |
||
313 | /* |
||
314 | // the pure-PHP approach is slower than the GMP approach BUT |
||
315 | // i want to the pure-PHP version to be easily unit tested as well |
||
316 | if (function_exists('gmp_import')) { |
||
317 | return gmp_strval(gmp_import($x), 2); |
||
318 | } |
||
319 | */ |
||
320 | |||
321 | $len = strlen($x); |
||
322 | $mod = $len % PHP_INT_SIZE; |
||
323 | if ($mod) { |
||
324 | $x = str_pad($x, $len + PHP_INT_SIZE - $mod, "\0", STR_PAD_LEFT); |
||
325 | } |
||
326 | |||
327 | $bits = ''; |
||
328 | if (PHP_INT_SIZE == 4) { |
||
329 | $digits = unpack('N*', $x); |
||
330 | foreach ($digits as $digit) { |
||
331 | $bits .= sprintf('%032b', $digit); |
||
332 | } |
||
333 | } else { |
||
334 | $digits = unpack('J*', $x); |
||
335 | foreach ($digits as $digit) { |
||
336 | $bits .= sprintf('%064b', $digit); |
||
337 | } |
||
338 | } |
||
339 | |||
340 | return $trim ? ltrim($bits, '0') : $bits; |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * Switch Endianness Bit Order |
||
345 | * |
||
874 | daniel-mar | 346 | * @access public |
827 | daniel-mar | 347 | * @param string $x |
348 | * @return string |
||
349 | */ |
||
350 | public static function switchEndianness($x) |
||
351 | { |
||
352 | $r = ''; |
||
353 | for ($i = strlen($x) - 1; $i >= 0; $i--) { |
||
354 | $b = ord($x[$i]); |
||
355 | if (PHP_INT_SIZE === 8) { |
||
356 | // 3 operations |
||
357 | // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64BitsDiv |
||
358 | $r .= chr((($b * 0x0202020202) & 0x010884422010) % 1023); |
||
359 | } else { |
||
360 | // 7 operations |
||
361 | // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith32Bits |
||
362 | $p1 = ($b * 0x0802) & 0x22110; |
||
363 | $p2 = ($b * 0x8020) & 0x88440; |
||
364 | $r .= chr( |
||
365 | (($p1 | $p2) * 0x10101) >> 16 |
||
366 | ); |
||
367 | } |
||
368 | } |
||
369 | return $r; |
||
370 | } |
||
371 | |||
372 | /** |
||
373 | * Increment the current string |
||
374 | * |
||
375 | * @param string $var |
||
376 | * @return string |
||
874 | daniel-mar | 377 | * @access public |
827 | daniel-mar | 378 | */ |
379 | public static function increment_str(&$var) |
||
380 | { |
||
381 | if (function_exists('sodium_increment')) { |
||
382 | $var = strrev($var); |
||
383 | sodium_increment($var); |
||
384 | $var = strrev($var); |
||
385 | return $var; |
||
386 | } |
||
387 | |||
388 | for ($i = 4; $i <= strlen($var); $i += 4) { |
||
389 | $temp = substr($var, -$i, 4); |
||
390 | switch ($temp) { |
||
391 | case "\xFF\xFF\xFF\xFF": |
||
392 | $var = substr_replace($var, "\x00\x00\x00\x00", -$i, 4); |
||
393 | break; |
||
394 | case "\x7F\xFF\xFF\xFF": |
||
395 | $var = substr_replace($var, "\x80\x00\x00\x00", -$i, 4); |
||
396 | return $var; |
||
397 | default: |
||
398 | $temp = unpack('Nnum', $temp); |
||
399 | $var = substr_replace($var, pack('N', $temp['num'] + 1), -$i, 4); |
||
400 | return $var; |
||
401 | } |
||
402 | } |
||
403 | |||
404 | $remainder = strlen($var) % 4; |
||
405 | |||
406 | if ($remainder == 0) { |
||
407 | return $var; |
||
408 | } |
||
409 | |||
410 | $temp = unpack('Nnum', str_pad(substr($var, 0, $remainder), 4, "\0", STR_PAD_LEFT)); |
||
411 | $temp = substr(pack('N', $temp['num'] + 1), -$remainder); |
||
412 | $var = substr_replace($var, $temp, 0, $remainder); |
||
413 | |||
414 | return $var; |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Find whether the type of a variable is string (or could be converted to one) |
||
419 | * |
||
420 | * @param mixed $var |
||
421 | * @return bool |
||
422 | * @psalm-assert-if-true string|\Stringable $var |
||
423 | */ |
||
424 | public static function is_stringable($var) |
||
425 | { |
||
426 | return is_string($var) || (is_object($var) && method_exists($var, '__toString')); |
||
427 | } |
||
428 | } |