Symfony\Polyfill\Mbstring

Mbstring{} │ final │ CF7 1.0

Partial mbstring implementation in PHP, iconv based, UTF-8 centric.

Implemented:

  • mb_chr - Returns a specific character from its Unicode code point
  • mb_convert_encoding - Convert character encoding
  • mb_convert_variables - Convert character code in variable(s)
  • mb_decode_mimeheader - Decode string in MIME header field
  • mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED
  • mb_decode_numericentity - Decode HTML numeric string reference to character
  • mb_encode_numericentity - Encode character to HTML numeric string reference
  • mb_convert_case - Perform case folding on a string
  • mb_detect_encoding - Detect character encoding
  • mb_get_info - Get internal settings of mbstring
  • mb_http_input - Detect HTTP input character encoding
  • mb_http_output - Set/Get HTTP output character encoding
  • mb_internal_encoding - Set/Get internal character encoding
  • mb_list_encodings - Returns an array of all supported encodings
  • mb_ord - Returns the Unicode code point of a character
  • mb_output_handler - Callback function converts character encoding in output buffer
  • mb_scrub - Replaces ill-formed byte sequences with substitute characters
  • mb_strlen - Get string length
  • mb_strpos - Find position of first occurrence of string in a string
  • mb_strrpos - Find position of last occurrence of a string in a string
  • mb_str_split - Convert a string to an array
  • mb_strtolower - Make a string lowercase
  • mb_strtoupper - Make a string uppercase
  • mb_substitute_character - Set/Get substitution character
  • mb_substr - Get part of string
  • mb_stripos - Finds position of first occurrence of a string within another, case insensitive
  • mb_stristr - Finds first occurrence of a string within another, case insensitive
  • mb_strrchr - Finds the last occurrence of a character in a string within another
  • mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive
  • mb_strripos - Finds position of last occurrence of a string within another, case insensitive
  • mb_strstr - Finds first occurrence of a string within another
  • mb_strwidth - Return width of string
  • mb_substr_count - Count the number of substring occurrences
  • mb_ucfirst - Make a string's first character uppercase
  • mb_lcfirst - Make a string's first character lowercase
  • mb_trim - Strip whitespace (or other characters) from the beginning and end of a string
  • mb_ltrim - Strip whitespace (or other characters) from the beginning of a string
  • mb_rtrim - Strip whitespace (or other characters) from the end of a string

Not implemented:

  • mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more)
  • mb_ereg_* - Regular expression with multibyte support
  • mb_parse_str - Parse GET/POST/COOKIE data and set global variable
  • mb_preferred_mime_name - Get MIME charset string
  • mb_regex_encoding - Returns current encoding for multibyte regex as string
  • mb_regex_set_options - Set/Get the default options for mbregex functions
  • mb_send_mail - Send encoded mail
  • mb_split - Split multibyte string using regular expression
  • mb_strcut - Get part of string
  • mb_strimwidth - Get truncated string with specified width

Хуков нет.

Использование

$Mbstring = new Mbstring();
// use class methods

Методы

  1. public static mb_check_encoding($var = null, $encoding = null)
  2. public static mb_chr($code, $encoding = null)
  3. public static mb_convert_case($s, $mode, $encoding = null)
  4. public static mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
  5. public static mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
  6. public static mb_decode_mimeheader($s)
  7. public static mb_decode_numericentity($s, $convmap, $encoding = null)
  8. public static mb_detect_encoding($str, $encodingList = null, $strict = false)
  9. public static mb_detect_order($encodingList = null)
  10. public static mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
  11. public static mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
  12. public static mb_encoding_aliases($encoding)
  13. public static mb_get_info($type = 'all')
  14. public static mb_http_input($type = '')
  15. public static mb_http_output($encoding = null)
  16. public static mb_internal_encoding($encoding = null)
  17. public static mb_language($lang = null)
  18. public static mb_lcfirst(string $string, ?string $encoding = null)
  19. public static mb_list_encodings()
  20. public static mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null)
  21. public static mb_ord($s, $encoding = null)
  22. public static mb_output_handler($contents, $status)
  23. public static mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null)
  24. public static mb_scrub(?string $string, ?string $encoding = null)
  25. public static mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
  26. public static mb_str_split($string, $split_length = 1, $encoding = null)
  27. public static mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
  28. public static mb_stristr($haystack, $needle, $part = false, $encoding = null)
  29. public static mb_strlen($s, $encoding = null)
  30. public static mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
  31. public static mb_strrchr($haystack, $needle, $part = false, $encoding = null)
  32. public static mb_strrichr($haystack, $needle, $part = false, $encoding = null)
  33. public static mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
  34. public static mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
  35. public static mb_strstr($haystack, $needle, $part = false, $encoding = null)
  36. public static mb_strtolower($s, $encoding = null)
  37. public static mb_strtoupper($s, $encoding = null)
  38. public static mb_strwidth($s, $encoding = null)
  39. public static mb_substitute_character($c = null)
  40. public static mb_substr($s, $start, $length = null, $encoding = null)
  41. public static mb_substr_count($haystack, $needle, $encoding = null)
  42. public static mb_trim(string $string, ?string $characters = null, ?string $encoding = null)
  43. public static mb_ucfirst(string $string, ?string $encoding = null)
  44. private static assertCodepointEncoding(string $encoding, string $function)
  45. private static assertEncoding(string $encoding, string $errorFormat)
  46. private static convertArray(array $array, $toEncoding, $fromEncoding, array $referenceIds = [])
  47. private static find(string $function, $haystack, $needle, $offset, $encoding, bool $reverse, bool $fold)
  48. private static findPart(string $function, $haystack, $needle, $part, $encoding, bool $reverse, bool $fold)
  49. private static foldCase(string $s)
  50. private static getConversionError(string $function, int $argument, $toEncoding, $fromEncoding)
  51. private static getData($file)
  52. private static getEncoding($encoding)
  53. private static html_encoding_callback(array $m)
  54. private static iconv($fromEncoding, $toEncoding, $s)
  55. private static isConvertibleEncoding(string $encoding)
  56. private static markIllFormedUtf8(string $s)
  57. private static mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function)
  58. private static mb_trim_invalid_utf8(string $string, ?string $characters, string $function)
  59. private static prepareSearch(string $haystack, string $needle, string $encoding, bool $fold, bool $markIllFormed)
  60. private static scrubUtf8(string $s)
  61. private static searchEncoding($encoding, bool $fold)
  62. private static title_case(array $s)
  63. private static utf8(string $s, string $encoding, &$dropped = 0)
  64. private static utf8Length(string $s)
  65. private static utf8Offset(string $s, int $offset, bool $bytes)

