WKRemoteObjectCoder.mm   [plain text]

/*
* Copyright (C) 2013 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 "config.h"
#import "WKRemoteObjectCoder.h"

#if WK_API_ENABLED

#import "APIArray.h"
#import "APIData.h"
#import "APIDictionary.h"
#import "APINumber.h"
#import "APIString.h"
#import "NSInvocationSPI.h"
#import "_WKRemoteObjectInterfaceInternal.h"
#import <objc/runtime.h>
#import <wtf/RetainPtr.h>
#import <wtf/SetForScope.h>
#import <wtf/text/CString.h>

static const char* const classNameKey = "$class";
static const char* const objectStreamKey = "$objectStream";
static const char* const stringKey = "$string";

static NSString * const selectorKey = @"selector";
static NSString * const typeStringKey = @"typeString";
static NSString * const isReplyBlockKey = @"isReplyBlock";

static RefPtr<API::Dictionary> createEncodedObject(WKRemoteObjectEncoder *, id);

@interface NSMethodSignature ()
- (NSString *)_typeString;
@end

@interface NSCoder ()
- (void)validateClassSupportsSecureCoding:(Class)objectClass;
@end

@implementation WKRemoteObjectEncoder {
RefPtr<API::Dictionary> _rootDictionary;
API::Array* _objectStream;

API::Dictionary* _currentDictionary;
}

- (id)init
{
if (!(self = [super init]))
return nil;

_rootDictionary = API::Dictionary::create();
_currentDictionary = _rootDictionary.get();

return self;
}

#if !ASSERT_DISABLED
- (void)dealloc
{
ASSERT(_currentDictionary == _rootDictionary);

[super dealloc];
}
#endif

- (API::Dictionary*)rootObjectDictionary
{
return _rootDictionary.get();
}

static void ensureObjectStream(WKRemoteObjectEncoder *encoder)
{
if (encoder->_objectStream)
return;

Ref<API::Array> objectStream = API::Array::create();
encoder->_objectStream = objectStream.ptr();

encoder->_rootDictionary->set(objectStreamKey, WTFMove(objectStream));
}

static void encodeToObjectStream(WKRemoteObjectEncoder *encoder, id value)
{
ensureObjectStream(encoder);

size_t position = encoder->_objectStream->size();
encoder->_objectStream->elements().append(nullptr);

auto encodedObject = createEncodedObject(encoder, value);
ASSERT(!encoder->_objectStream->elements()[position]);
encoder->_objectStream->elements()[position] = WTFMove(encodedObject);
}

static void encodeInvocationArguments(WKRemoteObjectEncoder *encoder, NSInvocation *invocation, NSUInteger firstArgument)
{
NSMethodSignature *methodSignature = invocation.methodSignature;
NSUInteger argumentCount = methodSignature.numberOfArguments;

ASSERT(firstArgument <= argumentCount);

for (NSUInteger i = firstArgument; i < argumentCount; ++i) {
const char* type = [methodSignature getArgumentTypeAtIndex:i];

switch (*type) {
// double
case 'd': {
double value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// float
case 'f': {
float value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// int
case 'i': {
int value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// unsigned
case 'I': {
unsigned value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// char
case 'c': {
char value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// bool
case 'B': {
BOOL value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// long
case 'q': {
long value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// unsigned long
case 'Q': {
unsigned long value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value));
break;
}

// Objective-C object
case '@': {
id value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, value);
break;
}

// struct
case '{':
if (!strcmp(type, @encode(NSRange))) {
NSRange value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, [NSValue valueWithRange:value]);
break;
} else if (!strcmp(type, @encode(CGSize))) {
CGSize value;
[invocation getArgument:&value atIndex:i];

encodeToObjectStream(encoder, @(value.width));
encodeToObjectStream(encoder, @(value.height));
break;
}
FALLTHROUGH;

default:
[NSException raise:NSInvalidArgumentException format:@"Unsupported invocation argument type '%s'", type];
}
}
}

static void encodeInvocation(WKRemoteObjectEncoder *encoder, NSInvocation *invocation)
{
NSMethodSignature *methodSignature = invocation.methodSignature;
[encoder encodeObject:methodSignature._typeString forKey:typeStringKey];

if ([invocation isKindOfClass:[NSBlockInvocation class]]) {
[encoder encodeBool:YES forKey:isReplyBlockKey];
encodeInvocationArguments(encoder, invocation, 1);
} else {
[encoder encodeObject:NSStringFromSelector(invocation.selector) forKey:selectorKey];
encodeInvocationArguments(encoder, invocation, 2);
}
}

static void encodeString(WKRemoteObjectEncoder *encoder, NSString *string)
{
encoder->_currentDictionary->set(stringKey, API::String::create(string));
}

static void encodeObject(WKRemoteObjectEncoder *encoder, id object)
{
ASSERT(object);

if (![object conformsToProtocol:@protocol(NSSecureCoding)] && ![object isKindOfClass:[NSInvocation class]])
[NSException raise:NSInvalidArgumentException format:@"%@ does not conform to NSSecureCoding", object];

if (class_isMetaClass(object_getClass(object)))
[NSException raise:NSInvalidArgumentException format:@"Class objects may not be encoded"];

Class objectClass = [object classForCoder];
if (!objectClass)
[NSException raise:NSInvalidArgumentException format:@"-classForCoder returned nil for %@", object];

encoder->_currentDictionary->set(classNameKey, API::String::create(class_getName(objectClass)));

if ([object isKindOfClass:[NSInvocation class]]) {
// We have to special case NSInvocation since we don't want to encode the target.
encodeInvocation(encoder, object);
return;
}

if (objectClass == [NSString class] || objectClass == [NSMutableString class]) {
encodeString(encoder, object);
return;
}

[object encodeWithCoder:encoder];
}

static RefPtr<API::Dictionary> createEncodedObject(WKRemoteObjectEncoder *encoder, id object)
{
if (!object)
return nil;

Ref<API::Dictionary> dictionary = API::Dictionary::create();
SetForScope<API::Dictionary*> dictionaryChange(encoder->_currentDictionary, dictionary.ptr());

encodeObject(encoder, object);

return WTFMove(dictionary);
}

- (void)encodeValueOfObjCType:(const char *)type at:(const void *)address
{
switch (*type) {
// int
case 'i':
encodeToObjectStream(self, @(*static_cast<const int*>(address)));
break;

// Objective-C object.
case '@':
encodeToObjectStream(self, *static_cast<const id*>(address));
break;

default:
[NSException raise:NSInvalidArgumentException format:@"Unsupported type '%s'", type];
}
}

- (BOOL)allowsKeyedCoding
{
return YES;
}

static NSString *escapeKey(NSString *key)
{
if (key.length && [key characterAtIndex:0] == '$')
return [@"$" stringByAppendingString:key];

return key;
}

- (void)encodeObject:(id)object forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), createEncodedObject(self, object));
}

- (void)encodeBytes:(const uint8_t *)bytes length:(NSUInteger)length forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), API::Data::create(bytes, length));
}

- (void)encodeBool:(BOOL)value forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), API::Boolean::create(value));
}

- (void)encodeInt:(int)value forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), API::UInt64::create(value));
}

- (void)encodeInt32:(int32_t)value forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), API::UInt64::create(value));
}

- (void)encodeInt64:(int64_t)value forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), API::UInt64::create(value));
}

- (void)encodeInteger:(NSInteger)intv forKey:(NSString *)key
{
return [self encodeInt64:intv forKey:key];
}

- (void)encodeFloat:(float)value forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), API::Double::create(value));
}

- (void)encodeDouble:(double)value forKey:(NSString *)key
{
_currentDictionary->set(escapeKey(key), API::Double::create(value));
}

- (BOOL)requiresSecureCoding
{
return YES;
}

@end

@implementation WKRemoteObjectDecoder {
RetainPtr<_WKRemoteObjectInterface> _interface;

const API::Dictionary* _rootDictionary;
const API::Dictionary* _currentDictionary;

SEL _replyToSelector;

const API::Array* _objectStream;
size_t _objectStreamPosition;

const HashSet<CFTypeRef>* _allowedClasses;
}

- (id)initWithInterface:(_WKRemoteObjectInterface *)interface rootObjectDictionary:(const API::Dictionary*)rootObjectDictionary replyToSelector:(SEL)replyToSelector
{
if (!(self = [super init]))
return nil;

_interface = interface;

_rootDictionary = rootObjectDictionary;
_currentDictionary = _rootDictionary;

_replyToSelector = replyToSelector;

_objectStream = _rootDictionary->get<API::Array>(objectStreamKey);

return self;
}

- (void)decodeValueOfObjCType:(const char *)type at:(void *)data
{
switch (*type) {
// int
case 'i':
*static_cast<int*>(data) = [decodeObjectFromObjectStream(self, { (__bridge CFTypeRef)[NSNumber class] }) intValue];
break;

default:
[NSException raise:NSInvalidUnarchiveOperationException format:@"Unsupported type '%s'", type];
}
}

- (BOOL)allowsKeyedCoding
{
return YES;
}

- (BOOL)containsValueForKey:(NSString *)key
{
return _currentDictionary->map().contains(escapeKey(key));
}

- (id)decodeObjectForKey:(NSString *)key
{
return [self decodeObjectOfClasses:nil forKey:key];
}

static id decodeObject(WKRemoteObjectDecoder *, const API::Dictionary*, const HashSet<CFTypeRef>& allowedClasses);

static id decodeObjectFromObjectStream(WKRemoteObjectDecoder *decoder, const HashSet<CFTypeRef>& allowedClasses)
{
if (!decoder->_objectStream)
return nil;

if (decoder->_objectStreamPosition == decoder->_objectStream->size())
return nil;

const API::Dictionary* dictionary = decoder->_objectStream->at<API::Dictionary>(decoder->_objectStreamPosition++);

return decodeObject(decoder, dictionary, allowedClasses);
}

static void checkIfClassIsAllowed(WKRemoteObjectDecoder *decoder, Class objectClass)
{
auto* allowedClasses = decoder->_allowedClasses;
if (!allowedClasses)
return;

if (allowedClasses->contains((__bridge CFTypeRef)objectClass))
return;

for (Class superclass = class_getSuperclass(objectClass); superclass; superclass = class_getSuperclass(superclass)) {
if (allowedClasses->contains((__bridge CFTypeRef)superclass))
return;
}

[NSException raise:NSInvalidUnarchiveOperationException format:@"Object of class \"%@\" is not allowed. Allowed classes are \"%@\"", objectClass, decoder.allowedClasses];
}

static void validateClass(WKRemoteObjectDecoder *decoder, Class objectClass)
{
ASSERT(objectClass);

checkIfClassIsAllowed(decoder, objectClass);

// NSInvocation and NSBlockInvocation don't support NSSecureCoding, but we allow them anyway.
if (objectClass == [NSInvocation class] || objectClass == [NSBlockInvocation class])
return;

[decoder validateClassSupportsSecureCoding:objectClass];
}

static void decodeInvocationArguments(WKRemoteObjectDecoder *decoder, NSInvocation *invocation, const Vector<HashSet<CFTypeRef>>& allowedArgumentClasses, NSUInteger firstArgument)
{
NSMethodSignature *methodSignature = invocation.methodSignature;
NSUInteger argumentCount = methodSignature.numberOfArguments;

ASSERT(firstArgument <= argumentCount);

for (NSUInteger i = firstArgument; i < argumentCount; ++i) {
const char* type = [methodSignature getArgumentTypeAtIndex:i];

switch (*type) {
// double
case 'd': {
double value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) doubleValue];
[invocation setArgument:&value atIndex:i];
break;
}

// float
case 'f': {
float value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) floatValue];
[invocation setArgument:&value atIndex:i];
break;
}

// int
case 'i': {
int value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) intValue];
[invocation setArgument:&value atIndex:i];
break;
}

// unsigned
case 'I': {
unsigned value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) unsignedIntValue];
[invocation setArgument:&value atIndex:i];
break;
}

// char
case 'c': {
char value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) charValue];
[invocation setArgument:&value atIndex:i];
break;
}

// bool
case 'B': {
bool value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) boolValue];
[invocation setArgument:&value atIndex:i];
break;
}

// long
case 'q': {
long value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) longValue];
[invocation setArgument:&value atIndex:i];
break;
}

// unsigned long
case 'Q': {
unsigned long value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) unsignedLongValue];
[invocation setArgument:&value atIndex:i];
break;
}

// Objective-C object
case '@': {
auto& allowedClasses = allowedArgumentClasses[i - firstArgument];

id value = decodeObjectFromObjectStream(decoder, allowedClasses);
[invocation setArgument:&value atIndex:i];

// FIXME: Make sure the invocation doesn't outlive the value.
break;
}

// struct
case '{':
if (!strcmp(type, @encode(NSRange))) {
NSRange value = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSValue class] }) rangeValue];
[invocation setArgument:&value atIndex:i];
break;
} else if (!strcmp(type, @encode(CGSize))) {
CGSize value;
value.width = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) doubleValue];
value.height = [decodeObjectFromObjectStream(decoder, { (__bridge CFTypeRef)[NSNumber class] }) doubleValue];
[invocation setArgument:&value atIndex:i];
break;
}
FALLTHROUGH;

default:
[NSException raise:NSInvalidArgumentException format:@"Unsupported invocation argument type '%s' for argument %zu", type, (unsigned long)i];
}
}
}

static NSInvocation *decodeInvocation(WKRemoteObjectDecoder *decoder)
{
SEL selector = nullptr;
NSMethodSignature *localMethodSignature = nil;

BOOL isReplyBlock = [decoder decodeBoolForKey:isReplyBlockKey];

if (isReplyBlock) {
if (!decoder->_replyToSelector)
[NSException raise:NSInvalidUnarchiveOperationException format:@"%@: Received unknown reply block", decoder];

localMethodSignature = [decoder->_interface _methodSignatureForReplyBlockOfSelector:decoder->_replyToSelector];
if (!localMethodSignature)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Reply block for selector \"%s\" is not defined in the local interface", sel_getName(decoder->_replyToSelector)];
} else {
NSString *selectorString = [decoder decodeObjectOfClass:[NSString class] forKey:selectorKey];
if (!selectorString)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Invocation had no selector"];

selector = NSSelectorFromString(selectorString);
ASSERT(selector);

localMethodSignature = [decoder->_interface _methodSignatureForSelector:selector];
if (!localMethodSignature)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Selector \"%@\" is not defined in the local interface", selectorString];
}

NSString *typeSignature = [decoder decodeObjectOfClass:[NSString class] forKey:typeStringKey];
if (!typeSignature)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Invocation had no type signature"];

NSMethodSignature *remoteMethodSignature = [NSMethodSignature signatureWithObjCTypes:typeSignature.UTF8String];
localMethodSignature = remoteMethodSignature;
if (![localMethodSignature isEqual:remoteMethodSignature])
[NSException raise:NSInvalidUnarchiveOperationException format:@"Local and remote method signatures are not equal for method \"%s\"", selector ? sel_getName(selector) : "(no selector)"];

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:localMethodSignature];

if (isReplyBlock) {
const auto& allowedClasses = [decoder->_interface _allowedArgumentClassesForReplyBlockOfSelector:decoder->_replyToSelector];
decodeInvocationArguments(decoder, invocation, allowedClasses, 1);
} else {
const auto& allowedClasses = [decoder->_interface _allowedArgumentClassesForSelector:selector];
decodeInvocationArguments(decoder, invocation, allowedClasses, 2);

[invocation setArgument:&selector atIndex:1];
}

return invocation;
}

static NSString *decodeString(WKRemoteObjectDecoder *decoder)
{
API::String* string = decoder->_currentDictionary->get<API::String>(stringKey);
if (!string)
[NSException raise:NSInvalidUnarchiveOperationException format:@"String missing"];

return string->string();
}

static id decodeObject(WKRemoteObjectDecoder *decoder)
{
API::String* classNameString = decoder->_currentDictionary->get<API::String>(classNameKey);
if (!classNameString)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Class name missing"];

CString className = classNameString->string().utf8();

Class objectClass = objc_lookUpClass(className.data());
if (!objectClass)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Class \"%s\" does not exist", className.data()];

validateClass(decoder, objectClass);

if (objectClass == [NSInvocation class] || objectClass == [NSBlockInvocation class])
return decodeInvocation(decoder);

if (objectClass == [NSString class])
return decodeString(decoder);

if (objectClass == [NSMutableString class])
return [NSMutableString stringWithString:decodeString(decoder)];

id result = [objectClass allocWithZone:decoder.zone];
if (!result)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Class \"%s\" returned nil from +alloc while being decoded", className.data()];

result = [result initWithCoder:decoder];
if (!result)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Object of class \"%s\" returned nil from -initWithCoder: while being decoded", className.data()];

result = [result awakeAfterUsingCoder:decoder];
if (!result)
[NSException raise:NSInvalidUnarchiveOperationException format:@"Object of class \"%s\" returned nil from -awakeAfterUsingCoder: while being decoded", className.data()];

return [result autorelease];
}

static id decodeObject(WKRemoteObjectDecoder *decoder, const API::Dictionary* dictionary, const HashSet<CFTypeRef>& allowedClasses)
{
if (!dictionary)
return nil;

SetForScope<const API::Dictionary*> dictionaryChange(decoder->_currentDictionary, dictionary);

// If no allowed classes were listed, just use the currently allowed classes.
if (allowedClasses.isEmpty())
return decodeObject(decoder);

SetForScope<const HashSet<CFTypeRef>*> allowedClassesChange(decoder->_allowedClasses, &allowedClasses);
return decodeObject(decoder);
}

- (BOOL)decodeBoolForKey:(NSString *)key
{
const API::Boolean* value = _currentDictionary->get<API::Boolean>(escapeKey(key));
if (!value)
return false;
return value->value();
}

- (int)decodeIntForKey:(NSString *)key
{
const API::UInt64* value = _currentDictionary->get<API::UInt64>(escapeKey(key));
if (!value)
return 0;
return static_cast<int>(value->value());
}

- (int32_t)decodeInt32ForKey:(NSString *)key
{
const API::UInt64* value = _currentDictionary->get<API::UInt64>(escapeKey(key));
if (!value)
return 0;
return static_cast<int32_t>(value->value());
}

- (int64_t)decodeInt64ForKey:(NSString *)key
{
const API::UInt64* value = _currentDictionary->get<API::UInt64>(escapeKey(key));
if (!value)
return 0;
return value->value();
}

- (NSInteger)decodeIntegerForKey:(NSString *)key
{
return [self decodeInt64ForKey:key];
}

- (float)decodeFloatForKey:(NSString *)key
{
const API::Double* value = _currentDictionary->get<API::Double>(escapeKey(key));
if (!value)
return 0;
return value->value();
}

- (double)decodeDoubleForKey:(NSString *)key
{
const API::Double* value = _currentDictionary->get<API::Double>(escapeKey(key));
if (!value)
return 0;
return value->value();
}

- (const uint8_t *)decodeBytesForKey:(NSString *)key returnedLength:(NSUInteger *)length
{
auto* data = _currentDictionary->get<API::Data>(escapeKey(key));
if (!data || !data->size()) {
*length = 0;
return nullptr;
}

*length = data->size();
return data->bytes();
}

- (BOOL)requiresSecureCoding
{
return YES;
}

- (id)decodeObjectOfClasses:(NSSet *)classes forKey:(NSString *)key
{
HashSet<CFTypeRef> allowedClasses;
for (Class allowedClass in classes)
allowedClasses.add((__bridge CFTypeRef)allowedClass);

return decodeObject(self, _currentDictionary->get<API::Dictionary>(escapeKey(key)), allowedClasses);
}

- (NSSet *)allowedClasses
{
if (!_allowedClasses)
return [NSSet set];

auto result = adoptNS([[NSMutableSet alloc] init]);
for (auto allowedClass : *_allowedClasses)
[result addObject:(__bridge Class)allowedClass];

return result.autorelease();
}

@end

#endif // WK_API_ENABLED