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