123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- //
- // 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"
- // 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
|