Punycode.php
1 year ago
PunycodeException.php
1 year ago
Uri.php
1 year ago
UriTemplate.php
1 year ago
Punycode.php
282 lines
| 1 | <?php |
| 2 | /* ============================================================================ |
| 3 | * Copyright 2021 Zindex Software |
| 4 | * |
| 5 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | * you may not use this file except in compliance with the License. |
| 7 | * You may obtain a copy of the License at |
| 8 | * |
| 9 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | * |
| 11 | * Unless required by applicable law or agreed to in writing, software |
| 12 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | * See the License for the specific language governing permissions and |
| 15 | * limitations under the License. |
| 16 | * ============================================================================ */ |
| 17 | |
| 18 | namespace Opis\Uri; |
| 19 | |
| 20 | use Opis\String\UnicodeString; |
| 21 | |
| 22 | final class Punycode |
| 23 | { |
| 24 | private const BASE = 36; |
| 25 | private const TMIN = 1; |
| 26 | private const TMAX = 26; |
| 27 | private const SKEW = 38; |
| 28 | private const DAMP = 700; |
| 29 | private const INITIAL_BIAS = 72; |
| 30 | private const INITIAL_N = 0x80; |
| 31 | private const PREFIX = 'xn--'; |
| 32 | private const PREFIX_LEN = 4; |
| 33 | private const DELIMITER = 0x2D; |
| 34 | private const MAX_INT = 0x7FFFFFFF; |
| 35 | private const NON_ASCII = '#[^\0-\x7E]#'; |
| 36 | |
| 37 | public static function encode(string $input): string |
| 38 | { |
| 39 | return implode('.', array_map([self::class, 'encodePart'], explode('.', $input))); |
| 40 | } |
| 41 | |
| 42 | public static function decode(string $input): string |
| 43 | { |
| 44 | return implode('.', array_map([self::class, 'decodePart'], explode('.', $input))); |
| 45 | } |
| 46 | |
| 47 | public static function normalize(string $input): string |
| 48 | { |
| 49 | return implode('.', array_map([self::class, 'normalizePart'], explode('.', $input))); |
| 50 | } |
| 51 | |
| 52 | public static function encodePart(string $input): string |
| 53 | { |
| 54 | if (!preg_match(self::NON_ASCII, $input)) { |
| 55 | return $input; |
| 56 | } |
| 57 | |
| 58 | $input = UnicodeString::getCodePointsFromString($input, UnicodeString::LOWER_CASE); |
| 59 | $input_len = count($input); |
| 60 | |
| 61 | $output = array_filter($input, static function (int $code): bool { |
| 62 | return $code < 0x80; |
| 63 | }); |
| 64 | |
| 65 | if ($output) { |
| 66 | $output = array_values($output); |
| 67 | } |
| 68 | |
| 69 | $delta = 0; |
| 70 | $n = self::INITIAL_N; |
| 71 | $bias = self::INITIAL_BIAS; |
| 72 | |
| 73 | $handled = $basic_length = count($output); |
| 74 | |
| 75 | if ($basic_length) { |
| 76 | $output[] = self::DELIMITER; |
| 77 | } |
| 78 | |
| 79 | while ($handled < $input_len) { |
| 80 | $m = self::MAX_INT; |
| 81 | |
| 82 | for ($i = 0; $i < $input_len; $i++) { |
| 83 | if ($input[$i] >= $n && $input[$i] < $m) { |
| 84 | $m = $input[$i]; |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | if (($m - $n) > intdiv(self::MAX_INT - $delta, $handled + 1)) { |
| 89 | throw new PunycodeException("Punycode overflow"); |
| 90 | } |
| 91 | |
| 92 | $delta += ($m - $n) * ($handled + 1); |
| 93 | |
| 94 | $n = $m; |
| 95 | |
| 96 | for ($i = 0; $i < $input_len; $i++) { |
| 97 | if ($input[$i] < $n && (++$delta === 0)) { |
| 98 | throw new PunycodeException("Punycode overflow"); |
| 99 | } |
| 100 | |
| 101 | if ($input[$i] === $n) { |
| 102 | $q = $delta; |
| 103 | for ($k = self::BASE; ; $k += self::BASE) { |
| 104 | $t = self::threshold($k, $bias); |
| 105 | if ($q < $t) { |
| 106 | break; |
| 107 | } |
| 108 | |
| 109 | $base_minus_t = self::BASE - $t; |
| 110 | |
| 111 | $q -= $t; |
| 112 | |
| 113 | $output[] = self::encodeDigit($t + ($q % $base_minus_t)); |
| 114 | |
| 115 | $q = intdiv($q, $base_minus_t); |
| 116 | } |
| 117 | |
| 118 | $output[] = self::encodeDigit($q); |
| 119 | |
| 120 | $bias = self::adapt($delta, $handled + 1, $handled === $basic_length); |
| 121 | $delta = 0; |
| 122 | $handled++; |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | $delta++; $n++; |
| 127 | } |
| 128 | |
| 129 | return self::PREFIX . UnicodeString::getStringFromCodePoints($output); |
| 130 | } |
| 131 | |
| 132 | public static function decodePart(string $input): string |
| 133 | { |
| 134 | if (stripos($input, self::PREFIX) !== 0) { |
| 135 | return $input; |
| 136 | } |
| 137 | |
| 138 | $input = UnicodeString::getCodePointsFromString(substr($input, self::PREFIX_LEN), UnicodeString::LOWER_CASE); |
| 139 | $input_len = count($input); |
| 140 | |
| 141 | $pos = array_keys($input, self::DELIMITER, true); |
| 142 | if ($pos) { |
| 143 | $pos = end($pos); |
| 144 | } else { |
| 145 | $pos = -1; |
| 146 | } |
| 147 | |
| 148 | /** @var int $pos */ |
| 149 | |
| 150 | if ($pos === -1) { |
| 151 | $output = []; |
| 152 | $pos = $output_len = 0; |
| 153 | } else { |
| 154 | $output = array_slice($input, 0, ++$pos); |
| 155 | $output_len = $pos; |
| 156 | for ($i = 0; $i < $pos; $i++) { |
| 157 | if ($output[$i] >= 0x80) { |
| 158 | throw new PunycodeException("Non-basic code point is not allowed: {$output[$i]}"); |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | $i = 0; |
| 164 | $n = self::INITIAL_N; |
| 165 | $bias = self::INITIAL_BIAS; |
| 166 | |
| 167 | while ($pos < $input_len) { |
| 168 | $old_i = $i; |
| 169 | |
| 170 | for ($w = 1, $k = self::BASE; ; $k += self::BASE) { |
| 171 | if ($pos >= $input_len) { |
| 172 | throw new PunycodeException("Punycode bad input"); |
| 173 | } |
| 174 | |
| 175 | $digit = self::decodeDigit($input[$pos++]); |
| 176 | |
| 177 | if ($digit >= self::BASE || $digit > intdiv(self::MAX_INT - $i, $w)) { |
| 178 | throw new PunycodeException("Punycode overflow"); |
| 179 | } |
| 180 | |
| 181 | $i += $digit * $w; |
| 182 | |
| 183 | $t = self::threshold($k, $bias); |
| 184 | if ($digit < $t) { |
| 185 | break; |
| 186 | } |
| 187 | |
| 188 | $t = self::BASE - $t; |
| 189 | |
| 190 | if ($w > intdiv(self::MAX_INT, $t)) { |
| 191 | throw new PunycodeException("Punycode overflow"); |
| 192 | } |
| 193 | |
| 194 | $w *= $t; |
| 195 | } |
| 196 | |
| 197 | $output_len++; |
| 198 | |
| 199 | if (intdiv($i, $output_len) > self::MAX_INT - $n) { |
| 200 | throw new PunycodeException("Punycode overflow"); |
| 201 | } |
| 202 | |
| 203 | $n += intdiv($i, $output_len); |
| 204 | |
| 205 | $bias = self::adapt($i - $old_i, $output_len, $old_i === 0); |
| 206 | |
| 207 | $i %= $output_len; |
| 208 | |
| 209 | array_splice($output, $i, 0, $n); |
| 210 | |
| 211 | $i++; |
| 212 | } |
| 213 | |
| 214 | return UnicodeString::getStringFromCodePoints($output); |
| 215 | } |
| 216 | |
| 217 | public static function normalizePart(string $input): string |
| 218 | { |
| 219 | $input = strtolower($input); |
| 220 | |
| 221 | if (strpos($input, self::DELIMITER) === 0) { |
| 222 | self::decodePart($input); // just validate |
| 223 | return $input; |
| 224 | } |
| 225 | |
| 226 | return self::encodePart($input); |
| 227 | } |
| 228 | |
| 229 | private static function encodeDigit(int $digit): int |
| 230 | { |
| 231 | return $digit + 0x16 + ($digit < 0x1A ? 0x4B: 0x00); |
| 232 | } |
| 233 | |
| 234 | private static function decodeDigit(int $code): int |
| 235 | { |
| 236 | if ($code < 0x3A) { |
| 237 | return $code - 0x16; |
| 238 | } |
| 239 | if ($code < 0x5B) { |
| 240 | return $code - 0x41; |
| 241 | } |
| 242 | if ($code < 0x7B) { |
| 243 | return $code - 0x61; |
| 244 | } |
| 245 | |
| 246 | return self::BASE; |
| 247 | } |
| 248 | |
| 249 | private static function threshold(int $k, int $bias): int |
| 250 | { |
| 251 | $d = $k - $bias; |
| 252 | |
| 253 | if ($d <= self::TMIN) { |
| 254 | return self::TMIN; |
| 255 | } |
| 256 | |
| 257 | if ($d >= self::TMAX) { |
| 258 | return self::TMAX; |
| 259 | } |
| 260 | |
| 261 | return $d; |
| 262 | } |
| 263 | |
| 264 | private static function adapt(int $delta, int $num_points, bool $first_time = false): int |
| 265 | { |
| 266 | $delta = intdiv($delta, $first_time ? self::DAMP : 2); |
| 267 | $delta += intdiv($delta, $num_points); |
| 268 | |
| 269 | $k = 0; |
| 270 | $base_tmin_diff = self::BASE - self::TMIN; |
| 271 | $lim = $base_tmin_diff * self::TMAX / 2; |
| 272 | |
| 273 | while ($delta > $lim) { |
| 274 | $delta = intdiv($delta, $base_tmin_diff); |
| 275 | $k += self::BASE; |
| 276 | } |
| 277 | |
| 278 | $k += intdiv(($base_tmin_diff + 1) * $delta, $delta + self::SKEW); |
| 279 | |
| 280 | return $k; |
| 281 | } |
| 282 | } |