@interface NJMapping : NSObject
@property (nonatomic, copy) NSString *name;
-@property (nonatomic, readonly) NSMutableDictionary *entries;
+@property (nonatomic, readonly) NSUInteger count;
+
++ (id)mappingWithContentsOfURL:(NSURL *)url
+ mappings:(NSArray *)mappings
+ error:(NSError **)error;
- (id)initWithName:(NSString *)name;
+- (id)initWithSerialization:(NSDictionary *)serialization
+ mappings:(NSArray *)mappings;
+
- (NJOutput *)objectForKeyedSubscript:(NJInput *)input;
- (void)setObject:(NJOutput *)output forKeyedSubscript:(NJInput *)input;
- (NSDictionary *)serialize;
- (BOOL)writeToURL:(NSURL *)url error:(NSError **)error;
-
-+ (id)mappingWithContentsOfURL:(NSURL *)url mappings:(NSArray *)mappings error:(NSError **)error;
+- (BOOL)hasConflictWith:(NJMapping *)other;
+- (void)mergeEntriesFrom:(NJMapping *)other;
@end
#import "NJInput.h"
#import "NJOutput.h"
-@implementation NJMapping
+@implementation NJMapping {
+ NSMutableDictionary *_entries;
+}
+
+// Extra checks during initialization because the data is often loaded
+// from untrusted serializations.
- (id)initWithName:(NSString *)name {
if ((self = [super init])) {
- self.name = name ? name : @"Untitled";
+ self.name = [name isKindOfClass:NSString.class] ? name : @"Untitled";
_entries = [[NSMutableDictionary alloc] init];
}
return self;
}
+- (id)initWithSerialization:(NSDictionary *)serialization
+ mappings:(NSArray *)mappings {
+ if ((self = [self initWithName:serialization[@"name"]])) {
+ NSDictionary *entries = serialization[@"entries"];
+ if ([entries isKindOfClass:NSDictionary.class]) {
+ for (id key in entries) {
+ if ([key isKindOfClass:NSString.class]) {
+ NJOutput *output = [NJOutput outputDeserialize:entries[key]
+ withMappings:mappings];
+ if (output)
+ _entries[key] = output;
+ }
+ }
+ }
+ }
+ return self;
+}
+
- (NJOutput *)objectForKeyedSubscript:(NJInput *)input {
return input ? _entries[input.uid] : nil;
}
return success;
}
+- (NSUInteger)count {
+ return _entries.count;
+}
+
+- (BOOL)hasConflictWith:(NJMapping *)other {
+ if (other.count < self.count)
+ return [other hasConflictWith:self];
+ for (NSString *uid in _entries) {
+ NJOutput *output = other->_entries[uid];
+ if (output && ![output isEqual:_entries[uid]])
+ return YES;
+ }
+ return NO;
+}
+
+ (id)mappingWithContentsOfURL:(NSURL *)url mappings:(NSArray *)mappings error:(NSError **)error {
NSInputStream *stream = [NSInputStream inputStreamWithURL:url];
[stream open];
return nil;
}
- NSDictionary *entries = serialization[@"entries"];
- NJMapping *mapping = [[NJMapping alloc] initWithName:serialization[@"name"]];
- for (id key in entries) {
- NSDictionary *value = entries[key];
- if ([key isKindOfClass:NSString.class]) {
- NJOutput *output = [NJOutput outputDeserialize:value
- withMappings:mappings];
- if (output)
- mapping.entries[key] = output;
- }
- }
- return mapping;
+ return [[NJMapping alloc] initWithSerialization:serialization
+ mappings:mappings];
+}
+
+- (void)mergeEntriesFrom:(NJMapping *)other {
+ if (other)
+ [_entries addEntriesFromDictionary:other->_entries];
}
@end
- (void)loadAllFrom:(NSArray *)storedMappings andActivate:(NSUInteger)selected {
NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count];
- // have to do two passes in case mapping1 refers to mapping2 via a NJOutputMapping
+ // Requires two passes to deal with inter-mapping references. First make
+ // an empty mapping for each serialized mapping. Then, deserialize the
+ // data pointing to the empty mappings. Then merge that data back into
+ // its equivalent empty one, which is the one we finally use.
for (NSDictionary *storedMapping in storedMappings) {
NJMapping *mapping = [[NJMapping alloc] initWithName:storedMapping[@"name"]];
[newMappings addObject:mapping];
}
for (unsigned i = 0; i < storedMappings.count; ++i) {
- NSDictionary *entries = storedMappings[i][@"entries"];
- NJMapping *mapping = newMappings[i];
- for (id key in entries) {
- NJOutput *output = [NJOutput outputDeserialize:entries[key]
- withMappings:newMappings];
- if (output)
- mapping.entries[key] = output;
- }
+ NJMapping *realMapping = [[NJMapping alloc] initWithSerialization:storedMappings[i]
+ mappings:newMappings];
+ [newMappings[i] mergeEntriesFrom:realMapping];
}
if (newMappings.count) {
error:&error];
if (mapping && !error) {
- BOOL conflict = NO;
NJMapping *mergeInto = self[mapping.name];
- for (id key in mapping.entries) {
- if (mergeInto.entries[key]
- && ![mergeInto.entries[key] isEqual:mapping.entries[key]]) {
- conflict = YES;
- break;
- }
- }
+ BOOL conflict = [mergeInto hasConflictWith:mapping];
if (conflict) {
NSAlert *conflictAlert = [[NSAlert alloc] init];
}
if (mergeInto) {
- [mergeInto.entries addEntriesFromDictionary:mapping.entries];
+ [mergeInto mergeEntriesFrom:mapping];
mapping = mergeInto;
} else {
[_mappings addObject:mapping];
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
- <string>106</string>
+ <string>108</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>NSHumanReadableCopyright</key>