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}