1<?php
2
3/**
4 * Common String Functions
5 *
6 * PHP version 5
7 *
8 * @category  Common
9 * @package   Functions\Strings
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
16namespace phpseclib3\Common\Functions;
17
18use phpseclib3\Math\BigInteger;
19use phpseclib3\Math\Common\FiniteField;
20
21/**
22 * Common String Functions
23 *
24 * @package Functions\Strings
25 * @author  Jim Wigginton <terrafrost@php.net>
26 */
27abstract class Strings
28{
29    /**
30     * String Shift
31     *
32     * Inspired by array_shift
33     *
34     * @param string $string
35     * @param int $index
36     * @access public
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
53     * @access public
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
163     * @access public
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     *
236     * @access private
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     *
260     * @access public
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     *
307     * @access public
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     *
346     * @access public
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
377     * @access public
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}
429