ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

Util.php (28069B)


      1 <?php
      2 
      3 if (class_exists('ParagonIE_Sodium_Core_Util', false)) {
      4     return;
      5 }
      6 
      7 /**
      8  * Class ParagonIE_Sodium_Core_Util
      9  */
     10 abstract class ParagonIE_Sodium_Core_Util
     11 {
     12     /**
     13      * @param int $integer
     14      * @param int $size (16, 32, 64)
     15      * @return int
     16      */
     17     public static function abs($integer, $size = 0)
     18     {
     19         /** @var int $realSize */
     20         $realSize = (PHP_INT_SIZE << 3) - 1;
     21         if ($size) {
     22             --$size;
     23         } else {
     24             /** @var int $size */
     25             $size = $realSize;
     26         }
     27 
     28         $negative = -(($integer >> $size) & 1);
     29         return (int) (
     30             ($integer ^ $negative)
     31                 +
     32             (($negative >> $realSize) & 1)
     33         );
     34     }
     35 
     36     /**
     37      * Convert a binary string into a hexadecimal string without cache-timing
     38      * leaks
     39      *
     40      * @internal You should not use this directly from another application
     41      *
     42      * @param string $binaryString (raw binary)
     43      * @return string
     44      * @throws TypeError
     45      */
     46     public static function bin2hex($binaryString)
     47     {
     48         /* Type checks: */
     49         if (!is_string($binaryString)) {
     50             throw new TypeError('Argument 1 must be a string, ' . gettype($binaryString) . ' given.');
     51         }
     52 
     53         $hex = '';
     54         $len = self::strlen($binaryString);
     55         for ($i = 0; $i < $len; ++$i) {
     56             /** @var array<int, int> $chunk */
     57             $chunk = unpack('C', $binaryString[$i]);
     58             /** @var int $c */
     59             $c = $chunk[1] & 0xf;
     60             /** @var int $b */
     61             $b = $chunk[1] >> 4;
     62             $hex .= pack(
     63                 'CC',
     64                 (87 + $b + ((($b - 10) >> 8) & ~38)),
     65                 (87 + $c + ((($c - 10) >> 8) & ~38))
     66             );
     67         }
     68         return $hex;
     69     }
     70 
     71     /**
     72      * Convert a binary string into a hexadecimal string without cache-timing
     73      * leaks, returning uppercase letters (as per RFC 4648)
     74      *
     75      * @internal You should not use this directly from another application
     76      *
     77      * @param string $bin_string (raw binary)
     78      * @return string
     79      * @throws TypeError
     80      */
     81     public static function bin2hexUpper($bin_string)
     82     {
     83         $hex = '';
     84         $len = self::strlen($bin_string);
     85         for ($i = 0; $i < $len; ++$i) {
     86             /** @var array<int, int> $chunk */
     87             $chunk = unpack('C', $bin_string[$i]);
     88             /**
     89              * Lower 16 bits
     90              *
     91              * @var int $c
     92              */
     93             $c = $chunk[1] & 0xf;
     94 
     95             /**
     96              * Upper 16 bits
     97              * @var int $b
     98              */
     99             $b = $chunk[1] >> 4;
    100 
    101             /**
    102              * Use pack() and binary operators to turn the two integers
    103              * into hexadecimal characters. We don't use chr() here, because
    104              * it uses a lookup table internally and we want to avoid
    105              * cache-timing side-channels.
    106              */
    107             $hex .= pack(
    108                 'CC',
    109                 (55 + $b + ((($b - 10) >> 8) & ~6)),
    110                 (55 + $c + ((($c - 10) >> 8) & ~6))
    111             );
    112         }
    113         return $hex;
    114     }
    115 
    116     /**
    117      * Cache-timing-safe variant of ord()
    118      *
    119      * @internal You should not use this directly from another application
    120      *
    121      * @param string $chr
    122      * @return int
    123      * @throws SodiumException
    124      * @throws TypeError
    125      */
    126     public static function chrToInt($chr)
    127     {
    128         /* Type checks: */
    129         if (!is_string($chr)) {
    130             throw new TypeError('Argument 1 must be a string, ' . gettype($chr) . ' given.');
    131         }
    132         if (self::strlen($chr) !== 1) {
    133             throw new SodiumException('chrToInt() expects a string that is exactly 1 character long');
    134         }
    135         /** @var array<int, int> $chunk */
    136         $chunk = unpack('C', $chr);
    137         return (int) ($chunk[1]);
    138     }
    139 
    140     /**
    141      * Compares two strings.
    142      *
    143      * @internal You should not use this directly from another application
    144      *
    145      * @param string $left
    146      * @param string $right
    147      * @param int $len
    148      * @return int
    149      * @throws SodiumException
    150      * @throws TypeError
    151      */
    152     public static function compare($left, $right, $len = null)
    153     {
    154         $leftLen = self::strlen($left);
    155         $rightLen = self::strlen($right);
    156         if ($len === null) {
    157             $len = max($leftLen, $rightLen);
    158             $left = str_pad($left, $len, "\x00", STR_PAD_RIGHT);
    159             $right = str_pad($right, $len, "\x00", STR_PAD_RIGHT);
    160         }
    161 
    162         $gt = 0;
    163         $eq = 1;
    164         $i = $len;
    165         while ($i !== 0) {
    166             --$i;
    167             $gt |= ((self::chrToInt($right[$i]) - self::chrToInt($left[$i])) >> 8) & $eq;
    168             $eq &= ((self::chrToInt($right[$i]) ^ self::chrToInt($left[$i])) - 1) >> 8;
    169         }
    170         return ($gt + $gt + $eq) - 1;
    171     }
    172 
    173     /**
    174      * If a variable does not match a given type, throw a TypeError.
    175      *
    176      * @param mixed $mixedVar
    177      * @param string $type
    178      * @param int $argumentIndex
    179      * @throws TypeError
    180      * @throws SodiumException
    181      * @return void
    182      */
    183     public static function declareScalarType(&$mixedVar = null, $type = 'void', $argumentIndex = 0)
    184     {
    185         if (func_num_args() === 0) {
    186             /* Tautology, by default */
    187             return;
    188         }
    189         if (func_num_args() === 1) {
    190             throw new TypeError('Declared void, but passed a variable');
    191         }
    192         $realType = strtolower(gettype($mixedVar));
    193         $type = strtolower($type);
    194         switch ($type) {
    195             case 'null':
    196                 if ($mixedVar !== null) {
    197                     throw new TypeError('Argument ' . $argumentIndex . ' must be null, ' . $realType . ' given.');
    198                 }
    199                 break;
    200             case 'integer':
    201             case 'int':
    202                 $allow = array('int', 'integer');
    203                 if (!in_array($type, $allow)) {
    204                     throw new TypeError('Argument ' . $argumentIndex . ' must be an integer, ' . $realType . ' given.');
    205                 }
    206                 $mixedVar = (int) $mixedVar;
    207                 break;
    208             case 'boolean':
    209             case 'bool':
    210                 $allow = array('bool', 'boolean');
    211                 if (!in_array($type, $allow)) {
    212                     throw new TypeError('Argument ' . $argumentIndex . ' must be a boolean, ' . $realType . ' given.');
    213                 }
    214                 $mixedVar = (bool) $mixedVar;
    215                 break;
    216             case 'string':
    217                 if (!is_string($mixedVar)) {
    218                     throw new TypeError('Argument ' . $argumentIndex . ' must be a string, ' . $realType . ' given.');
    219                 }
    220                 $mixedVar = (string) $mixedVar;
    221                 break;
    222             case 'decimal':
    223             case 'double':
    224             case 'float':
    225                 $allow = array('decimal', 'double', 'float');
    226                 if (!in_array($type, $allow)) {
    227                     throw new TypeError('Argument ' . $argumentIndex . ' must be a float, ' . $realType . ' given.');
    228                 }
    229                 $mixedVar = (float) $mixedVar;
    230                 break;
    231             case 'object':
    232                 if (!is_object($mixedVar)) {
    233                     throw new TypeError('Argument ' . $argumentIndex . ' must be an object, ' . $realType . ' given.');
    234                 }
    235                 break;
    236             case 'array':
    237                 if (!is_array($mixedVar)) {
    238                     if (is_object($mixedVar)) {
    239                         if ($mixedVar instanceof ArrayAccess) {
    240                             return;
    241                         }
    242                     }
    243                     throw new TypeError('Argument ' . $argumentIndex . ' must be an array, ' . $realType . ' given.');
    244                 }
    245                 break;
    246             default:
    247                 throw new SodiumException('Unknown type (' . $realType .') does not match expect type (' . $type . ')');
    248         }
    249     }
    250 
    251     /**
    252      * Evaluate whether or not two strings are equal (in constant-time)
    253      *
    254      * @param string $left
    255      * @param string $right
    256      * @return bool
    257      * @throws SodiumException
    258      * @throws TypeError
    259      */
    260     public static function hashEquals($left, $right)
    261     {
    262         /* Type checks: */
    263         if (!is_string($left)) {
    264             throw new TypeError('Argument 1 must be a string, ' . gettype($left) . ' given.');
    265         }
    266         if (!is_string($right)) {
    267             throw new TypeError('Argument 2 must be a string, ' . gettype($right) . ' given.');
    268         }
    269 
    270         if (is_callable('hash_equals')) {
    271             return hash_equals($left, $right);
    272         }
    273         $d = 0;
    274         /** @var int $len */
    275         $len = self::strlen($left);
    276         if ($len !== self::strlen($right)) {
    277             return false;
    278         }
    279         for ($i = 0; $i < $len; ++$i) {
    280             $d |= self::chrToInt($left[$i]) ^ self::chrToInt($right[$i]);
    281         }
    282 
    283         if ($d !== 0) {
    284             return false;
    285         }
    286         return $left === $right;
    287     }
    288 
    289     /**
    290      * Catch hash_update() failures and throw instead of silently proceding
    291      *
    292      * @param HashContext|resource &$hs
    293      * @param string $data
    294      * @return void
    295      * @throws SodiumException
    296      * @psalm-suppress PossiblyInvalidArgument
    297      */
    298     protected static function hash_update(&$hs, $data)
    299     {
    300         if (!hash_update($hs, $data)) {
    301             throw new SodiumException('hash_update() failed');
    302         }
    303     }
    304 
    305     /**
    306      * Convert a hexadecimal string into a binary string without cache-timing
    307      * leaks
    308      *
    309      * @internal You should not use this directly from another application
    310      *
    311      * @param string $hexString
    312      * @param bool $strictPadding
    313      * @return string (raw binary)
    314      * @throws RangeException
    315      * @throws TypeError
    316      */
    317     public static function hex2bin($hexString, $strictPadding = false)
    318     {
    319         /* Type checks: */
    320         if (!is_string($hexString)) {
    321             throw new TypeError('Argument 1 must be a string, ' . gettype($hexString) . ' given.');
    322         }
    323 
    324         /** @var int $hex_pos */
    325         $hex_pos = 0;
    326         /** @var string $bin */
    327         $bin = '';
    328         /** @var int $c_acc */
    329         $c_acc = 0;
    330         /** @var int $hex_len */
    331         $hex_len = self::strlen($hexString);
    332         /** @var int $state */
    333         $state = 0;
    334         if (($hex_len & 1) !== 0) {
    335             if ($strictPadding) {
    336                 throw new RangeException(
    337                     'Expected an even number of hexadecimal characters'
    338                 );
    339             } else {
    340                 $hexString = '0' . $hexString;
    341                 ++$hex_len;
    342             }
    343         }
    344 
    345         $chunk = unpack('C*', $hexString);
    346         while ($hex_pos < $hex_len) {
    347             ++$hex_pos;
    348             /** @var int $c */
    349             $c = $chunk[$hex_pos];
    350             /** @var int $c_num */
    351             $c_num = $c ^ 48;
    352             /** @var int $c_num0 */
    353             $c_num0 = ($c_num - 10) >> 8;
    354             /** @var int $c_alpha */
    355             $c_alpha = ($c & ~32) - 55;
    356             /** @var int $c_alpha0 */
    357             $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
    358             if (($c_num0 | $c_alpha0) === 0) {
    359                 throw new RangeException(
    360                     'hex2bin() only expects hexadecimal characters'
    361                 );
    362             }
    363             /** @var int $c_val */
    364             $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
    365             if ($state === 0) {
    366                 $c_acc = $c_val * 16;
    367             } else {
    368                 $bin .= pack('C', $c_acc | $c_val);
    369             }
    370             $state ^= 1;
    371         }
    372         return $bin;
    373     }
    374 
    375     /**
    376      * Turn an array of integers into a string
    377      *
    378      * @internal You should not use this directly from another application
    379      *
    380      * @param array<int, int> $ints
    381      * @return string
    382      */
    383     public static function intArrayToString(array $ints)
    384     {
    385         /** @var array<int, int> $args */
    386         $args = $ints;
    387         foreach ($args as $i => $v) {
    388             $args[$i] = (int) ($v & 0xff);
    389         }
    390         array_unshift($args, str_repeat('C', count($ints)));
    391         return (string) (call_user_func_array('pack', $args));
    392     }
    393 
    394     /**
    395      * Cache-timing-safe variant of ord()
    396      *
    397      * @internal You should not use this directly from another application
    398      *
    399      * @param int $int
    400      * @return string
    401      * @throws TypeError
    402      */
    403     public static function intToChr($int)
    404     {
    405         return pack('C', $int);
    406     }
    407 
    408     /**
    409      * Load a 3 character substring into an integer
    410      *
    411      * @internal You should not use this directly from another application
    412      *
    413      * @param string $string
    414      * @return int
    415      * @throws RangeException
    416      * @throws TypeError
    417      */
    418     public static function load_3($string)
    419     {
    420         /* Type checks: */
    421         if (!is_string($string)) {
    422             throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
    423         }
    424 
    425         /* Input validation: */
    426         if (self::strlen($string) < 3) {
    427             throw new RangeException(
    428                 'String must be 3 bytes or more; ' . self::strlen($string) . ' given.'
    429             );
    430         }
    431         /** @var array<int, int> $unpacked */
    432         $unpacked = unpack('V', $string . "\0");
    433         return (int) ($unpacked[1] & 0xffffff);
    434     }
    435 
    436     /**
    437      * Load a 4 character substring into an integer
    438      *
    439      * @internal You should not use this directly from another application
    440      *
    441      * @param string $string
    442      * @return int
    443      * @throws RangeException
    444      * @throws TypeError
    445      */
    446     public static function load_4($string)
    447     {
    448         /* Type checks: */
    449         if (!is_string($string)) {
    450             throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
    451         }
    452 
    453         /* Input validation: */
    454         if (self::strlen($string) < 4) {
    455             throw new RangeException(
    456                 'String must be 4 bytes or more; ' . self::strlen($string) . ' given.'
    457             );
    458         }
    459         /** @var array<int, int> $unpacked */
    460         $unpacked = unpack('V', $string);
    461         return (int) ($unpacked[1] & 0xffffffff);
    462     }
    463 
    464     /**
    465      * Load a 8 character substring into an integer
    466      *
    467      * @internal You should not use this directly from another application
    468      *
    469      * @param string $string
    470      * @return int
    471      * @throws RangeException
    472      * @throws SodiumException
    473      * @throws TypeError
    474      */
    475     public static function load64_le($string)
    476     {
    477         /* Type checks: */
    478         if (!is_string($string)) {
    479             throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
    480         }
    481 
    482         /* Input validation: */
    483         if (self::strlen($string) < 4) {
    484             throw new RangeException(
    485                 'String must be 4 bytes or more; ' . self::strlen($string) . ' given.'
    486             );
    487         }
    488         if (PHP_VERSION_ID >= 50603 && PHP_INT_SIZE === 8) {
    489             /** @var array<int, int> $unpacked */
    490             $unpacked = unpack('P', $string);
    491             return (int) $unpacked[1];
    492         }
    493 
    494         /** @var int $result */
    495         $result  = (self::chrToInt($string[0]) & 0xff);
    496         $result |= (self::chrToInt($string[1]) & 0xff) <<  8;
    497         $result |= (self::chrToInt($string[2]) & 0xff) << 16;
    498         $result |= (self::chrToInt($string[3]) & 0xff) << 24;
    499         $result |= (self::chrToInt($string[4]) & 0xff) << 32;
    500         $result |= (self::chrToInt($string[5]) & 0xff) << 40;
    501         $result |= (self::chrToInt($string[6]) & 0xff) << 48;
    502         $result |= (self::chrToInt($string[7]) & 0xff) << 56;
    503         return (int) $result;
    504     }
    505 
    506     /**
    507      * @internal You should not use this directly from another application
    508      *
    509      * @param string $left
    510      * @param string $right
    511      * @return int
    512      * @throws SodiumException
    513      * @throws TypeError
    514      */
    515     public static function memcmp($left, $right)
    516     {
    517         if (self::hashEquals($left, $right)) {
    518             return 0;
    519         }
    520         return -1;
    521     }
    522 
    523     /**
    524      * Multiply two integers in constant-time
    525      *
    526      * Micro-architecture timing side-channels caused by how your CPU
    527      * implements multiplication are best prevented by never using the
    528      * multiplication operators and ensuring that our code always takes
    529      * the same number of operations to complete, regardless of the values
    530      * of $a and $b.
    531      *
    532      * @internal You should not use this directly from another application
    533      *
    534      * @param int $a
    535      * @param int $b
    536      * @param int $size Limits the number of operations (useful for small,
    537      *                  constant operands)
    538      * @return int
    539      */
    540     public static function mul($a, $b, $size = 0)
    541     {
    542         if (ParagonIE_Sodium_Compat::$fastMult) {
    543             return (int) ($a * $b);
    544         }
    545 
    546         static $defaultSize = null;
    547         /** @var int $defaultSize */
    548         if (!$defaultSize) {
    549             /** @var int $defaultSize */
    550             $defaultSize = (PHP_INT_SIZE << 3) - 1;
    551         }
    552         if ($size < 1) {
    553             /** @var int $size */
    554             $size = $defaultSize;
    555         }
    556         /** @var int $size */
    557 
    558         $c = 0;
    559 
    560         /**
    561          * Mask is either -1 or 0.
    562          *
    563          * -1 in binary looks like 0x1111 ... 1111
    564          *  0 in binary looks like 0x0000 ... 0000
    565          *
    566          * @var int
    567          */
    568         $mask = -(($b >> ((int) $defaultSize)) & 1);
    569 
    570         /**
    571          * Ensure $b is a positive integer, without creating
    572          * a branching side-channel
    573          *
    574          * @var int $b
    575          */
    576         $b = ($b & ~$mask) | ($mask & -$b);
    577 
    578         /**
    579          * Unless $size is provided:
    580          *
    581          * This loop always runs 32 times when PHP_INT_SIZE is 4.
    582          * This loop always runs 64 times when PHP_INT_SIZE is 8.
    583          */
    584         for ($i = $size; $i >= 0; --$i) {
    585             $c += (int) ($a & -($b & 1));
    586             $a <<= 1;
    587             $b >>= 1;
    588         }
    589 
    590         /**
    591          * If $b was negative, we then apply the same value to $c here.
    592          * It doesn't matter much if $a was negative; the $c += above would
    593          * have produced a negative integer to begin with. But a negative $b
    594          * makes $b >>= 1 never return 0, so we would end up with incorrect
    595          * results.
    596          *
    597          * The end result is what we'd expect from integer multiplication.
    598          */
    599         return (int) (($c & ~$mask) | ($mask & -$c));
    600     }
    601 
    602     /**
    603      * Convert any arbitrary numbers into two 32-bit integers that represent
    604      * a 64-bit integer.
    605      *
    606      * @internal You should not use this directly from another application
    607      *
    608      * @param int|float $num
    609      * @return array<int, int>
    610      */
    611     public static function numericTo64BitInteger($num)
    612     {
    613         $high = 0;
    614         /** @var int $low */
    615         $low = $num & 0xffffffff;
    616 
    617         if ((+(abs($num))) >= 1) {
    618             if ($num > 0) {
    619                 /** @var int $high */
    620                 $high = min((+(floor($num/4294967296))), 4294967295);
    621             } else {
    622                 /** @var int $high */
    623                 $high = ~~((+(ceil(($num - (+((~~($num)))))/4294967296))));
    624             }
    625         }
    626         return array((int) $high, (int) $low);
    627     }
    628 
    629     /**
    630      * Store a 24-bit integer into a string, treating it as big-endian.
    631      *
    632      * @internal You should not use this directly from another application
    633      *
    634      * @param int $int
    635      * @return string
    636      * @throws TypeError
    637      */
    638     public static function store_3($int)
    639     {
    640         /* Type checks: */
    641         if (!is_int($int)) {
    642             if (is_numeric($int)) {
    643                 $int = (int) $int;
    644             } else {
    645                 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
    646             }
    647         }
    648         /** @var string $packed */
    649         $packed = pack('N', $int);
    650         return self::substr($packed, 1, 3);
    651     }
    652 
    653     /**
    654      * Store a 32-bit integer into a string, treating it as little-endian.
    655      *
    656      * @internal You should not use this directly from another application
    657      *
    658      * @param int $int
    659      * @return string
    660      * @throws TypeError
    661      */
    662     public static function store32_le($int)
    663     {
    664         /* Type checks: */
    665         if (!is_int($int)) {
    666             if (is_numeric($int)) {
    667                 $int = (int) $int;
    668             } else {
    669                 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
    670             }
    671         }
    672 
    673         /** @var string $packed */
    674         $packed = pack('V', $int);
    675         return $packed;
    676     }
    677 
    678     /**
    679      * Store a 32-bit integer into a string, treating it as big-endian.
    680      *
    681      * @internal You should not use this directly from another application
    682      *
    683      * @param int $int
    684      * @return string
    685      * @throws TypeError
    686      */
    687     public static function store_4($int)
    688     {
    689         /* Type checks: */
    690         if (!is_int($int)) {
    691             if (is_numeric($int)) {
    692                 $int = (int) $int;
    693             } else {
    694                 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
    695             }
    696         }
    697 
    698         /** @var string $packed */
    699         $packed = pack('N', $int);
    700         return $packed;
    701     }
    702 
    703     /**
    704      * Stores a 64-bit integer as an string, treating it as little-endian.
    705      *
    706      * @internal You should not use this directly from another application
    707      *
    708      * @param int $int
    709      * @return string
    710      * @throws TypeError
    711      */
    712     public static function store64_le($int)
    713     {
    714         /* Type checks: */
    715         if (!is_int($int)) {
    716             if (is_numeric($int)) {
    717                 $int = (int) $int;
    718             } else {
    719                 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
    720             }
    721         }
    722 
    723         if (PHP_INT_SIZE === 8) {
    724             if (PHP_VERSION_ID >= 50603) {
    725                 /** @var string $packed */
    726                 $packed = pack('P', $int);
    727                 return $packed;
    728             }
    729             return self::intToChr($int & 0xff) .
    730                 self::intToChr(($int >>  8) & 0xff) .
    731                 self::intToChr(($int >> 16) & 0xff) .
    732                 self::intToChr(($int >> 24) & 0xff) .
    733                 self::intToChr(($int >> 32) & 0xff) .
    734                 self::intToChr(($int >> 40) & 0xff) .
    735                 self::intToChr(($int >> 48) & 0xff) .
    736                 self::intToChr(($int >> 56) & 0xff);
    737         }
    738         if ($int > PHP_INT_MAX) {
    739             list($hiB, $int) = self::numericTo64BitInteger($int);
    740         } else {
    741             $hiB = 0;
    742         }
    743         return
    744             self::intToChr(($int      ) & 0xff) .
    745             self::intToChr(($int >>  8) & 0xff) .
    746             self::intToChr(($int >> 16) & 0xff) .
    747             self::intToChr(($int >> 24) & 0xff) .
    748             self::intToChr($hiB & 0xff) .
    749             self::intToChr(($hiB >>  8) & 0xff) .
    750             self::intToChr(($hiB >> 16) & 0xff) .
    751             self::intToChr(($hiB >> 24) & 0xff);
    752     }
    753 
    754     /**
    755      * Safe string length
    756      *
    757      * @internal You should not use this directly from another application
    758      *
    759      * @ref mbstring.func_overload
    760      *
    761      * @param string $str
    762      * @return int
    763      * @throws TypeError
    764      */
    765     public static function strlen($str)
    766     {
    767         /* Type checks: */
    768         if (!is_string($str)) {
    769             throw new TypeError('String expected');
    770         }
    771 
    772         return (int) (
    773         self::isMbStringOverride()
    774             ? mb_strlen($str, '8bit')
    775             : strlen($str)
    776         );
    777     }
    778 
    779     /**
    780      * Turn a string into an array of integers
    781      *
    782      * @internal You should not use this directly from another application
    783      *
    784      * @param string $string
    785      * @return array<int, int>
    786      * @throws TypeError
    787      */
    788     public static function stringToIntArray($string)
    789     {
    790         if (!is_string($string)) {
    791             throw new TypeError('String expected');
    792         }
    793         /**
    794          * @var array<int, int>
    795          */
    796         $values = array_values(
    797             unpack('C*', $string)
    798         );
    799         return $values;
    800     }
    801 
    802     /**
    803      * Safe substring
    804      *
    805      * @internal You should not use this directly from another application
    806      *
    807      * @ref mbstring.func_overload
    808      *
    809      * @param string $str
    810      * @param int $start
    811      * @param int $length
    812      * @return string
    813      * @throws TypeError
    814      */
    815     public static function substr($str, $start = 0, $length = null)
    816     {
    817         /* Type checks: */
    818         if (!is_string($str)) {
    819             throw new TypeError('String expected');
    820         }
    821 
    822         if ($length === 0) {
    823             return '';
    824         }
    825 
    826         if (self::isMbStringOverride()) {
    827             if (PHP_VERSION_ID < 50400 && $length === null) {
    828                 $length = self::strlen($str);
    829             }
    830             $sub = (string) mb_substr($str, $start, $length, '8bit');
    831         } elseif ($length === null) {
    832             $sub = (string) substr($str, $start);
    833         } else {
    834             $sub = (string) substr($str, $start, $length);
    835         }
    836         if ($sub !== '') {
    837             return $sub;
    838         }
    839         return '';
    840     }
    841 
    842     /**
    843      * Compare a 16-character byte string in constant time.
    844      *
    845      * @internal You should not use this directly from another application
    846      *
    847      * @param string $a
    848      * @param string $b
    849      * @return bool
    850      * @throws SodiumException
    851      * @throws TypeError
    852      */
    853     public static function verify_16($a, $b)
    854     {
    855         /* Type checks: */
    856         if (!is_string($a)) {
    857             throw new TypeError('String expected');
    858         }
    859         if (!is_string($b)) {
    860             throw new TypeError('String expected');
    861         }
    862         return self::hashEquals(
    863             self::substr($a, 0, 16),
    864             self::substr($b, 0, 16)
    865         );
    866     }
    867 
    868     /**
    869      * Compare a 32-character byte string in constant time.
    870      *
    871      * @internal You should not use this directly from another application
    872      *
    873      * @param string $a
    874      * @param string $b
    875      * @return bool
    876      * @throws SodiumException
    877      * @throws TypeError
    878      */
    879     public static function verify_32($a, $b)
    880     {
    881         /* Type checks: */
    882         if (!is_string($a)) {
    883             throw new TypeError('String expected');
    884         }
    885         if (!is_string($b)) {
    886             throw new TypeError('String expected');
    887         }
    888         return self::hashEquals(
    889             self::substr($a, 0, 32),
    890             self::substr($b, 0, 32)
    891         );
    892     }
    893 
    894     /**
    895      * Calculate $a ^ $b for two strings.
    896      *
    897      * @internal You should not use this directly from another application
    898      *
    899      * @param string $a
    900      * @param string $b
    901      * @return string
    902      * @throws TypeError
    903      */
    904     public static function xorStrings($a, $b)
    905     {
    906         /* Type checks: */
    907         if (!is_string($a)) {
    908             throw new TypeError('Argument 1 must be a string');
    909         }
    910         if (!is_string($b)) {
    911             throw new TypeError('Argument 2 must be a string');
    912         }
    913 
    914         return (string) ($a ^ $b);
    915     }
    916 
    917     /**
    918      * Returns whether or not mbstring.func_overload is in effect.
    919      *
    920      * @internal You should not use this directly from another application
    921      *
    922      * Note: MB_OVERLOAD_STRING === 2, but we don't reference the constant
    923      * (for nuisance-free PHP 8 support)
    924      *
    925      * @return bool
    926      */
    927     protected static function isMbStringOverride()
    928     {
    929         static $mbstring = null;
    930 
    931         if ($mbstring === null) {
    932             $mbstring = extension_loaded('mbstring')
    933                 && defined('MB_OVERLOAD_STRING')
    934                 &&
    935             ((int) (ini_get('mbstring.func_overload')) & 2);
    936             // MB_OVERLOAD_STRING === 2
    937         }
    938         /** @var bool $mbstring */
    939 
    940         return $mbstring;
    941     }
    942 }