262 lines
9.6 KiB
Mathematica
262 lines
9.6 KiB
Mathematica
|
//
|
||
|
// MTLModel+NSCoding.m
|
||
|
// Mantle
|
||
|
//
|
||
|
// Created by Justin Spahr-Summers on 2013-02-12.
|
||
|
// Copyright (c) 2013 GitHub. All rights reserved.
|
||
|
//
|
||
|
|
||
|
#import "MTLEXTRuntimeExtensions.h"
|
||
|
#import "MTLEXTScope.h"
|
||
|
#import "MTLModel+NSCoding.h"
|
||
|
#import "MTLReflection.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]) {
|
||
|
IMP imp = [self methodForSelector:selector];
|
||
|
id (*function)(id, SEL, NSCoder *, NSUInteger) = (__typeof__(function))imp;
|
||
|
id result = function(self, selector, coder, modelVersion);
|
||
|
|
||
|
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
|