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