[GTK][WPE] Need a function to convert internal URI to display ("pretty") URI
[WebKit-https.git] / Source / WTF / wtf / URLHelpers.cpp
1 /*
2  * Copyright (C) 2005, 2007, 2014 Apple Inc. All rights reserved.
3  * Copyright (C) 2018 Igalia S.L.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer.
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution.
14  * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #include "config.h"
31 #include "URLHelpers.h"
32
33 #include "URLParser.h"
34 #include <mutex>
35 #include <unicode/uidna.h>
36 #include <unicode/unorm.h>
37 #include <unicode/uscript.h>
38
39 namespace WTF {
40 namespace URLHelpers {
41
42 // Needs to be big enough to hold an IDN-encoded name.
43 // For host names bigger than this, we won't do IDN encoding, which is almost certainly OK.
44 const unsigned hostNameBufferLength = 2048;
45 const unsigned urlBytesBufferLength = 2048;
46
47 static uint32_t IDNScriptWhiteList[(USCRIPT_CODE_LIMIT + 31) / 32];
48
49 #if !PLATFORM(COCOA)
50
51 // Cocoa has an implementation that uses a whitelist in /Library or ~/Library,
52 // if it exists.
53 void loadIDNScriptWhiteList()
54 {
55     static std::once_flag flag;
56     std::call_once(flag, initializeDefaultIDNScriptWhiteList);
57 }
58
59 #endif // !PLATFORM(COCOA)
60
61 static bool isArmenianLookalikeCharacter(UChar32 codePoint)
62 {
63     return codePoint == 0x0548 || codePoint == 0x054D || codePoint == 0x0578 || codePoint == 0x057D;
64 }
65
66 static bool isArmenianScriptCharacter(UChar32 codePoint)
67 {
68     UErrorCode error = U_ZERO_ERROR;
69     UScriptCode script = uscript_getScript(codePoint, &error);
70     if (error != U_ZERO_ERROR) {
71         LOG_ERROR("got ICU error while trying to look at scripts: %d", error);
72         return false;
73     }
74
75     return script == USCRIPT_ARMENIAN;
76 }
77
78 template<typename CharacterType> inline bool isASCIIDigitOrValidHostCharacter(CharacterType charCode)
79 {
80     if (!isASCIIDigitOrPunctuation(charCode))
81         return false;
82
83     // Things the URL Parser rejects:
84     switch (charCode) {
85     case '#':
86     case '%':
87     case '/':
88     case ':':
89     case '?':
90     case '@':
91     case '[':
92     case '\\':
93     case ']':
94         return false;
95     default:
96         return true;
97     }
98 }
99
100 static bool isLookalikeCharacter(const std::optional<UChar32>& previousCodePoint, UChar32 charCode)
101 {
102     // This function treats the following as unsafe, lookalike characters:
103     // any non-printable character, any character considered as whitespace,
104     // any ignorable character, and emoji characters related to locks.
105     
106     // We also considered the characters in Mozilla's blacklist <http://kb.mozillazine.org/Network.IDN.blacklist_chars>.
107
108     // Some of the characters here will never appear once ICU has encoded.
109     // For example, ICU transforms most spaces into an ASCII space and most
110     // slashes into an ASCII solidus. But one of the two callers uses this
111     // on characters that have not been processed by ICU, so they are needed here.
112     
113     if (!u_isprint(charCode) || u_isUWhiteSpace(charCode) || u_hasBinaryProperty(charCode, UCHAR_DEFAULT_IGNORABLE_CODE_POINT))
114         return true;
115     
116     switch (charCode) {
117     case 0x00BC: /* VULGAR FRACTION ONE QUARTER */
118     case 0x00BD: /* VULGAR FRACTION ONE HALF */
119     case 0x00BE: /* VULGAR FRACTION THREE QUARTERS */
120     case 0x00ED: /* LATIN SMALL LETTER I WITH ACUTE */
121     case 0x01C3: /* LATIN LETTER RETROFLEX CLICK */
122     case 0x0251: /* LATIN SMALL LETTER ALPHA */
123     case 0x0261: /* LATIN SMALL LETTER SCRIPT G */
124     case 0x02D0: /* MODIFIER LETTER TRIANGULAR COLON */
125     case 0x0335: /* COMBINING SHORT STROKE OVERLAY */
126     case 0x0337: /* COMBINING SHORT SOLIDUS OVERLAY */
127     case 0x0338: /* COMBINING LONG SOLIDUS OVERLAY */
128     case 0x0589: /* ARMENIAN FULL STOP */
129     case 0x05B4: /* HEBREW POINT HIRIQ */
130     case 0x05BC: /* HEBREW POINT DAGESH OR MAPIQ */
131     case 0x05C3: /* HEBREW PUNCTUATION SOF PASUQ */
132     case 0x05F4: /* HEBREW PUNCTUATION GERSHAYIM */
133     case 0x0609: /* ARABIC-INDIC PER MILLE SIGN */
134     case 0x060A: /* ARABIC-INDIC PER TEN THOUSAND SIGN */
135     case 0x0650: /* ARABIC KASRA */
136     case 0x0660: /* ARABIC INDIC DIGIT ZERO */
137     case 0x066A: /* ARABIC PERCENT SIGN */
138     case 0x06D4: /* ARABIC FULL STOP */
139     case 0x06F0: /* EXTENDED ARABIC INDIC DIGIT ZERO */
140     case 0x0701: /* SYRIAC SUPRALINEAR FULL STOP */
141     case 0x0702: /* SYRIAC SUBLINEAR FULL STOP */
142     case 0x0703: /* SYRIAC SUPRALINEAR COLON */
143     case 0x0704: /* SYRIAC SUBLINEAR COLON */
144     case 0x1735: /* PHILIPPINE SINGLE PUNCTUATION */
145     case 0x1D04: /* LATIN LETTER SMALL CAPITAL C */
146     case 0x1D0F: /* LATIN LETTER SMALL CAPITAL O */
147     case 0x1D1C: /* LATIN LETTER SMALL CAPITAL U */
148     case 0x1D20: /* LATIN LETTER SMALL CAPITAL V */
149     case 0x1D21: /* LATIN LETTER SMALL CAPITAL W */
150     case 0x1D22: /* LATIN LETTER SMALL CAPITAL Z */
151     case 0x1ECD: /* LATIN SMALL LETTER O WITH DOT BELOW */
152     case 0x2010: /* HYPHEN */
153     case 0x2011: /* NON-BREAKING HYPHEN */
154     case 0x2024: /* ONE DOT LEADER */
155     case 0x2027: /* HYPHENATION POINT */
156     case 0x2039: /* SINGLE LEFT-POINTING ANGLE QUOTATION MARK */
157     case 0x203A: /* SINGLE RIGHT-POINTING ANGLE QUOTATION MARK */
158     case 0x2041: /* CARET INSERTION POINT */
159     case 0x2044: /* FRACTION SLASH */
160     case 0x2052: /* COMMERCIAL MINUS SIGN */
161     case 0x2153: /* VULGAR FRACTION ONE THIRD */
162     case 0x2154: /* VULGAR FRACTION TWO THIRDS */
163     case 0x2155: /* VULGAR FRACTION ONE FIFTH */
164     case 0x2156: /* VULGAR FRACTION TWO FIFTHS */
165     case 0x2157: /* VULGAR FRACTION THREE FIFTHS */
166     case 0x2158: /* VULGAR FRACTION FOUR FIFTHS */
167     case 0x2159: /* VULGAR FRACTION ONE SIXTH */
168     case 0x215A: /* VULGAR FRACTION FIVE SIXTHS */
169     case 0x215B: /* VULGAR FRACTION ONE EIGHT */
170     case 0x215C: /* VULGAR FRACTION THREE EIGHTHS */
171     case 0x215D: /* VULGAR FRACTION FIVE EIGHTHS */
172     case 0x215E: /* VULGAR FRACTION SEVEN EIGHTHS */
173     case 0x215F: /* FRACTION NUMERATOR ONE */
174     case 0x2212: /* MINUS SIGN */
175     case 0x2215: /* DIVISION SLASH */
176     case 0x2216: /* SET MINUS */
177     case 0x2236: /* RATIO */
178     case 0x233F: /* APL FUNCTIONAL SYMBOL SLASH BAR */
179     case 0x23AE: /* INTEGRAL EXTENSION */
180     case 0x244A: /* OCR DOUBLE BACKSLASH */
181     case 0x2571: /* DisplayType::Box DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT */
182     case 0x2572: /* DisplayType::Box DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT */
183     case 0x29F6: /* SOLIDUS WITH OVERBAR */
184     case 0x29F8: /* BIG SOLIDUS */
185     case 0x2AFB: /* TRIPLE SOLIDUS BINARY RELATION */
186     case 0x2AFD: /* DOUBLE SOLIDUS OPERATOR */
187     case 0x2FF0: /* IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT */
188     case 0x2FF1: /* IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO BELOW */
189     case 0x2FF2: /* IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO MIDDLE AND RIGHT */
190     case 0x2FF3: /* IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO MIDDLE AND BELOW */
191     case 0x2FF4: /* IDEOGRAPHIC DESCRIPTION CHARACTER FULL SURROUND */
192     case 0x2FF5: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM ABOVE */
193     case 0x2FF6: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM BELOW */
194     case 0x2FF7: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LEFT */
195     case 0x2FF8: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER LEFT */
196     case 0x2FF9: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER RIGHT */
197     case 0x2FFA: /* IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LOWER LEFT */
198     case 0x2FFB: /* IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID */
199     case 0x3002: /* IDEOGRAPHIC FULL STOP */
200     case 0x3008: /* LEFT ANGLE BRACKET */
201     case 0x3014: /* LEFT TORTOISE SHELL BRACKET */
202     case 0x3015: /* RIGHT TORTOISE SHELL BRACKET */
203     case 0x3033: /* VERTICAL KANA REPEAT MARK UPPER HALF */
204     case 0x3035: /* VERTICAL KANA REPEAT MARK LOWER HALF */
205     case 0x321D: /* PARENTHESIZED KOREAN CHARACTER OJEON */
206     case 0x321E: /* PARENTHESIZED KOREAN CHARACTER O HU */
207     case 0x33AE: /* SQUARE RAD OVER S */
208     case 0x33AF: /* SQUARE RAD OVER S SQUARED */
209     case 0x33C6: /* SQUARE C OVER KG */
210     case 0x33DF: /* SQUARE A OVER M */
211     case 0x05B9: /* HEBREW POINT HOLAM */
212     case 0x05BA: /* HEBREW POINT HOLAM HASER FOR VAV */
213     case 0x05C1: /* HEBREW POINT SHIN DOT */
214     case 0x05C2: /* HEBREW POINT SIN DOT */
215     case 0x05C4: /* HEBREW MARK UPPER DOT */
216     case 0xA731: /* LATIN LETTER SMALL CAPITAL S */
217     case 0xA771: /* LATIN SMALL LETTER DUM */
218     case 0xA789: /* MODIFIER LETTER COLON */
219     case 0xFE14: /* PRESENTATION FORM FOR VERTICAL SEMICOLON */
220     case 0xFE15: /* PRESENTATION FORM FOR VERTICAL EXCLAMATION MARK */
221     case 0xFE3F: /* PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET */
222     case 0xFE5D: /* SMALL LEFT TORTOISE SHELL BRACKET */
223     case 0xFE5E: /* SMALL RIGHT TORTOISE SHELL BRACKET */
224     case 0xFF0E: /* FULLWIDTH FULL STOP */
225     case 0xFF0F: /* FULL WIDTH SOLIDUS */
226     case 0xFF61: /* HALFWIDTH IDEOGRAPHIC FULL STOP */
227     case 0xFFFC: /* OBJECT REPLACEMENT CHARACTER */
228     case 0xFFFD: /* REPLACEMENT CHARACTER */
229     case 0x1F50F: /* LOCK WITH INK PEN */
230     case 0x1F510: /* CLOSED LOCK WITH KEY */
231     case 0x1F511: /* KEY */
232     case 0x1F512: /* LOCK */
233     case 0x1F513: /* OPEN LOCK */
234         return true;
235     case 0x0307: /* COMBINING DOT ABOVE */
236         return previousCodePoint == 0x0237 /* LATIN SMALL LETTER DOTLESS J */
237             || previousCodePoint == 0x0131 /* LATIN SMALL LETTER DOTLESS I */
238             || previousCodePoint == 0x05D5; /* HEBREW LETTER VAV */
239     case 0x0548: /* ARMENIAN CAPITAL LETTER VO */
240     case 0x054D: /* ARMENIAN CAPITAL LETTER SEH */
241     case 0x0578: /* ARMENIAN SMALL LETTER VO */
242     case 0x057D: /* ARMENIAN SMALL LETTER SEH */
243         return previousCodePoint
244             && !isASCIIDigitOrValidHostCharacter(previousCodePoint.value())
245             && !isArmenianScriptCharacter(previousCodePoint.value());
246     case '.':
247         return false;
248     default:
249         return previousCodePoint
250             && isArmenianLookalikeCharacter(previousCodePoint.value())
251             && !(isArmenianScriptCharacter(charCode) || isASCIIDigitOrValidHostCharacter(charCode));
252     }
253 }
254
255 void whiteListIDNScript(const char* scriptName)
256 {
257     int32_t script = u_getPropertyValueEnum(UCHAR_SCRIPT, scriptName);
258     if (script >= 0 && script < USCRIPT_CODE_LIMIT) {
259         size_t index = script / 32;
260         uint32_t mask = 1 << (script % 32);
261         IDNScriptWhiteList[index] |= mask;
262     }
263 }
264
265 void initializeDefaultIDNScriptWhiteList()
266 {
267     const char* defaultIDNScriptWhiteList[20] = {
268         "Common",
269         "Inherited",
270         "Arabic",
271         "Armenian",
272         "Bopomofo",
273         "Canadian_Aboriginal",
274         "Devanagari",
275         "Deseret",
276         "Gujarati",
277         "Gurmukhi",
278         "Hangul",
279         "Han",
280         "Hebrew",
281         "Hiragana",
282         "Katakana_Or_Hiragana",
283         "Katakana",
284         "Latin",
285         "Tamil",
286         "Thai",
287         "Yi",
288     };
289     for (const char* scriptName : defaultIDNScriptWhiteList)
290         whiteListIDNScript(scriptName);
291 }
292
293 static bool allCharactersInIDNScriptWhiteList(const UChar* buffer, int32_t length)
294 {
295     loadIDNScriptWhiteList();
296     int32_t i = 0;
297     std::optional<UChar32> previousCodePoint;
298     while (i < length) {
299         UChar32 c;
300         U16_NEXT(buffer, i, length, c)
301         UErrorCode error = U_ZERO_ERROR;
302         UScriptCode script = uscript_getScript(c, &error);
303         if (error != U_ZERO_ERROR) {
304             LOG_ERROR("got ICU error while trying to look at scripts: %d", error);
305             return false;
306         }
307         if (script < 0) {
308             LOG_ERROR("got negative number for script code from ICU: %d", script);
309             return false;
310         }
311         if (script >= USCRIPT_CODE_LIMIT)
312             return false;
313
314         size_t index = script / 32;
315         uint32_t mask = 1 << (script % 32);
316         if (!(IDNScriptWhiteList[index] & mask))
317             return false;
318         
319         if (isLookalikeCharacter(previousCodePoint, c))
320             return false;
321         previousCodePoint = c;
322     }
323     return true;
324 }
325
326 static bool isSecondLevelDomainNameAllowedByTLDRules(const UChar* buffer, int32_t length, const WTF::Function<bool(UChar)>& characterIsAllowed)
327 {
328     ASSERT(length > 0);
329
330     for (int32_t i = length - 1; i >= 0; --i) {
331         UChar ch = buffer[i];
332         
333         if (characterIsAllowed(ch))
334             continue;
335         
336         // Only check the second level domain. Lower level registrars may have different rules.
337         if (ch == '.')
338             break;
339         
340         return false;
341     }
342     return true;
343 }
344
345 #define CHECK_RULES_IF_SUFFIX_MATCHES(suffix, function) \
346     { \
347         static const int32_t suffixLength = sizeof(suffix) / sizeof(suffix[0]); \
348         if (length > suffixLength && !memcmp(buffer + length - suffixLength, suffix, sizeof(suffix))) \
349             return isSecondLevelDomainNameAllowedByTLDRules(buffer, length - suffixLength, function); \
350     }
351
352 static bool isRussianDomainNameCharacter(UChar ch)
353 {
354     // Only modern Russian letters, digits and dashes are allowed.
355     return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || isASCIIDigit(ch) || ch == '-';
356 }
357
358 static bool allCharactersAllowedByTLDRules(const UChar* buffer, int32_t length)
359 {
360     // Skip trailing dot for root domain.
361     if (buffer[length - 1] == '.')
362         length--;
363
364     // http://cctld.ru/files/pdf/docs/rules_ru-rf.pdf
365     static const UChar cyrillicRF[] = {
366         '.',
367         0x0440, // CYRILLIC SMALL LETTER ER
368         0x0444, // CYRILLIC SMALL LETTER EF
369     };
370     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicRF, isRussianDomainNameCharacter);
371
372     // http://rusnames.ru/rules.pl
373     static const UChar cyrillicRUS[] = {
374         '.',
375         0x0440, // CYRILLIC SMALL LETTER ER
376         0x0443, // CYRILLIC SMALL LETTER U
377         0x0441, // CYRILLIC SMALL LETTER ES
378     };
379     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicRUS, isRussianDomainNameCharacter);
380
381     // http://ru.faitid.org/projects/moscow/documents/moskva/idn
382     static const UChar cyrillicMOSKVA[] = {
383         '.',
384         0x043C, // CYRILLIC SMALL LETTER EM
385         0x043E, // CYRILLIC SMALL LETTER O
386         0x0441, // CYRILLIC SMALL LETTER ES
387         0x043A, // CYRILLIC SMALL LETTER KA
388         0x0432, // CYRILLIC SMALL LETTER VE
389         0x0430, // CYRILLIC SMALL LETTER A
390     };
391     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMOSKVA, isRussianDomainNameCharacter);
392
393     // http://www.dotdeti.ru/foruser/docs/regrules.php
394     static const UChar cyrillicDETI[] = {
395         '.',
396         0x0434, // CYRILLIC SMALL LETTER DE
397         0x0435, // CYRILLIC SMALL LETTER IE
398         0x0442, // CYRILLIC SMALL LETTER TE
399         0x0438, // CYRILLIC SMALL LETTER I
400     };
401     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicDETI, isRussianDomainNameCharacter);
402
403     // http://corenic.org - rules not published. The word is Russian, so only allowing Russian at this time,
404     // although we may need to revise the checks if this ends up being used with other languages spoken in Russia.
405     static const UChar cyrillicONLAYN[] = {
406         '.',
407         0x043E, // CYRILLIC SMALL LETTER O
408         0x043D, // CYRILLIC SMALL LETTER EN
409         0x043B, // CYRILLIC SMALL LETTER EL
410         0x0430, // CYRILLIC SMALL LETTER A
411         0x0439, // CYRILLIC SMALL LETTER SHORT I
412         0x043D, // CYRILLIC SMALL LETTER EN
413     };
414     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicONLAYN, isRussianDomainNameCharacter);
415
416     // http://corenic.org - same as above.
417     static const UChar cyrillicSAYT[] = {
418         '.',
419         0x0441, // CYRILLIC SMALL LETTER ES
420         0x0430, // CYRILLIC SMALL LETTER A
421         0x0439, // CYRILLIC SMALL LETTER SHORT I
422         0x0442, // CYRILLIC SMALL LETTER TE
423     };
424     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicSAYT, isRussianDomainNameCharacter);
425
426     // http://pir.org/products/opr-domain/ - rules not published. According to the registry site,
427     // the intended audience is "Russian and other Slavic-speaking markets".
428     // Chrome appears to only allow Russian, so sticking with that for now.
429     static const UChar cyrillicORG[] = {
430         '.',
431         0x043E, // CYRILLIC SMALL LETTER O
432         0x0440, // CYRILLIC SMALL LETTER ER
433         0x0433, // CYRILLIC SMALL LETTER GHE
434     };
435     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicORG, isRussianDomainNameCharacter);
436
437     // http://cctld.by/rules.html
438     static const UChar cyrillicBEL[] = {
439         '.',
440         0x0431, // CYRILLIC SMALL LETTER BE
441         0x0435, // CYRILLIC SMALL LETTER IE
442         0x043B, // CYRILLIC SMALL LETTER EL
443     };
444     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicBEL, [](UChar ch) {
445         // Russian and Byelorussian letters, digits and dashes are allowed.
446         return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x0456 || ch == 0x045E || ch == 0x2019 || isASCIIDigit(ch) || ch == '-';
447     });
448
449     // http://www.nic.kz/docs/poryadok_vnedreniya_kaz_ru.pdf
450     static const UChar cyrillicKAZ[] = {
451         '.',
452         0x049B, // CYRILLIC SMALL LETTER KA WITH DESCENDER
453         0x0430, // CYRILLIC SMALL LETTER A
454         0x0437, // CYRILLIC SMALL LETTER ZE
455     };
456     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicKAZ, [](UChar ch) {
457         // Kazakh letters, digits and dashes are allowed.
458         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 == '-';
459     });
460
461     // http://uanic.net/docs/documents-ukr/Rules%20of%20UKR_v4.0.pdf
462     static const UChar cyrillicUKR[] = {
463         '.',
464         0x0443, // CYRILLIC SMALL LETTER U
465         0x043A, // CYRILLIC SMALL LETTER KA
466         0x0440, // CYRILLIC SMALL LETTER ER
467     };
468     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicUKR, [](UChar ch) {
469         // Russian and Ukrainian letters, digits and dashes are allowed.
470         return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x0491 || ch == 0x0404 || ch == 0x0456 || ch == 0x0457 || isASCIIDigit(ch) || ch == '-';
471     });
472
473     // http://www.rnids.rs/data/DOKUMENTI/idn-srb-policy-termsofuse-v1.4-eng.pdf
474     static const UChar cyrillicSRB[] = {
475         '.',
476         0x0441, // CYRILLIC SMALL LETTER ES
477         0x0440, // CYRILLIC SMALL LETTER ER
478         0x0431, // CYRILLIC SMALL LETTER BE
479     };
480     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicSRB, [](UChar ch) {
481         // Serbian letters, digits and dashes are allowed.
482         return (ch >= 0x0430 && ch <= 0x0438) || (ch >= 0x043A && ch <= 0x0448) || ch == 0x0452 || ch == 0x0458 || ch == 0x0459 || ch == 0x045A || ch == 0x045B || ch == 0x045F || isASCIIDigit(ch) || ch == '-';
483     });
484
485     // http://marnet.mk/doc/pravilnik-mk-mkd.pdf
486     static const UChar cyrillicMKD[] = {
487         '.',
488         0x043C, // CYRILLIC SMALL LETTER EM
489         0x043A, // CYRILLIC SMALL LETTER KA
490         0x0434, // CYRILLIC SMALL LETTER DE
491     };
492     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMKD, [](UChar ch) {
493         // Macedonian letters, digits and dashes are allowed.
494         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 == '-';
495     });
496
497     // https://www.mon.mn/cs/
498     static const UChar cyrillicMON[] = {
499         '.',
500         0x043C, // CYRILLIC SMALL LETTER EM
501         0x043E, // CYRILLIC SMALL LETTER O
502         0x043D, // CYRILLIC SMALL LETTER EN
503     };
504     CHECK_RULES_IF_SUFFIX_MATCHES(cyrillicMON, [](UChar ch) {
505         // Mongolian letters, digits and dashes are allowed.
506         return (ch >= 0x0430 && ch <= 0x044f) || ch == 0x0451 || ch == 0x04E9 || ch == 0x04AF || isASCIIDigit(ch) || ch == '-';
507     });
508
509     // Not a known top level domain with special rules.
510     return false;
511 }
512
513 // Return value of null means no mapping is necessary.
514 std::optional<String> mapHostName(const String& hostName, const std::optional<URLDecodeFunction>& decodeFunction)
515 {
516     if (hostName.length() > hostNameBufferLength)
517         return String();
518     
519     if (!hostName.length())
520         return String();
521
522     String string;
523     if (decodeFunction && string.contains('%'))
524         string = (*decodeFunction)(hostName);
525     else
526         string = hostName;
527
528     unsigned length = string.length();
529
530     auto sourceBuffer = string.charactersWithNullTermination();
531     
532     UChar destinationBuffer[hostNameBufferLength];
533     UErrorCode uerror = U_ZERO_ERROR;
534     UIDNAInfo processingDetails = UIDNA_INFO_INITIALIZER;
535     int32_t numCharactersConverted = (decodeFunction ? uidna_nameToASCII : uidna_nameToUnicode)(&URLParser::internationalDomainNameTranscoder(), sourceBuffer.data(), length, destinationBuffer, hostNameBufferLength, &processingDetails, &uerror);
536     if (length && (U_FAILURE(uerror) || processingDetails.errors))
537         return std::nullopt;
538     
539     if (numCharactersConverted == static_cast<int32_t>(length) && !memcmp(sourceBuffer.data(), destinationBuffer, length * sizeof(UChar)))
540         return String();
541
542     if (!decodeFunction && !allCharactersInIDNScriptWhiteList(destinationBuffer, numCharactersConverted) && !allCharactersAllowedByTLDRules(destinationBuffer, numCharactersConverted))
543         return String();
544
545     return String(destinationBuffer, numCharactersConverted);
546 }
547
548 using MappingRangesVector = std::optional<Vector<std::tuple<unsigned, unsigned, String>>>;
549
550 static void collectRangesThatNeedMapping(const String& string, unsigned location, unsigned length, MappingRangesVector& array, const std::optional<URLDecodeFunction>& decodeFunction)
551 {
552     // Generally, we want to optimize for the case where there is one host name that does not need mapping.
553     // Therefore, we use null to indicate no mapping here and an empty array to indicate error.
554
555     String substring = string.substringSharingImpl(location, length);
556     std::optional<String> host = mapHostName(substring, decodeFunction);
557
558     if (host && !*host)
559         return;
560     
561     if (!array)
562         array = Vector<std::tuple<unsigned, unsigned, String>>();
563
564     if (host)
565         array->constructAndAppend(location, length, *host);
566 }
567
568 static void applyHostNameFunctionToMailToURLString(const String& string, const std::optional<URLDecodeFunction>& decodeFunction, MappingRangesVector& array)
569 {
570     // In a mailto: URL, host names come after a '@' character and end with a '>' or ',' or '?' character.
571     // Skip quoted strings so that characters in them don't confuse us.
572     // When we find a '?' character, we are past the part of the URL that contains host names.
573     
574     unsigned stringLength = string.length();
575     unsigned current = 0;
576     
577     while (1) {
578         // Find start of host name or of quoted string.
579         auto hostNameOrStringStart = string.find([](UChar ch) {
580             return ch == '"' || ch == '@' || ch == '?';
581         }, current);
582         if (hostNameOrStringStart == notFound)
583             return;
584
585         UChar c = string[hostNameOrStringStart];
586         current = hostNameOrStringStart + 1;
587         
588         if (c == '?')
589             return;
590         
591         if (c == '@') {
592             // Find end of host name.
593             unsigned hostNameStart = current;
594             auto hostNameEnd = string.find([](UChar ch) {
595                 return ch == '>' || ch == ',' || ch == '?';
596             }, current);
597
598             bool done;
599             if (hostNameEnd == notFound) {
600                 hostNameEnd = stringLength;
601                 done = true;
602             } else {
603                 current = hostNameEnd;
604                 done = false;
605             }
606             
607             // Process host name range.
608             collectRangesThatNeedMapping(string, hostNameStart, hostNameEnd - hostNameStart, array, decodeFunction);
609
610             if (done)
611                 return;
612         } else {
613             // Skip quoted string.
614             ASSERT(c == '"');
615             while (1) {
616                 auto escapedCharacterOrStringEnd = string.find([](UChar ch) {
617                     return ch == '"' || ch == '\\';
618                 }, current);
619                 if (escapedCharacterOrStringEnd == notFound)
620                     return;
621
622                 c = string[escapedCharacterOrStringEnd];
623                 current = escapedCharacterOrStringEnd + 1;
624
625                 // If we are the end of the string, then break from the string loop back to the host name loop.
626                 if (c == '"')
627                     break;
628                 
629                 // Skip escaped character.
630                 ASSERT(c == '\\');
631                 if (current == stringLength)
632                     return;
633
634                 ++current;
635             }
636         }
637     }
638 }
639
640 static void applyHostNameFunctionToURLString(const String& string, const std::optional<URLDecodeFunction>& decodeFunction, MappingRangesVector& array)
641 {
642     // Find hostnames. Too bad we can't use any real URL-parsing code to do this,
643     // but we have to do it before doing all the %-escaping, and this is the only
644     // code we have that parses mailto URLs anyway.
645     
646     // Maybe we should implement this using a character buffer instead?
647     
648     if (protocolIs(string, "mailto")) {
649         applyHostNameFunctionToMailToURLString(string, decodeFunction, array);
650         return;
651     }
652     
653     // Find the host name in a hierarchical URL.
654     // It comes after a "://" sequence, with scheme characters preceding.
655     // If ends with the end of the string or a ":", "/", or a "?".
656     // If there is a "@" character, the host part is just the part after the "@".
657     static const char* separator = "://";
658     auto separatorIndex = string.find(separator);
659     if (separatorIndex == notFound)
660         return;
661
662     unsigned authorityStart = separatorIndex + strlen(separator);
663     
664     // Check that all characters before the :// are valid scheme characters.
665     auto invalidSchemeCharacter = string.substringSharingImpl(0, separatorIndex).find([](UChar ch) {
666         static const char* allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-.";
667         static size_t length = strlen(allowedCharacters);
668         for (size_t i = 0; i < length; ++i) {
669             if (allowedCharacters[i] == ch)
670                 return false;
671         }
672         return true;
673     });
674
675     if (invalidSchemeCharacter != notFound)
676         return;
677     
678     unsigned stringLength = string.length();
679     
680     // Find terminating character.
681     auto hostNameTerminator = string.find([](UChar ch) {
682         static const char* terminatingCharacters = ":/?#";
683         static size_t length = strlen(terminatingCharacters);
684         for (size_t i = 0; i < length; ++i) {
685             if (terminatingCharacters[i] == ch)
686                 return true;
687         }
688         return false;
689     }, authorityStart);
690     unsigned hostNameEnd = hostNameTerminator == notFound ? stringLength : hostNameTerminator;
691     
692     // Find "@" for the start of the host name.
693     auto userInfoTerminator = string.substringSharingImpl(0, hostNameEnd).find('@', authorityStart);
694     unsigned hostNameStart = userInfoTerminator == notFound ? authorityStart : userInfoTerminator + 1;
695     
696     collectRangesThatNeedMapping(string, hostNameStart, hostNameEnd - hostNameStart, array, decodeFunction);
697 }
698
699 String mapHostNames(const String& string, const std::optional<URLDecodeFunction>& decodeFunction)
700 {
701     // Generally, we want to optimize for the case where there is one host name that does not need mapping.
702     
703     if (decodeFunction && string.isAllASCII())
704         return string;
705     
706     // Make a list of ranges that actually need mapping.
707     MappingRangesVector hostNameRanges;
708     applyHostNameFunctionToURLString(string, decodeFunction, hostNameRanges);
709     if (!hostNameRanges)
710         return string;
711
712     if (hostNameRanges->isEmpty())
713         return { };
714
715     // Do the mapping.
716     String result = string;
717     while (!hostNameRanges->isEmpty()) {
718         unsigned location, length;
719         String mappedHostName;
720         std::tie(location, length, mappedHostName) = hostNameRanges->takeLast();
721         result = result.replace(location, length, mappedHostName);
722     }
723     return result;
724 }
725
726 static String createStringWithEscapedUnsafeCharacters(const Vector<UChar, urlBytesBufferLength>& sourceBuffer)
727 {
728     Vector<UChar, urlBytesBufferLength> outBuffer;
729
730     const size_t length = sourceBuffer.size();
731     
732     std::optional<UChar32> previousCodePoint;
733     size_t i = 0;
734     while (i < length) {
735         UChar32 c;
736         U16_NEXT(sourceBuffer, i, length, c)
737         
738         if (isLookalikeCharacter(previousCodePoint, c)) {
739             uint8_t utf8Buffer[4];
740             size_t offset = 0;
741             UBool failure = false;
742             U8_APPEND(utf8Buffer, offset, 4, c, failure)
743             ASSERT(!failure);
744             
745             for (size_t j = 0; j < offset; ++j) {
746                 outBuffer.append('%');
747                 outBuffer.append(upperNibbleToASCIIHexDigit(utf8Buffer[j]));
748                 outBuffer.append(lowerNibbleToASCIIHexDigit(utf8Buffer[j]));
749             }
750         } else {
751             UChar utf16Buffer[2];
752             size_t offset = 0;
753             UBool failure = false;
754             U16_APPEND(utf16Buffer, offset, 2, c, failure)
755             ASSERT(!failure);
756             for (size_t j = 0; j < offset; ++j)
757                 outBuffer.append(utf16Buffer[j]);
758         }
759         previousCodePoint = c;
760     }
761
762     return String::adopt(WTFMove(outBuffer));
763 }
764
765 String userVisibleURL(const CString& url)
766 {
767     auto* before = reinterpret_cast<const unsigned char*>(url.data());
768     int length = url.length();
769
770     if (!length)
771         return { };
772
773     bool mayNeedHostNameDecoding = false;
774
775     // The buffer should be large enough to %-escape every character.
776     int bufferLength = (length * 3) + 1;
777     Vector<char, urlBytesBufferLength> after(bufferLength);
778
779     char* q = after.data();
780     {
781         const unsigned char* p = before;
782         for (int i = 0; i < length; i++) {
783             unsigned char c = p[i];
784             // unescape escape sequences that indicate bytes greater than 0x7f
785             if (c == '%' && i + 2 < length && isASCIIHexDigit(p[i + 1]) && isASCIIHexDigit(p[i + 2])) {
786                 auto u = toASCIIHexValue(p[i + 1], p[i + 2]);
787                 if (u > 0x7f) {
788                     // unescape
789                     *q++ = u;
790                 } else {
791                     // do not unescape
792                     *q++ = p[i];
793                     *q++ = p[i + 1];
794                     *q++ = p[i + 2];
795                 }
796                 i += 2;
797             } else {
798                 *q++ = c;
799                 
800                 // Check for "xn--" in an efficient, non-case-sensitive, way.
801                 if (c == '-' && i >= 3 && !mayNeedHostNameDecoding && (q[-4] | 0x20) == 'x' && (q[-3] | 0x20) == 'n' && q[-2] == '-')
802                     mayNeedHostNameDecoding = true;
803             }
804         }
805         *q = '\0';
806     }
807     
808     // Check string to see if it can be converted to display using UTF-8  
809     String result = String::fromUTF8(after.data());
810     if (!result) {
811         // Could not convert to UTF-8.
812         // Convert characters greater than 0x7f to escape sequences.
813         // Shift current string to the end of the buffer
814         // then we will copy back bytes to the start of the buffer 
815         // as we convert.
816         int afterlength = q - after.data();
817         char* p = after.data() + bufferLength - afterlength - 1;
818         memmove(p, after.data(), afterlength + 1); // copies trailing '\0'
819         char* q = after.data();
820         while (*p) {
821             unsigned char c = *p;
822             if (c > 0x7f) {
823                 *q++ = '%';
824                 *q++ = upperNibbleToASCIIHexDigit(c);
825                 *q++ = lowerNibbleToASCIIHexDigit(c);
826             } else
827                 *q++ = *p;
828             p++;
829         }
830         *q = '\0';
831         // Note: after.data() points to a null-terminated, pure ASCII string.
832         result = String::fromUTF8(after.data());
833         ASSERT(!!result);
834     }
835
836     // Note: result is UTF–16 string, created from either a valid UTF-8 string,
837     //       or a pure ASCII string (where all bytes with the high bit set are
838     //       percent-encoded).
839
840     if (mayNeedHostNameDecoding) {
841         // FIXME: Is it good to ignore the failure of mapHostNames and keep result intact?
842         auto mappedResult = mapHostNames(result, std::nullopt);
843         if (!!mappedResult)
844             result = mappedResult;
845     }
846
847     auto sourceBuffer = result.charactersWithNullTermination();
848     ASSERT(sourceBuffer.last() == '\0');
849     sourceBuffer.removeLast();
850
851     Vector<UChar, urlBytesBufferLength> normalizedCharacters(sourceBuffer.size());
852     UErrorCode uerror = U_ZERO_ERROR;
853     int32_t normalizedLength = unorm_normalize(sourceBuffer.data(), sourceBuffer.size(), UNORM_NFC, 0, normalizedCharacters.data(), sourceBuffer.size(), &uerror);
854     if (uerror == U_BUFFER_OVERFLOW_ERROR) {
855         uerror = U_ZERO_ERROR;
856         normalizedCharacters.resize(normalizedLength);
857         normalizedLength = unorm_normalize(sourceBuffer.data(), sourceBuffer.size(), UNORM_NFC, 0, normalizedCharacters.data(), normalizedLength, &uerror);
858     }
859     if (U_FAILURE(uerror))
860         return { };
861
862     return createStringWithEscapedUnsafeCharacters(normalizedCharacters);
863 }
864
865 } // namespace URLHelpers
866 } // namespace WTF