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