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