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