Reviewed by Darin Adler.
[WebKit-https.git] / WebKit / mac / Misc / WebNSURLExtras.mm
1 /*
2  * Copyright (C) 2005, 2007, 2008 Apple Inc. All rights reserved.
3  * Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
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 Computer, 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 #import "WebNSURLExtras.h"
31
32 #import "WebKitNSStringExtras.h"
33 #import "WebLocalizableStrings.h"
34 #import "WebNSDataExtras.h"
35 #import "WebNSObjectExtras.h"
36 #import "WebSystemInterface.h"
37 #import <Foundation/NSURLRequest.h>
38 #import <JavaScriptCore/Assertions.h>
39 #import <WebCore/KURL.h>
40 #import <WebCore/LoaderNSURLExtras.h>
41 #import <WebKitSystemInterface.h>
42 #import <unicode/uchar.h>
43 #import <unicode/uidna.h>
44 #import <unicode/uscript.h>
45
46 using namespace WebCore;
47 using namespace WTF;
48
49 typedef void (* StringRangeApplierFunction)(NSString *string, NSRange range, void *context);
50
51 // Needs to be big enough to hold an IDN-encoded name.
52 // For host names bigger than this, we won't do IDN encoding, which is almost certainly OK.
53 #define HOST_NAME_BUFFER_LENGTH 2048
54
55 #define URL_BYTES_BUFFER_LENGTH 2048
56
57 static pthread_once_t IDNScriptWhiteListFileRead = PTHREAD_ONCE_INIT;
58 static uint32_t IDNScriptWhiteList[(USCRIPT_CODE_LIMIT + 31) / 32];
59
60 static inline BOOL isLookalikeCharacter(int charCode)
61 {
62 // FIXME: Move this code down into WebCore so it can be shared with other platforms.
63
64 // This function treats the following as unsafe, lookalike characters:
65 // any non-printable character, any character considered as whitespace that isn't already converted to a space by ICU, 
66 // and any ignorable character.
67
68 // We also considered the characters in Mozilla's blacklist (http://kb.mozillazine.org/Network.IDN.blacklist_chars), 
69 // and included all of these characters that ICU can encode.
70
71     if (!u_isprint(charCode) || u_isUWhiteSpace(charCode) || u_hasBinaryProperty(charCode, UCHAR_DEFAULT_IGNORABLE_CODE_POINT))
72         return YES;
73
74     switch (charCode) {
75         case 0x01C3: /* LATIN LETTER RETROFLEX CLICK */
76         case 0x0337: /* COMBINING SHORT SOLIDUS OVERLAY */
77         case 0x0338: /* COMBINING LONG SOLIDUS OVERLAY */
78         case 0x05B4: /* HEBREW POINT HIRIQ */
79         case 0x05BC: /* HEBREW POINT DAGESH OR MAPIQ */
80         case 0x05C3: /* HEBREW PUNCTUATION SOF PASUQ */
81         case 0x05F4: /* HEBREW PUNCTUATION GERSHAYIM */
82         case 0x0660: /* ARABIC INDIC DIGIT ZERO */
83         case 0x06D4: /* ARABIC FULL STOP */
84         case 0x06F0: /* EXTENDED ARABIC INDIC DIGIT ZERO */
85         case 0x2027: /* HYPHENATION POINT */
86         case 0x2039: /* SINGLE LEFT-POINTING ANGLE QUOTATION MARK */
87         case 0x203A: /* SINGLE RIGHT-POINTING ANGLE QUOTATION MARK */
88         case 0x2044: /* FRACTION SLASH */
89         case 0x2215: /* DIVISION SLASH */
90         case 0x23ae: /* INTEGRAL EXTENSION */
91         case 0x2571: /* BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT */
92         case 0x29F8: /* BIG SOLIDUS */
93         case 0x29f6: /* SOLIDUS WITH OVERBAR */
94         case 0x2AFB: /* TRIPLE SOLIDUS BINARY RELATION */
95         case 0x2AFD: /* DOUBLE SOLIDUS OPERATOR */
96         case 0x3008: /* LEFT ANGLE BRACKET */
97         case 0x3014: /* LEFT TORTOISE SHELL BRACKET */
98         case 0x3015: /* RIGHT TORTOISE SHELL BRACKET */
99         case 0x3033: /* VERTICAL KANA REPEAT MARK UPPER HALF */
100         case 0x321D: /* PARENTHESIZED KOREAN CHARACTER OJEON */
101         case 0x321E: /* PARENTHESIZED KOREAN CHARACTER O HU */
102         case 0x33DF: /* SQUARE A OVER M */
103         case 0xFE14: /* PRESENTATION FORM FOR VERTICAL SEMICOLON */
104         case 0xFE15: /* PRESENTATION FORM FOR VERTICAL EXCLAMATION MARK */
105         case 0xFE3F: /* PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET */
106         case 0xFE5D: /* SMALL LEFT TORTOISE SHELL BRACKET */
107         case 0xFE5E: /* SMALL RIGHT TORTOISE SHELL BRACKET */
108             return YES;
109         default:
110             return NO;
111     }
112 }
113
114 static char hexDigit(int i)
115 {
116     if (i < 0 || i > 16) {
117         LOG_ERROR("illegal hex digit");
118         return '0';
119     }
120     int h = i;
121     if (h >= 10) {
122         h = h - 10 + 'A'; 
123     }
124     else {
125         h += '0';
126     }
127     return h;
128 }
129
130 static BOOL isHexDigit(char c)
131 {
132     return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
133 }
134
135 static int hexDigitValue(char c)
136 {
137     if (c >= '0' && c <= '9') {
138         return c - '0';
139     }
140     if (c >= 'A' && c <= 'F') {
141         return c - 'A' + 10;
142     }
143     if (c >= 'a' && c <= 'f') {
144         return c - 'a' + 10;
145     }
146     LOG_ERROR("illegal hex digit");
147     return 0;
148 }
149
150 static void applyHostNameFunctionToMailToURLString(NSString *string, StringRangeApplierFunction f, void *context)
151 {
152     // In a mailto: URL, host names come after a '@' character and end with a '>' or ',' or '?' character.
153     // Skip quoted strings so that characters in them don't confuse us.
154     // When we find a '?' character, we are past the part of the URL that contains host names.
155
156     static NSCharacterSet *hostNameOrStringStartCharacters;
157     if (hostNameOrStringStartCharacters == nil) {
158         hostNameOrStringStartCharacters = [NSCharacterSet characterSetWithCharactersInString:@"\"@?"];
159         CFRetain(hostNameOrStringStartCharacters);
160     }
161     static NSCharacterSet *hostNameEndCharacters;
162     if (hostNameEndCharacters == nil) {
163         hostNameEndCharacters = [NSCharacterSet characterSetWithCharactersInString:@">,?"];
164         CFRetain(hostNameEndCharacters);
165     }
166     static NSCharacterSet *quotedStringCharacters;
167     if (quotedStringCharacters == nil) {
168         quotedStringCharacters = [NSCharacterSet characterSetWithCharactersInString:@"\"\\"];
169         CFRetain(quotedStringCharacters);
170     }
171
172     unsigned stringLength = [string length];
173     NSRange remaining = NSMakeRange(0, stringLength);
174     
175     while (1) {
176         // Find start of host name or of quoted string.
177         NSRange hostNameOrStringStart = [string rangeOfCharacterFromSet:hostNameOrStringStartCharacters options:0 range:remaining];
178         if (hostNameOrStringStart.location == NSNotFound) {
179             return;
180         }
181         unichar c = [string characterAtIndex:hostNameOrStringStart.location];
182         remaining.location = NSMaxRange(hostNameOrStringStart);
183         remaining.length = stringLength - remaining.location;
184
185         if (c == '?') {
186             return;
187         }
188         
189         if (c == '@') {
190             // Find end of host name.
191             unsigned hostNameStart = remaining.location;
192             NSRange hostNameEnd = [string rangeOfCharacterFromSet:hostNameEndCharacters options:0 range:remaining];
193             BOOL done;
194             if (hostNameEnd.location == NSNotFound) {
195                 hostNameEnd.location = stringLength;
196                 done = YES;
197             } else {
198                 remaining.location = hostNameEnd.location;
199                 remaining.length = stringLength - remaining.location;
200                 done = NO;
201             }
202
203             // Process host name range.
204             f(string, NSMakeRange(hostNameStart, hostNameEnd.location - hostNameStart), context);
205
206             if (done) {
207                 return;
208             }
209         } else {
210             // Skip quoted string.
211             ASSERT(c == '"');
212             while (1) {
213                 NSRange escapedCharacterOrStringEnd = [string rangeOfCharacterFromSet:quotedStringCharacters options:0 range:remaining];
214                 if (escapedCharacterOrStringEnd.location == NSNotFound) {
215                     return;
216                 }
217                 c = [string characterAtIndex:escapedCharacterOrStringEnd.location];
218                 remaining.location = NSMaxRange(escapedCharacterOrStringEnd);
219                 remaining.length = stringLength - remaining.location;
220                 
221                 // If we are the end of the string, then break from the string loop back to the host name loop.
222                 if (c == '"') {
223                     break;
224                 }
225                 
226                 // Skip escaped character.
227                 ASSERT(c == '\\');
228                 if (remaining.length == 0) {
229                     return;
230                 }                
231                 remaining.location += 1;
232                 remaining.length -= 1;
233             }
234         }
235     }
236 }
237
238 static void applyHostNameFunctionToURLString(NSString *string, StringRangeApplierFunction f, void *context)
239 {
240     // Find hostnames. Too bad we can't use any real URL-parsing code to do this,
241     // but we have to do it before doing all the %-escaping, and this is the only
242     // code we have that parses mailto URLs anyway.
243
244     // Maybe we should implement this using a character buffer instead?
245
246     if ([string _webkit_hasCaseInsensitivePrefix:@"mailto:"]) {
247         applyHostNameFunctionToMailToURLString(string, f, context);
248         return;
249     }
250
251     // Find the host name in a hierarchical URL.
252     // It comes after a "://" sequence, with scheme characters preceding.
253     // If ends with the end of the string or a ":", "/", or a "?".
254     // If there is a "@" character, the host part is just the part after the "@".
255     NSRange separatorRange = [string rangeOfString:@"://"];
256     if (separatorRange.location == NSNotFound) {
257         return;
258     }
259
260     // Check that all characters before the :// are valid scheme characters.
261     static NSCharacterSet *nonSchemeCharacters;
262     if (nonSchemeCharacters == nil) {
263         nonSchemeCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-."] invertedSet];
264         CFRetain(nonSchemeCharacters);
265     }
266     if ([string rangeOfCharacterFromSet:nonSchemeCharacters options:0 range:NSMakeRange(0, separatorRange.location)].location != NSNotFound) {
267         return;
268     }
269
270     unsigned stringLength = [string length];
271
272     static NSCharacterSet *hostTerminators;
273     if (hostTerminators == nil) {
274         hostTerminators = [NSCharacterSet characterSetWithCharactersInString:@":/?#"];
275         CFRetain(hostTerminators);
276     }
277
278     // Start after the separator.
279     unsigned authorityStart = NSMaxRange(separatorRange);
280
281     // Find terminating character.
282     NSRange hostNameTerminator = [string rangeOfCharacterFromSet:hostTerminators options:0 range:NSMakeRange(authorityStart, stringLength - authorityStart)];
283     unsigned hostNameEnd = hostNameTerminator.location == NSNotFound ? stringLength : hostNameTerminator.location;
284
285     // Find "@" for the start of the host name.
286     NSRange userInfoTerminator = [string rangeOfString:@"@" options:0 range:NSMakeRange(authorityStart, hostNameEnd - authorityStart)];
287     unsigned hostNameStart = userInfoTerminator.location == NSNotFound ? authorityStart : NSMaxRange(userInfoTerminator);
288
289     f(string, NSMakeRange(hostNameStart, hostNameEnd - hostNameStart), context);
290 }
291
292 @implementation NSURL (WebNSURLExtras)
293
294 static void collectRangesThatNeedMapping(NSString *string, NSRange range, void *context, BOOL encode)
295 {
296     BOOL needsMapping = encode
297         ? [string _web_hostNameNeedsEncodingWithRange:range]
298         : [string _web_hostNameNeedsDecodingWithRange:range];
299     if (!needsMapping) {
300         return;
301     }
302
303     NSMutableArray **array = (NSMutableArray **)context;
304     if (*array == nil) {
305         *array = [[NSMutableArray alloc] init];
306     }
307
308     [*array addObject:[NSValue valueWithRange:range]];
309 }
310
311 static void collectRangesThatNeedEncoding(NSString *string, NSRange range, void *context)
312 {
313     return collectRangesThatNeedMapping(string, range, context, YES);
314 }
315
316 static void collectRangesThatNeedDecoding(NSString *string, NSRange range, void *context)
317 {
318     return collectRangesThatNeedMapping(string, range, context, NO);
319 }
320
321 static NSString *mapHostNames(NSString *string, BOOL encode)
322 {
323     // Generally, we want to optimize for the case where there is one host name that does not need mapping.
324     
325     if (encode && [string canBeConvertedToEncoding:NSASCIIStringEncoding])
326         return string;
327
328     // Make a list of ranges that actually need mapping.
329     NSMutableArray *hostNameRanges = nil;
330     StringRangeApplierFunction f = encode
331         ? collectRangesThatNeedEncoding
332         : collectRangesThatNeedDecoding;
333     applyHostNameFunctionToURLString(string, f, &hostNameRanges);
334     if (hostNameRanges == nil)
335         return string;
336
337     // Do the mapping.
338     NSMutableString *mutableCopy = [string mutableCopy];
339     unsigned i = [hostNameRanges count];
340     while (i-- != 0) {
341         NSRange hostNameRange = [[hostNameRanges objectAtIndex:i] rangeValue];
342         NSString *mappedHostName = encode
343             ? [string _web_encodeHostNameWithRange:hostNameRange]
344             : [string _web_decodeHostNameWithRange:hostNameRange];
345         [mutableCopy replaceCharactersInRange:hostNameRange withString:mappedHostName];
346     }
347     [hostNameRanges release];
348     return [mutableCopy autorelease];
349 }
350
351 + (NSURL *)_web_URLWithUserTypedString:(NSString *)string relativeToURL:(NSURL *)URL
352 {
353     if (string == nil) {
354         return nil;
355     }
356     string = mapHostNames([string _webkit_stringByTrimmingWhitespace], YES);
357
358     NSData *userTypedData = [string dataUsingEncoding:NSUTF8StringEncoding];
359     ASSERT(userTypedData);
360
361     const UInt8 *inBytes = static_cast<const UInt8 *>([userTypedData bytes]);
362     int inLength = [userTypedData length];
363     if (inLength == 0) {
364         return [NSURL URLWithString:@""];
365     }
366     
367     char *outBytes = static_cast<char *>(malloc(inLength * 3)); // large enough to %-escape every character
368     char *p = outBytes;
369     int outLength = 0;
370     int i;
371     for (i = 0; i < inLength; i++) {
372         UInt8 c = inBytes[i];
373         if (c <= 0x20 || c >= 0x7f) {
374             *p++ = '%';
375             *p++ = hexDigit(c >> 4);
376             *p++ = hexDigit(c & 0xf);
377             outLength += 3;
378         }
379         else {
380             *p++ = c;
381             outLength++;
382         }
383     }
384  
385     NSData *data = [NSData dataWithBytesNoCopy:outBytes length:outLength]; // adopts outBytes
386     return [self _web_URLWithData:data relativeToURL:URL];
387 }
388
389 + (NSURL *)_web_URLWithUserTypedString:(NSString *)string
390 {
391     return [self _web_URLWithUserTypedString:string relativeToURL:nil];
392 }
393
394 + (NSURL *)_web_URLWithDataAsString:(NSString *)string
395 {
396     if (string == nil) {
397         return nil;
398     }
399     return [self _web_URLWithDataAsString:string relativeToURL:nil];
400 }
401
402 + (NSURL *)_web_URLWithDataAsString:(NSString *)string relativeToURL:(NSURL *)baseURL
403 {
404     if (string == nil) {
405         return nil;
406     }
407     string = [string _webkit_stringByTrimmingWhitespace];
408     NSData *data = [string dataUsingEncoding:NSISOLatin1StringEncoding];
409     return [self _web_URLWithData:data relativeToURL:baseURL];
410 }
411
412 + (NSURL *)_web_URLWithData:(NSData *)data
413 {
414     return urlWithData(data);
415 }      
416
417 + (NSURL *)_web_URLWithData:(NSData *)data relativeToURL:(NSURL *)baseURL
418 {
419     return urlWithDataRelativeToURL(data, baseURL);
420 }
421
422 - (NSData *)_web_originalData
423 {
424     return urlOriginalData(self);
425 }
426
427 - (NSString *)_web_originalDataAsString
428 {
429     return urlOriginalDataAsString(self);
430 }
431
432 - (NSString *)_web_userVisibleString
433 {
434     NSData *data = [self _web_originalData];
435     const unsigned char *before = static_cast<const unsigned char*>([data bytes]);
436     int length = [data length];
437
438     bool needsHostNameDecoding = false;
439
440     const unsigned char *p = before;
441     int bufferLength = (length * 3) + 1;
442     char *after = static_cast<char *>(malloc(bufferLength)); // large enough to %-escape every character
443     char *q = after;
444     int i;
445     for (i = 0; i < length; i++) {
446         unsigned char c = p[i];
447         // escape control characters, space, and delete
448         if (c <= 0x20 || c == 0x7f) {
449             *q++ = '%';
450             *q++ = hexDigit(c >> 4);
451             *q++ = hexDigit(c & 0xf);
452         }
453         // unescape escape sequences that indicate bytes greater than 0x7f
454         else if (c == '%' && (i + 1 < length && isHexDigit(p[i + 1])) && i + 2 < length && isHexDigit(p[i + 2])) {
455             unsigned char u = (hexDigitValue(p[i + 1]) << 4) | hexDigitValue(p[i + 2]);
456             if (u > 0x7f) {
457                 // unescape
458                 *q++ = u;
459             }
460             else {
461                 // do not unescape
462                 *q++ = p[i];
463                 *q++ = p[i + 1];
464                 *q++ = p[i + 2];
465             }
466             i += 2;
467         } 
468         else {
469             *q++ = c;
470
471             // Check for "xn--" in an efficient, non-case-sensitive, way.
472             if (c == '-' && i >= 3 && !needsHostNameDecoding && (q[-4] | 0x20) == 'x' && (q[-3] | 0x20) == 'n' && q[-2] == '-')
473                 needsHostNameDecoding = true;
474         }
475     }
476     *q = '\0';
477     
478     // Check string to see if it can be converted to display using UTF-8  
479     NSString *result = [NSString stringWithUTF8String:after];
480     if (!result) {
481         // Could not convert to UTF-8.
482         // Convert characters greater than 0x7f to escape sequences.
483         // Shift current string to the end of the buffer
484         // then we will copy back bytes to the start of the buffer 
485         // as we convert.
486         int afterlength = q - after;
487         char *p = after + bufferLength - afterlength - 1;
488         memmove(p, after, afterlength + 1); // copies trailing '\0'
489         char *q = after;
490         while (*p) {
491             unsigned char c = *p;
492             if (c > 0x7f) {
493                 *q++ = '%';
494                 *q++ = hexDigit(c >> 4);
495                 *q++ = hexDigit(c & 0xf);
496             }
497             else {
498                 *q++ = *p;
499             }
500             p++;
501         }
502         *q = '\0';
503         result = [NSString stringWithUTF8String:after];
504     }
505
506     free(after);
507     
508     // As an optimization, only do host name decoding if we have "xn--" somewhere.
509     return needsHostNameDecoding ? mapHostNames(result, NO) : result;
510 }
511
512 - (BOOL)_web_isEmpty
513 {
514     return urlIsEmpty(self);
515 }
516
517 - (const char *)_web_URLCString
518 {
519     NSMutableData *data = [NSMutableData data];
520     [data appendData:[self _web_originalData]];
521     [data appendBytes:"\0" length:1];
522     return (const char *)[data bytes];
523  }
524
525 - (NSURL *)_webkit_canonicalize
526 {
527     InitWebCoreSystemInterface();
528     return canonicalURL(self);
529 }
530
531 typedef struct {
532     NSString *scheme;
533     NSString *user;
534     NSString *password;
535     NSString *host;
536     CFIndex port; // kCFNotFound means ignore/omit
537     NSString *path;
538     NSString *query;
539     NSString *fragment;
540 } WebKitURLComponents;
541
542
543
544 - (NSURL *)_webkit_URLByRemovingComponent:(CFURLComponentType)component
545 {
546     return urlByRemovingComponent(self, component);
547 }
548
549 - (NSURL *)_webkit_URLByRemovingFragment
550 {
551     return urlByRemovingFragment(self);
552 }
553
554 - (NSURL *)_webkit_URLByRemovingResourceSpecifier
555 {
556     return urlByRemovingResourceSpecifier(self);
557 }
558
559 - (BOOL)_webkit_isJavaScriptURL
560 {
561     return [[self _web_originalDataAsString] _webkit_isJavaScriptURL];
562 }
563
564 - (NSString *)_webkit_scriptIfJavaScriptURL
565 {
566     return [[self absoluteString] _webkit_scriptIfJavaScriptURL];
567 }
568
569 - (BOOL)_webkit_isFileURL
570 {
571     return urlIsFileURL(self);
572 }
573
574 - (BOOL)_webkit_isFTPDirectoryURL
575 {
576     return [[self _web_originalDataAsString] _webkit_isFTPDirectoryURL];
577 }
578
579 - (BOOL)_webkit_shouldLoadAsEmptyDocument
580 {
581     return [[self _web_originalDataAsString] _webkit_hasCaseInsensitivePrefix:@"about:"] || [self _web_isEmpty];
582 }
583
584 - (NSURL *)_web_URLWithLowercasedScheme
585 {
586     CFRange range;
587     CFURLGetByteRangeForComponent((CFURLRef)self, kCFURLComponentScheme, &range);
588     if (range.location == kCFNotFound) {
589         return self;
590     }
591     
592     UInt8 static_buffer[URL_BYTES_BUFFER_LENGTH];
593     UInt8 *buffer = static_buffer;
594     CFIndex bytesFilled = CFURLGetBytes((CFURLRef)self, buffer, URL_BYTES_BUFFER_LENGTH);
595     if (bytesFilled == -1) {
596         CFIndex bytesToAllocate = CFURLGetBytes((CFURLRef)self, NULL, 0);
597         buffer = static_cast<UInt8 *>(malloc(bytesToAllocate));
598         bytesFilled = CFURLGetBytes((CFURLRef)self, buffer, bytesToAllocate);
599         ASSERT(bytesFilled == bytesToAllocate);
600     }
601     
602     int i;
603     BOOL changed = NO;
604     for (i = 0; i < range.length; ++i) {
605         char c = buffer[range.location + i];
606         char lower = toASCIILower(c);
607         if (c != lower) {
608             buffer[range.location + i] = lower;
609             changed = YES;
610         }
611     }
612     
613     NSURL *result = changed
614         ? (NSURL *)WebCFAutorelease(CFURLCreateAbsoluteURLWithBytes(NULL, buffer, bytesFilled, kCFStringEncodingUTF8, nil, YES))
615         : (NSURL *)self;
616
617     if (buffer != static_buffer) {
618         free(buffer);
619     }
620     
621     return result;
622 }
623
624
625 -(BOOL)_web_hasQuestionMarkOnlyQueryString
626 {
627     CFRange rangeWithSeparators;
628     CFURLGetByteRangeForComponent((CFURLRef)self, kCFURLComponentQuery, &rangeWithSeparators);
629     if (rangeWithSeparators.location != kCFNotFound && rangeWithSeparators.length == 1) {
630         return YES;
631     }
632     return NO;
633 }
634
635 -(NSData *)_web_schemeSeparatorWithoutColon
636 {
637     NSData *result = nil;
638     CFRange rangeWithSeparators;
639     CFRange range = CFURLGetByteRangeForComponent((CFURLRef)self, kCFURLComponentScheme, &rangeWithSeparators);
640     if (rangeWithSeparators.location != kCFNotFound) {
641         NSString *absoluteString = [self absoluteString];
642         NSRange separatorsRange = NSMakeRange(range.location + range.length + 1, rangeWithSeparators.length - range.length - 1);
643         if (separatorsRange.location + separatorsRange.length <= [absoluteString length]) {
644             NSString *slashes = [absoluteString substringWithRange:separatorsRange];
645             result = [slashes dataUsingEncoding:NSISOLatin1StringEncoding];
646         }
647     }
648     return result;
649 }
650
651 #define completeURL (CFURLComponentType)-1
652
653 -(NSData *)_web_dataForURLComponentType:(CFURLComponentType)componentType
654 {
655     static int URLComponentTypeBufferLength = 2048;
656     
657     UInt8 staticAllBytesBuffer[URLComponentTypeBufferLength];
658     UInt8 *allBytesBuffer = staticAllBytesBuffer;
659     
660     CFIndex bytesFilled = CFURLGetBytes((CFURLRef)self, allBytesBuffer, URLComponentTypeBufferLength);
661     if (bytesFilled == -1) {
662         CFIndex bytesToAllocate = CFURLGetBytes((CFURLRef)self, NULL, 0);
663         allBytesBuffer = static_cast<UInt8 *>(malloc(bytesToAllocate));
664         bytesFilled = CFURLGetBytes((CFURLRef)self, allBytesBuffer, bytesToAllocate);
665     }
666     
667     CFRange range;
668     if (componentType != completeURL) {
669         range = CFURLGetByteRangeForComponent((CFURLRef)self, componentType, NULL);
670         if (range.location == kCFNotFound) {
671             return nil;
672         }
673     }
674     else {
675         range.location = 0;
676         range.length = bytesFilled;
677     }
678     
679     NSData *componentData = [NSData dataWithBytes:allBytesBuffer + range.location length:range.length]; 
680     
681     const unsigned char *bytes = static_cast<const unsigned char *>([componentData bytes]);
682     NSMutableData *resultData = [NSMutableData data];
683     // NOTE: add leading '?' to query strings non-zero length query strings.
684     // NOTE: retain question-mark only query strings.
685     if (componentType == kCFURLComponentQuery) {
686         if (range.length > 0 || [self _web_hasQuestionMarkOnlyQueryString]) {
687             [resultData appendBytes:"?" length:1];    
688         }
689     }
690     int i;
691     for (i = 0; i < range.length; i++) {
692         unsigned char c = bytes[i];
693         if (c <= 0x20 || c >= 0x7f) {
694             char escaped[3];
695             escaped[0] = '%';
696             escaped[1] = hexDigit(c >> 4);
697             escaped[2] = hexDigit(c & 0xf);
698             [resultData appendBytes:escaped length:3];    
699         }
700         else {
701             char b[1];
702             b[0] = c;
703             [resultData appendBytes:b length:1];    
704         }               
705     }
706     
707     if (staticAllBytesBuffer != allBytesBuffer) {
708         free(allBytesBuffer);
709     }
710     
711     return resultData;
712 }
713
714 -(NSData *)_web_schemeData
715 {
716     return [self _web_dataForURLComponentType:kCFURLComponentScheme];
717 }
718
719 -(NSData *)_web_hostData
720 {
721     NSData *result = [self _web_dataForURLComponentType:kCFURLComponentHost];
722     NSData *scheme = [self _web_schemeData];
723     // Take off localhost for file
724     if ([scheme _web_isCaseInsensitiveEqualToCString:"file"]) {
725         return ([result _web_isCaseInsensitiveEqualToCString:"localhost"]) ? nil : result;
726     }
727     return result;
728 }
729
730 - (NSString *)_web_hostString
731 {
732     NSData *data = [self _web_hostData];
733     if (!data) {
734         data = [NSData data];
735     }
736     return [[[NSString alloc] initWithData:[self _web_hostData] encoding:NSUTF8StringEncoding] autorelease];
737 }
738
739 - (NSString *)_webkit_suggestedFilenameWithMIMEType:(NSString *)MIMEType
740 {
741     return suggestedFilenameWithMIMEType(self, MIMEType);
742 }
743
744 @end
745
746 @implementation NSString (WebNSURLExtras)
747
748 - (BOOL)_web_isUserVisibleURL
749 {
750     BOOL valid = YES;
751     // get buffer
752
753     char static_buffer[1024];
754     const char *p;
755     BOOL success = CFStringGetCString((CFStringRef)self, static_buffer, 1023, kCFStringEncodingUTF8);
756     if (success) {
757         p = static_buffer;
758     } else {
759         p = [self UTF8String];
760     }
761
762     int length = strlen(p);
763
764     // check for characters <= 0x20 or >=0x7f, %-escape sequences of %7f, and xn--, these
765     // are the things that will lead _web_userVisibleString to actually change things.
766     int i;
767     for (i = 0; i < length; i++) {
768         unsigned char c = p[i];
769         // escape control characters, space, and delete
770         if (c <= 0x20 || c == 0x7f) {
771             valid = NO;
772             break;
773         } else if (c == '%' && (i + 1 < length && isHexDigit(p[i + 1])) && i + 2 < length && isHexDigit(p[i + 2])) {
774             unsigned char u = (hexDigitValue(p[i + 1]) << 4) | hexDigitValue(p[i + 2]);
775             if (u > 0x7f) {
776                 valid = NO;
777                 break;
778             }
779             i += 2;
780         } else {
781             // Check for "xn--" in an efficient, non-case-sensitive, way.
782             if (c == '-' && i >= 3 && (p[i - 3] | 0x20) == 'x' && (p[i - 2] | 0x20) == 'n' && p[i - 1] == '-') {
783                 valid = NO;
784                 break;
785             }
786         }
787     }
788
789     return valid;
790 }
791
792
793 - (BOOL)_webkit_isJavaScriptURL
794 {
795     return [self _webkit_hasCaseInsensitivePrefix:@"javascript:"];
796 }
797
798 - (BOOL)_webkit_isFileURL
799 {
800     return stringIsFileURL(self);
801 }
802
803 - (NSString *)_webkit_stringByReplacingValidPercentEscapes
804 {
805     return decodeURLEscapeSequences(self);
806 }
807
808 - (NSString *)_webkit_scriptIfJavaScriptURL
809 {
810     if (![self _webkit_isJavaScriptURL]) {
811         return nil;
812     }
813     return [[self substringFromIndex:11] _webkit_stringByReplacingValidPercentEscapes];
814 }
815
816 - (BOOL)_webkit_isFTPDirectoryURL
817 {
818     int length = [self length];
819     if (length < 5) {  // 5 is length of "ftp:/"
820         return NO;
821     }
822     unichar lastChar = [self characterAtIndex:length - 1];
823     return lastChar == '/' && [self _webkit_hasCaseInsensitivePrefix:@"ftp:"];
824 }
825
826
827 static BOOL readIDNScriptWhiteListFile(NSString *filename)
828 {
829     if (!filename) {
830         return NO;
831     }
832     FILE *file = fopen([filename fileSystemRepresentation], "r");
833     if (file == NULL) {
834         return NO;
835     }
836
837     // Read a word at a time.
838     // Allow comments, starting with # character to the end of the line.
839     while (1) {
840         // Skip a comment if present.
841         int result = fscanf(file, " #%*[^\n\r]%*[\n\r]");
842         if (result == EOF) {
843             break;
844         }
845
846         // Read a script name if present.
847         char word[33];
848         result = fscanf(file, " %32[^# \t\n\r]%*[^# \t\n\r] ", word);
849         if (result == EOF) {
850             break;
851         }
852         if (result == 1) {
853             // Got a word, map to script code and put it into the array.
854             int32_t script = u_getPropertyValueEnum(UCHAR_SCRIPT, word);
855             if (script >= 0 && script < USCRIPT_CODE_LIMIT) {
856                 size_t index = script / 32;
857                 uint32_t mask = 1 << (script % 32);
858                 IDNScriptWhiteList[index] |= mask;
859             }
860         }
861     }
862     fclose(file);
863     return YES;
864 }
865
866 static void readIDNScriptWhiteList(void)
867 {
868     // Read white list from library.
869     NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
870     int i, numDirs = [dirs count];
871     for (i = 0; i < numDirs; i++) {
872         NSString *dir = [dirs objectAtIndex:i];
873         if (readIDNScriptWhiteListFile([dir stringByAppendingPathComponent:@"IDNScriptWhiteList.txt"])) {
874             return;
875         }
876     }
877
878     // Fall back on white list inside bundle.
879     NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.apple.WebKit"];
880     readIDNScriptWhiteListFile([bundle pathForResource:@"IDNScriptWhiteList" ofType:@"txt"]);
881 }
882
883 static BOOL allCharactersInIDNScriptWhiteList(const UChar *buffer, int32_t length)
884 {
885     pthread_once(&IDNScriptWhiteListFileRead, readIDNScriptWhiteList);
886
887     int32_t i = 0;
888     while (i < length) {
889         UChar32 c;
890         U16_NEXT(buffer, i, length, c)
891         UErrorCode error = U_ZERO_ERROR;
892         UScriptCode script = uscript_getScript(c, &error);
893         if (error != U_ZERO_ERROR) {
894             LOG_ERROR("got ICU error while trying to look at scripts: %d", error);
895             return NO;
896         }
897         if (script < 0) {
898             LOG_ERROR("got negative number for script code from ICU: %d", script);
899             return NO;
900         }
901         if (script >= USCRIPT_CODE_LIMIT) {
902             return NO;
903         }
904         size_t index = script / 32;
905         uint32_t mask = 1 << (script % 32);
906         if (!(IDNScriptWhiteList[index] & mask)) {
907             return NO;
908         }
909
910         if (isLookalikeCharacter(c))
911             return NO;
912     }
913     return YES;
914 }
915
916 // Return value of nil means no mapping is necessary.
917 // If makeString is NO, then return value is either nil or self to indicate mapping is necessary.
918 // If makeString is YES, then return value is either nil or the mapped string.
919 - (NSString *)_web_mapHostNameWithRange:(NSRange)range encode:(BOOL)encode makeString:(BOOL)makeString
920 {
921     if (range.length > HOST_NAME_BUFFER_LENGTH) {
922         return nil;
923     }
924
925     if ([self length] == 0)
926         return nil;
927     
928     UChar sourceBuffer[HOST_NAME_BUFFER_LENGTH];
929     UChar destinationBuffer[HOST_NAME_BUFFER_LENGTH];
930     
931     NSString *string = self;
932     if (encode && [self rangeOfString:@"%" options:NSLiteralSearch range:range].location != NSNotFound) {
933         NSString *substring = [self substringWithRange:range];
934         substring = WebCFAutorelease(CFURLCreateStringByReplacingPercentEscapes(NULL, (CFStringRef)substring, CFSTR("")));
935         if (substring != nil) {
936             string = substring;
937             range = NSMakeRange(0, [string length]);
938         }
939     }
940     
941     int length = range.length;
942     [string getCharacters:sourceBuffer range:range];
943
944     UErrorCode error = U_ZERO_ERROR;
945     int32_t numCharactersConverted = (encode ? uidna_IDNToASCII : uidna_IDNToUnicode)
946         (sourceBuffer, length, destinationBuffer, HOST_NAME_BUFFER_LENGTH, UIDNA_ALLOW_UNASSIGNED, NULL, &error);
947     if (error != U_ZERO_ERROR) {
948         return nil;
949     }
950     if (numCharactersConverted == length && memcmp(sourceBuffer, destinationBuffer, length * sizeof(UChar)) == 0) {
951         return nil;
952     }
953     if (!encode && !allCharactersInIDNScriptWhiteList(destinationBuffer, numCharactersConverted)) {
954         return nil;
955     }
956     return makeString ? (NSString *)[NSString stringWithCharacters:destinationBuffer length:numCharactersConverted] : (NSString *)self;
957 }
958
959 - (BOOL)_web_hostNameNeedsDecodingWithRange:(NSRange)range
960 {
961     return [self _web_mapHostNameWithRange:range encode:NO makeString:NO] != nil;
962 }
963
964 - (BOOL)_web_hostNameNeedsEncodingWithRange:(NSRange)range
965 {
966     return [self _web_mapHostNameWithRange:range encode:YES makeString:NO] != nil;
967 }
968
969 - (NSString *)_web_decodeHostNameWithRange:(NSRange)range
970 {
971     return [self _web_mapHostNameWithRange:range encode:NO makeString:YES];
972 }
973
974 - (NSString *)_web_encodeHostNameWithRange:(NSRange)range
975 {
976     return [self _web_mapHostNameWithRange:range encode:YES makeString:YES];
977 }
978
979 - (NSString *)_web_decodeHostName
980 {
981     NSString *name = [self _web_mapHostNameWithRange:NSMakeRange(0, [self length]) encode:NO makeString:YES];
982     return name == nil ? self : name;
983 }
984
985 - (NSString *)_web_encodeHostName
986 {
987     NSString *name = [self _web_mapHostNameWithRange:NSMakeRange(0, [self length]) encode:YES makeString:YES];
988     return name == nil ? self : name;
989 }
990
991 -(NSRange)_webkit_rangeOfURLScheme
992 {
993     NSRange colon = [self rangeOfString:@":"];
994     if (colon.location != NSNotFound && colon.location > 0) {
995         NSRange scheme = {0, colon.location};
996         static NSCharacterSet *InverseSchemeCharacterSet = nil;
997         if (!InverseSchemeCharacterSet) {
998             /*
999              This stuff is very expensive.  10-15 msec on a 2x1.2GHz.  If not cached it swamps
1000              everything else when adding items to the autocomplete DB.  Makes me wonder if we
1001              even need to enforce the character set here.
1002             */
1003             NSString *acceptableCharacters = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+.-";
1004             InverseSchemeCharacterSet = [[[NSCharacterSet characterSetWithCharactersInString:acceptableCharacters] invertedSet] retain];
1005         }
1006         NSRange illegals = [self rangeOfCharacterFromSet:InverseSchemeCharacterSet options:0 range:scheme];
1007         if (illegals.location == NSNotFound)
1008             return scheme;
1009     }
1010     return NSMakeRange(NSNotFound, 0);
1011 }
1012
1013 -(BOOL)_webkit_looksLikeAbsoluteURL
1014 {
1015     // Trim whitespace because _web_URLWithString allows whitespace.
1016     return [[self _webkit_stringByTrimmingWhitespace] _webkit_rangeOfURLScheme].location != NSNotFound;
1017 }
1018
1019 - (NSString *)_webkit_URLFragment
1020 {
1021     NSRange fragmentRange;
1022     
1023     fragmentRange = [self rangeOfString:@"#" options:NSLiteralSearch];
1024     if (fragmentRange.location == NSNotFound)
1025         return nil;
1026     return [self substringFromIndex:fragmentRange.location + 1];
1027 }
1028
1029 @end