Код Mbstring{} CF7 6.2.4.1

final class Mbstring
{
    public const MB_CASE_FOLD = \PHP_INT_MAX;

    private const SIMPLE_CASE_FOLD = [
        ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
        ['μ', 's', 'ι',        'σ', 'β',        'θ',        'φ',        'π',        'κ',        'ρ',        'ε',        "\xE1\xB9\xA1", 'ι'],
    ];

    // Encodings rejected by mb_ord() and mb_chr(), with their aliases
    private const UNSUPPORTED_CODEPOINT_ENCODINGS = [
        'BASE64' => 'BASE64',
        'UUENCODE' => 'UUENCODE',
        'X-UUENCODE' => 'UUENCODE',
        'HTML-ENTITIES' => 'HTML-ENTITIES',
        'HTML' => 'HTML-ENTITIES',
        'QUOTED-PRINTABLE' => 'Quoted-Printable',
        'QPRINT' => 'Quoted-Printable',
        'UTF-7' => 'UTF-7',
        'UTF7' => 'UTF-7',
        'UTF7-IMAP' => 'UTF7-IMAP',
        'MUTF-7' => 'UTF7-IMAP',
        'JIS' => 'JIS',
        'ISO-2022-JP' => 'ISO-2022-JP',
        'ISO-2022-JP-2004' => 'ISO-2022-JP-2004',
        'ISO-2022-JP-MOBILE#KDDI' => 'ISO-2022-JP-MOBILE#KDDI',
        'ISO-2022-JP-KDDI' => 'ISO-2022-JP-MOBILE#KDDI',
        'ISO-2022-JP-MS' => 'ISO-2022-JP-MS',
        'ISO2022JPMS' => 'ISO-2022-JP-MS',
        'CP50220' => 'CP50220',
        'CP50220RAW' => 'CP50220',
        'CP50220-RAW' => 'CP50220',
        'JIS-MS' => 'CP50220',
        'CP50221' => 'CP50221',
        'CP50222' => 'CP50222',
    ];

    // Well-formed characters in group 1, then the maximal subparts of ill-formed sequences
    private const UTF8_CHAR_REGEX = '/([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})|\xE0[\xA0-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]|\xED[\x80-\x9F]|\xF0[\x90-\xBF][\x80-\xBF]?|[\xF1-\xF3][\x80-\xBF]{1,2}|\xF4[\x80-\x8F][\x80-\xBF]?|[\x80-\xFF]/';

    private static $encodingList = ['ASCII', 'UTF-8'];
    private static $language = 'neutral';
    private static $internalEncoding = 'UTF-8';
    private static $iconvSupportsIgnore;
    private static $convertibleEncodings = ['UTF-8' => true];

    public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
    {
        if (\is_string($fromEncoding) && 2 < \strlen($fromEncoding) && '"' === $fromEncoding[0] && '"' === $fromEncoding[-1]) {
            $fromEncoding = substr($fromEncoding, 1, -1);
        }

        if (80000 <= \PHP_VERSION_ID
            && (!isset(self::$convertibleEncodings[$toEncoding]) || (null !== $fromEncoding && !(\is_string($fromEncoding) && isset(self::$convertibleEncodings[$fromEncoding]))))
            && null !== $error = self::getConversionError('mb_convert_encoding', 2, $toEncoding, $fromEncoding)
        ) {
            throw new \ValueError($error);
        }

        if (\is_array($s)) {
            return self::convertArray($s, $toEncoding, $fromEncoding);
        }

        if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) {
            $fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
        } else {
            $fromEncoding = self::getEncoding($fromEncoding);
        }

        $toEncoding = self::getEncoding($toEncoding);

        if ('BASE64' === $fromEncoding) {
            $s = base64_decode($s);
            $fromEncoding = $toEncoding;
        }

        if ('BASE64' === $toEncoding) {
            return base64_encode($s);
        }

