blob: 852f5aeb880934dfd5105306d4d32214278463aa [file] [log] [blame]
//
// MTLModel+NSCoding.m
// Mantle
//
// Created by Justin Spahr-Summers on 2013-02-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "MTLModel+NSCoding.h"
#import "EXTRuntimeExtensions.h"
#import "EXTScope.h"
#import "MTLReflection.h"
#import <objc/runtime.h>
// Used in archives to store the modelVersion of the archived instance.
static NSString * const MTLModelVersionKey = @"MTLModelVersion";
// Used to cache the reflection performed in +allowedSecureCodingClassesByPropertyKey.
static void *MTLModelCachedAllowedClassesKey = &MTLModelCachedAllowedClassesKey;
// Returns whether the given NSCoder requires secure coding.
static BOOL coderRequiresSecureCoding(NSCoder *coder) {
SEL requiresSecureCodingSelector = @selector(requiresSecureCoding);
// Only invoke the method if it's implemented (i.e., only on OS X 10.8+ and
// iOS 6+).
if (![coder respondsToSelector:requiresSecureCodingSelector]) return NO;
BOOL (*requiresSecureCodingIMP)(NSCoder *, SEL) = (__typeof__(requiresSecureCodingIMP))[coder methodForSelector:requiresSecureCodingSelector];
if (requiresSecureCodingIMP == NULL) return NO;
return requiresSecureCodingIMP(coder, requiresSecureCodingSelector);
}
// Returns all of the given class' encodable property keys (those that will not
// be excluded from archives).
static NSSet *encodablePropertyKeysForClass(Class modelClass) {
return [[modelClass encodingBehaviorsByPropertyKey] keysOfEntriesPassingTest:^ BOOL (NSString *propertyKey, NSNumber *behavior, BOOL *stop) {
return behavior.unsignedIntegerValue != MTLModelEncodingBehaviorExcluded;
}];
}
// Verifies that all of the specified class' encodable property keys are present
// in +allowedSecureCodingClassesByPropertyKey, and throws an exception if not.
static void verifyAllowedClassesByPropertyKey(Class modelClass) {
NSDictionary *allowedClasses = [modelClass allowedSecureCodingClassesByPropertyKey];
NSMutableSet *specifiedPropertyKeys = [[NSMutableSet alloc] initWithArray:allowedClasses.allKeys];
[specifiedPropertyKeys minusSet:encodablePropertyKeysForClass(modelClass)];
if (specifiedPropertyKeys.count > 0) {
[NSException raise:NSInvalidArgumentException format:@"Cannot encode %@ securely, because keys are missing from +allowedSecureCodingClassesByPropertyKey: %@", modelClass, specifiedPropertyKeys];
}
}
@implementation MTLModel (NSCoding)
#pragma mark Versioning
+ (NSUInteger)modelVersion {
return 0;
}
#pragma mark Encoding Behaviors
+ (NSDictionary *)encodingBehaviorsByPropertyKey {
NSSet *propertyKeys = self.propertyKeys;
NSMutableDictionary *behaviors = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
for (NSString *key in propertyKeys) {
objc_property_t property = class_getProperty(self, key.UTF8String);
NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
free(attributes);
};
MTLModelEncodingBehavior behavior = (attributes->weak ? MTLModelEncodingBehaviorConditional : MTLModelEncodingBehaviorUnconditional);
behaviors[key] = @(behavior);
}
return behaviors;
}
+ (NSDictionary *)allowedSecureCodingClassesByPropertyKey {
NSDictionary *cachedClasses = objc_getAssociatedObject(self, MTLModelCachedAllowedClassesKey);
if (cachedClasses != nil) return cachedClasses;
// Get all property keys that could potentially be encoded.
NSSet *propertyKeys = [self.encodingBehaviorsByPropertyKey keysOfEntriesPassingTest:^ BOOL (NSString *propertyKey, NSNumber *behavior, BOOL *stop) {
return behavior.unsignedIntegerValue != MTLModelEncodingBehaviorExcluded;
}];
NSMutableDictionary *allowedClasses = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
for (NSString *key in propertyKeys) {
objc_property_t property = class_getProperty(self, key.UTF8String);
NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
free(attributes);
};
// If the property is not of object or class type, assume that it's
// a primitive which would be boxed into an NSValue.
if (attributes->type[0] != '@' && attributes->type[0] != '#') {
allowedClasses[key] = @[ NSValue.class ];
continue;
}
// Omit this property from the dictionary if its class isn't known.
if (attributes->objectClass != nil) {
allowedClasses[key] = @[ attributes->objectClass ];
}
}
// It doesn't really matter if we replace another thread's work, since we do
// it atomically and the result should be the same.
objc_setAssociatedObject(self, MTLModelCachedAllowedClassesKey, allowedClasses, OBJC_ASSOCIATION_COPY);
return allowedClasses;
}
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion {
NSParameterAssert(key != nil);
NSParameterAssert(coder != nil);
SEL selector = MTLSelectorWithCapitalizedKeyPattern("decode", key, "WithCoder:modelVersion:");
if ([self respondsToSelector:selector]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
invocation.target = self;
invocation.selector = selector;
[invocation setArgument:&coder atIndex:2];
[invocation setArgument:&modelVersion atIndex:3];
[invocation invoke];
__unsafe_unretained id result = nil;
[invocation getReturnValue:&result];
return result;
}
@try {
if (coderRequiresSecureCoding(coder)) {
NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
} else {
return [coder decodeObjectForKey:key];
}
} @catch (NSException *ex) {
NSLog(@"*** Caught exception decoding value for key \"%@\" on class %@: %@", key, self.class, ex);
@throw ex;
}
}
#pragma mark NSCoding
- (instancetype)initWithCoder:(NSCoder *)coder {
BOOL requiresSecureCoding = coderRequiresSecureCoding(coder);
NSNumber *version = nil;
if (requiresSecureCoding) {
version = [coder decodeObjectOfClass:NSNumber.class forKey:MTLModelVersionKey];
} else {
version = [coder decodeObjectForKey:MTLModelVersionKey];
}
if (version == nil) {
NSLog(@"Warning: decoding an archive of %@ without a version, assuming 0", self.class);
} else if (version.unsignedIntegerValue > self.class.modelVersion) {
// Don't try to decode newer versions.
return nil;
}
if (requiresSecureCoding) {
verifyAllowedClassesByPropertyKey(self.class);
} else {
// Handle the old archive format.
NSDictionary *externalRepresentation = [coder decodeObjectForKey:@"externalRepresentation"];
if (externalRepresentation != nil) {
NSAssert([self.class methodForSelector:@selector(dictionaryValueFromArchivedExternalRepresentation:version:)] != [MTLModel methodForSelector:@selector(dictionaryValueFromArchivedExternalRepresentation:version:)], @"Decoded an old archive of %@ that contains an externalRepresentation, but +dictionaryValueFromArchivedExternalRepresentation:version: is not overridden to handle it", self.class);
NSDictionary *dictionaryValue = [self.class dictionaryValueFromArchivedExternalRepresentation:externalRepresentation version:version.unsignedIntegerValue];
if (dictionaryValue == nil) return nil;
NSError *error = nil;
self = [self initWithDictionary:dictionaryValue error:&error];
if (self == nil) NSLog(@"*** Could not decode old %@ archive: %@", self.class, error);
return self;
}
}
NSSet *propertyKeys = self.class.propertyKeys;
NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
for (NSString *key in propertyKeys) {
id value = [self decodeValueForKey:key withCoder:coder modelVersion:version.unsignedIntegerValue];
if (value == nil) continue;
dictionaryValue[key] = value;
}
NSError *error = nil;
self = [self initWithDictionary:dictionaryValue error:&error];
if (self == nil) NSLog(@"*** Could not unarchive %@: %@", self.class, error);
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
if (coderRequiresSecureCoding(coder)) verifyAllowedClassesByPropertyKey(self.class);
[coder encodeObject:@(self.class.modelVersion) forKey:MTLModelVersionKey];
NSDictionary *encodingBehaviors = self.class.encodingBehaviorsByPropertyKey;
[self.dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
@try {
// Skip nil values.
if ([value isEqual:NSNull.null]) return;
switch ([encodingBehaviors[key] unsignedIntegerValue]) {
// This will also match a nil behavior.
case MTLModelEncodingBehaviorExcluded:
break;
case MTLModelEncodingBehaviorUnconditional:
[coder encodeObject:value forKey:key];
break;
case MTLModelEncodingBehaviorConditional:
[coder encodeConditionalObject:value forKey:key];
break;
default:
NSAssert(NO, @"Unrecognized encoding behavior %@ on class %@ for key \"%@\"", self.class, encodingBehaviors[key], key);
}
} @catch (NSException *ex) {
NSLog(@"*** Caught exception encoding value for key \"%@\" on class %@: %@", key, self.class, ex);
@throw ex;
}
}];
}
#pragma mark NSSecureCoding
+ (BOOL)supportsSecureCoding {
// Disable secure coding support by default, so subclasses are forced to
// opt-in by conforming to the protocol and overriding this method.
//
// We only implement this method because XPC complains if a subclass tries
// to implement it but does not override -initWithCoder:. See
// https://github.com/github/Mantle/issues/74.
return NO;
}
@end
@implementation MTLModel (OldArchiveSupport)
+ (NSDictionary *)dictionaryValueFromArchivedExternalRepresentation:(NSDictionary *)externalRepresentation version:(NSUInteger)fromVersion {
return nil;
}
@end