Subversion Repositories oidplus

Rev

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
}