        if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) {
            if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) {
                $fromEncoding = 'Windows-1252';
            }
            if ('UTF-8' !== $fromEncoding) {
                $s = self::iconv($fromEncoding, 'UTF-8', $s);
            } elseif (!preg_match('//u', $s)) {
                $s = self::scrubUtf8($s);
            }

            return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s);
        }

        if ('HTML-ENTITIES' === $fromEncoding) {
            $decodeControlChars = static function ($m) {
                $code = '' !== ($m[2] ?? '') ? hexdec($m[2]) : (int) $m[1];

                if ($code < 32 || 127 === $code) {
                    return \chr($code);
                }
                if (128 <= $code && $code <= 159) {
                    return "\xC2".\chr(0x80 | ($code & 0x3F));
                }

                return $m[0];
            };

            if (\PHP_VERSION_ID >= 70400) {
                $s = html_entity_decode($s, \ENT_QUOTES, 'UTF-8');
                // html_entity_decode() leaves numeric entities for C0/C1 control
                // characters as-is (HTML spec), but mb_convert_encoding() decodes
                // them. Catch what html_entity_decode() missed.
                if (false !== strpos($s, '&#')) {
                    $s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
                }
            } else {
                // PHP < 7.4: html_entity_decode() truncates strings at NUL bytes,
                // so decode the control character entities first then call
                // html_entity_decode() on each NUL-delimited chunk independently.
                $s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
                $s = implode("\0", array_map(static function ($chunk) {
                    return html_entity_decode($chunk, \ENT_QUOTES, 'UTF-8');
                }, explode("\0", $s)));
            }
            $fromEncoding = 'UTF-8';
        }

        if ('UTF-8' === $fromEncoding && !preg_match('//u', $s)) {
            $s = self::scrubUtf8($s);
        }

        return self::iconv($fromEncoding, $toEncoding, $s);
    }

    public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
    {
        if (\is_string($fromEncoding) && 2 < \strlen($fromEncoding) && '"' === $fromEncoding[0] && '"' === $fromEncoding[-1]) {
            $fromEncoding = substr($fromEncoding, 1, -1);
        }

        if (80000 <= \PHP_VERSION_ID && null !== $error = self::getConversionError('mb_convert_variables', 1, $toEncoding, $fromEncoding)) {
            throw new \ValueError($error);
        }

        $ok = true;
        foreach ($vars as $i => &$var) {
            $convert = static function (&$v) use (&$ok, $toEncoding, $fromEncoding, $i) {
                if (\is_string($v)) {
                    if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
                        $ok = false;
                    }
                } elseif (80600 <= \PHP_VERSION_ID && !\is_object($v)) {
                    trigger_error(\sprintf('mb_convert_variables(): Argument #%d must be of type string|array|object or only contain entries of type string|array|object, %s given', 3 + $i, strtok(get_debug_type($v), ' ')), \E_USER_WARNING);
                }
            };

            if (\is_array($var)) {
                array_walk_recursive($var, $convert);
            } else {
                $convert($var);
            }
        }
        unset($var);

        return $ok ? $fromEncoding : false;
    }

    public static function mb_decode_mimeheader($s)
    {
        return iconv_mime_decode($s, 2, self::$internalEncoding);
    }

    public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
    {
        trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING);
    }

    public static function mb_decode_numericentity($s, $convmap, $encoding = null)
    {
        if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
            trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return null;
        }

        if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
            return false;
        }

        if (null !== $encoding && !\is_scalar($encoding)) {
            trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return '';  // Instead of null (cf. mb_encode_numericentity).
        }

        $s = (string) $s;
        if ('' === $s) {
            return '';
        }

        $encoding = self::getEncoding($encoding);

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $s)) {
                $s = self::scrubUtf8($s);
            }
        } else {
            $s = self::iconv($encoding, 'UTF-8', $s);
        }

        $cnt = floor(\count($convmap) / 4) * 4;

        for ($i = 0; $i < $cnt; $i += 4) {
            // collector_decode_htmlnumericentity ignores $convmap[$i + 3]
            $convmap[$i] += $convmap[$i + 2];
            $convmap[$i + 1] += $convmap[$i + 2];
        }

        $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))'.(\PHP_VERSION_ID >= 80200 ? '' : '(?!&)').';?/', static function (array $m) use ($cnt, $convmap) {
            $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1];
            for ($i = 0; $i < $cnt; $i += 4) {
                if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) {
                    return self::mb_chr($c - $convmap[$i + 2]);
                }
            }

            return $m[0];
        }, $s);

        if (null === $encoding) {
            return $s;
        }

        return self::iconv('UTF-8', $encoding, $s);
    }

    public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
    {
        if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
            trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return null;
        }

        if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
            return false;
        }

        if (null !== $encoding && !\is_scalar($encoding)) {
            trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return null;  // Instead of '' (cf. mb_decode_numericentity).
        }

        if (null !== $is_hex && !\is_scalar($is_hex)) {
            trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING);

            return null;
        }

        $s = (string) $s;
        if ('' === $s) {
            return '';
        }

        $encoding = self::getEncoding($encoding);

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $s)) {
                $s = self::scrubUtf8($s);
            }
        } else {
            $s = self::iconv($encoding, 'UTF-8', $s);
        }

        static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];

        $cnt = floor(\count($convmap) / 4) * 4;
        $i = 0;
        $len = \strlen($s);
        $result = '';

        while ($i < $len) {
            $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"] ?? 1;
            $uchr = substr($s, $i, $ulen);
            $i += $ulen;

            // code points above U+10FFFF are outside any convmap, but mb_ord() rejects them
            if (false === $c = self::mb_ord($uchr)) {
                $result .= $uchr;
                continue;
            }

            for ($j = 0; $j < $cnt; $j += 4) {
                if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) {
                    $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3];
                    $result .= $is_hex ? \sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';';
                    continue 2;
                }
            }
            $result .= $uchr;
        }

        if (null === $encoding) {
            return $result;
        }

        return self::iconv('UTF-8', $encoding, $result);
    }

    public static function mb_convert_case($s, $mode, $encoding = null)
    {
        $s = (string) $s;
        if ('' === $s) {
            return '';
        }

        $encoding = self::getEncoding($encoding);

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $s)) {
                $s = self::scrubUtf8($s);
            }
        } else {
            $s = self::iconv($encoding, 'UTF-8', $s);
        }

        if (\MB_CASE_TITLE == $mode) {
            static $titleRegexp = null;
            if (null === $titleRegexp) {
                $titleRegexp = self::getData('titleCaseRegexp');
            }
            $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s);
        } else {
            if (\MB_CASE_UPPER == $mode) {
                static $upper = null;
                if (null === $upper) {
                    $upper = self::getData('upperCase');
                }
                $map = $upper;
            } else {
                if (self::MB_CASE_FOLD === $mode) {
                    static $caseFolding = null;
                    if (null === $caseFolding) {
                        $caseFolding = self::getData('caseFolding');
                    }
                    $s = strtr($s, $caseFolding);
                }

                static $lower = null;
                if (null === $lower) {
                    $lower = self::getData('lowerCase');
                }
                $map = $lower;
            }

            static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];

            $i = 0;
            $len = \strlen($s);

            while ($i < $len) {
                $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"] ?? 1;
                $uchr = substr($s, $i, $ulen);
                $i += $ulen;

                if (isset($map[$uchr])) {
                    $uchr = $map[$uchr];
                    $nlen = \strlen($uchr);

                    if ($nlen == $ulen) {
                        $nlen = $i;
                        do {
                            $s[--$nlen] = $uchr[--$ulen];
                        } while ($ulen);
                    } else {
                        $s = substr_replace($s, $uchr, $i - $ulen, $ulen);
                        $len += $nlen - $ulen;
                        $i += $nlen - $ulen;
                    }
                }
            }
        }

        if (null === $encoding) {
            return $s;
        }

        return self::iconv('UTF-8', $encoding, $s);
    }

    public static function mb_internal_encoding($encoding = null)
    {
        if (null === $encoding) {
            return self::$internalEncoding;
        }

        $normalizedEncoding = self::getEncoding($encoding);

        if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) {
            self::$internalEncoding = $normalizedEncoding;

            return true;
        }

        if (80000 > \PHP_VERSION_ID) {
            return false;
        }

        throw new \ValueError(\sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
    }

    public static function mb_language($lang = null)
    {
        if (null === $lang) {
            return self::$language;
        }

        switch ($normalizedLang = strtolower($lang)) {
            case 'uni':
            case 'neutral':
                self::$language = $normalizedLang;

                return true;
        }

        if (80000 > \PHP_VERSION_ID) {
            return false;
        }

        throw new \ValueError(\sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
    }

    public static function mb_list_encodings()
    {
        return ['UTF-8'];
    }

    public static function mb_encoding_aliases($encoding)
    {
        switch (strtoupper($encoding)) {
            case 'UTF8':
            case 'UTF-8':
                return ['utf8'];
        }

        return false;
    }

    public static function mb_check_encoding($var = null, $encoding = null)
    {
        if (null === $encoding) {
            if (null === $var) {
                return false;
            }
            $encoding = self::$internalEncoding;
        }

        if (!\is_array($var)) {
            if ('UTF-8' === self::getEncoding($encoding)) {
                return (bool) preg_match('//u', $var);
            }

            return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var);
        }

        foreach ($var as $key => $value) {
            if (!self::mb_check_encoding($key, $encoding)) {
                return false;
            }
            if (!self::mb_check_encoding($value, $encoding)) {
                return false;
            }
        }

        return true;
    }

    public static function mb_detect_encoding($str, $encodingList = null, $strict = false)
    {
        if (null === $encodingList) {
            $encodingList = self::$encodingList;
        } else {
            if (!\is_array($encodingList)) {
                $encodingList = array_map('trim', explode(',', $encodingList));
            }
            $encodingList = array_map('strtoupper', $encodingList);
        }

        foreach ($encodingList as $enc) {
            switch ($enc) {
                case 'ASCII':
                    if (!preg_match('/[\x80-\xFF]/', $str)) {
                        return $enc;
                    }
                    break;

                case 'UTF8':
                case 'UTF-8':
                    if (preg_match('//u', $str)) {
                        return 'UTF-8';
                    }
                    break;

                default:
                    if (0 === strncmp($enc, 'ISO-8859-', 9)) {
                        return $enc;
                    }
            }
        }

        return false;
    }

    public static function mb_detect_order($encodingList = null)
    {
        if (null === $encodingList) {
            return self::$encodingList;
        }

        if (!\is_array($encodingList)) {
            $encodingList = array_map('trim', explode(',', $encodingList));
        }
        $encodingList = array_map('strtoupper', $encodingList);

        foreach ($encodingList as $enc) {
            switch ($enc) {
                default:
                    if (strncmp($enc, 'ISO-8859-', 9)) {
                        return false;
                    }
                    // no break
                case 'ASCII':
                case 'UTF8':
                case 'UTF-8':
            }
        }

        self::$encodingList = $encodingList;

        return true;
    }

    public static function mb_strlen($s, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        if ('CP850' === $encoding || 'ASCII' === $encoding) {
            return \strlen($s);
        }

        if (false !== $len = @iconv_strlen($s, $encoding)) {
            return $len;
        }

        if ('UTF-8' !== $encoding) {
            return $len;
        }

        return preg_match_all('/[\x00-\x7F]|[\xC0-\xDF][\x80-\xBF]?|[\xE0-\xEF][\x80-\xBF]{0,2}|[\xF0-\xF7][\x80-\xBF]{0,3}|[\xF8-\xFB][\x80-\xBF]{0,4}|[\xFC-\xFD][\x80-\xBF]{0,5}|[\x80-\xBF\xFE\xFF]/s', $s);
    }

    public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
    {
        return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, false, false);
    }

    public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
    {
        if ($offset != (int) $offset) {
            $offset = 0;
        }

        return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, true, false);
    }

    public static function mb_str_split($string, $split_length = 1, $encoding = null)
    {
        if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
            trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING);

            return null;
        }

        if (1 > $split_length = (int) $split_length) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('Argument #2 ($length) must be greater than 0');
        }

        if (80300 <= \PHP_VERSION_ID && 0x3FFFFFFF < $split_length) {
            throw new \ValueError('Argument #2 ($length) is too large');
        }

        if (null === $encoding) {
            $encoding = mb_internal_encoding();
        }

        if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
            $string = (string) $string;

            if (\strlen($string) <= $split_length) {
                return '' === $string ? [] : [$string];
            }

            $rx = '/(';
            while (65535 < $split_length) {
                $rx .= '.{65535}';
                $split_length -= 65535;
            }
            $rx .= '.{'.$split_length.'})/Aus';

            return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
        }

        $result = [];
        $length = mb_strlen($string, $encoding);

        for ($i = 0; $i < $length; $i += $split_length) {
            $result[] = mb_substr($string, $i, $split_length, $encoding);
        }

        return $result;
    }

    public static function mb_strtolower($s, $encoding = null)
    {
        return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding);
    }

    public static function mb_strtoupper($s, $encoding = null)
    {
        return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding);
    }

    public static function mb_substitute_character($c = null)
    {
        if (null === $c) {
            return 'none';
        }
        if (0 === strcasecmp($c, 'none')) {
            return true;
        }
        if (80000 > \PHP_VERSION_ID) {
            return false;
        }
        if (\is_int($c) || 'long' === $c || 'entity' === $c) {
            return false;
        }

        throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint');
    }

    public static function mb_substr($s, $start, $length = null, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        if ('CP850' === $encoding || 'ASCII' === $encoding) {
            return (string) substr($s, $start, null === $length ? 2147483647 : $length);
        }

        if ($start < 0) {
            $start = iconv_strlen($s, $encoding) + $start;
            if ($start < 0) {
                $start = 0;
            }
        }

        if (null === $length) {
            $length = 2147483647;
        } elseif ($length < 0) {
            $length = iconv_strlen($s, $encoding) + $length - $start;
            if ($length < 0) {
                return '';
            }
        }

        return (string) iconv_substr($s, $start, $length, $encoding);
    }

    public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
    {
        return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, false, true);
    }

    public static function mb_stristr($haystack, $needle, $part = false, $encoding = null)
    {
        return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, false, true);
    }

    public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null)
    {
        return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, true, false);
    }

    public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null)
    {
        return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, true, true);
    }

    public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
    {
        return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, true, true);
    }

    public static function mb_strstr($haystack, $needle, $part = false, $encoding = null)
    {
        return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, false, false);
    }

    public static function mb_get_info($type = 'all')
    {
        $info = [
            'internal_encoding' => self::$internalEncoding,
            'http_output' => 'pass',
            'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)',
            'func_overload' => 0,
            'func_overload_list' => 'no overload',
            'mail_charset' => 'UTF-8',
            'mail_header_encoding' => 'BASE64',
            'mail_body_encoding' => 'BASE64',
            'illegal_chars' => 0,
            'encoding_translation' => 'Off',
            'language' => self::$language,
            'detect_order' => self::$encodingList,
            'substitute_character' => 'none',
            'strict_detection' => 'Off',
        ];

        if ('all' === $type) {
            return $info;
        }
        if (isset($info[$type])) {
            return $info[$type];
        }

        return false;
    }

    public static function mb_http_input($type = '')
    {
        return false;
    }

    public static function mb_http_output($encoding = null)
    {
        return null !== $encoding ? 'pass' === $encoding : 'pass';
    }

    public static function mb_strwidth($s, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        static $wideChars = null;
        if (null === $wideChars) {
            $wideChars = self::getData('eastAsianWidthRegexp');
        }

        if ('UTF-8' !== $encoding) {
            $s = self::iconv($encoding, 'UTF-8', $s);
        } elseif (!preg_match('//u', $s)) {
            // Each maximal subpart of an ill-formed sequence is one column wide
            return preg_match_all(self::UTF8_CHAR_REGEX, $s) + preg_match_all($wideChars, self::scrubUtf8($s));
        }

        $s = preg_replace($wideChars, '', $s, -1, $wide);

        return ($wide << 1) + iconv_strlen($s, 'UTF-8');
    }

    public static function mb_substr_count($haystack, $needle, $encoding = null)
    {
        if ('' === $needle = (string) $needle) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error('mb_substr_count(): Empty substring', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('mb_substr_count(): Argument #2 ($needle) must not be empty');
        }

        if (!$search = self::prepareSearch((string) $haystack, $needle, self::getEncoding($encoding), false, true)) {
            return 0;
        }

        return substr_count($search[0], $search[1]);
    }

    public static function mb_output_handler($contents, $status)
    {
        return $contents;
    }

    public static function mb_chr($code, $encoding = null)
    {
        if (null !== $encoding && !self::assertCodepointEncoding($encoding, 'mb_chr')) {
            return false;
        }

        if (0x80 > $code %= 0x200000) {
            $s = \chr($code);
        } elseif (0x800 > $code) {
            $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
        } elseif (0x10000 > $code) {
            $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
        } else {
            $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
        }

        if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
            $s = mb_convert_encoding($s, $encoding, 'UTF-8');
        }

        return $s;
    }

    public static function mb_ord($s, $encoding = null)
    {
        if ('' === (string) $s) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error('mb_ord(): Empty string', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('mb_ord(): Argument #1 ($string) must not be empty');
        }

        if (null !== $encoding && !self::assertCodepointEncoding($encoding, 'mb_ord')) {
            return false;
        }

        if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
            $s = mb_convert_encoding($s, 'UTF-8', $encoding);
        }

        $s = unpack('C*', substr($s, 0, 4));
        $code = $s[1] ?? 0;
        if (0x80 > $code) {
            return $code;
        }

        if (0xC2 > $code || 0xF4 < $code) {
            return false;
        }

        $c2 = $s[2] ?? 0;
        if (0x80 !== ($c2 & 0xC0)) {
            return false;
        }

        if (0xE0 > $code) {
            return (($code & 0x1F) << 6) | ($c2 & 0x3F);
        }

        $c3 = $s[3] ?? 0;
        if (0x80 !== ($c3 & 0xC0)
            || 0xE0 === $code && 0xA0 > $c2
            || 0xED === $code && 0xA0 <= $c2
        ) {
            return false;
        }

        if (0xF0 > $code) {
            return (($code & 0x0F) << 12) | (($c2 & 0x3F) << 6) | ($c3 & 0x3F);
        }

        $c4 = $s[4] ?? 0;
        if (0x80 !== ($c4 & 0xC0)
            || 0xF0 === $code && 0x90 > $c2
            || 0xF4 === $code && 0x90 <= $c2
        ) {
            return false;
        }

        return (($code & 0x07) << 18) | (($c2 & 0x3F) << 12) | (($c3 & 0x3F) << 6) | ($c4 & 0x3F);
    }

    /** @return string|false */
    public static function mb_scrub(?string $string, ?string $encoding = null): string
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_scrub(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        return self::mb_convert_encoding((string) $string, $encoding, $encoding);
    }

    /** @return string|false */
    public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        if (0 >= $padStringLength = self::mb_strlen($pad_string, $encoding)) {
            if (\PHP_VERSION_ID < 80000) {
                trigger_error('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
        }

        if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
            if (\PHP_VERSION_ID < 80000) {
                trigger_error('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
        }

        $paddingRequired = $length - self::mb_strlen($string, $encoding);

        if ($paddingRequired < 1) {
            return $string;
        }

        switch ($pad_type) {
            case \STR_PAD_LEFT:
                $leftPaddingLength = $paddingRequired;
                break;
            case \STR_PAD_RIGHT:
                $leftPaddingLength = 0;
                break;
            default:
                $leftPaddingLength = intdiv($paddingRequired, 2);
        }

        $rightPaddingLength = $paddingRequired - $leftPaddingLength;

        return str_repeat($pad_string, intdiv($leftPaddingLength, $padStringLength)).self::mb_substr($pad_string, 0, $leftPaddingLength % $padStringLength, $encoding)
            .$string
            .str_repeat($pad_string, intdiv($rightPaddingLength, $padStringLength)).self::mb_substr($pad_string, 0, $rightPaddingLength % $padStringLength, $encoding);
    }

    /** @return string|false */
    public static function mb_ucfirst(string $string, ?string $encoding = null)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        $firstChar = mb_substr($string, 0, 1, $encoding);
        $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding);

        return $firstChar.mb_substr($string, 1, null, $encoding);
    }

    /** @return string|false */
    public static function mb_lcfirst(string $string, ?string $encoding = null)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        $firstChar = mb_substr($string, 0, 1, $encoding);
        $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding);

        return $firstChar.mb_substr($string, 1, null, $encoding);
    }

    /** @return string|false */
    public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null)
    {
        return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    /** @return string|false */
    public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null)
    {
        return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    /** @return string|false */
    public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null)
    {
        return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    /**
     * Finds the position of $needle in $haystack like native mbstring.
     *
     * @return int|false
     */
    private static function find(string $function, $haystack, $needle, $offset, $encoding, bool $reverse, bool $fold)
    {
        $needle = (string) $needle;

        if (80000 > \PHP_VERSION_ID && '' === $needle && 'mb_stripos' === $function) {
            trigger_error($function.'(): Empty delimiter', \E_USER_WARNING);

            return false;
        }

        if (!$search = self::prepareSearch((string) $haystack, $needle, self::searchEncoding($encoding, $fold), $fold, $fold)) {
            return false;
        }

        [$haystack, $needle, $bytes, $dropped] = $search;

        if (80000 > \PHP_VERSION_ID && $fold && ('' === $haystack || '' === $needle)) {
            return false;
        }

        if (false === $start = self::utf8Offset($haystack, $offset = (int) $offset, $bytes)) {
            // Native mbstring counts the ill-formed sequences that iconv() dropped: the offset could be in range
            if ($dropped && abs($offset) <= self::utf8Length($haystack) + $dropped) {
                return false;
            }

            if (80000 > \PHP_VERSION_ID) {
                trigger_error($function.'(): '.($reverse ? 'Offset is greater than the length of haystack string' : 'Offset not contained in string'), \E_USER_WARNING);

                return false;
            }

            throw new \ValueError($function.'(): Argument #3 ($offset) must be contained in argument #1 ($haystack)');
        }

        if (80000 > \PHP_VERSION_ID && '' === $needle) {
            if ('mb_strpos' === $function) {
                trigger_error($function.'(): Empty delimiter', \E_USER_WARNING);
            }

            return false;
        }

        if (!$reverse) {
            $pos = strpos($haystack, $needle, $start);
        } else {
            $pos = strrpos($haystack, $needle, 0 > $offset ? $start - \strlen($haystack) : $start);
        }

        return false === $pos || $bytes ? $pos : self::utf8Length(substr($haystack, 0, $pos));
    }

    /**
     * Returns the part of $haystack before or from $needle like native mbstring.
     *
     * @return string|false
     */
    private static function findPart(string $function, $haystack, $needle, $part, $encoding, bool $reverse, bool $fold)
    {
        $needle = (string) $needle;

        if (80000 > \PHP_VERSION_ID && '' === $needle) {
            if (!$reverse) {
                trigger_error($function.'(): Empty delimiter', \E_USER_WARNING);
            }

            return false;
        }

        $haystack = (string) $haystack;

        if (!$search = self::prepareSearch($haystack, $needle, $encoding = self::searchEncoding($encoding, $fold), $fold, $fold)) {
            return false;
        }

        [$h, $n, $bytes] = $search;

        if (false === $pos = $reverse ? strrpos($h, $n) : strpos($h, $n)) {
            return false;
        }

        if ($bytes) {
            return (string) ($part ? substr($haystack, 0, $pos) : substr($haystack, $pos));
        }

        if ($fold || ('UTF-8' === $encoding && (!preg_match('//u', $haystack) || !preg_match('//u', $needle)))) {
            // Cut after as many characters as native mbstring counts before the match
            $pos = self::utf8Length(substr($h, 0, $pos));
            $h = 'UTF-8' === $encoding ? self::markIllFormedUtf8($haystack) : self::utf8($haystack, $encoding);
            $pos = self::utf8Offset($h, $pos, false);
        }

        $h = (string) ($part ? substr($h, 0, $pos) : substr($h, $pos));

        if (false !== strpos($h, "\xFF")) {
            $h = str_replace("\xFF", 'none' === mb_substitute_character() ? '' : '?', $h);
        }

        return 'UTF-8' === $encoding ? $h : (string) @self::iconv('UTF-8', $encoding, $h);
    }

    /**
     * @return array{string, string, bool, int}|null The haystack and the needle to search, whether offsets in them count bytes rather than UTF-8 characters, and how many bytes of ill-formed sequences iconv() dropped from the haystack
     */
    private static function prepareSearch(string $haystack, string $needle, string $encoding, bool $fold, bool $markIllFormed)
    {
        if ('ASCII' === $encoding || ('CP850' === $encoding && !$fold)) {
            if ('ASCII' === $encoding) {
                // Native mbstring turns every byte above 0x7F into the same error marker
                $haystack = preg_replace('/[\x80-\xFF]/', "\xFF", $haystack);
                $needle = preg_replace('/[\x80-\xFF]/', "\xFF", $needle);
            }

            if ($fold) {
                $haystack = strtr($haystack, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
                $needle = strtr($needle, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
            }

            return [$haystack, $needle, true, 0];
        }

        $dropped = 0;

        if ('UTF-8' !== $encoding) {
            if (false === ($haystack = self::utf8($haystack, $encoding, $dropped)) || false === $needle = self::utf8($needle, $encoding)) {
                return null;
            }
        } elseif ($markIllFormed) {
            $haystack = self::markIllFormedUtf8($haystack);
            $needle = self::markIllFormedUtf8($needle);
        }

        return $fold ? [self::foldCase($haystack), self::foldCase($needle), false, $dropped] : [$haystack, $needle, false, $dropped];
    }

    private static function searchEncoding($encoding, bool $fold): string
    {
        $normalizedEncoding = self::getEncoding($encoding);

        // Native mbstring folds the case of 8bit strings as Latin-1
        return $fold && 'CP850' === $normalizedEncoding && null !== $encoding && 'CP850' !== strtoupper($encoding) ? 'ISO-8859-1' : $normalizedEncoding;
    }

    /**
     * Converts $s to UTF-8 to search it, ending with "\xFF", a byte that is invalid in UTF-8, when its last sequence is truncated, like native mbstring.
     *
     * @param int $dropped Set to the number of bytes of the ill-formed sequences that iconv() dropped elsewhere
     *
     * @return string|false False when the encoding is unknown
     */
    private static function utf8(string $s, string $encoding, &$dropped = 0)
    {
        if ('UTF-8' === $encoding || '' === $s) {
            return $s;
        }

        if (false !== $u = @iconv($encoding, 'UTF-8', $s)) {
            return $u;
        }

        // Let iconv() warn about an unknown encoding
        if (false === iconv($encoding, 'UTF-8', '')) {
            return false;
        }

        // iconv() drops the ill-formed sequences, but fails on a truncated one at the end
        for ($i = 0; $i < 4; ++$i) {
            if (false !== $u = @self::iconv($encoding, 'UTF-8', $i ? substr($s, 0, -$i) : $s)) {
                $dropped = \strlen($s) - $i - \strlen((string) @iconv('UTF-8', $encoding, $u));

                return $i || '' === $u ? $u."\xFF" : $u;
            }
        }

        $dropped = \strlen($s);

        return "\xFF";
    }

    /**
     * Replaces each maximal subpart of ill-formed UTF-8 sequences with "\xFF", like native mbstring before searching case-insensitively.
     */
    private static function markIllFormedUtf8(string $s): string
    {
        if (preg_match('//u', $s)) {
            return $s;
        }

        return preg_replace('/\G(?:[\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*+\K(?:\xE0[\xA0-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]|\xED[\x80-\x9F]|\xF0[\x90-\xBF][\x80-\xBF]?|[\xF1-\xF3][\x80-\xBF]{1,2}|\xF4[\x80-\x8F][\x80-\xBF]?|[\x80-\xFF])/', "\xFF", $s);
    }

    /**
     * Folds the case of UTF-8 $s like the simple case folding of native mbstring.
     */
    private static function foldCase(string $s): string
    {
        static $map = null;

        if (null === $map) {
            $map = array_combine(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1]) + self::getData('lowerCase');
            unset($map["\xC4\xB0"]); // U+0130 has no simple case folding
        }

        return strtr($s, $map);
    }

    /**
     * Counts the characters of UTF-8 $s like native mbstring, even when ill-formed: one per byte that is not a continuation byte.
     */
    private static function utf8Length(string $s): int
    {
        return \strlen($s) - array_sum(\array_slice(count_chars($s), 0x80, 0x40));
    }

    /**
     * @return int|false The offset in bytes of the character at $offset, false when it is out of range, like native mbstring
     */
    private static function utf8Offset(string $s, int $offset, bool $bytes)
    {
        if (0 === $offset) {
            return 0;
        }

        if ($bytes || preg_match('//u', $u = str_replace("\xFF", '?', $s))) {
            $length = $bytes ? \strlen($s) : self::utf8Length($s);

            if (0 > $offset) {
                $offset += $length;
            }

            if (0 > $offset || $offset > $length) {
                return false;
            }

            if ($bytes) {
                return $offset;
            }

            for ($rx = ''; 65535 < $offset; $offset -= 65535) {
                $rx .= '.{65535}';
            }
            preg_match('/^'.$rx.'.{'.$offset.'}\K/su', $u, $m, \PREG_OFFSET_CAPTURE);

            return $m[0][1];
        }

        // Walk ill-formed UTF-8 like native mbstring: backward over the bytes that are not continuation bytes, forward by the lengths that lead bytes announce
        $length = \strlen($s);

        if (0 > $offset) {
            $i = $length;

            while (0 > $offset) {
                if (0 === $i) {
                    return false;
                }

                if (0x80 !== (\ord($s[--$i]) & 0xC0)) {
                    ++$offset;
                }
            }

            return $i;
        }

        $i = 0;

        while (0 < $offset--) {
            if ($i >= $length) {
                return false;
            }

            $c = \ord($s[$i]);
            $i += $c < 0xC2 ? 1 : ($c < 0xE0 ? 2 : ($c < 0xF0 ? 3 : ($c < 0xF5 ? 4 : 1)));
        }

        return min($i, $length);
    }

    private static function html_encoding_callback(array $m)
    {
        $i = 1;
        $entities = '';
        $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8'));

        while (isset($m[$i])) {
            if (0x80 > $m[$i]) {
                $entities .= \chr($m[$i++]);
                continue;
            }
            if (0xF0 <= $m[$i]) {
                $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
            } elseif (0xE0 <= $m[$i]) {
                $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
            } else {
                $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80;
            }

            $entities .= '&#'.$c.';';
        }

        return $entities;
    }

    private static function title_case(array $s)
    {
        static $titleCase = null;
        if (null === $titleCase) {
            $titleCase = self::getData('titleCase');
        }

        return ($titleCase[$s[1]] ?? self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8')).self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
    }

    private static function getData($file)
    {
        if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
            return require $file;
        }

        return false;
    }

    private static function getEncoding($encoding)
    {
        if (null === $encoding) {
            return self::$internalEncoding;
        }

        if ('UTF-8' === $encoding) {
            return 'UTF-8';
        }

        $encoding = strtoupper($encoding);

        if ('8BIT' === $encoding || 'BINARY' === $encoding) {
            return 'CP850';
        }

        if ('UTF8' === $encoding) {
            return 'UTF-8';
        }

        if ('UTF-32' === $encoding) {
            return 'UTF-32BE';
        }

        if ('UTF-16' === $encoding) {
            return 'UTF-16BE';
        }

        if ('UCS-2' === $encoding) {
            return 'UCS-2BE';
        }

        return $encoding;
    }

    private static function iconv($fromEncoding, $toEncoding, $s)
    {
        if (null === self::$iconvSupportsIgnore) {
            self::$iconvSupportsIgnore = false !== @iconv('UTF-8', 'UTF-8//IGNORE', '');
        }

        return self::$iconvSupportsIgnore
            ? iconv($fromEncoding, $toEncoding.'//IGNORE', $s)
            : iconv($fromEncoding, $toEncoding, $s);
    }

    /** @return string|false */
    private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        if ('' === $characters) {
            return $string;
        }

        if ('UTF-8' === self::getEncoding($encoding)) {
            $encoding = null;
            if (!preg_match('//u', $string) || (null !== $characters && !preg_match('//u', $characters))) {
                return self::mb_trim_invalid_utf8($string, $characters, $function);
            }
        } else {
            $string = self::iconv($encoding, 'UTF-8', $string);

            if (null !== $characters) {
                $characters = self::iconv($encoding, 'UTF-8', $characters);
            }
        }

        if (null === $characters) {
            $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}";
        } else {
            $characters = preg_quote($characters);
        }

        $string = preg_replace(\sprintf($regex, $characters), '', $string);

        if (null === $encoding) {
            return $string;
        }

        return self::iconv('UTF-8', $encoding, $string);
    }

    /**
     * Trims ill-formed UTF-8 like mbstring: each maximal subpart of an ill-formed
     * sequence counts as one character, and the string is returned as is when
     * nothing is trimmed, else re-encoded with the substitute character.
     */
    private static function mb_trim_invalid_utf8(string $string, ?string $characters, string $function): string
    {
        $regex = self::UTF8_CHAR_REGEX;

        // Ill-formed sequences leave group 1 empty: they are trimmed when $characters has one too
        preg_match_all($regex, $characters ?? "\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}", $m);
        $trimmed = array_flip($m[1]);

        $start = 0;
        $end = \strlen($string);

        while ('mb_rtrim' !== $function && $start < $end && preg_match($regex, $string, $m, 0, $start) && isset($trimmed[$m[1] ?? ''])) {
            $start += \strlen($m[0]);
        }

        while ('mb_ltrim' !== $function && $start < $end) {
            // Characters are at most 4 bytes long, and only continuation bytes don't start one
            for ($i = $end - 1; $i > $start && $i > $end - 4 && 0x80 === (\ord($string[$i]) & 0xC0); --$i) {
            }

            preg_match_all($regex, substr($string, $i, $end - $i), $m);

            if (!isset($trimmed[end($m[1])])) {
                break;
            }

            $end -= \strlen(end($m[0]));
        }

        if (0 === $start && \strlen($string) === $end) {
            return $string;
        }

        $string = substr($string, $start, $end - $start);

        return 'none' === mb_substitute_character() ? self::scrubUtf8($string) : mb_convert_encoding($string, 'UTF-8', 'UTF-8');
    }

    private static function scrubUtf8(string $s): string
    {
        return preg_replace(self::UTF8_CHAR_REGEX, '$1', $s);
    }

    /**
     * @param true[] $referenceIds The ids of the references followed to reach $array
     */
    private static function convertArray(array $array, $toEncoding, $fromEncoding, array $referenceIds = []): array
    {
        $r = [];

        foreach ($array as $key => $v) {
            if (null !== $v && !\is_scalar($v) && !\is_array($v)) {
                trigger_error('mb_convert_encoding(): Object is not supported', \E_USER_WARNING);
                continue;
            }

            $k = \is_string($key) ? self::mb_convert_encoding($key, $toEncoding, $fromEncoding) : $key;

            if (\is_string($v)) {
                $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding);
            } elseif (\is_array($v)) {
                // Only a reference can close a cycle: following the same one twice means one was found
                $id = 70400 <= \PHP_VERSION_ID && ($reference = \ReflectionReference::fromArrayElement($array, $key)) ? $reference->getId() : null;

                if (null !== $id && isset($referenceIds[$id])) {
                    trigger_error('mb_convert_encoding(): Cannot convert recursively referenced values', \E_USER_WARNING);
                    $v = [];
                } else {
                    $v = self::convertArray($v, $toEncoding, $fromEncoding, null === $id ? $referenceIds : $referenceIds + [$id => true]);
                }
            }

            // Keys that collide once converted keep their first value
            $r += [$k => $v];
        }

        return $r;
    }

    /**
     * Returns the error native mbstring throws for these encoding arguments, if any.
     */
    private static function getConversionError(string $function, int $argument, $toEncoding, $fromEncoding): ?string
    {
        if (!self::isConvertibleEncoding((string) $toEncoding)) {
            return \sprintf('%s(): Argument #%d ($to_encoding) must be a valid encoding, "%s" given', $function, $argument, $toEncoding);
        }

        if (null === $fromEncoding) {
            return null;
        }

        if (!$list = \is_array($fromEncoding) ? $fromEncoding : ('' === $fromEncoding ? [] : explode(',', $fromEncoding))) {
            return \sprintf('%s(): Argument #%d ($from_encoding) must specify at least one encoding', $function, 1 + $argument);
        }

        foreach ($list as $encoding) {
            $encoding = \is_array($fromEncoding) ? (string) $encoding : trim($encoding, " \t");

            // PHP 8.4 takes an empty entry of a comma-separated list for "auto"
            if ('' === $encoding && !\is_array($fromEncoding) && 80400 <= \PHP_VERSION_ID) {
                continue;
            }

            if (0 !== strcasecmp($encoding, 'auto') && !self::isConvertibleEncoding($encoding)) {
                return \sprintf('%s(): Argument #%d ($from_encoding) contains invalid encoding "%s"', $function, 1 + $argument, $encoding);
            }
        }

        return null;
    }

    private static function isConvertibleEncoding(string $encoding): bool
    {
        if (isset(self::$convertibleEncodings[$encoding])) {
            return true;
        }

        $normalizedEncoding = self::getEncoding($encoding);

        // Native encoding names use no other characters, while iconv() also accepts suffixes like //TRANSLIT
        if ('' === $normalizedEncoding || preg_match('/[^\w#\-.:]/', $encoding) || (null === (self::UNSUPPORTED_CODEPOINT_ENCODINGS[$normalizedEncoding] ?? null) && false === @iconv($normalizedEncoding, $normalizedEncoding, ''))) {
            return false;
        }

        return self::$convertibleEncodings[$encoding] = true;
    }

    private static function assertEncoding(string $encoding, string $errorFormat): bool
    {
        try {
            $validEncoding = @self::mb_check_encoding('', $encoding);
        } catch (\ValueError $e) {
            throw new \ValueError(\sprintf($errorFormat, $encoding));
        }

        if (!$validEncoding) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error(\sprintf($errorFormat, $encoding), \E_USER_WARNING);
            } else {
                throw new \ValueError(\sprintf($errorFormat, $encoding));
            }
        }

        return $validEncoding;
    }

    private static function assertCodepointEncoding(string $encoding, string $function): bool
    {
        if (null !== $name = self::UNSUPPORTED_CODEPOINT_ENCODINGS[strtoupper($encoding)] ?? null) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error(\sprintf('%s(): Unsupported encoding "%s"', $function, $name), \E_USER_WARNING);

                return false;
            }

            throw new \ValueError(\sprintf('%s() does not support the "%s" encoding', $function, $name));
        }

        $normalizedEncoding = self::getEncoding($encoding);

        if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, '')) {
            return true;
        }

        if (80000 > \PHP_VERSION_ID) {
            trigger_error(\sprintf('%s(): Unknown encoding "%s"', $function, $encoding), \E_USER_WARNING);

            return false;
        }

        throw new \ValueError(\sprintf('%s(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $function, $encoding));
    }
}