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