MTLModel+NSCoding.m 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. //
  2. // MTLModel+NSCoding.m
  3. // Mantle
  4. //
  5. // Created by Justin Spahr-Summers on 2013-02-12.
  6. // Copyright (c) 2013 GitHub. All rights reserved.
  7. //
  8. #import "MTLModel+NSCoding.h"
  9. #import "EXTRuntimeExtensions.h"
  10. #import "EXTScope.h"
  11. #import "MTLReflection.h"
  12. // Used in archives to store the modelVersion of the archived instance.
  13. static NSString * const MTLModelVersionKey = @"MTLModelVersion";
  14. // Used to cache the reflection performed in +allowedSecureCodingClassesByPropertyKey.
  15. static void *MTLModelCachedAllowedClassesKey = &MTLModelCachedAllowedClassesKey;
  16. // Returns whether the given NSCoder requires secure coding.
  17. static BOOL coderRequiresSecureCoding(NSCoder *coder) {
  18. SEL requiresSecureCodingSelector = @selector(requiresSecureCoding);
  19. // Only invoke the method if it's implemented (i.e., only on OS X 10.8+ and
  20. // iOS 6+).
  21. if (![coder respondsToSelector:requiresSecureCodingSelector]) return NO;
  22. BOOL (*requiresSecureCodingIMP)(NSCoder *, SEL) = (__typeof__(requiresSecureCodingIMP))[coder methodForSelector:requiresSecureCodingSelector];
  23. if (requiresSecureCodingIMP == NULL) return NO;
  24. return requiresSecureCodingIMP(coder, requiresSecureCodingSelector);
  25. }
  26. // Returns all of the given class' encodable property keys (those that will not
  27. // be excluded from archives).
  28. static NSSet *encodablePropertyKeysForClass(Class modelClass) {
  29. return [[modelClass encodingBehaviorsByPropertyKey] keysOfEntriesPassingTest:^ BOOL (NSString *propertyKey, NSNumber *behavior, BOOL *stop) {
  30. return behavior.unsignedIntegerValue != MTLModelEncodingBehaviorExcluded;
  31. }];
  32. }
  33. // Verifies that all of the specified class' encodable property keys are present
  34. // in +allowedSecureCodingClassesByPropertyKey, and throws an exception if not.
  35. static void verifyAllowedClassesByPropertyKey(Class modelClass) {
  36. NSDictionary *allowedClasses = [modelClass allowedSecureCodingClassesByPropertyKey];
  37. NSMutableSet *specifiedPropertyKeys = [[NSMutableSet alloc] initWithArray:allowedClasses.allKeys];
  38. [specifiedPropertyKeys minusSet:encodablePropertyKeysForClass(modelClass)];
  39. if (specifiedPropertyKeys.count > 0) {
  40. [NSException raise:NSInvalidArgumentException format:@"Cannot encode %@ securely, because keys are missing from +allowedSecureCodingClassesByPropertyKey: %@", modelClass, specifiedPropertyKeys];
  41. }
  42. }
  43. @implementation MTLModel (NSCoding)
  44. #pragma mark Versioning
  45. + (NSUInteger)modelVersion {
  46. return 0;
  47. }
  48. #pragma mark Encoding Behaviors
  49. + (NSDictionary *)encodingBehaviorsByPropertyKey {
  50. NSSet *propertyKeys = self.propertyKeys;
  51. NSMutableDictionary *behaviors = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
  52. for (NSString *key in propertyKeys) {
  53. objc_property_t property = class_getProperty(self, key.UTF8String);
  54. NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
  55. mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
  56. @onExit {
  57. free(attributes);
  58. };
  59. MTLModelEncodingBehavior behavior = (attributes->weak ? MTLModelEncodingBehaviorConditional : MTLModelEncodingBehaviorUnconditional);
  60. behaviors[key] = @(behavior);
  61. }
  62. return behaviors;
  63. }
  64. + (NSDictionary *)allowedSecureCodingClassesByPropertyKey {
  65. NSDictionary *cachedClasses = objc_getAssociatedObject(self, MTLModelCachedAllowedClassesKey);
  66. if (cachedClasses != nil) return cachedClasses;
  67. // Get all property keys that could potentially be encoded.
  68. NSSet *propertyKeys = [self.encodingBehaviorsByPropertyKey keysOfEntriesPassingTest:^ BOOL (NSString *propertyKey, NSNumber *behavior, BOOL *stop) {
  69. return behavior.unsignedIntegerValue != MTLModelEncodingBehaviorExcluded;
  70. }];
  71. NSMutableDictionary *allowedClasses = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
  72. for (NSString *key in propertyKeys) {
  73. objc_property_t property = class_getProperty(self, key.UTF8String);
  74. NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
  75. mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
  76. @onExit {
  77. free(attributes);
  78. };
  79. // If the property is not of object or class type, assume that it's
  80. // a primitive which would be boxed into an NSValue.
  81. if (attributes->type[0] != '@' && attributes->type[0] != '#') {
  82. allowedClasses[key] = @[ NSValue.class ];
  83. continue;
  84. }
  85. // Omit this property from the dictionary if its class isn't known.
  86. if (attributes->objectClass != nil) {
  87. allowedClasses[key] = @[ attributes->objectClass ];
  88. }
  89. }
  90. // It doesn't really matter if we replace another thread's work, since we do
  91. // it atomically and the result should be the same.
  92. objc_setAssociatedObject(self, MTLModelCachedAllowedClassesKey, allowedClasses, OBJC_ASSOCIATION_COPY);
  93. return allowedClasses;
  94. }
  95. - (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion {
  96. NSParameterAssert(key != nil);
  97. NSParameterAssert(coder != nil);
  98. SEL selector = MTLSelectorWithCapitalizedKeyPattern("decode", key, "WithCoder:modelVersion:");
  99. if ([self respondsToSelector:selector]) {
  100. IMP imp = [self methodForSelector:selector];
  101. id (*function)(id, SEL, NSCoder *, NSUInteger) = (__typeof__(function))imp;
  102. id result = function(self, selector, coder, modelVersion);
  103. return result;
  104. }
  105. @try {
  106. if (coderRequiresSecureCoding(coder)) {
  107. NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
  108. NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
  109. return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
  110. } else {
  111. return [coder decodeObjectForKey:key];
  112. }
  113. } @catch (NSException *ex) {
  114. NSLog(@"*** Caught exception decoding value for key \"%@\" on class %@: %@", key, self.class, ex);
  115. @throw ex;
  116. }
  117. }
  118. #pragma mark NSCoding
  119. - (instancetype)initWithCoder:(NSCoder *)coder {
  120. BOOL requiresSecureCoding = coderRequiresSecureCoding(coder);
  121. NSNumber *version = nil;
  122. if (requiresSecureCoding) {
  123. version = [coder decodeObjectOfClass:NSNumber.class forKey:MTLModelVersionKey];
  124. } else {
  125. version = [coder decodeObjectForKey:MTLModelVersionKey];
  126. }
  127. if (version == nil) {
  128. NSLog(@"Warning: decoding an archive of %@ without a version, assuming 0", self.class);
  129. } else if (version.unsignedIntegerValue > self.class.modelVersion) {
  130. // Don't try to decode newer versions.
  131. return nil;
  132. }
  133. if (requiresSecureCoding) {
  134. verifyAllowedClassesByPropertyKey(self.class);
  135. } else {
  136. // Handle the old archive format.
  137. NSDictionary *externalRepresentation = [coder decodeObjectForKey:@"externalRepresentation"];
  138. if (externalRepresentation != nil) {
  139. 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);
  140. NSDictionary *dictionaryValue = [self.class dictionaryValueFromArchivedExternalRepresentation:externalRepresentation version:version.unsignedIntegerValue];
  141. if (dictionaryValue == nil) return nil;
  142. NSError *error = nil;
  143. self = [self initWithDictionary:dictionaryValue error:&error];
  144. if (self == nil) NSLog(@"*** Could not decode old %@ archive: %@", self.class, error);
  145. return self;
  146. }
  147. }
  148. NSSet *propertyKeys = self.class.propertyKeys;
  149. NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
  150. for (NSString *key in propertyKeys) {
  151. id value = [self decodeValueForKey:key withCoder:coder modelVersion:version.unsignedIntegerValue];
  152. if (value == nil) continue;
  153. dictionaryValue[key] = value;
  154. }
  155. NSError *error = nil;
  156. self = [self initWithDictionary:dictionaryValue error:&error];
  157. if (self == nil) NSLog(@"*** Could not unarchive %@: %@", self.class, error);
  158. return self;
  159. }
  160. - (void)encodeWithCoder:(NSCoder *)coder {
  161. if (coderRequiresSecureCoding(coder)) verifyAllowedClassesByPropertyKey(self.class);
  162. [coder encodeObject:@(self.class.modelVersion) forKey:MTLModelVersionKey];
  163. NSDictionary *encodingBehaviors = self.class.encodingBehaviorsByPropertyKey;
  164. [self.dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
  165. @try {
  166. // Skip nil values.
  167. if ([value isEqual:NSNull.null]) return;
  168. switch ([encodingBehaviors[key] unsignedIntegerValue]) {
  169. // This will also match a nil behavior.
  170. case MTLModelEncodingBehaviorExcluded:
  171. break;
  172. case MTLModelEncodingBehaviorUnconditional:
  173. [coder encodeObject:value forKey:key];
  174. break;
  175. case MTLModelEncodingBehaviorConditional:
  176. [coder encodeConditionalObject:value forKey:key];
  177. break;
  178. default:
  179. NSAssert(NO, @"Unrecognized encoding behavior %@ on class %@ for key \"%@\"", self.class, encodingBehaviors[key], key);
  180. }
  181. } @catch (NSException *ex) {
  182. NSLog(@"*** Caught exception encoding value for key \"%@\" on class %@: %@", key, self.class, ex);
  183. @throw ex;
  184. }
  185. }];
  186. }
  187. #pragma mark NSSecureCoding
  188. + (BOOL)supportsSecureCoding {
  189. // Disable secure coding support by default, so subclasses are forced to
  190. // opt-in by conforming to the protocol and overriding this method.
  191. //
  192. // We only implement this method because XPC complains if a subclass tries
  193. // to implement it but does not override -initWithCoder:. See
  194. // https://github.com/github/Mantle/issues/74.
  195. return NO;
  196. }
  197. @end
  198. @implementation MTLModel (OldArchiveSupport)
  199. + (NSDictionary *)dictionaryValueFromArchivedExternalRepresentation:(NSDictionary *)externalRepresentation version:(NSUInteger)fromVersion {
  200. return nil;
  201. }
  202. @end