krz/vox-populi

A Tumblr web client.

clone: git clone https://gitbay.org/krz/vox-populi.git

main: vendor/symfony/polyfill-intl-idn/Idn.php · raw

  1<?php
  2
  3/*
  4 * This file is part of the Symfony package.
  5 *
  6 * (c) Fabien Potencier <fabien@symfony.com> and Trevor Rowbotham <trevor.rowbotham@pm.me>
  7 *
  8 * For the full copyright and license information, please view the LICENSE
  9 * file that was distributed with this source code.
 10 */
 11
 12namespace Symfony\Polyfill\Intl\Idn;
 13
 14use Exception;
 15use Normalizer;
 16use Symfony\Polyfill\Intl\Idn\Resources\unidata\DisallowedRanges;
 17use Symfony\Polyfill\Intl\Idn\Resources\unidata\Regex;
 18
 19/**
 20 * @see https://www.unicode.org/reports/tr46/
 21 *
 22 * @internal
 23 */
 24final class Idn
 25{
 26    const ERROR_EMPTY_LABEL = 1;
 27    const ERROR_LABEL_TOO_LONG = 2;
 28    const ERROR_DOMAIN_NAME_TOO_LONG = 4;
 29    const ERROR_LEADING_HYPHEN = 8;
 30    const ERROR_TRAILING_HYPHEN = 0x10;
 31    const ERROR_HYPHEN_3_4 = 0x20;
 32    const ERROR_LEADING_COMBINING_MARK = 0x40;
 33    const ERROR_DISALLOWED = 0x80;
 34    const ERROR_PUNYCODE = 0x100;
 35    const ERROR_LABEL_HAS_DOT = 0x200;
 36    const ERROR_INVALID_ACE_LABEL = 0x400;
 37    const ERROR_BIDI = 0x800;
 38    const ERROR_CONTEXTJ = 0x1000;
 39    const ERROR_CONTEXTO_PUNCTUATION = 0x2000;
 40    const ERROR_CONTEXTO_DIGITS = 0x4000;
 41
 42    const INTL_IDNA_VARIANT_2003 = 0;
 43    const INTL_IDNA_VARIANT_UTS46 = 1;
 44
 45    const MAX_DOMAIN_SIZE = 253;
 46    const MAX_LABEL_SIZE = 63;
 47
 48    const BASE = 36;
 49    const TMIN = 1;
 50    const TMAX = 26;
 51    const SKEW = 38;
 52    const DAMP = 700;
 53    const INITIAL_BIAS = 72;
 54    const INITIAL_N = 128;
 55    const DELIMITER = '-';
 56    const MAX_INT = 2147483647;
 57
 58    /**
 59     * Contains the numeric value of a basic code point (for use in representing integers) in the
 60     * range 0 to BASE-1, or -1 if b is does not represent a value.
 61     *
 62     * @var array<int, int>
 63     */
 64    private static $basicToDigit = array(
 65        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 66        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 67
 68        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 69        26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, -1,
 70
 71        -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
 72        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
 73
 74        -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
 75        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
 76
 77        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 78        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 79
 80        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 81        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 82
 83        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 84        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 85
 86        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 87        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 88    );
 89
 90    /**
 91     * @var array<int, int>
 92     */
 93    private static $virama;
 94
 95    /**
 96     * @var array<int, string>
 97     */
 98    private static $mapped;
 99
100    /**
101     * @var array<int, bool>
102     */
103    private static $ignored;
104
105    /**
106     * @var array<int, string>
107     */
108    private static $deviation;
109
110    /**
111     * @var array<int, bool>
112     */
113    private static $disallowed;
114
115    /**
116     * @var array<int, string>
117     */
118    private static $disallowed_STD3_mapped;
119
120    /**
121     * @var array<int, bool>
122     */
123    private static $disallowed_STD3_valid;
124
125    /**
126     * @var bool
127     */
128    private static $mappingTableLoaded = false;
129
130    /**
131     * @see https://www.unicode.org/reports/tr46/#ToASCII
132     *
133     * @param string $domainName
134     * @param int    $options
135     * @param int    $variant
136     * @param array  $idna_info
137     *
138     * @return string|false
139     */
140    public static function idn_to_ascii($domainName, $options = IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = array())
141    {
142        if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) {
143            @trigger_error('idn_to_ascii(): INTL_IDNA_VARIANT_2003 is deprecated', E_USER_DEPRECATED);
144        }
145
146        $options = array(
147            'CheckHyphens' => true,
148            'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & \IDNA_CHECK_BIDI),
149            'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & \IDNA_CHECK_CONTEXTJ),
150            'UseSTD3ASCIIRules' => 0 !== ($options & \IDNA_USE_STD3_RULES),
151            'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & \IDNA_NONTRANSITIONAL_TO_ASCII),
152            'VerifyDnsLength' => true,
153        );
154        $info = new Info();
155        $labels = self::process((string) $domainName, $options, $info);
156
157        foreach ($labels as $i => $label) {
158            // Only convert labels to punycode that contain non-ASCII code points
159            if (1 === preg_match('/[^\x00-\x7F]/', $label)) {
160                try {
161                    $label = 'xn--'.self::punycodeEncode($label);
162                } catch (Exception $e) {
163                    $info->errors |= self::ERROR_PUNYCODE;
164                }
165
166                $labels[$i] = $label;
167            }
168        }
169
170        if ($options['VerifyDnsLength']) {
171            self::validateDomainAndLabelLength($labels, $info);
172        }
173
174        $idna_info = array(
175            'result' => implode('.', $labels),
176            'isTransitionalDifferent' => $info->transitionalDifferent,
177            'errors' => $info->errors,
178        );
179
180        return 0 === $info->errors ? $idna_info['result'] : false;
181    }
182
183    /**
184     * @see https://www.unicode.org/reports/tr46/#ToUnicode
185     *
186     * @param string $domainName
187     * @param int    $options
188     * @param int    $variant
189     * @param array  $idna_info
190     *
191     * @return string|false
192     */
193    public static function idn_to_utf8($domainName, $options = IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = array())
194    {
195        if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) {
196            @trigger_error('idn_to_utf8(): INTL_IDNA_VARIANT_2003 is deprecated', E_USER_DEPRECATED);
197        }
198
199        $info = new Info();
200        $labels = self::process((string) $domainName, array(
201            'CheckHyphens' => true,
202            'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & \IDNA_CHECK_BIDI),
203            'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & \IDNA_CHECK_CONTEXTJ),
204            'UseSTD3ASCIIRules' => 0 !== ($options & \IDNA_USE_STD3_RULES),
205            'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & \IDNA_NONTRANSITIONAL_TO_UNICODE),
206        ), $info);
207        $idna_info = array(
208            'result' => implode('.', $labels),
209            'isTransitionalDifferent' => $info->transitionalDifferent,
210            'errors' => $info->errors,
211        );
212
213        return 0 === $info->errors ? $idna_info['result'] : false;
214    }
215
216    /**
217     * @param string $label
218     *
219     * @return bool
220     */
221    private static function isValidContextJ(array $codePoints, $label)
222    {
223        if (!isset(self::$virama)) {
224            self::$virama = require __DIR__.\DIRECTORY_SEPARATOR.'Resources'.\DIRECTORY_SEPARATOR.'unidata'.\DIRECTORY_SEPARATOR.'virama.php';
225        }
226
227        $offset = 0;
228
229        foreach ($codePoints as $i => $codePoint) {
230            if (0x200C !== $codePoint && 0x200D !== $codePoint) {
231                continue;
232            }
233
234            if (!isset($codePoints[$i - 1])) {
235                return false;
236            }
237
238            // If Canonical_Combining_Class(Before(cp)) .eq. Virama Then True;
239            if (isset(self::$virama[$codePoints[$i - 1]])) {
240                continue;
241            }
242
243            // If RegExpMatch((Joining_Type:{L,D})(Joining_Type:T)*\u200C(Joining_Type:T)*(Joining_Type:{R,D})) Then
244            // True;
245            // Generated RegExp = ([Joining_Type:{L,D}][Joining_Type:T]*\u200C[Joining_Type:T]*)[Joining_Type:{R,D}]
246            if (0x200C === $codePoint && 1 === preg_match(Regex::ZWNJ, $label, $matches, PREG_OFFSET_CAPTURE, $offset)) {
247                $offset += \strlen($matches[1][0]);
248
249                continue;
250            }
251
252            return false;
253        }
254
255        return true;
256    }
257
258    /**
259     * @see https://www.unicode.org/reports/tr46/#ProcessingStepMap
260     *
261     * @param string              $input
262     * @param array<string, bool> $options
263     *
264     * @return string
265     */
266    private static function mapCodePoints($input, array $options, Info $info)
267    {
268        $str = '';
269        $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules'];
270        $transitional = $options['Transitional_Processing'];
271
272        foreach (self::utf8Decode($input) as $codePoint) {
273            $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules);
274
275            switch ($data['status']) {
276                case 'disallowed':
277                    $info->errors |= self::ERROR_DISALLOWED;
278
279                    // no break.
280
281                case 'valid':
282                    $str .= mb_chr($codePoint, 'utf-8');
283
284                    break;
285
286                case 'ignored':
287                    // Do nothing.
288                    break;
289
290                case 'mapped':
291                    $str .= $data['mapping'];
292
293                    break;
294
295                case 'deviation':
296                    $info->transitionalDifferent = true;
297                    $str .= ($transitional ? $data['mapping'] : mb_chr($codePoint, 'utf-8'));
298
299                    break;
300            }
301        }
302
303        return $str;
304    }
305
306    /**
307     * @see https://www.unicode.org/reports/tr46/#Processing
308     *
309     * @param string              $domain
310     * @param array<string, bool> $options
311     *
312     * @return array<int, string>
313     */
314    private static function process($domain, array $options, Info $info)
315    {
316        // If VerifyDnsLength is not set, we are doing ToUnicode otherwise we are doing ToASCII and
317        // we need to respect the VerifyDnsLength option.
318        $checkForEmptyLabels = !isset($options['VerifyDnsLength']) || $options['VerifyDnsLength'];
319
320        if ($checkForEmptyLabels && '' === $domain) {
321            $info->errors |= self::ERROR_EMPTY_LABEL;
322
323            return array($domain);
324        }
325
326        // Step 1. Map each code point in the domain name string
327        $domain = self::mapCodePoints($domain, $options, $info);
328
329        // Step 2. Normalize the domain name string to Unicode Normalization Form C.
330        if (!Normalizer::isNormalized($domain, Normalizer::FORM_C)) {
331            $domain = Normalizer::normalize($domain, Normalizer::FORM_C);
332        }
333
334        // Step 3. Break the string into labels at U+002E (.) FULL STOP.
335        $labels = explode('.', $domain);
336        $lastLabelIndex = \count($labels) - 1;
337
338        // Step 4. Convert and validate each label in the domain name string.
339        foreach ($labels as $i => $label) {
340            $validationOptions = $options;
341
342            if ('xn--' === substr($label, 0, 4)) {
343                try {
344                    $label = self::punycodeDecode(substr($label, 4));
345                } catch (Exception $e) {
346                    $info->errors |= self::ERROR_PUNYCODE;
347
348                    continue;
349                }
350
351                $validationOptions['Transitional_Processing'] = false;
352                $labels[$i] = $label;
353            }
354
355            self::validateLabel($label, $info, $validationOptions, $i > 0 && $i === $lastLabelIndex);
356        }
357
358        if ($info->bidiDomain && !$info->validBidiDomain) {
359            $info->errors |= self::ERROR_BIDI;
360        }
361
362        // Any input domain name string that does not record an error has been successfully
363        // processed according to this specification. Conversely, if an input domain_name string
364        // causes an error, then the processing of the input domain_name string fails. Determining
365        // what to do with error input is up to the caller, and not in the scope of this document.
366        return $labels;
367    }
368
369    /**
370     * @see https://tools.ietf.org/html/rfc5893#section-2
371     *
372     * @param string $label
373     */
374    private static function validateBidiLabel($label, Info $info)
375    {
376        if (1 === preg_match(Regex::RTL_LABEL, $label)) {
377            $info->bidiDomain = true;
378
379            // Step 1. The first character must be a character with Bidi property L, R, or AL.
380            // If it has the R or AL property, it is an RTL label
381            if (1 !== preg_match(Regex::BIDI_STEP_1_RTL, $label)) {
382                $info->validBidiDomain = false;
383
384                return;
385            }
386
387            // Step 2. In an RTL label, only characters with the Bidi properties R, AL, AN, EN, ES,
388            // CS, ET, ON, BN, or NSM are allowed.
389            if (1 === preg_match(Regex::BIDI_STEP_2, $label)) {
390                $info->validBidiDomain = false;
391
392                return;
393            }
394
395            // Step 3. In an RTL label, the end of the label must be a character with Bidi property
396            // R, AL, EN, or AN, followed by zero or more characters with Bidi property NSM.
397            if (1 !== preg_match(Regex::BIDI_STEP_3, $label)) {
398                $info->validBidiDomain = false;
399
400                return;
401            }
402
403            // Step 4. In an RTL label, if an EN is present, no AN may be present, and vice versa.
404            if (1 === preg_match(Regex::BIDI_STEP_4_AN, $label) && 1 === preg_match(Regex::BIDI_STEP_4_EN, $label)) {
405                $info->validBidiDomain = false;
406
407                return;
408            }
409
410            return;
411        }
412
413        // We are a LTR label
414        // Step 1. The first character must be a character with Bidi property L, R, or AL.
415        // If it has the L property, it is an LTR label.
416        if (1 !== preg_match(Regex::BIDI_STEP_1_LTR, $label)) {
417            $info->validBidiDomain = false;
418
419            return;
420        }
421
422        // Step 5. In an LTR label, only characters with the Bidi properties L, EN,
423        // ES, CS, ET, ON, BN, or NSM are allowed.
424        if (1 === preg_match(Regex::BIDI_STEP_5, $label)) {
425            $info->validBidiDomain = false;
426
427            return;
428        }
429
430        // Step 6.In an LTR label, the end of the label must be a character with Bidi property L or
431        // EN, followed by zero or more characters with Bidi property NSM.
432        if (1 !== preg_match(Regex::BIDI_STEP_6, $label)) {
433            $info->validBidiDomain = false;
434
435            return;
436        }
437    }
438
439    /**
440     * @param array<int, string> $labels
441     */
442    private static function validateDomainAndLabelLength(array $labels, Info $info)
443    {
444        $maxDomainSize = self::MAX_DOMAIN_SIZE;
445        $length = \count($labels);
446
447        // Number of "." delimiters.
448        $domainLength = $length - 1;
449
450        // If the last label is empty and it is not the first label, then it is the root label.
451        // Increase the max size by 1, making it 254, to account for the root label's "."
452        // delimiter. This also means we don't need to check the last label's length for being too
453        // long.
454        if ($length > 1 && '' === $labels[$length - 1]) {
455            ++$maxDomainSize;
456            --$length;
457        }
458
459        for ($i = 0; $i < $length; ++$i) {
460            $bytes = \strlen($labels[$i]);
461            $domainLength += $bytes;
462
463            if ($bytes > self::MAX_LABEL_SIZE) {
464                $info->errors |= self::ERROR_LABEL_TOO_LONG;
465            }
466        }
467
468        if ($domainLength > $maxDomainSize) {
469            $info->errors |= self::ERROR_DOMAIN_NAME_TOO_LONG;
470        }
471    }
472
473    /**
474     * @see https://www.unicode.org/reports/tr46/#Validity_Criteria
475     *
476     * @param string              $label
477     * @param array<string, bool> $options
478     * @param bool                $canBeEmpty
479     */
480    private static function validateLabel($label, Info $info, array $options, $canBeEmpty)
481    {
482        if ('' === $label) {
483            if (!$canBeEmpty && (!isset($options['VerifyDnsLength']) || $options['VerifyDnsLength'])) {
484                $info->errors |= self::ERROR_EMPTY_LABEL;
485            }
486
487            return;
488        }
489
490        // Step 1. The label must be in Unicode Normalization Form C.
491        if (!Normalizer::isNormalized($label, Normalizer::FORM_C)) {
492            $info->errors |= self::ERROR_INVALID_ACE_LABEL;
493        }
494
495        $codePoints = self::utf8Decode($label);
496
497        if ($options['CheckHyphens']) {
498            // Step 2. If CheckHyphens, the label must not contain a U+002D HYPHEN-MINUS character
499            // in both the thrid and fourth positions.
500            if (isset($codePoints[2], $codePoints[3]) && 0x002D === $codePoints[2] && 0x002D === $codePoints[3]) {
501                $info->errors |= self::ERROR_HYPHEN_3_4;
502            }
503
504            // Step 3. If CheckHyphens, the label must neither begin nor end with a U+002D
505            // HYPHEN-MINUS character.
506            if ('-' === substr($label, 0, 1)) {
507                $info->errors |= self::ERROR_LEADING_HYPHEN;
508            }
509
510            if ('-' === substr($label, -1, 1)) {
511                $info->errors |= self::ERROR_TRAILING_HYPHEN;
512            }
513        }
514
515        // Step 4. The label must not contain a U+002E (.) FULL STOP.
516        if (false !== strpos($label, '.')) {
517            $info->errors |= self::ERROR_LABEL_HAS_DOT;
518        }
519
520        // Step 5. The label must not begin with a combining mark, that is: General_Category=Mark.
521        if (1 === preg_match(Regex::COMBINING_MARK, $label)) {
522            $info->errors |= self::ERROR_LEADING_COMBINING_MARK;
523        }
524
525        // Step 6. Each code point in the label must only have certain status values according to
526        // Section 5, IDNA Mapping Table:
527        $transitional = $options['Transitional_Processing'];
528        $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules'];
529
530        foreach ($codePoints as $codePoint) {
531            $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules);
532            $status = $data['status'];
533
534            if ('valid' === $status || (!$transitional && 'deviation' === $status)) {
535                continue;
536            }
537
538            $info->errors |= self::ERROR_DISALLOWED;
539
540            break;
541        }
542
543        // Step 7. If CheckJoiners, the label must satisify the ContextJ rules from Appendix A, in
544        // The Unicode Code Points and Internationalized Domain Names for Applications (IDNA)
545        // [IDNA2008].
546        if ($options['CheckJoiners'] && !self::isValidContextJ($codePoints, $label)) {
547            $info->errors |= self::ERROR_CONTEXTJ;
548        }
549
550        // Step 8. If CheckBidi, and if the domain name is a  Bidi domain name, then the label must
551        // satisfy all six of the numbered conditions in [IDNA2008] RFC 5893, Section 2.
552        if ($options['CheckBidi'] && (!$info->bidiDomain || $info->validBidiDomain)) {
553            self::validateBidiLabel($label, $info);
554        }
555    }
556
557    /**
558     * @see https://tools.ietf.org/html/rfc3492#section-6.2
559     *
560     * @param string $input
561     *
562     * @return string
563     */
564    private static function punycodeDecode($input)
565    {
566        $n = self::INITIAL_N;
567        $out = 0;
568        $i = 0;
569        $bias = self::INITIAL_BIAS;
570        $lastDelimIndex = strrpos($input, self::DELIMITER);
571        $b = false === $lastDelimIndex ? 0 : $lastDelimIndex;
572        $inputLength = \strlen($input);
573        $output = array();
574        $bytes = array_map('ord', str_split($input));
575
576        for ($j = 0; $j < $b; ++$j) {
577            if ($bytes[$j] > 0x7F) {
578                throw new Exception('Invalid input');
579            }
580
581            $output[$out++] = $input[$j];
582        }
583
584        if ($b > 0) {
585            ++$b;
586        }
587
588        for ($in = $b; $in < $inputLength; ++$out) {
589            $oldi = $i;
590            $w = 1;
591
592            for ($k = self::BASE; /* no condition */; $k += self::BASE) {
593                if ($in >= $inputLength) {
594                    throw new Exception('Invalid input');
595                }
596
597                $digit = self::$basicToDigit[$bytes[$in++] & 0xFF];
598
599                if ($digit < 0) {
600                    throw new Exception('Invalid input');
601                }
602
603                if ($digit > intdiv(self::MAX_INT - $i, $w)) {
604                    throw new Exception('Integer overflow');
605                }
606
607                $i += $digit * $w;
608
609                if ($k <= $bias) {
610                    $t = self::TMIN;
611                } elseif ($k >= $bias + self::TMAX) {
612                    $t = self::TMAX;
613                } else {
614                    $t = $k - $bias;
615                }
616
617                if ($digit < $t) {
618                    break;
619                }
620
621                $baseMinusT = self::BASE - $t;
622
623                if ($w > intdiv(self::MAX_INT, $baseMinusT)) {
624                    throw new Exception('Integer overflow');
625                }
626
627                $w *= $baseMinusT;
628            }
629
630            $outPlusOne = $out + 1;
631            $bias = self::adaptBias($i - $oldi, $outPlusOne, 0 === $oldi);
632
633            if (intdiv($i, $outPlusOne) > self::MAX_INT - $n) {
634                throw new Exception('Integer overflow');
635            }
636
637            $n += intdiv($i, $outPlusOne);
638            $i %= $outPlusOne;
639            array_splice($output, $i++, 0, array(mb_chr($n, 'utf-8')));
640        }
641
642        return implode('', $output);
643    }
644
645    /**
646     * @see https://tools.ietf.org/html/rfc3492#section-6.3
647     *
648     * @param string $input
649     *
650     * @return string
651     */
652    private static function punycodeEncode($input)
653    {
654        $n = self::INITIAL_N;
655        $delta = 0;
656        $out = 0;
657        $bias = self::INITIAL_BIAS;
658        $inputLength = 0;
659        $output = '';
660        $iter = self::utf8Decode($input);
661
662        foreach ($iter as $codePoint) {
663            ++$inputLength;
664
665            if ($codePoint < 0x80) {
666                $output .= \chr($codePoint);
667                ++$out;
668            }
669        }
670
671        $h = $out;
672        $b = $out;
673
674        if ($b > 0) {
675            $output .= self::DELIMITER;
676            ++$out;
677        }
678
679        while ($h < $inputLength) {
680            $m = self::MAX_INT;
681
682            foreach ($iter as $codePoint) {
683                if ($codePoint >= $n && $codePoint < $m) {
684                    $m = $codePoint;
685                }
686            }
687
688            if ($m - $n > intdiv(self::MAX_INT - $delta, $h + 1)) {
689                throw new Exception('Integer overflow');
690            }
691
692            $delta += ($m - $n) * ($h + 1);
693            $n = $m;
694
695            foreach ($iter as $codePoint) {
696                if ($codePoint < $n && 0 === ++$delta) {
697                    throw new Exception('Integer overflow');
698                } elseif ($codePoint === $n) {
699                    $q = $delta;
700
701                    for ($k = self::BASE; /* no condition */; $k += self::BASE) {
702                        if ($k <= $bias) {
703                            $t = self::TMIN;
704                        } elseif ($k >= $bias + self::TMAX) {
705                            $t = self::TMAX;
706                        } else {
707                            $t = $k - $bias;
708                        }
709
710                        if ($q < $t) {
711                            break;
712                        }
713
714                        $qMinusT = $q - $t;
715                        $baseMinusT = self::BASE - $t;
716                        $output .= self::encodeDigit($t + ($qMinusT) % ($baseMinusT), false);
717                        ++$out;
718                        $q = intdiv($qMinusT, $baseMinusT);
719                    }
720
721                    $output .= self::encodeDigit($q, false);
722                    ++$out;
723                    $bias = self::adaptBias($delta, $h + 1, $h === $b);
724                    $delta = 0;
725                    ++$h;
726                }
727            }
728
729            ++$delta;
730            ++$n;
731        }
732
733        return $output;
734    }
735
736    /**
737     * @see https://tools.ietf.org/html/rfc3492#section-6.1
738     *
739     * @param int  $delta
740     * @param int  $numPoints
741     * @param bool $firstTime
742     *
743     * @return int
744     */
745    private static function adaptBias($delta, $numPoints, $firstTime)
746    {
747        // xxx >> 1 is a faster way of doing intdiv(xxx, 2)
748        $delta = $firstTime ? intdiv($delta, self::DAMP) : $delta >> 1;
749        $delta += intdiv($delta, $numPoints);
750        $k = 0;
751
752        while ($delta > ((self::BASE - self::TMIN) * self::TMAX) >> 1) {
753            $delta = intdiv($delta, self::BASE - self::TMIN);
754            $k += self::BASE;
755        }
756
757        return $k + intdiv((self::BASE - self::TMIN + 1) * $delta, $delta + self::SKEW);
758    }
759
760    /**
761     * @param int  $d
762     * @param bool $flag
763     *
764     * @return string
765     */
766    private static function encodeDigit($d, $flag)
767    {
768        return \chr($d + 22 + 75 * ($d < 26 ? 1 : 0) - (($flag ? 1 : 0) << 5));
769    }
770
771    /**
772     * Takes a UTF-8 encoded string and converts it into a series of integer code points. Any
773     * invalid byte sequences will be replaced by a U+FFFD replacement code point.
774     *
775     * @see https://encoding.spec.whatwg.org/#utf-8-decoder
776     *
777     * @param string $input
778     *
779     * @return array<int, int>
780     */
781    private static function utf8Decode($input)
782    {
783        $bytesSeen = 0;
784        $bytesNeeded = 0;
785        $lowerBoundary = 0x80;
786        $upperBoundary = 0xBF;
787        $codePoint = 0;
788        $codePoints = array();
789        $length = \strlen($input);
790
791        for ($i = 0; $i < $length; ++$i) {
792            $byte = \ord($input[$i]);
793
794            if (0 === $bytesNeeded) {
795                if ($byte >= 0x00 && $byte <= 0x7F) {
796                    $codePoints[] = $byte;
797
798                    continue;
799                }
800
801                if ($byte >= 0xC2 && $byte <= 0xDF) {
802                    $bytesNeeded = 1;
803                    $codePoint = $byte & 0x1F;
804                } elseif ($byte >= 0xE0 && $byte <= 0xEF) {
805                    if (0xE0 === $byte) {
806                        $lowerBoundary = 0xA0;
807                    } elseif (0xED === $byte) {
808                        $upperBoundary = 0x9F;
809                    }
810
811                    $bytesNeeded = 2;
812                    $codePoint = $byte & 0xF;
813                } elseif ($byte >= 0xF0 && $byte <= 0xF4) {
814                    if (0xF0 === $byte) {
815                        $lowerBoundary = 0x90;
816                    } elseif (0xF4 === $byte) {
817                        $upperBoundary = 0x8F;
818                    }
819
820                    $bytesNeeded = 3;
821                    $codePoint = $byte & 0x7;
822                } else {
823                    $codePoints[] = 0xFFFD;
824                }
825
826                continue;
827            }
828
829            if ($byte < $lowerBoundary || $byte > $upperBoundary) {
830                $codePoint = 0;
831                $bytesNeeded = 0;
832                $bytesSeen = 0;
833                $lowerBoundary = 0x80;
834                $upperBoundary = 0xBF;
835                --$i;
836                $codePoints[] = 0xFFFD;
837
838                continue;
839            }
840
841            $lowerBoundary = 0x80;
842            $upperBoundary = 0xBF;
843            $codePoint = ($codePoint << 6) | ($byte & 0x3F);
844
845            if (++$bytesSeen !== $bytesNeeded) {
846                continue;
847            }
848
849            $codePoints[] = $codePoint;
850            $codePoint = 0;
851            $bytesNeeded = 0;
852            $bytesSeen = 0;
853        }
854
855        // String unexpectedly ended, so append a U+FFFD code point.
856        if (0 !== $bytesNeeded) {
857            $codePoints[] = 0xFFFD;
858        }
859
860        return $codePoints;
861    }
862
863    /**
864     * @param int  $codePoint
865     * @param bool $useSTD3ASCIIRules
866     *
867     * @return array{status: string, mapping?: string}
868     */
869    private static function lookupCodePointStatus($codePoint, $useSTD3ASCIIRules)
870    {
871        if (!self::$mappingTableLoaded) {
872            self::$mappingTableLoaded = true;
873            self::$mapped = require __DIR__.'/Resources/unidata/mapped.php';
874            self::$ignored = require __DIR__.'/Resources/unidata/ignored.php';
875            self::$deviation = require __DIR__.'/Resources/unidata/deviation.php';
876            self::$disallowed = require __DIR__.'/Resources/unidata/disallowed.php';
877            self::$disallowed_STD3_mapped = require __DIR__.'/Resources/unidata/disallowed_STD3_mapped.php';
878            self::$disallowed_STD3_valid = require __DIR__.'/Resources/unidata/disallowed_STD3_valid.php';
879        }
880
881        if (isset(self::$mapped[$codePoint])) {
882            return array('status' => 'mapped', 'mapping' => self::$mapped[$codePoint]);
883        }
884
885        if (isset(self::$ignored[$codePoint])) {
886            return array('status' => 'ignored');
887        }
888
889        if (isset(self::$deviation[$codePoint])) {
890            return array('status' => 'deviation', 'mapping' => self::$deviation[$codePoint]);
891        }
892
893        if (isset(self::$disallowed[$codePoint]) || DisallowedRanges::inRange($codePoint)) {
894            return array('status' => 'disallowed');
895        }
896
897        $isDisallowedMapped = isset(self::$disallowed_STD3_mapped[$codePoint]);
898
899        if ($isDisallowedMapped || isset(self::$disallowed_STD3_valid[$codePoint])) {
900            $status = 'disallowed';
901
902            if (!$useSTD3ASCIIRules) {
903                $status = $isDisallowedMapped ? 'mapped' : 'valid';
904            }
905
906            if ($isDisallowedMapped) {
907                return array('status' => $status, 'mapping' => self::$disallowed_STD3_mapped[$codePoint]);
908            }
909
910            return array('status' => $status);
911        }
912
913        return array('status' => 'valid');
914    }
915}