WebNSDataExtras.mm   [plain text]


/*
 * Copyright (C) 2005-2018 Apple Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#import <WebKitLegacy/WebNSDataExtras.h>
#import <WebKitLegacy/WebNSDataExtrasPrivate.h>

#import <wtf/Assertions.h>

@interface NSString (WebNSDataExtrasInternal)
- (NSString *)_web_capitalizeRFC822HeaderFieldName;
@end

@implementation NSString (WebNSDataExtrasInternal)

- (NSString *)_web_capitalizeRFC822HeaderFieldName
{
    CFStringRef name = (__bridge CFStringRef)self;
    NSString *result = nil;

    CFIndex len = CFStringGetLength(name);
    char* charPtr = nullptr;
    UniChar* uniCharPtr = nullptr;
    Boolean useUniCharPtr = FALSE;
    Boolean shouldCapitalize = TRUE;
    Boolean somethingChanged = FALSE;

    for (CFIndex i = 0; i < len; i ++) {
        UniChar ch = CFStringGetCharacterAtIndex(name, i);
        Boolean replace = FALSE;
        if (shouldCapitalize && ch >= 'a' && ch <= 'z') {
            ch = ch + 'A' - 'a';
            replace = TRUE;
        } else if (!shouldCapitalize && ch >= 'A' && ch <= 'Z') {
            ch = ch + 'a' - 'A';
            replace = TRUE;
        }
        if (replace) {
            if (!somethingChanged) {
                somethingChanged = TRUE;
                if (CFStringGetBytes(name, CFRangeMake(0, len), kCFStringEncodingISOLatin1, 0, FALSE, NULL, 0, NULL) == len) {
                    // Can be encoded in ISOLatin1
                    useUniCharPtr = FALSE;
                    charPtr = static_cast<char*>(CFAllocatorAllocate(kCFAllocatorDefault, len + 1, 0));
                    CFStringGetCString(name, charPtr, len+1, kCFStringEncodingISOLatin1);
                } else {
                    useUniCharPtr = TRUE;
                    uniCharPtr = static_cast<UniChar*>(CFAllocatorAllocate(kCFAllocatorDefault, len * sizeof(UniChar), 0));
                    CFStringGetCharacters(name, CFRangeMake(0, len), uniCharPtr);
                }
            }
            if (useUniCharPtr)
                uniCharPtr[i] = ch;
            else
                charPtr[i] = ch;
        }
        if (ch == '-')
            shouldCapitalize = TRUE;
        else
            shouldCapitalize = FALSE;
    }
    if (somethingChanged) {
        if (useUniCharPtr)
            result = CFBridgingRelease(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, uniCharPtr, len, nullptr));
        else
            result = CFBridgingRelease(CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, charPtr, kCFStringEncodingISOLatin1, nullptr));
    } else
        result = self;

    return result;
}

@end

@implementation NSData (WebKitExtras)

- (NSString *)_webkit_guessedMIMETypeForXML
{
    NSUInteger length = [self length];
    const UInt8* bytes = static_cast<const UInt8*>([self bytes]);

#define CHANNEL_TAG_LENGTH 7

    const char* p = reinterpret_cast<const char*>(bytes);
    int remaining = std::min<NSUInteger>(length, WEB_GUESS_MIME_TYPE_PEEK_LENGTH) - (CHANNEL_TAG_LENGTH - 1);

    BOOL foundRDF = false;

    while (remaining > 0) {
        // Look for a "<".
        const char* hit = static_cast<const char*>(memchr(p, '<', remaining));
        if (!hit)
            break;

        // We are trying to identify RSS or Atom. RSS has a top-level
        // element of either <rss> or <rdf>. However, there are
        // non-RSS RDF files, so in the case of <rdf> we further look
        // for a <channel> element. In the case of an Atom file, a
        // top-level <feed> element is all we need to see. Only tags
        // starting with <? or <! can precede the root element. We
        // bail if we don't find an <rss>, <feed> or <rdf> element
        // right after those.

        if (foundRDF) {
            if (!strncasecmp(hit, "<channel", strlen("<channel")))
                return @"application/rss+xml";
        } else if (!strncasecmp(hit, "<rdf", strlen("<rdf")))
            foundRDF = TRUE;
        else if (!strncasecmp(hit, "<rss", strlen("<rss")))
            return @"application/rss+xml";
        else if (!strncasecmp(hit, "<feed", strlen("<feed")))
            return @"application/atom+xml";
        else if (strncasecmp(hit, "<?", strlen("<?")) && strncasecmp(hit, "<!", strlen("<!")))
            return nil;

        // Skip the "<" and continue.
        remaining -= (hit + 1) - p;
        p = hit + 1;
    }

    return nil;
}

- (NSString *)_webkit_guessedMIMEType
{
#define JPEG_MAGIC_NUMBER_LENGTH 4
#define SCRIPT_TAG_LENGTH 7
#define TEXT_HTML_LENGTH 9
#define VCARD_HEADER_LENGTH 11
#define VCAL_HEADER_LENGTH 15

    NSString *MIMEType = [self _webkit_guessedMIMETypeForXML];
    if ([MIMEType length])
        return MIMEType;

    NSUInteger length = [self length];
    const char* bytes = static_cast<const char*>([self bytes]);

    const char* p = bytes;
    int remaining = std::min<NSUInteger>(length, WEB_GUESS_MIME_TYPE_PEEK_LENGTH) - (SCRIPT_TAG_LENGTH - 1);
    while (remaining > 0) {
        // Look for a "<".
        const char* hit = static_cast<const char*>(memchr(p, '<', remaining));
        if (!hit)
            break;

        // If we found a "<", look for "<html>" or "<a " or "<script".
        if (!strncasecmp(hit, "<html>", strlen("<html>"))
            || !strncasecmp(hit, "<a ", strlen("<a "))
            || !strncasecmp(hit, "<script", strlen("<script"))
            || !strncasecmp(hit, "<title>", strlen("<title>"))) {
            return @"text/html";
        }

        // Skip the "<" and continue.
        remaining -= (hit + 1) - p;
        p = hit + 1;
    }

    // Test for a broken server which has sent the content type as part of the content.
    // This code could be improved to look for other mime types.
    p = bytes;
    remaining = std::min<NSUInteger>(length, WEB_GUESS_MIME_TYPE_PEEK_LENGTH) - (TEXT_HTML_LENGTH - 1);
    while (remaining > 0) {
        // Look for a "t" or "T".
        const char* hit = nullptr;
        const char* lowerhit = static_cast<const char*>(memchr(p, 't', remaining));
        const char* upperhit = static_cast<const char*>(memchr(p, 'T', remaining));
        if (!lowerhit && !upperhit)
            break;

        if (!lowerhit)
            hit = upperhit;
        else if (!upperhit)
            hit = lowerhit;
        else
            hit = std::min<const char*>(lowerhit, upperhit);

        // If we found a "t/T", look for "text/html".
        if (!strncasecmp(hit, "text/html", TEXT_HTML_LENGTH))
            return @"text/html";

        // Skip the "t/T" and continue.
        remaining -= (hit + 1) - p;
        p = hit + 1;
    }

    if ((length >= VCARD_HEADER_LENGTH) && !strncmp(bytes, "BEGIN:VCARD", VCARD_HEADER_LENGTH))
        return @"text/vcard";
    if ((length >= VCAL_HEADER_LENGTH) && !strncmp(bytes, "BEGIN:VCALENDAR", VCAL_HEADER_LENGTH))
        return @"text/calendar";

    // Test for plain text.
    NSUInteger i;
    for (i = 0; i < length; ++i) {
        char c = bytes[i];
        if ((c < 0x20 || c > 0x7E) && (c != '\t' && c != '\r' && c != '\n'))
            break;
    }
    if (i == length) {
        // Didn't encounter any bad characters, looks like plain text.
        return @"text/plain";
    }

    // Looks like this is a binary file.

    // Sniff for the JPEG magic number.
    if ((length >= JPEG_MAGIC_NUMBER_LENGTH) && !strncmp(bytes, "\xFF\xD8\xFF\xE0", JPEG_MAGIC_NUMBER_LENGTH))
        return @"image/jpeg";

#undef JPEG_MAGIC_NUMBER_LENGTH
#undef SCRIPT_TAG_LENGTH
#undef TEXT_HTML_LENGTH
#undef VCARD_HEADER_LENGTH
#undef VCAL_HEADER_LENGTH

    return nil;
}

@end

@implementation NSData (WebNSDataExtras)

- (BOOL)_web_isCaseInsensitiveEqualToCString:(const char *)string
{
    ASSERT(string);

    const char* bytes = static_cast<const char*>([self bytes]);
    return !strncasecmp(bytes, string, [self length]);
}

static const UInt8 *_findEOL(const UInt8 *bytes, CFIndex len)
{
    // According to the HTTP specification EOL is defined as
    // a CRLF pair. Unfortunately, some servers will use LF
    // instead. Worse yet, some servers will use a combination
    // of both (e.g. <header>CRLFLF<body>), so findEOL needs
    // to be more forgiving. It will now accept CRLF, LF, or
    // CR.
    //
    // It returns NULL if EOL is not found or it will return
    // a pointer to the first terminating character.
    for (CFIndex i = 0;  i < len; i++) {
        UInt8 c = bytes[i];
        if ('\n' == c)
            return bytes + i;
        if ('\r' == c) {
            // Check to see if spanning buffer bounds
            // (CRLF is across reads). If so, wait for
            // next read.
            if (i + 1 == len)
                break;

            return bytes + i;
        }
    }

    return nullptr;
}

- (NSMutableDictionary *)_webkit_parseRFC822HeaderFields
{
    NSMutableDictionary *headerFields = [NSMutableDictionary dictionary];

    const UInt8* bytes = static_cast<const UInt8*>([self bytes]);
    NSUInteger length = [self length];
    NSString *lastKey = nil;
    const UInt8 *eol;

    // Loop over lines until we're past the header, or we can't find any more end-of-lines
    while ((eol = _findEOL(bytes, length))) {
        const UInt8 *line = bytes;
        SInt32 lineLength = eol - bytes;

        // Move bytes to the character after the terminator as returned by _findEOL.
        bytes = eol + 1;
        if (('\r' == *eol) && ('\n' == *bytes))
            bytes++; // Safe since _findEOL won't return a spanning CRLF.

        length -= (bytes - line);
        if (!lineLength) {
            // Blank line; we're at the end of the header
            break;
        }
        if (*line == ' ' || *line == '\t') {
            // Continuation of the previous header
            if (!lastKey) {
                // malformed header; ignore it and continue
                continue;
            }
            // Merge the continuation of the previous header
            NSString *currentValue = [headerFields objectForKey:lastKey];
            NSString *newValue = [[NSString alloc] initWithBytes:line length:lineLength encoding:NSISOLatin1StringEncoding];
            ASSERT(currentValue);
            ASSERT(newValue);
            [headerFields setObject:[currentValue stringByAppendingString:newValue] forKey:lastKey];
            [newValue release];
        } else {
            // Brand new header
            const UInt8* colon;
            for (colon = line; *colon != ':' && colon != eol; colon++) { }
            if (colon == eol) {
                // malformed header; ignore it and continue
                continue;
            }
            lastKey = [[NSString alloc] initWithBytes:line length:colon - line encoding:NSISOLatin1StringEncoding];
            [lastKey autorelease];
            lastKey = [lastKey _web_capitalizeRFC822HeaderFieldName];
            for (colon++; colon != eol; colon++) {
                if (*colon != ' ' && *colon != '\t')
                    break;
            }
            NSString *value = [[NSString alloc] initWithBytes:colon length:eol - colon encoding:NSISOLatin1StringEncoding];
            if (NSString *oldValue = [headerFields objectForKey:lastKey]) {
                [value autorelease];
                value = [[NSString alloc] initWithFormat:@"%@, %@", oldValue, value];
            }
            [headerFields setObject:value forKey:lastKey];
            [value release];
        }
    }

    return headerFields;
}

- (BOOL)_web_startsWithBlankLine
{
    return [self length] > 0 && ((const char *)[self bytes])[0] == '\n';
}

- (NSInteger)_web_locationAfterFirstBlankLine
{
    const char *bytes = (const char *)[self bytes];
    NSUInteger length = [self length];

    unsigned i;
    for (i = 0; i < length - 4; i++) {

        //  Support for Acrobat. It sends "\n\n".
        if (bytes[i] == '\n' && bytes[i+1] == '\n')
            return i+2;

        // Returns the position after 2 CRLF's or 1 CRLF if it is the first line.
        if (bytes[i] == '\r' && bytes[i+1] == '\n') {
            i += 2;
            if (i == 2)
                return i;
            if (bytes[i] == '\n') {
                // Support for Director. It sends "\r\n\n" (3880387).
                return i+1;
            }
            if (bytes[i] == '\r' && bytes[i+1] == '\n') {
                // Support for Flash. It sends "\r\n\r\n" (3758113).
                return i+2;
            }
        }
    }
    return NSNotFound;
}

@end