Skip to content
Browse files

Merge branch 'master' into better-error-reporting

Conflicts:
	Mantle/MTLModel+NSCoding.m
  • Loading branch information...
2 parents edf767a + b33db13 commit 42690268972342d04c25a24bea40177d3a0229c5 @jspahrsummers jspahrsummers committed
View
31 Mantle/MTLModel+NSCoding.h
@@ -56,11 +56,38 @@ typedef enum : NSUInteger {
// MTLModelEncodingBehaviorUnconditional.
+ (NSDictionary *)encodingBehaviorsByPropertyKey;
+// Determines the classes that are allowed to be decoded for each of the
+// receiver's properties when using <NSSecureCoding>. The values of this
+// dictionary should be NSArrays of Class objects.
+//
+// If any encodable keys (as determined by +encodingBehaviorsByPropertyKey) are
+// not present in the dictionary, an exception will be thrown during secure
+// encoding or decoding.
+//
+// Subclasses overriding this method should combine their values with those of
+// `super`.
+//
+// Returns a dictionary mapping the receiver's encodable keys (as determined by
+// +encodingBehaviorsByPropertyKey) to default allowed classes, based on the
+// type that each property is declared as. If type of an encodable property
+// cannot be determined (e.g., it is declared as `id`), it will be omitted from
+// the dictionary, and subclasses must provide a valid value to prevent an
+// exception being thrown during encoding/decoding.
++ (NSDictionary *)allowedSecureCodingClassesByPropertyKey;
+
// Decodes the value of the given property key from an archive.
//
// By default, this method looks for a `-decode<Key>WithCoder:modelVersion:`
-// method on the receiver, and invokes it if found. If not found, `-[NSCoder
-// decodeObjectForKey:]` will be used with the given `key`.
+// method on the receiver, and invokes it if found.
+//
+// If the custom method is not implemented and `coder` does not require secure
+// coding, `-[NSCoder decodeObjectForKey:]` will be invoked with the given
+// `key`.
+//
+// If the custom method is not implemented and `coder` requires secure coding,
+// `-[NSCoder decodeObjectOfClasses:forKey:]` will be invoked with the
+// information from +allowedSecureCodingClassesByPropertyKey and the given `key`. The
+// receiver must conform to <NSSecureCoding> for this to work correctly.
//
// key - The property key to decode the value for. This argument cannot
// be nil.
View
139 Mantle/MTLModel+NSCoding.m
@@ -14,6 +14,44 @@
// 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
@@ -25,7 +63,7 @@ + (NSUInteger)modelVersion {
#pragma mark Encoding Behaviors
+ (NSDictionary *)encodingBehaviorsByPropertyKey {
- NSSet *propertyKeys = self.class.propertyKeys;
+ NSSet *propertyKeys = self.propertyKeys;
NSMutableDictionary *behaviors = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
for (NSString *key in propertyKeys) {
@@ -44,6 +82,46 @@ + (NSDictionary *)encodingBehaviorsByPropertyKey {
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);
+
+ ext_propertyAttributes *attributes = ext_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);
@@ -64,13 +142,27 @@ - (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:
return result;
}
- return [coder decodeObjectForKey:key];
+ 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];
+ }
}
#pragma mark NSCoding
- (instancetype)initWithCoder:(NSCoder *)coder {
- NSNumber *version = [coder decodeObjectForKey:MTLModelVersionKey];
+ 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) {
@@ -78,36 +170,45 @@ - (instancetype)initWithCoder:(NSCoder *)coder {
return nil;
}
- NSDictionary *externalRepresentation = [coder decodeObjectForKey:@"externalRepresentation"];
- NSError *error = 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);
- if (externalRepresentation == nil) {
- NSSet *propertyKeys = self.class.propertyKeys;
- NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
+ NSDictionary *dictionaryValue = [self.class dictionaryValueFromArchivedExternalRepresentation:externalRepresentation version:version.unsignedIntegerValue];
+ if (dictionaryValue == nil) return nil;
- for (NSString *key in propertyKeys) {
- id value = [self decodeValueForKey:key withCoder:coder modelVersion:version.unsignedIntegerValue];
- if (value == nil) continue;
+ NSError *error = nil;
+ self = [self initWithDictionary:dictionaryValue error:&error];
+ if (self == nil) NSLog(@"*** Could not decode old %@ archive: %@", self.class, error);
- dictionaryValue[key] = value;
+ return self;
}
+ }
- self = [self initWithDictionary:dictionaryValue error:&error];
- } else {
- // Handle the old archive format.
- 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);
+ NSSet *propertyKeys = self.class.propertyKeys;
+ NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
- NSDictionary *dictionaryValue = [self.class dictionaryValueFromArchivedExternalRepresentation:externalRepresentation version:version.unsignedIntegerValue];
- if (dictionaryValue == nil) return nil;
+ for (NSString *key in propertyKeys) {
+ id value = [self decodeValueForKey:key withCoder:coder modelVersion:version.unsignedIntegerValue];
+ if (value == nil) continue;
- self = [self initWithDictionary:dictionaryValue error:&error];
+ 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;
View
13 MantleTests/MTLModelNSCodingSpec.m
@@ -20,6 +20,19 @@
expect(behaviors[@"dynamicName"]).to.beNil();
});
+it(@"should have default allowed classes", ^{
+ NSDictionary *allowedClasses = MTLTestModel.allowedSecureCodingClassesByPropertyKey;
+ expect(allowedClasses).notTo.beNil();
+
+ expect(allowedClasses[@"name"]).to.equal(@[ NSString.class ]);
+ expect(allowedClasses[@"count"]).to.equal(@[ NSValue.class ]);
+ expect(allowedClasses[@"weakModel"]).to.equal(@[ MTLEmptyTestModel.class ]);
+
+ // Not encoded into archives.
+ expect(allowedClasses[@"nestedName"]).to.beNil();
+ expect(allowedClasses[@"dynamicName"]).to.beNil();
+});
+
it(@"should default to version 0", ^{
expect(MTLEmptyTestModel.modelVersion).to.equal(0);
});
View
13 README.md
@@ -9,9 +9,8 @@ Mantle is still new and moving fast, so we may make breaking changes from
time-to-time, but it has excellent unit test coverage and is already being used
in GitHub for Mac's production code.
-To start building the framework, clone this repository and then run `git
-submodule update --init --recursive`. This will automatically pull down any
-dependencies.
+To start building the framework, clone this repository and then run `script/bootstrap`.
+This will automatically pull down any dependencies.
## The Typical Model Object
@@ -207,21 +206,21 @@ typedef enum : NSUInteger {
}
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
- return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
+ return @{
@"URL": @"url",
@"HTMLURL": @"html_url",
@"reporterLogin": @"user.login",
@"assigneeLogin": @"assignee.login",
@"updatedAt": @"updated_at"
- }];
+ };
}
+ (NSValueTransformer *)URLJSONTransformer {
- return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
+ return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)HTMLURLJSONTransformer {
- return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
+ return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)stateJSONTransformer {
View
0 script/README.md
No changes.
View
9 script/bootstrap
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+SCRIPT_DIR=$(dirname "$0")
+cd "$SCRIPT_DIR/.."
+
+echo "*** Updating submodules..."
+git submodule sync --quiet || exit $?
+git submodule update --init || exit $?
+git submodule foreach --recursive --quiet "git submodule sync --quiet && git submodule update --init" || exit $?
View
114 script/cibuild
@@ -0,0 +1,114 @@
+#!/bin/bash
+
+SCRIPT_DIR=$(dirname "$0")
+cd "$SCRIPT_DIR/.."
+
+##
+## Configuration Variables
+##
+
+# The build configuration to use.
+if [ -z "$XCCONFIGURATION" ]
+then
+ XCCONFIGURATION="Release"
+fi
+
+# The workspace to build.
+#
+# If not set and no workspace is found, the -workspace flag will not be passed
+# to xcodebuild.
+if [ -z "$XCWORKSPACE" ]
+then
+ XCWORKSPACE=$(ls -d *.xcworkspace 2>/dev/null | head -n 1)
+fi
+
+# A bootstrap script to run before building.
+#
+# If this file does not exist, it is not considered an error.
+BOOTSTRAP="$SCRIPT_DIR/bootstrap"
+
+# A whitespace-separated list of default targets or schemes to build, if none
+# are specified on the command line.
+#
+# Individual names can be quoted to avoid word splitting.
+DEFAULT_TARGETS=
+
+# Extra build settings to pass to xcodebuild.
+XCODEBUILD_SETTINGS="TEST_AFTER_BUILD=YES"
+
+##
+## Build Process
+##
+
+if [ -z "$*" ]
+then
+ # lol recursive shell script
+ if [ -n "$DEFAULT_TARGETS" ]
+ then
+ echo "$DEFAULT_TARGETS" | xargs "$SCRIPT_DIR/cibuild"
+ else
+ xcodebuild -list | awk -f "$SCRIPT_DIR/targets.awk" | xargs "$SCRIPT_DIR/cibuild"
+ fi
+
+ exit $?
+fi
+
+if [ -f "$BOOTSTRAP" ]
+then
+ echo "*** Bootstrapping..."
+ bash "$BOOTSTRAP"
+fi
+
+echo "*** The following targets will be built:"
+
+for target in "$@"
+do
+ echo "$target"
+done
+
+echo "*** Cleaning all targets..."
+xcodebuild -alltargets clean OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
+
+run_xcodebuild ()
+{
+ local scheme=$1
+
+ if [ -n "$XCWORKSPACE" ]
+ then
+ xcodebuild -workspace "$XCWORKSPACE" -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
+ else
+ xcodebuild -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
+ fi
+
+ local status=$?
+
+ return $status
+}
+
+build_scheme ()
+{
+ local scheme=$1
+
+ run_xcodebuild "$scheme" 2>&1 | awk -f "$SCRIPT_DIR/xcodebuild.awk"
+
+ local awkstatus=$?
+ local xcstatus=${PIPESTATUS[0]}
+
+ if [ "$xcstatus" -eq "65" ]
+ then
+ # This probably means that there's no scheme by that name. Give up.
+ echo "*** Error building scheme $scheme -- perhaps it doesn't exist"
+ elif [ "$awkstatus" -eq "1" ]
+ then
+ return $awkstatus
+ fi
+
+ return $xcstatus
+}
+
+echo "*** Building..."
+
+for scheme in "$@"
+do
+ build_scheme "$scheme" || exit $?
+done
View
12 script/targets.awk
@@ -0,0 +1,12 @@
+BEGIN {
+ FS = "\n";
+}
+
+/Targets:/ {
+ while (getline && $0 != "") {
+ if ($0 ~ /Tests/) continue;
+
+ sub(/^ +/, "");
+ print "'" $0 "'";
+ }
+}
View
35 script/xcodebuild.awk
@@ -0,0 +1,35 @@
+# Exit statuses:
+#
+# 0 - No errors found.
+# 1 - Build or test failure. Errors will be logged automatically.
+# 2 - Untestable target. Retry with the "build" action.
+
+BEGIN {
+ status = 0;
+}
+
+{
+ print;
+ fflush(stdout);
+}
+
+/is not valid for Testing/ {
+ exit 2;
+}
+
+/[0-9]+: (error|warning):/ {
+ errors = errors $0 "\n";
+}
+
+/(TEST|BUILD) FAILED/ {
+ status = 1;
+}
+
+END {
+ if (length(errors) > 0) {
+ print "\n*** All errors:\n" errors;
+ }
+
+ fflush(stdout);
+ exit status;
+}

0 comments on commit 4269026

Please sign in to comment.
Something went wrong with that request. Please try again.