Reviewed by Hyatt.
[WebKit.git] / WebKit / Misc.subproj / WebNSDataExtras.m
1 /*
2  * Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import <WebKit/WebNSDataExtras.h>
30
31 #import <WebKit/WebAssertions.h>
32
33 @interface NSString (WebNSDataExtrasInternal)
34 - (NSString *)_web_capitalizeRFC822HeaderFieldName;
35 @end
36
37 @implementation NSString (WebNSDataExtrasInternal)
38
39 -(NSString *)_web_capitalizeRFC822HeaderFieldName
40 {
41     CFStringRef name = (CFStringRef)self;
42     NSString *result = nil;
43
44     CFIndex i; 
45     CFIndex len = CFStringGetLength(name);
46     char *charPtr = NULL;
47     UniChar *uniCharPtr = NULL;
48     Boolean useUniCharPtr = FALSE;
49     Boolean shouldCapitalize = TRUE;
50     Boolean somethingChanged = FALSE;
51     
52     for (i = 0; i < len; i ++) {
53         UniChar ch = CFStringGetCharacterAtIndex(name, i);
54         Boolean replace = FALSE;
55         if (shouldCapitalize && ch >= 'a' && ch <= 'z') {
56             ch = ch + 'A' - 'a';
57             replace = TRUE;
58         } 
59         else if (!shouldCapitalize && ch >= 'A' && ch <= 'Z') {
60             ch = ch + 'a' - 'A';
61             replace = TRUE;
62         }
63         if (replace) {
64             if (!somethingChanged) {
65                 somethingChanged = TRUE;
66                 if (CFStringGetBytes(name, CFRangeMake(0, len), kCFStringEncodingISOLatin1, 0, FALSE, NULL, 0, NULL) == len) {
67                     // Can be encoded in ISOLatin1
68                     useUniCharPtr = FALSE;
69                     charPtr = CFAllocatorAllocate(NULL, len + 1, 0);
70                     CFStringGetCString(name, charPtr, len+1, kCFStringEncodingISOLatin1);
71                 } 
72                 else {
73                     useUniCharPtr = TRUE;
74                     uniCharPtr = CFAllocatorAllocate(NULL, len * sizeof(UniChar), 0);
75                     CFStringGetCharacters(name, CFRangeMake(0, len), uniCharPtr);
76                 }
77             }
78             if (useUniCharPtr) {
79                 uniCharPtr[i] = ch;
80             } 
81             else {
82                 charPtr[i] = ch;
83             }
84         }
85         if (ch == '-') {
86             shouldCapitalize = TRUE;
87         } 
88         else {
89             shouldCapitalize = FALSE;
90         }
91     }
92     if (somethingChanged) {
93         if (useUniCharPtr) {
94             result = (NSString *)CFMakeCollectable(CFStringCreateWithCharacters(NULL, uniCharPtr, len));
95         } 
96         else {
97             result = (NSString *)CFMakeCollectable(CFStringCreateWithCString(NULL, charPtr, kCFStringEncodingISOLatin1));
98         }
99     } 
100     else {
101         result = [self retain];
102     }
103     
104     return [result autorelease];
105 }
106
107 @end
108
109 @implementation NSData (WebNSDataExtras)
110
111 -(BOOL)_web_isCaseInsensitiveEqualToCString:(const char *)string
112 {
113     ASSERT(string);
114     
115     const char *bytes = [self bytes];
116     return strncasecmp(bytes, string, [self length]) == 0;
117 }
118
119 static const UInt8 *_findEOL(const UInt8 *bytes, CFIndex len) {
120     
121     // According to the HTTP specification EOL is defined as
122     // a CRLF pair.  Unfortunately, some servers will use LF
123     // instead.  Worse yet, some servers will use a combination
124     // of both (e.g. <header>CRLFLF<body>), so findEOL needs
125     // to be more forgiving.  It will now accept CRLF, LF, or
126     // CR.
127     //
128     // It returns NULL if EOL is not found or it will return
129     // a pointer to the first terminating character.
130     CFIndex i;
131     for (i = 0;  i < len; i++)
132     {
133         UInt8 c = bytes[i];
134         if ('\n' == c) return bytes + i;
135         if ('\r' == c)
136         {
137             // Check to see if spanning buffer bounds
138             // (CRLF is across reads).  If so, wait for
139             // next read.
140             if (i + 1 == len) break;
141                 
142             return bytes + i;
143         }
144     }
145     
146     return NULL;
147 }
148
149 -(NSMutableDictionary *)_web_parseRFC822HeaderFields
150 {
151     NSMutableDictionary *headerFields = [NSMutableDictionary dictionary];
152
153     const UInt8 *bytes = [self bytes];
154     unsigned length = [self length];
155     NSString *lastKey = nil;
156     const UInt8 *eol;
157
158     // Loop over lines until we're past the header, or we can't find any more end-of-lines
159     while ((eol = _findEOL(bytes, length))) {
160         const UInt8 *line = bytes;
161         SInt32 lineLength = eol - bytes;
162
163         // Move bytes to the character after the terminator as returned by _findEOL.
164         bytes = eol + 1;
165         if (('\r' == *eol) && ('\n' == *bytes)) {
166             bytes++; // Safe since _findEOL won't return a spanning CRLF.
167         }
168
169         length -= (bytes - line);
170         if (lineLength == 0) {
171             // Blank line; we're at the end of the header
172             break;
173         }
174         else if (*line == ' ' || *line == '\t') {
175             // Continuation of the previous header
176             if (!lastKey) {
177                 // malformed header; ignore it and continue
178                 continue;
179             }
180             else {
181                 // Merge the continuation of the previous header
182                 NSString *currentValue = [headerFields objectForKey:lastKey];
183                 NSString *newValue = (NSString *)CFMakeCollectable(CFStringCreateWithBytes(NULL, line, lineLength, kCFStringEncodingISOLatin1, FALSE));
184                 ASSERT(currentValue);
185                 ASSERT(newValue);
186                 NSString *mergedValue = [[NSString alloc] initWithFormat:@"%@%@", currentValue, newValue];
187                 [headerFields setObject:(NSString *)mergedValue forKey:lastKey];
188                 [newValue release];
189                 [mergedValue release];
190                 // Note: currentValue is autoreleased
191             }
192         }
193         else {
194             // Brand new header
195             const UInt8 *colon;
196             for (colon = line; *colon != ':' && colon != eol; colon ++) {
197                 // empty loop
198             }
199             if (colon == eol) {
200                 // malformed header; ignore it and continue
201                 continue;
202             }
203             else {
204                 lastKey = (NSString *)CFMakeCollectable(CFStringCreateWithBytes(NULL, line, colon - line, kCFStringEncodingISOLatin1, FALSE));
205                 [lastKey autorelease];
206                 NSString *value = [lastKey _web_capitalizeRFC822HeaderFieldName];
207                 lastKey = value;
208                 for (colon++; colon != eol; colon++) {
209                     if (*colon != ' ' && *colon != '\t') {
210                         break;
211                     }
212                 }
213                 if (colon == eol) {
214                     value = [[NSString alloc] initWithString:@""];
215                     [value autorelease];
216                 }
217                 else {
218                     value = (NSString *)CFMakeCollectable(CFStringCreateWithBytes(NULL, colon, eol-colon, kCFStringEncodingISOLatin1, FALSE));
219                     [value autorelease];
220                 }
221                 NSString *oldValue = [headerFields objectForKey:lastKey];
222                 if (oldValue) {
223                     NSString *newValue = [[NSString alloc] initWithFormat:@"%@, %@", oldValue, value];
224                     value = newValue;
225                     [newValue autorelease];
226                 }
227                 [headerFields setObject:(NSString *)value forKey:lastKey];
228             }
229         }
230     }
231
232     return headerFields;
233 }
234
235 -(NSString *)_webkit_guessedMIMETypeForXML
236 {
237     int length = [self length];
238     const UInt8 *bytes = [self bytes];
239
240 #define CHANNEL_TAG_LENGTH 7
241
242     const char *p = (const char *)bytes;
243     int remaining = MIN(length, WEB_GUESS_MIME_TYPE_PEEK_LENGTH) - (CHANNEL_TAG_LENGTH - 1);
244
245     BOOL foundRDF = false;
246
247     while (remaining > 0) {
248         // Look for a "<".
249         const char *hit = memchr(p, '<', remaining);
250         if (!hit) {
251             break;
252         }
253
254         // We are trying to identify RSS or Atom. RSS has a top-level
255         // element of either <rss> or <rdf>. However, there are
256         // non-RSS RDF files, so in the case of <rdf> we further look
257         // for a <channel> element. In the case of an Atom file, a
258         // top-level <feed> element is all we need to see. Only tags
259         // starting with <? or <! can precede the root element. We
260         // bail if we don't find an <rss>, <feed> or <rdf> element
261         // right after those.
262
263         if (foundRDF) {
264             if (strncasecmp(hit, "<channel", strlen("<channel")) == 0) {
265                 return @"application/rss+xml";
266             }
267         } else if (strncasecmp(hit, "<rdf", strlen("<rdf")) == 0) {
268             foundRDF = TRUE;
269         } else if (strncasecmp(hit, "<rss", strlen("<rss")) == 0) {
270             return @"application/rss+xml";
271         } else if (strncasecmp(hit, "<feed", strlen("<feed")) == 0) {
272             return @"application/atom+xml";
273         } else if (strncasecmp(hit, "<?", strlen("<?")) != 0 && strncasecmp(hit, "<!", strlen("<!")) != 0) {
274             return nil;
275         }
276
277         // Skip the "<" and continue.
278         remaining -= (hit + 1) - p;
279         p = hit + 1;
280     }
281
282     return nil;
283 }
284
285 -(NSString *)_webkit_guessedMIMEType
286 {
287 #define JPEG_MAGIC_NUMBER_LENGTH 4
288 #define SCRIPT_TAG_LENGTH 7
289 #define TEXT_HTML_LENGTH 9
290 #define VCARD_HEADER_LENGTH 11
291 #define VCAL_HEADER_LENGTH 15
292
293     NSString *MIMEType = [self _webkit_guessedMIMETypeForXML];
294     if ([MIMEType length] != nil) {
295         return MIMEType;
296     }
297     
298     int length = [self length];
299     const char *bytes = [self bytes];
300
301     const char *p = bytes;
302     int remaining = MIN(length, WEB_GUESS_MIME_TYPE_PEEK_LENGTH) - (SCRIPT_TAG_LENGTH - 1);
303     while (remaining > 0) {
304         // Look for a "<".
305         const char *hit = memchr(p, '<', remaining);
306         if (!hit) {
307             break;
308         }
309
310         // If we found a "<", look for "<html>" or "<a " or "<script".
311         if (strncasecmp(hit, "<html>",  strlen("<html>")) == 0 ||
312             strncasecmp(hit, "<a ",     strlen("<a ")) == 0 ||
313             strncasecmp(hit, "<script", strlen("<script")) == 0 ||
314             strncasecmp(hit, "<title>", strlen("<title>")) == 0) {
315             return @"text/html";
316         }
317
318         // Skip the "<" and continue.
319         remaining -= (hit + 1) - p;
320         p = hit + 1;
321     }
322
323     // Test for a broken server which has sent the content type as part of the content.
324     // This code could be improved to look for other mime types.
325     p = bytes;
326     remaining = MIN(length, WEB_GUESS_MIME_TYPE_PEEK_LENGTH) - (TEXT_HTML_LENGTH - 1);
327     while (remaining > 0) {
328         // Look for a "t" or "T".
329         const char *hit = NULL;
330         const char *lowerhit = memchr(p, 't', remaining);
331         const char *upperhit = memchr(p, 'T', remaining);
332         if (!lowerhit && !upperhit) {
333             break;
334         }
335         if (!lowerhit) {
336             hit = upperhit;
337         }
338         else if (!upperhit) {
339             hit = lowerhit;
340         }
341         else {
342             hit = MIN(lowerhit, upperhit);
343         }
344
345         // If we found a "t/T", look for "text/html".
346         if (strncasecmp(hit, "text/html", TEXT_HTML_LENGTH) == 0) {
347             return @"text/html";
348         }
349
350         // Skip the "t/T" and continue.
351         remaining -= (hit + 1) - p;
352         p = hit + 1;
353     }
354
355     if ((length >= VCARD_HEADER_LENGTH) && strncmp(bytes, "BEGIN:VCARD", VCARD_HEADER_LENGTH) == 0) {
356         return @"text/vcard";
357     }
358     if ((length >= VCAL_HEADER_LENGTH) && strncmp(bytes, "BEGIN:VCALENDAR", VCAL_HEADER_LENGTH) == 0) {
359         return @"text/calendar";
360     }
361     
362     // Test for plain text.
363     int i;
364     for(i=0; i<length; i++){
365         char c = bytes[i];
366         if ((c < 0x20 || c > 0x7E) && (c != '\t' && c != '\r' && c != '\n')) {
367             break;
368         }
369     }
370     if (i == length) {
371         // Didn't encounter any bad characters, looks like plain text.
372         return @"text/plain";
373     }
374
375     // Looks like this is a binary file.
376
377     // Sniff for the JPEG magic number.
378     if ((length >= JPEG_MAGIC_NUMBER_LENGTH) && strncmp(bytes, "\xFF\xD8\xFF\xE0", JPEG_MAGIC_NUMBER_LENGTH) == 0) {
379         return @"image/jpeg";
380     }
381
382 #undef JPEG_MAGIC_NUMBER_LENGTH
383 #undef SCRIPT_TAG_LENGTH
384 #undef TEXT_HTML_LENGTH
385 #undef VCARD_HEADER_LENGTH
386 #undef VCAL_HEADER_LENGTH
387
388     return nil;
389 }
390
391 @end