Move URL from WebCore to WTF
[WebKit-https.git] / Source / WTF / wtf / cocoa / NSURLExtras.mm
1 /*
2  * Copyright (C) 2005, 2007, 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import "config.h"
30 #import <Foundation/Foundation.h>
31 #import "NSURLExtras.h"
32
33 #import "CFURLExtras.h"
34 #import "URLParser.h"
35 #import <wtf/Function.h>
36 #import <wtf/HexNumber.h>
37 #import <wtf/ObjCRuntimeExtras.h>
38 #import <wtf/RetainPtr.h>
39 #import <wtf/Vector.h>
40 #import <unicode/uchar.h>
41 #import <unicode/uidna.h>
42 #import <unicode/uscript.h>
43
44 // Needs to be big enough to hold an IDN-encoded name.
45 // For host names bigger than this, we won't do IDN encoding, which is almost certainly OK.
46 #define HOST_NAME_BUFFER_LENGTH 2048
47 #define URL_BYTES_BUFFER_LENGTH 2048
48
49 typedef void (* StringRangeApplierFunction)(NSString *, NSRange, RetainPtr<NSMutableArray>&);
50
51 static uint32_t IDNScriptWhiteList[(USCRIPT_CODE_LIMIT + 31) / 32];
52
53 namespace WTF {
54
55 static bool isArmenianLookalikeCharacter(UChar32 codePoint)
56 {
57     return codePoint == 0x0548 || codePoint == 0x054D || codePoint == 0x0578 || codePoint == 0x057D;
58 }
59
60 static bool isArmenianScriptCharacter(UChar32 codePoint)
61 {
62     UErrorCode error = U_ZERO_ERROR;
63     UScriptCode script = uscript_getScript(codePoint, &error);
64     if (error != U_ZERO_ERROR) {
65         LOG_ERROR("got ICU error while trying to look at scripts: %d", error);
66         return false;
67     }
68
69     return script == USCRIPT_ARMENIAN;
70 }
71
72
73 template<typename CharacterType> inline bool isASCIIDigitOrValidHostCharacter(CharacterType charCode)
74 {
75     if (!isASCIIDigitOrPunctuation(charCode))
76         return false;
77
78     // Things the URL Parser rejects:
79     switch (charCode) {
80     case '#':
81     case '%':
82     case '/':
83     case ':':
84     case '?':
85     case '@':
86     case '[':
87     case '\\':
88     case ']':
89         return false;
90     default:
91         return true;
92     }
93 }
94
95
96
97 static BOOL isLookalikeCharacter(std::optional<UChar32> previousCodePoint, UChar32 charCode)
98 {
99     // This function treats the following as unsafe, lookalike characters:
100     // any non-printable character, any character considered as whitespace,
101     // any ignorable character, and emoji characters related to locks.
102     
103     // We also considered the characters in Mozilla's blacklist <http://kb.mozillazine.org/Network.IDN.blacklist_chars>.
104
105     // Some of the characters here will never appear once ICU has encoded.
106     // For example, ICU transforms most spaces into an ASCII space and most
107     // slashes into an ASCII solidus. But one of the two callers uses this
108     // on characters that have not been processed by ICU, so they are needed here.
109     
110     if (!u_isprint(charCode) || u_isUWhiteSpace(charCode) || u_hasBinaryProperty(charCode, UCHAR_DEFAULT_IGNORABLE_CODE_POINT))
111         return YES;
112     
113     switch (charCode) {
114         case 0x00BC: /* VULGAR FRACTION ONE QUARTER */
115         case 0x00BD: /* VULGAR FRACTION ONE HALF */
116         case 0x00BE: /* VULGAR FRACTION THREE QUARTERS */
117         case 0x00ED: /* LATIN SMALL LETTER I WITH ACUTE */
118         case 0x01C3: /* LATIN LETTER RETROFLEX CLICK */
119         case 0x0251: /* LATIN SMALL LETTER ALPHA */
120         case 0x0261: /* LATIN SMALL LETTER SCRIPT G */
121         case 0x02D0: /* MODIFIER LETTER TRIANGULAR COLON */
122         case 0x0335: /* COMBINING SHORT STROKE OVERLAY */
123         case 0x0337: /* COMBINING SHORT SOLIDUS OVERLAY */
124         case 0x0338: /* COMBINING LONG SOLIDUS OVERLAY */
125         case 0x0589: /* ARMENIAN FULL STOP */
126         case 0x05B4: /* HEBREW POINT HIRIQ */
127         case 0x05BC: /* HEBREW POINT DAGESH OR MAPIQ */
128         case 0x05C3: /* HEBREW PUNCTUATION SOF PASUQ */
129         case 0x05F4: /* HEBREW PUNCTUATION GERSHAYIM */
130         case 0x0609: /* ARABIC-INDIC PER MILLE SIGN */
131         case 0x060A: /* ARABIC-INDIC PER TEN THOUSAND SIGN */
132         case 0x0650: /* ARABIC KASRA */
133         case 0x0660: /* ARABIC INDIC DIGIT ZERO */
134         case 0x066A: /* ARABIC PERCENT SIGN */
135         case 0x06D4: /* ARABIC FULL STOP */
136         case 0x06F0: /* EXTENDED ARABIC INDIC DIGIT ZERO */
137         case 0x0701: /* SYRIAC SUPRALINEAR FULL STOP */
138         case 0x0702: /* SYRIAC SUBLINEAR FULL STOP */
139         case 0x0703: /* SYRIAC SUPRALINEAR COLON */
140         case 0x0704: /* SYRIAC SUBLINEAR COLON */
141         case 0x1735: /* PHILIPPINE SINGLE PUNCTUATION */
142         case 0x1D04: /* LATIN LETTER SMALL CAPITAL C */
143         case 0x1D0F: /* LATIN LETTER SMALL CAPITAL O */
144         case 0x1D1C: /* LATIN LETTER SMALL CAPITAL U */
145         case 0x1D20: /* LATIN LETTER SMALL CAPITAL V */
146         case 0x1D21: /* LATIN LETTER SMALL CAPITAL W */
147         case 0x1D22: /* LATIN LETTER SMALL CAPITAL Z */
148         case 0x1ECD: /* LATIN SMALL LETTER O WITH DOT BELOW */
149         case 0x2010: /* HYPHEN */
150         case 0x2011: /* NON-BREAKING HYPHEN */
151         case 0x2024: /* ONE DOT LEADER */
152         case 0x2027: /* HYPHENATION POINT */
153         case 0x2039: /* SINGLE LEFT-POINTING ANGLE QUOTATION MARK */
154         case 0x203A: /* SINGLE RIGHT-POINTING ANGLE QUOTATION MARK */
155         case 0x2041: /* CARET INSERTION POINT */
156         case 0x2044: /* FRACTION SLASH */
157         case 0x2052: /* COMMERCIAL MINUS SIGN */
158         case 0x2153: /* VULGAR FRACTION ONE THIRD */
159         case 0x2154: /* VULGAR FRACTION TWO THIRDS */
160         case 0x2155: /* VULGAR FRACTION ONE FIFTH */
161         case 0x2156: /* VULGAR FRACTION TWO FIFTHS */
162         case 0x2157: /* VULGAR FRACTION THREE FIFTHS */
163         case 0x2158: /* VULGAR FRACTION FOUR FIFTHS */
164         case 0x2159: /* VULGAR FRACTION ONE SIXTH */
165         case 0x215A: /* VULGAR FRACTION FIVE SIXTHS */
166         case 0x215B: /* VULGAR FRACTION ONE EIGHT */
167         case 0x215C: /* VULGAR FRACTION THREE EIGHTHS */
168         case 0x215D: /* VULGAR FRACTION FIVE EIGHTHS */
169         case 0x215E: /* VULGAR FRACTION SEVEN EIGHTHS */
170         case 0x215F: /* FRACTION NUMERATOR ONE */
171         case 0x2212: /* MINUS SIGN */
172         case 0x2215: /* DIVISION SLASH */
173         case 0x2216: /* SET MINUS */
174         case 0x2236: /* RATIO */
175         case 0x233F: /* APL FUNCTIONAL SYMBOL SLASH BAR */
176         case 0x23AE: /* INTEGRAL EXTENSION */
177         case 0x244A: /* OCR DOUBLE BACKSLASH */
178         case 0x2571: /* DisplayType::Box DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT */
179         case 0x2572: /* DisplayType::Box DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT */
180         case 0x29F6: /* SOLIDUS WITH OVERBAR */
181         case 0x29F8: /* BIG SOLIDUS */
182         case 0x2AFB: /* TRIPLE SOLIDUS BINARY RELATION */
183         case 0x2AFD: /* DOUBLE SOLIDUS OPERATOR */
184         case 0x2FF0: /* IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT */
185         case 0x2FF1: /* IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO BELOW */
186         case 0x2FF2: /* IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO MIDDLE AND RIGHT */
187         case 0x2FF3: /* IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO MIDDLE AND BELOW */
188         case 0x2FF4: /* IDEOGRAPHIC DESCRIPTION CHARACTER FULL SURROUND */
189         case 0x2FF5: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM ABOVE */
190         case 0x2FF6: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM BELOW */
191         case 0x2FF7: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LEFT */
192         case 0x2FF8: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER LEFT */
193         case 0x2FF9: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER RIGHT */
194         case 0x2FFA: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LOWER LEFT */
195         case 0x2FFB: /* IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID */
196         case 0x3002: /* IDEOGRAPHIC FULL STOP */
197         case 0x3008: /* LEFT ANGLE BRACKET */
198         case 0x3014: /* LEFT TORTOISE SHELL BRACKET */
199         case 0x3015: /* RIGHT TORTOISE SHELL BRACKET */
200         case 0x3033: /* VERTICAL KANA REPEAT MARK UPPER HALF */
201         case 0x3035: /* VERTICAL KANA REPEAT MARK LOWER HALF */
202         case 0x321D: /* PARENTHESIZED KOREAN CHARACTER OJEON */
203         case 0x321E: /* PARENTHESIZED KOREAN CHARACTER O HU */
204         case 0x33AE: /* SQUARE RAD OVER S */
205         case 0x33AF: /* SQUARE RAD OVER S SQUARED */
206         case 0x33C6: /* SQUARE C OVER KG */
207         case 0x33DF: /* SQUARE A OVER M */
208         case 0x05B9: /* HEBREW POINT HOLAM */
209         case 0x05BA: /* HEBREW POINT HOLAM HASER FOR VAV */
210         case 0x05C1: /* HEBREW POINT SHIN DOT */
211         case 0x05C2: /* HEBREW POINT SIN DOT */
212         case 0x05C4: /* HEBREW MARK UPPER DOT */
213         case 0xA731: /* LATIN LETTER SMALL CAPITAL S */
214         case 0xA771: /* LATIN SMALL LETTER DUM */
215         case 0xA789: /* MODIFIER LETTER COLON */
216         case 0xFE14: /* PRESENTATION FORM FOR VERTICAL SEMICOLON */
217         case 0xFE15: /* PRESENTATION FORM FOR VERTICAL EXCLAMATION MARK */
218         case 0xFE3F: /* PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET */
219         case 0xFE5D: /* SMALL LEFT TORTOISE SHELL BRACKET */
220         case 0xFE5E: /* SMALL RIGHT TORTOISE SHELL BRACKET */
221         case 0xFF0E: /* FULLWIDTH FULL STOP */
222         case 0xFF0F: /* FULL WIDTH SOLIDUS */
223         case 0xFF61: /* HALFWIDTH IDEOGRAPHIC FULL STOP */
224         case 0xFFFC: /* OBJECT REPLACEMENT CHARACTER */
225         case 0xFFFD: /* REPLACEMENT CHARACTER */
226         case 0x1F50F: /* LOCK WITH INK PEN */
227         case 0x1F510: /* CLOSED LOCK WITH KEY */
228         case 0x1F511: /* KEY */
229         case 0x1F512: /* LOCK */
230         case 0x1F513: /* OPEN LOCK */
231             return YES;
232         case 0x0307: /* COMBINING DOT ABOVE */
233             return previousCodePoint == 0x0237 /* LATIN SMALL LETTER DOTLESS J */
234                 || previousCodePoint == 0x0131 /* LATIN SMALL LETTER DOTLESS I */
235                 || previousCodePoint == 0x05D5; /* HEBREW LETTER VAV */
236         case 0x0548: /* ARMENIAN CAPITAL LETTER VO */
237         case 0x054D: /* ARMENIAN CAPITAL LETTER SEH */
238         case 0x0578: /* ARMENIAN SMALL LETTER VO */
239         case 0x057D: /* ARMENIAN SMALL LETTER SEH */
240             return previousCodePoint
241                 && !isASCIIDigitOrValidHostCharacter(previousCodePoint.value())
242                 && !isArmenianScriptCharacter(previousCodePoint.value());
243         case '.':
244             return NO;
245         default:
246             return previousCodePoint
247                 && isArmenianLookalikeCharacter(previousCodePoint.value())
248                 && !(isArmenianScriptCharacter(charCode) || isASCIIDigitOrValidHostCharacter(charCode));
249     }
250 }
251
252 static void whiteListIDNScript(const char* scriptName)
253 {
254     int32_t script = u_getPropertyValueEnum(UCHAR_SCRIPT, scriptName);
255     if (script >= 0 && script < USCRIPT_CODE_LIMIT) {
256         size_t index = script / 32;
257         uint32_t mask = 1 << (script % 32);
258         IDNScriptWhiteList[index] |= mask;
259     }
260 }
261
262 static BOOL readIDNScriptWhiteListFile(NSString *filename)
263 {
264     if (!filename)
265         return NO;
266
267     FILE *file = fopen([filename fileSystemRepresentation], "r");
268     if (!file)
269         return NO;
270     
271     // Read a word at a time.
272     // Allow comments, starting with # character to the end of the line.
273     while (1) {
274         // Skip a comment if present.
275         if (fscanf(file, " #%*[^\n\r]%*[\n\r]") == EOF)
276             break;
277         
278         // Read a script name if present.
279         char word[33];
280         int result = fscanf(file, " %32[^# \t\n\r]%*[^# \t\n\r] ", word);
281         if (result == EOF)
282             break;
283         
284         if (result == 1) {
285             // Got a word, map to script code and put it into the array.
286             whiteListIDNScript(word);
287         }
288     }
289     fclose(file);
290     return YES;
291 }
292
293 static BOOL allCharactersInIDNScriptWhiteList(const UChar *buffer, int32_t length)
294 {
295     static dispatch_once_t flag;
296     dispatch_once(&flag, ^{
297         // Read white list from library.
298         NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
299         int numDirs = [dirs count];
300         for (int i = 0; i < numDirs; i++) {
301             if (readIDNScriptWhiteListFile([[dirs objectAtIndex:i] stringByAppendingPathComponent:@"IDNScriptWhiteList.txt"]))
302                 return;
303         }
304         const char* defaultIDNScriptWhiteList[20] = {
305             "Common",
306             "Inherited",
307             "Arabic",
308             "Armenian",
309             "Bopomofo",
310             "Canadian_Aboriginal",
311             "Devanagari",
312             "Deseret",
313             "Gujarati",
314             "Gurmukhi",
315             "Hangul",
316             "Han",
317             "Hebrew",
318             "Hiragana",
319             "Katakana_Or_Hiragana",
320             "Katakana",
321             "Latin",
322             "Tamil",
323             "Thai",
324             "Yi",
325         };
326         for (const char* scriptName : defaultIDNScriptWhiteList)
327             whiteListIDNScript(scriptName);
328     });
329     
330     int32_t i = 0;
331     std::optional<UChar32> previousCodePoint;
332     while (i < length) {
333         UChar32 c;
334         U16_NEXT(buffer, i, length, c)
335         UErrorCode error = U_ZERO_ERROR;
336         UScriptCode script = uscript_getScript(c, &error);
337         if (error != U_ZERO_ERROR) {
338             LOG_ERROR("got ICU error while trying to look at scripts: %d", error);
339             return NO;
340         }
341         if (script < 0) {
342             LOG_ERROR("got negative number for script code from ICU: %d", script);
343             return NO;
344         }
345         if (script >= USCRIPT_CODE_LIMIT)
346             return NO;
347
348         size_t index = script / 32;
349         uint32_t mask = 1 << (script % 32);
350         if (!(IDNScriptWhiteList[index] & mask))
351             return NO;
352         
353         if (isLookalikeCharacter(previousCodePoint, c))
354             return NO;
355         previousCodePoint = c;
356     }
357     return YES;
358 }
359
360 static bool isSecondLevelDomainNameAllowedByTLDRules(const UChar* buffer, int32_t length, const WTF::Function<bool(UChar)>& characterIsAllowed)
361 {
362     ASSERT(length > 0);
363
364     for (int32_t i = length - 1; i >= 0; --i) {
365         UChar ch = buffer[i];
366         
367         if (characterIsAllowed(ch))
368             continue;
369         
370         // Only check the second level domain. Lower level registrars may have different rules.
371         if (ch == '.')
372             break;
373         
374         return false;
375     }
376     return true;
377 }
378
379 #define CHECK_RULES_IF_SUFFIX_MATCHES(suffix, function) \
380     { \
381         static const int32_t suffixLength = sizeof(suffix) / sizeof(suffix[0]); \
382         if (length > suffixLength && 0 == memcmp(buffer + length - suffixLength, suffix, sizeof(suffix))) \
383             return isSecondLevelDomainNameAllowedByTLDRules(buffer, length - suffixLength, function); \
384     }
385
386 static bool isRussianDomainNameCharacter(UChar ch)
387 {
388     // Only modern Russian letters, digits and dashes are allowed.
389     return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || isASCIIDigit(ch) || ch == '-';
390 }
391
392 static BOOL allCharactersAllowedByTLDRules(const UChar* buffer, int32_t length)
393 {
394     // Skip trailing dot for root domain.
395     if (buffer[length - 1] == '.')
396         length--;
397
398     // http://cctld.ru/files/pdf/docs/rules_ru-rf.pdf
399     static const UChar cyrillicRF[] = {
400         '.',
401         0x0440, // CYRILLIC SMALL LETTER ER
402         0x0444  // CYRILLIC SMALL LETTER EF
403     };
404     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicRF, isRussianDomainNameCharacter);
405
406     // http://rusnames.ru/rules.pl
407     static const UChar cyrillicRUS[] = {
408         '.',
409         0x0440, // CYRILLIC SMALL LETTER ER
410         0x0443, // CYRILLIC SMALL LETTER U
411         0x0441  // CYRILLIC SMALL LETTER ES
412     };
413     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicRUS, isRussianDomainNameCharacter);
414
415     // http://ru.faitid.org/projects/moscow/documents/moskva/idn
416     static const UChar cyrillicMOSKVA[] = {
417         '.',
418         0x043C, // CYRILLIC SMALL LETTER EM
419         0x043E, // CYRILLIC SMALL LETTER O
420         0x0441, // CYRILLIC SMALL LETTER ES
421         0x043A, // CYRILLIC SMALL LETTER KA
422         0x0432, // CYRILLIC SMALL LETTER VE
423         0x0430  // CYRILLIC SMALL LETTER A
424     };
425     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMOSKVA, isRussianDomainNameCharacter);
426
427     // http://www.dotdeti.ru/foruser/docs/regrules.php
428     static const UChar cyrillicDETI[] = {
429         '.',
430         0x0434, // CYRILLIC SMALL LETTER DE
431         0x0435, // CYRILLIC SMALL LETTER IE
432         0x0442, // CYRILLIC SMALL LETTER TE
433         0x0438  // CYRILLIC SMALL LETTER I
434     };
435     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicDETI, isRussianDomainNameCharacter);
436
437     // http://corenic.org - rules not published. The word is Russian, so only allowing Russian at this time,
438     // although we may need to revise the checks if this ends up being used with other languages spoken in Russia.
439     static const UChar cyrillicONLAYN[] = {
440         '.',
441         0x043E, // CYRILLIC SMALL LETTER O
442         0x043D, // CYRILLIC SMALL LETTER EN
443         0x043B, // CYRILLIC SMALL LETTER EL
444         0x0430, // CYRILLIC SMALL LETTER A
445         0x0439, // CYRILLIC SMALL LETTER SHORT I
446         0x043D  // CYRILLIC SMALL LETTER EN
447     };
448     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicONLAYN, isRussianDomainNameCharacter);
449
450     // http://corenic.org - same as above.
451     static const UChar cyrillicSAYT[] = {
452         '.',
453         0x0441, // CYRILLIC SMALL LETTER ES
454         0x0430, // CYRILLIC SMALL LETTER A
455         0x0439, // CYRILLIC SMALL LETTER SHORT I
456         0x0442  // CYRILLIC SMALL LETTER TE
457     };
458     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicSAYT, isRussianDomainNameCharacter);
459
460     // http://pir.org/products/opr-domain/ - rules not published. According to the registry site,
461     // the intended audience is "Russian and other Slavic-speaking markets".
462     // Chrome appears to only allow Russian, so sticking with that for now.
463     static const UChar cyrillicORG[] = {
464         '.',
465         0x043E, // CYRILLIC SMALL LETTER O
466         0x0440, // CYRILLIC SMALL LETTER ER
467         0x0433  // CYRILLIC SMALL LETTER GHE
468     };
469     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicORG, isRussianDomainNameCharacter);
470
471     // http://cctld.by/rules.html
472     static const UChar cyrillicBEL[] = {
473         '.',
474         0x0431, // CYRILLIC SMALL LETTER BE
475         0x0435, // CYRILLIC SMALL LETTER IE
476         0x043B  // CYRILLIC SMALL LETTER EL
477     };
478     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicBEL, [](UChar ch) {
479         // Russian and Byelorussian letters, digits and dashes are allowed.
480         return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x0456 || ch == 0x045E || ch == 0x2019 || isASCIIDigit(ch) || ch == '-';
481     });
482
483     // http://www.nic.kz/docs/poryadok_vnedreniya_kaz_ru.pdf
484     static const UChar cyrillicKAZ[] = {
485         '.',
486         0x049B, // CYRILLIC SMALL LETTER KA WITH DESCENDER
487         0x0430, // CYRILLIC SMALL LETTER A
488         0x0437  // CYRILLIC SMALL LETTER ZE
489     };
490     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicKAZ, [](UChar ch) {
491         // Kazakh letters, digits and dashes are allowed.
492         return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x04D9 || ch == 0x0493 || ch == 0x049B || ch == 0x04A3 || ch == 0x04E9 || ch == 0x04B1 || ch == 0x04AF || ch == 0x04BB || ch == 0x0456 || isASCIIDigit(ch) || ch == '-';
493     });
494
495     // http://uanic.net/docs/documents-ukr/Rules%20of%20UKR_v4.0.pdf
496     static const UChar cyrillicUKR[] = {
497         '.',
498         0x0443, // CYRILLIC SMALL LETTER U
499         0x043A, // CYRILLIC SMALL LETTER KA
500         0x0440  // CYRILLIC SMALL LETTER ER
501     };
502     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicUKR, [](UChar ch) {
503         // Russian and Ukrainian letters, digits and dashes are allowed.
504         return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x0491 || ch == 0x0404 || ch == 0x0456 || ch == 0x0457 || isASCIIDigit(ch) || ch == '-';
505     });
506
507     // http://www.rnids.rs/data/DOKUMENTI/idn-srb-policy-termsofuse-v1.4-eng.pdf
508     static const UChar cyrillicSRB[] = {
509         '.',
510         0x0441, // CYRILLIC SMALL LETTER ES
511         0x0440, // CYRILLIC SMALL LETTER ER
512         0x0431  // CYRILLIC SMALL LETTER BE
513     };
514     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicSRB, [](UChar ch) {
515         // Serbian letters, digits and dashes are allowed.
516         return (ch >= 0x0430 && ch <= 0x0438) || (ch >= 0x043A && ch <= 0x0448) || ch == 0x0452 || ch == 0x0458 || ch == 0x0459 || ch == 0x045A || ch == 0x045B || ch == 0x045F || isASCIIDigit(ch) || ch == '-';
517     });
518
519     // http://marnet.mk/doc/pravilnik-mk-mkd.pdf
520     static const UChar cyrillicMKD[] = {
521         '.',
522         0x043C, // CYRILLIC SMALL LETTER EM
523         0x043A, // CYRILLIC SMALL LETTER KA
524         0x0434  // CYRILLIC SMALL LETTER DE
525     };
526     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMKD, [](UChar ch) {
527         // Macedonian letters, digits and dashes are allowed.
528         return (ch >= 0x0430 && ch <= 0x0438) || (ch >= 0x043A && ch <= 0x0448) || ch == 0x0453 || ch == 0x0455 || ch == 0x0458 || ch == 0x0459 || ch == 0x045A || ch == 0x045C || ch == 0x045F || isASCIIDigit(ch) || ch == '-';
529     });
530
531     // https://www.mon.mn/cs/
532     static const UChar cyrillicMON[] = {
533         '.',
534         0x043C, // CYRILLIC SMALL LETTER EM
535         0x043E, // CYRILLIC SMALL LETTER O
536         0x043D  // CYRILLIC SMALL LETTER EN
537     };
538     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMON, [](UChar ch) {
539         // Mongolian letters, digits and dashes are allowed.
540         return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x04E9 || ch == 0x04AF || isASCIIDigit(ch) || ch == '-';
541     });
542
543     // Not a known top level domain with special rules.
544     return NO;
545 }
546
547 // Return value of nil means no mapping is necessary.
548 // If makeString is NO, then return value is either nil or self to indicate mapping is necessary.
549 // If makeString is YES, then return value is either nil or the mapped string.
550 static NSString *mapHostNameWithRange(NSString *string, NSRange range, BOOL encode, BOOL makeString, BOOL *error)
551 {
552     if (range.length > HOST_NAME_BUFFER_LENGTH)
553         return nil;
554     
555     if (![string length])
556         return nil;
557     
558     UChar sourceBuffer[HOST_NAME_BUFFER_LENGTH];
559     UChar destinationBuffer[HOST_NAME_BUFFER_LENGTH];
560     
561     if (encode && [string rangeOfString:@"%" options:NSLiteralSearch range:range].location != NSNotFound) {
562         NSString *substring = [string substringWithRange:range];
563         substring = CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapes(nullptr, (CFStringRef)substring, CFSTR("")));
564         if (substring) {
565             string = substring;
566             range = NSMakeRange(0, [string length]);
567         }
568     }
569     
570     int length = range.length;
571     [string getCharacters:sourceBuffer range:range];
572     
573     UErrorCode uerror = U_ZERO_ERROR;
574     UIDNAInfo processingDetails = UIDNA_INFO_INITIALIZER;
575     int32_t numCharactersConverted = (encode ? uidna_nameToASCII : uidna_nameToUnicode)(&URLParser::internationalDomainNameTranscoder(), sourceBuffer, length, destinationBuffer, HOST_NAME_BUFFER_LENGTH, &processingDetails, &uerror);
576     if (length && (U_FAILURE(uerror) || processingDetails.errors)) {
577         *error = YES;
578         return nil;
579     }
580     
581     if (numCharactersConverted == length && !memcmp(sourceBuffer, destinationBuffer, length * sizeof(UChar)))
582         return nil;
583     
584     if (!encode && !allCharactersInIDNScriptWhiteList(destinationBuffer, numCharactersConverted) && !allCharactersAllowedByTLDRules(destinationBuffer, numCharactersConverted))
585         return nil;
586     
587     return makeString ? [NSString stringWithCharacters:destinationBuffer length:numCharactersConverted] : string;
588 }
589
590 static BOOL hostNameNeedsDecodingWithRange(NSString *string, NSRange range, BOOL *error)
591 {
592     return mapHostNameWithRange(string, range, NO, NO, error) != nil;
593 }
594  
595 static BOOL hostNameNeedsEncodingWithRange(NSString *string, NSRange range, BOOL *error)
596 {
597     return mapHostNameWithRange(string, range, YES,  NO, error) != nil;
598 }
599
600 static NSString *decodeHostNameWithRange(NSString *string, NSRange range)
601 {
602     BOOL error = NO;
603     NSString *host = mapHostNameWithRange(string, range, NO, YES, &error);
604     if (error)
605         return nil;
606     return !host ? string : host;
607 }
608
609 static NSString *encodeHostNameWithRange(NSString *string, NSRange range)
610 {
611     BOOL error = NO;
612     NSString *host = mapHostNameWithRange(string, range, YES, YES, &error);
613     if (error)
614         return nil;
615     return !host ? string : host;
616 }
617
618 NSString *decodeHostName(NSString *string)
619 {
620     BOOL error = NO;
621     NSString *host = mapHostNameWithRange(string, NSMakeRange(0, [string length]), NO, YES, &error);
622     if (error)
623         return nil;
624     return !host ? string : host;
625 }
626
627 NSString *encodeHostName(NSString *string)
628 {
629     BOOL error = NO;
630     NSString *host = mapHostNameWithRange(string, NSMakeRange(0, [string length]), YES, YES, &error);
631     if (error)
632         return nil;
633     return !host ? string : host;
634 }
635
636 static void collectRangesThatNeedMapping(NSString *string, NSRange range, RetainPtr<NSMutableArray>& array, BOOL encode)
637 {
638     // Generally, we want to optimize for the case where there is one host name that does not need mapping.
639     // Therefore, we use nil to indicate no mapping here and an empty array to indicate error.
640
641     BOOL error = NO;
642     BOOL needsMapping = encode ? hostNameNeedsEncodingWithRange(string, range, &error) : hostNameNeedsDecodingWithRange(string, range, &error);
643     if (!error && !needsMapping)
644         return;
645     
646     if (!array)
647         array = adoptNS([NSMutableArray new]);
648
649     if (!error)
650         [array addObject:[NSValue valueWithRange:range]];
651 }
652
653 static void collectRangesThatNeedEncoding(NSString *string, NSRange range, RetainPtr<NSMutableArray>& array)
654 {
655     return collectRangesThatNeedMapping(string, range, array, YES);
656 }
657
658 static void collectRangesThatNeedDecoding(NSString *string, NSRange range, RetainPtr<NSMutableArray>& array)
659 {
660     return collectRangesThatNeedMapping(string, range, array, NO);
661 }
662
663 static void applyHostNameFunctionToMailToURLString(NSString *string, StringRangeApplierFunction f, RetainPtr<NSMutableArray>& array)
664 {
665     // In a mailto: URL, host names come after a '@' character and end with a '>' or ',' or '?' character.
666     // Skip quoted strings so that characters in them don't confuse us.
667     // When we find a '?' character, we are past the part of the URL that contains host names.
668     
669     static NeverDestroyed<RetainPtr<NSCharacterSet>> hostNameOrStringStartCharacters = [NSCharacterSet characterSetWithCharactersInString:@"\"@?"];
670     static NeverDestroyed<RetainPtr<NSCharacterSet>> hostNameEndCharacters = [NSCharacterSet characterSetWithCharactersInString:@">,?"];
671     static NeverDestroyed<RetainPtr<NSCharacterSet>> quotedStringCharacters = [NSCharacterSet characterSetWithCharactersInString:@"\"\\"];
672     
673     unsigned stringLength = [string length];
674     NSRange remaining = NSMakeRange(0, stringLength);
675     
676     while (1) {
677         // Find start of host name or of quoted string.
678         NSRange hostNameOrStringStart = [string rangeOfCharacterFromSet:hostNameOrStringStartCharacters.get().get() options:0 range:remaining];
679         if (hostNameOrStringStart.location == NSNotFound)
680             return;
681
682         unichar c = [string characterAtIndex:hostNameOrStringStart.location];
683         remaining.location = NSMaxRange(hostNameOrStringStart);
684         remaining.length = stringLength - remaining.location;
685         
686         if (c == '?')
687             return;
688         
689         if (c == '@') {
690             // Find end of host name.
691             unsigned hostNameStart = remaining.location;
692             NSRange hostNameEnd = [string rangeOfCharacterFromSet:hostNameEndCharacters.get().get() options:0 range:remaining];
693             BOOL done;
694             if (hostNameEnd.location == NSNotFound) {
695                 hostNameEnd.location = stringLength;
696                 done = YES;
697             } else {
698                 remaining.location = hostNameEnd.location;
699                 remaining.length = stringLength - remaining.location;
700                 done = NO;
701             }
702             
703             // Process host name range.
704             f(string, NSMakeRange(hostNameStart, hostNameEnd.location - hostNameStart), array);
705             
706             if (done)
707                 return;
708         } else {
709             // Skip quoted string.
710             ASSERT(c == '"');
711             while (1) {
712                 NSRange escapedCharacterOrStringEnd = [string rangeOfCharacterFromSet:quotedStringCharacters.get().get() options:0 range:remaining];
713                 if (escapedCharacterOrStringEnd.location == NSNotFound)
714                     return;
715
716                 c = [string characterAtIndex:escapedCharacterOrStringEnd.location];
717                 remaining.location = NSMaxRange(escapedCharacterOrStringEnd);
718                 remaining.length = stringLength - remaining.location;
719                 
720                 // If we are the end of the string, then break from the string loop back to the host name loop.
721                 if (c == '"')
722                     break;
723                 
724                 // Skip escaped character.
725                 ASSERT(c == '\\');
726                 if (!remaining.length)
727                     return;
728
729                 remaining.location += 1;
730                 remaining.length -= 1;
731             }
732         }
733     }
734 }
735
736 static void applyHostNameFunctionToURLString(NSString *string, StringRangeApplierFunction f, RetainPtr<NSMutableArray>& array)
737 {
738     // Find hostnames. Too bad we can't use any real URL-parsing code to do this,
739     // but we have to do it before doing all the %-escaping, and this is the only
740     // code we have that parses mailto URLs anyway.
741     
742     // Maybe we should implement this using a character buffer instead?
743     
744     if (protocolIs(string, "mailto")) {
745         applyHostNameFunctionToMailToURLString(string, f, array);
746         return;
747     }
748     
749     // Find the host name in a hierarchical URL.
750     // It comes after a "://" sequence, with scheme characters preceding.
751     // If ends with the end of the string or a ":", "/", or a "?".
752     // If there is a "@" character, the host part is just the part after the "@".
753     NSRange separatorRange = [string rangeOfString:@"://"];
754     if (separatorRange.location == NSNotFound)
755         return;
756     
757     // Check that all characters before the :// are valid scheme characters.
758     static NeverDestroyed<RetainPtr<NSCharacterSet>> nonSchemeCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-."] invertedSet];
759     if ([string rangeOfCharacterFromSet:nonSchemeCharacters.get().get() options:0 range:NSMakeRange(0, separatorRange.location)].location != NSNotFound)
760         return;
761     
762     unsigned stringLength = [string length];
763     
764     static NeverDestroyed<RetainPtr<NSCharacterSet>> hostTerminators = [NSCharacterSet characterSetWithCharactersInString:@":/?#"];
765     
766     // Start after the separator.
767     unsigned authorityStart = NSMaxRange(separatorRange);
768     
769     // Find terminating character.
770     NSRange hostNameTerminator = [string rangeOfCharacterFromSet:hostTerminators.get().get() options:0 range:NSMakeRange(authorityStart, stringLength - authorityStart)];
771     unsigned hostNameEnd = hostNameTerminator.location == NSNotFound ? stringLength : hostNameTerminator.location;
772     
773     // Find "@" for the start of the host name.
774     NSRange userInfoTerminator = [string rangeOfString:@"@" options:0 range:NSMakeRange(authorityStart, hostNameEnd - authorityStart)];
775     unsigned hostNameStart = userInfoTerminator.location == NSNotFound ? authorityStart : NSMaxRange(userInfoTerminator);
776     
777     return f(string, NSMakeRange(hostNameStart, hostNameEnd - hostNameStart), array);
778 }
779
780 static RetainPtr<NSString> mapHostNames(NSString *string, BOOL encode)
781 {
782     // Generally, we want to optimize for the case where there is one host name that does not need mapping.
783     
784     if (encode && [string canBeConvertedToEncoding:NSASCIIStringEncoding])
785         return string;
786     
787     // Make a list of ranges that actually need mapping.
788     RetainPtr<NSMutableArray> hostNameRanges;
789     StringRangeApplierFunction f = encode ? collectRangesThatNeedEncoding : collectRangesThatNeedDecoding;
790     applyHostNameFunctionToURLString(string, f, hostNameRanges);
791     if (!hostNameRanges)
792         return string;
793
794     if (![hostNameRanges count])
795         return nil;
796     
797     // Do the mapping.
798     auto mutableCopy = adoptNS([string mutableCopy]);
799     unsigned i = [hostNameRanges count];
800     while (i--) {
801         NSRange hostNameRange = [[hostNameRanges objectAtIndex:i] rangeValue];
802         NSString *mappedHostName = encode ? encodeHostNameWithRange(string, hostNameRange) : decodeHostNameWithRange(string, hostNameRange);
803         [mutableCopy replaceCharactersInRange:hostNameRange withString:mappedHostName];
804     }
805     return mutableCopy;
806 }
807
808 static RetainPtr<NSString> stringByTrimmingWhitespace(NSString *string)
809 {
810     auto trimmed = adoptNS([string mutableCopy]);
811     CFStringTrimWhitespace((__bridge CFMutableStringRef)trimmed.get());
812     return trimmed;
813 }
814
815 NSURL *URLByTruncatingOneCharacterBeforeComponent(NSURL *URL, CFURLComponentType component)
816 {
817     if (!URL)
818         return nil;
819     
820     CFRange fragRg = CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, component, nullptr);
821     if (fragRg.location == kCFNotFound)
822         return URL;
823
824     Vector<UInt8, URL_BYTES_BUFFER_LENGTH> urlBytes(URL_BYTES_BUFFER_LENGTH);
825     CFIndex numBytes = CFURLGetBytes((__bridge CFURLRef)URL, urlBytes.data(), urlBytes.size());
826     if (numBytes == -1) {
827         numBytes = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
828         urlBytes.grow(numBytes);
829         CFURLGetBytes((__bridge CFURLRef)URL, urlBytes.data(), numBytes);
830     }
831
832     CFURLRef result = CFURLCreateWithBytes(nullptr, urlBytes.data(), fragRg.location - 1, kCFStringEncodingUTF8, nullptr);
833     if (!result)
834         result = CFURLCreateWithBytes(nullptr, urlBytes.data(), fragRg.location - 1, kCFStringEncodingISOLatin1, nullptr);
835         
836     return result ? CFBridgingRelease(result) : URL;
837 }
838
839 static NSURL *URLByRemovingResourceSpecifier(NSURL *URL)
840 {
841     return URLByTruncatingOneCharacterBeforeComponent(URL, kCFURLComponentResourceSpecifier);
842 }
843
844 NSURL *URLWithData(NSData *data, NSURL *baseURL)
845 {
846     if (!data)
847         return nil;
848     
849     NSURL *result = nil;
850     size_t length = [data length];
851     if (length > 0) {
852         // work around <rdar://4470771>: CFURLCreateAbsoluteURLWithBytes(.., TRUE) doesn't remove non-path components.
853         baseURL = URLByRemovingResourceSpecifier(baseURL);
854         
855         const UInt8 *bytes = static_cast<const UInt8*>([data bytes]);
856         
857         // CFURLCreateAbsoluteURLWithBytes would complain to console if we passed a path to it.
858         if (bytes[0] == '/' && !baseURL)
859             return nil;
860         
861         // NOTE: We use UTF-8 here since this encoding is used when computing strings when returning URL components
862         // (e.g calls to NSURL -path). However, this function is not tolerant of illegal UTF-8 sequences, which
863         // could either be a malformed string or bytes in a different encoding, like shift-jis, so we fall back
864         // onto using ISO Latin 1 in those cases.
865         result = CFBridgingRelease(CFURLCreateAbsoluteURLWithBytes(nullptr, bytes, length, kCFStringEncodingUTF8, (__bridge CFURLRef)baseURL, YES));
866         if (!result)
867             result = CFBridgingRelease(CFURLCreateAbsoluteURLWithBytes(nullptr, bytes, length, kCFStringEncodingISOLatin1, (__bridge CFURLRef)baseURL, YES));
868     } else
869         result = [NSURL URLWithString:@""];
870
871     return result;
872 }
873 static NSData *dataWithUserTypedString(NSString *string)
874 {
875     NSData *userTypedData = [string dataUsingEncoding:NSUTF8StringEncoding];
876     ASSERT(userTypedData);
877     
878     const UInt8* inBytes = static_cast<const UInt8 *>([userTypedData bytes]);
879     int inLength = [userTypedData length];
880     if (!inLength)
881         return nil;
882     
883     char* outBytes = static_cast<char *>(malloc(inLength * 3)); // large enough to %-escape every character
884     char* p = outBytes;
885     int outLength = 0;
886     for (int i = 0; i < inLength; i++) {
887         UInt8 c = inBytes[i];
888         if (c <= 0x20 || c >= 0x7f) {
889             *p++ = '%';
890             *p++ = upperNibbleToASCIIHexDigit(c);
891             *p++ = lowerNibbleToASCIIHexDigit(c);
892             outLength += 3;
893         } else {
894             *p++ = c;
895             outLength++;
896         }
897     }
898     
899     return [NSData dataWithBytesNoCopy:outBytes length:outLength]; // adopts outBytes
900 }
901
902 NSURL *URLWithUserTypedString(NSString *string, NSURL *nsURL)
903 {
904     if (!string)
905         return nil;
906
907     auto mappedString = mapHostNames(stringByTrimmingWhitespace(string).get(), YES);
908     if (!mappedString)
909         return nil;
910
911     // Let's check whether the URL is bogus.
912     URL url { URL { nsURL }, mappedString.get() };
913     if (!url.createCFURL())
914         return nil;
915
916     // FIXME: https://bugs.webkit.org/show_bug.cgi?id=186057
917     // We should be able to use url.createCFURL instead of using directly CFURL parsing routines.
918     NSData *data = dataWithUserTypedString(mappedString.get());
919     if (!data)
920         return [NSURL URLWithString:@""];
921
922     return URLWithData(data, nsURL);
923 }
924
925 NSURL *URLWithUserTypedStringDeprecated(NSString *string, NSURL *URL)
926 {
927     if (!string)
928         return nil;
929
930     NSURL *result = URLWithUserTypedString(string, URL);
931     if (!result) {
932         NSData *resultData = dataWithUserTypedString(string);
933         if (!resultData)
934             return [NSURL URLWithString:@""];
935         result = URLWithData(resultData, URL);
936     }
937
938     return result;
939 }
940
941 static BOOL hasQuestionMarkOnlyQueryString(NSURL *URL)
942 {
943     CFRange rangeWithSeparators;
944     CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, kCFURLComponentQuery, &rangeWithSeparators);
945     if (rangeWithSeparators.location != kCFNotFound && rangeWithSeparators.length == 1)
946         return YES;
947
948     return NO;
949 }
950
951 NSData *dataForURLComponentType(NSURL *URL, CFURLComponentType componentType)
952 {
953     Vector<UInt8, URL_BYTES_BUFFER_LENGTH> allBytesBuffer(URL_BYTES_BUFFER_LENGTH);
954     CFIndex bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, allBytesBuffer.data(), allBytesBuffer.size());
955     if (bytesFilled == -1) {
956         CFIndex bytesToAllocate = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
957         allBytesBuffer.grow(bytesToAllocate);
958         bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, allBytesBuffer.data(), bytesToAllocate);
959     }
960     
961     const CFURLComponentType completeURL = (CFURLComponentType)-1;
962     CFRange range;
963     if (componentType != completeURL) {
964         range = CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, componentType, nullptr);
965         if (range.location == kCFNotFound)
966             return nil;
967     } else {
968         range.location = 0;
969         range.length = bytesFilled;
970     }
971     
972     NSData *componentData = [NSData dataWithBytes:allBytesBuffer.data() + range.location length:range.length]; 
973     
974     const unsigned char *bytes = static_cast<const unsigned char *>([componentData bytes]);
975     NSMutableData *resultData = [NSMutableData data];
976     // NOTE: add leading '?' to query strings non-zero length query strings.
977     // NOTE: retain question-mark only query strings.
978     if (componentType == kCFURLComponentQuery) {
979         if (range.length > 0 || hasQuestionMarkOnlyQueryString(URL))
980             [resultData appendBytes:"?" length:1];    
981     }
982     for (int i = 0; i < range.length; i++) {
983         unsigned char c = bytes[i];
984         if (c <= 0x20 || c >= 0x7f) {
985             char escaped[3];
986             escaped[0] = '%';
987             escaped[1] = upperNibbleToASCIIHexDigit(c);
988             escaped[2] = lowerNibbleToASCIIHexDigit(c);
989             [resultData appendBytes:escaped length:3];    
990         } else {
991             char b[1];
992             b[0] = c;
993             [resultData appendBytes:b length:1];    
994         }               
995     }
996     
997     return resultData;
998 }
999
1000 static NSURL *URLByRemovingComponentAndSubsequentCharacter(NSURL *URL, CFURLComponentType component)
1001 {
1002     CFRange range = CFURLGetByteRangeForComponent((__bridge CFURLRef)URL, component, 0);
1003     if (range.location == kCFNotFound)
1004         return URL;
1005     
1006     // Remove one subsequent character.
1007     range.length++;
1008
1009     Vector<UInt8, URL_BYTES_BUFFER_LENGTH> buffer(URL_BYTES_BUFFER_LENGTH);
1010     CFIndex numBytes = CFURLGetBytes((__bridge CFURLRef)URL, buffer.data(), buffer.size());
1011     if (numBytes == -1) {
1012         numBytes = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
1013         buffer.grow(numBytes);
1014         CFURLGetBytes((__bridge CFURLRef)URL, buffer.data(), numBytes);
1015     }
1016     UInt8* urlBytes = buffer.data();
1017         
1018     if (numBytes < range.location)
1019         return URL;
1020     if (numBytes < range.location + range.length)
1021         range.length = numBytes - range.location;
1022         
1023     memmove(urlBytes + range.location, urlBytes + range.location + range.length, numBytes - range.location + range.length);
1024     
1025     CFURLRef result = CFURLCreateWithBytes(nullptr, urlBytes, numBytes - range.length, kCFStringEncodingUTF8, nullptr);
1026     if (!result)
1027         result = CFURLCreateWithBytes(nullptr, urlBytes, numBytes - range.length, kCFStringEncodingISOLatin1, nullptr);
1028                 
1029     return result ? CFBridgingRelease(result) : URL;
1030 }
1031
1032 NSURL *URLByRemovingUserInfo(NSURL *URL)
1033 {
1034     return URLByRemovingComponentAndSubsequentCharacter(URL, kCFURLComponentUserInfo);
1035 }
1036
1037 NSData *originalURLData(NSURL *URL)
1038 {
1039     UInt8 *buffer = (UInt8 *)malloc(URL_BYTES_BUFFER_LENGTH);
1040     CFIndex bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, buffer, URL_BYTES_BUFFER_LENGTH);
1041     if (bytesFilled == -1) {
1042         CFIndex bytesToAllocate = CFURLGetBytes((__bridge CFURLRef)URL, nullptr, 0);
1043         buffer = (UInt8 *)realloc(buffer, bytesToAllocate);
1044         bytesFilled = CFURLGetBytes((__bridge CFURLRef)URL, buffer, bytesToAllocate);
1045         ASSERT(bytesFilled == bytesToAllocate);
1046     }
1047     
1048     // buffer is adopted by the NSData
1049     NSData *data = [NSData dataWithBytesNoCopy:buffer length:bytesFilled freeWhenDone:YES];
1050     
1051     NSURL *baseURL = (__bridge NSURL *)CFURLGetBaseURL((__bridge CFURLRef)URL);
1052     if (baseURL)
1053         return originalURLData(URLWithData(data, baseURL));
1054     return data;
1055 }
1056
1057 static CFStringRef createStringWithEscapedUnsafeCharacters(CFStringRef string)
1058 {
1059     CFIndex length = CFStringGetLength(string);
1060     Vector<UChar, URL_BYTES_BUFFER_LENGTH> sourceBuffer(length);
1061     CFStringGetCharacters(string, CFRangeMake(0, length), sourceBuffer.data());
1062     
1063     Vector<UChar, URL_BYTES_BUFFER_LENGTH> outBuffer;
1064     
1065     std::optional<UChar32> previousCodePoint;
1066     CFIndex i = 0;
1067     while (i < length) {
1068         UChar32 c;
1069         U16_NEXT(sourceBuffer, i, length, c)
1070         
1071         if (isLookalikeCharacter(previousCodePoint, c)) {
1072             uint8_t utf8Buffer[4];
1073             CFIndex offset = 0;
1074             UBool failure = false;
1075             U8_APPEND(utf8Buffer, offset, 4, c, failure)
1076             ASSERT(!failure);
1077             
1078             for (CFIndex j = 0; j < offset; ++j) {
1079                 outBuffer.append('%');
1080                 outBuffer.append(upperNibbleToASCIIHexDigit(utf8Buffer[j]));
1081                 outBuffer.append(lowerNibbleToASCIIHexDigit(utf8Buffer[j]));
1082             }
1083         } else {
1084             UChar utf16Buffer[2];
1085             CFIndex offset = 0;
1086             UBool failure = false;
1087             U16_APPEND(utf16Buffer, offset, 2, c, failure)
1088             ASSERT(!failure);
1089             for (CFIndex j = 0; j < offset; ++j)
1090                 outBuffer.append(utf16Buffer[j]);
1091         }
1092         previousCodePoint = c;
1093     }
1094     
1095     return CFStringCreateWithCharacters(nullptr, outBuffer.data(), outBuffer.size());
1096 }
1097
1098 NSString *userVisibleString(NSURL *URL)
1099 {
1100     NSData *data = originalURLData(URL);
1101     const unsigned char *before = static_cast<const unsigned char*>([data bytes]);
1102     int length = [data length];
1103     
1104     bool mayNeedHostNameDecoding = false;
1105     
1106     const unsigned char *p = before;
1107     int bufferLength = (length * 3) + 1;
1108     Vector<char, URL_BYTES_BUFFER_LENGTH> after(bufferLength); // large enough to %-escape every character
1109     char *q = after.data();
1110     for (int i = 0; i < length; i++) {
1111         unsigned char c = p[i];
1112         // unescape escape sequences that indicate bytes greater than 0x7f
1113         if (c == '%' && (i + 1 < length && isASCIIHexDigit(p[i + 1])) && i + 2 < length && isASCIIHexDigit(p[i + 2])) {
1114             auto u = toASCIIHexValue(p[i + 1], p[i + 2]);
1115             if (u > 0x7f) {
1116                 // unescape
1117                 *q++ = u;
1118             } else {
1119                 // do not unescape
1120                 *q++ = p[i];
1121                 *q++ = p[i + 1];
1122                 *q++ = p[i + 2];
1123             }
1124             i += 2;
1125         } else {
1126             *q++ = c;
1127             
1128             // Check for "xn--" in an efficient, non-case-sensitive, way.
1129             if (c == '-' && i >= 3 && !mayNeedHostNameDecoding && (q[-4] | 0x20) == 'x' && (q[-3] | 0x20) == 'n' && q[-2] == '-')
1130                 mayNeedHostNameDecoding = true;
1131         }
1132     }
1133     *q = '\0';
1134     
1135     // Check string to see if it can be converted to display using UTF-8  
1136     RetainPtr<NSString> result = [NSString stringWithUTF8String:after.data()];
1137     if (!result) {
1138         // Could not convert to UTF-8.
1139         // Convert characters greater than 0x7f to escape sequences.
1140         // Shift current string to the end of the buffer
1141         // then we will copy back bytes to the start of the buffer 
1142         // as we convert.
1143         int afterlength = q - after.data();
1144         char *p = after.data() + bufferLength - afterlength - 1;
1145         memmove(p, after.data(), afterlength + 1); // copies trailing '\0'
1146         char *q = after.data();
1147         while (*p) {
1148             unsigned char c = *p;
1149             if (c > 0x7f) {
1150                 *q++ = '%';
1151                 *q++ = upperNibbleToASCIIHexDigit(c);
1152                 *q++ = lowerNibbleToASCIIHexDigit(c);
1153             } else
1154                 *q++ = *p;
1155             p++;
1156         }
1157         *q = '\0';
1158         result = [NSString stringWithUTF8String:after.data()];
1159     }
1160     
1161     if (mayNeedHostNameDecoding) {
1162         // FIXME: Is it good to ignore the failure of mapHostNames and keep result intact?
1163         auto mappedResult = mapHostNames(result.get(), NO);
1164         if (mappedResult)
1165             result = mappedResult;
1166     }
1167
1168     result = [result precomposedStringWithCanonicalMapping];
1169     return CFBridgingRelease(createStringWithEscapedUnsafeCharacters((__bridge CFStringRef)result.get()));
1170 }
1171
1172 BOOL isUserVisibleURL(NSString *string)
1173 {
1174     BOOL valid = YES;
1175     // get buffer
1176     
1177     char static_buffer[1024];
1178     const char *p;
1179     BOOL success = CFStringGetCString((__bridge CFStringRef)string, static_buffer, 1023, kCFStringEncodingUTF8);
1180     p = success ? static_buffer : [string UTF8String];
1181     
1182     int length = strlen(p);
1183     
1184     // check for characters <= 0x20 or >=0x7f, %-escape sequences of %7f, and xn--, these
1185     // are the things that will lead _web_userVisibleString to actually change things.
1186     for (int i = 0; i < length; i++) {
1187         unsigned char c = p[i];
1188         // escape control characters, space, and delete
1189         if (c <= 0x20 || c == 0x7f) {
1190             valid = NO;
1191             break;
1192         } else if (c == '%' && (i + 1 < length && isASCIIHexDigit(p[i + 1])) && i + 2 < length && isASCIIHexDigit(p[i + 2])) {
1193             auto u = toASCIIHexValue(p[i + 1], p[i + 2]);
1194             if (u > 0x7f) {
1195                 valid = NO;
1196                 break;
1197             }
1198             i += 2;
1199         } else {
1200             // Check for "xn--" in an efficient, non-case-sensitive, way.
1201             if (c == '-' && i >= 3 && (p[i - 3] | 0x20) == 'x' && (p[i - 2] | 0x20) == 'n' && p[i - 1] == '-') {
1202                 valid = NO;
1203                 break;
1204             }
1205         }
1206     }
1207     
1208     return valid;
1209 }
1210
1211 NSRange rangeOfURLScheme(NSString *string)
1212 {
1213     NSRange colon = [string rangeOfString:@":"];
1214     if (colon.location != NSNotFound && colon.location > 0) {
1215         NSRange scheme = {0, colon.location};
1216         /*
1217          This stuff is very expensive.  10-15 msec on a 2x1.2GHz.  If not cached it swamps
1218          everything else when adding items to the autocomplete DB.  Makes me wonder if we
1219          even need to enforce the character set here.
1220          */
1221         NSString *acceptableCharacters = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+.-";
1222         static NeverDestroyed<RetainPtr<NSCharacterSet>> InverseSchemeCharacterSet([[NSCharacterSet characterSetWithCharactersInString:acceptableCharacters] invertedSet]);
1223         NSRange illegals = [string rangeOfCharacterFromSet:InverseSchemeCharacterSet.get().get() options:0 range:scheme];
1224         if (illegals.location == NSNotFound)
1225             return scheme;
1226     }
1227     return NSMakeRange(NSNotFound, 0);
1228 }
1229
1230 BOOL looksLikeAbsoluteURL(NSString *string)
1231 {
1232     // Trim whitespace because _web_URLWithString allows whitespace.
1233     return rangeOfURLScheme(stringByTrimmingWhitespace(string).get()).location != NSNotFound;
1234 }
1235
1236 } // namespace WebCore