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