// // OrderedDictionary.m // // Version 1.4 // // Created by Nick Lockwood on 21/09/2010. // Copyright 2010 Charcoal Design // // Distributed under the permissive zlib license // Get the latest version from here: // // https://github.com/nicklockwood/OrderedDictionary // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source distribution. // #import "OrderedDictionary.h" #pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis" #pragma GCC diagnostic ignored "-Wnullable-to-nonnull-conversion" #pragma GCC diagnostic ignored "-Wdirect-ivar-access" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wgnu" #import #if !__has_feature(objc_arc) #error This class requires automatic reference counting #endif @implementation NSThread (XMLPlist) - (NSDateFormatter *)XMLPlistDateFormatter { static NSString *const key = @"XMLPlistDateFormatter"; NSDateFormatter *formatter = self.threadDictionary[key]; if (!formatter) { formatter = [[NSDateFormatter alloc] init]; formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'"; self.threadDictionary[key] = formatter; } return formatter; } @end @interface NSObject (XMLPlistWriting) - (NSString *)XMLPlistStringWithIndent:(NSString *)indent; @end @interface OrderedDictionaryXMLPlistParser : NSObject @property (nonatomic, readonly) OrderedDictionary *root; - (instancetype)initWithData:(NSData *)data root:(OrderedDictionary *)root; @end @implementation OrderedDictionary { @protected NSArray *_values; NSOrderedSet *_keys; } + (instancetype)dictionaryWithContentsOfFile:(NSString *)path { NSData *data = [NSData dataWithContentsOfFile:path]; __autoreleasing id dictionary = [[self alloc] initWithPlistData:data]; return dictionary; } + (instancetype)dictionaryWithContentsOfURL:(NSURL *)url { NSData *data = [NSData dataWithContentsOfURL:url]; __autoreleasing id dictionary = [[self alloc] initWithPlistData:data]; return dictionary; } - (instancetype)initWithContentsOfFile:(NSString *)path { NSData *data = [NSData dataWithContentsOfFile:path]; return [self initWithPlistData:data]; return nil; } - (instancetype)initWithContentsOfURL:(NSURL *)url { NSData *data = [NSData dataWithContentsOfURL:url]; return [self initWithPlistData:data]; return nil; } - (instancetype)initWithObjects:(NSArray *)objects forKeys:(NSArray *)keys { if ((self = [super init])) { _values = [objects copy]; _keys = [NSOrderedSet orderedSetWithArray:keys]; NSParameterAssert([_keys count] == [_values count]); } return self; } - (instancetype)initWithPlistData:(NSData *)data { if (data) { const void *bytes = data.bytes; char header[7]; memcpy(header, &bytes, 6); header[6] = '\0'; NSAssert(strcmp(header, "bplist") != 0, @"OrderedDictionary does not support loading binary plist files. Use an XML plist file instead. Xcode automatically converts XML plist files to binary files in built apps - see documentation for tips on how to disable this."); return [[OrderedDictionaryXMLPlistParser alloc] initWithData:data root:[self init]].root; } return nil; } - (instancetype)initWithObjects:(const __unsafe_unretained id [])objects forKeys:(const __unsafe_unretained id [])keys count:(NSUInteger)count { if ((self = [super init])) { _values = [[NSArray alloc] initWithObjects:objects count:count]; _keys = [[NSOrderedSet alloc] initWithObjects:keys count:count]; NSParameterAssert([_values count] == count); NSParameterAssert([_keys count] == count); } return self; } - (Class)classForCoder { return [self class]; } - (instancetype)init { if ((self = [super init]) && [self class] == [OrderedDictionary class]) { static OrderedDictionary *singleton; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ singleton = self; self->_values = @[]; self->_keys = [[NSOrderedSet alloc] init]; }); return singleton; } return self; } - (instancetype)initWithCoder:(NSCoder *)decoder { if ((self = [super init])) { _values = [decoder decodeObjectOfClass:[NSArray class] forKey:@"values"]; _keys = [decoder decodeObjectOfClass:[NSOrderedSet class] forKey:@"keys"]; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:_values forKey:@"values"]; [coder encodeObject:_keys forKey:@"keys"]; } + (BOOL)supportsSecureCoding { return YES; } - (instancetype)copyWithZone:(__unused NSZone *)zone { return self; } - (instancetype)mutableCopyWithZone:(NSZone *)zone { return [[MutableOrderedDictionary allocWithZone:zone] initWithDictionary:self]; } - (NSArray *)allKeys { return _keys.array; } - (NSArray *)allValues { return [_values copy]; } - (NSUInteger)count { return _keys.count; } - (NSUInteger)indexOfKey:(id)key { return [_keys indexOfObject:key]; } - (id)objectForKey:(id)key { NSUInteger index = [_keys indexOfObject:key]; if (index != NSNotFound) { return _values[index]; } return nil; } - (NSEnumerator *)keyEnumerator { return [_keys objectEnumerator]; } - (NSEnumerator *)reverseKeyEnumerator { return [_keys reverseObjectEnumerator]; } - (NSEnumerator *)objectEnumerator { return [_values objectEnumerator]; } - (NSEnumerator *)reverseObjectEnumerator { return [_values reverseObjectEnumerator]; } - (void)enumerateKeysAndObjectsWithIndexUsingBlock:(void (^)(id key, id obj, NSUInteger idx, BOOL *stop))block { [_keys enumerateObjectsUsingBlock:^(id key, NSUInteger idx, BOOL *stop) { block(key, self->_values[idx], idx, stop); }]; } - (id)keyAtIndex:(NSUInteger)index { return _keys[index]; } - (id)objectAtIndex:(NSUInteger)index { return _values[index]; } - (id)objectAtIndexedSubscript:(NSUInteger)index { return _values[index]; } - (NSString *)descriptionWithLocale:(nullable id)locale indent:(NSUInteger)level { NSMutableString *indent = [NSMutableString string]; for (int i = 0; i < level; i++) { [indent appendString:@" "]; } NSMutableString *string = [NSMutableString string]; [string appendString:indent]; [string appendString:@"{\n"]; [self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, __unused BOOL *stop) { NSString *description; if ([obj respondsToSelector:@selector(descriptionWithLocale:indent:)]) { description = [obj descriptionWithLocale:locale indent:level + 1]; } else if ([obj respondsToSelector:@selector(descriptionWithLocale:)]) { description = [obj descriptionWithLocale:locale]; } else { description = [obj description]; } [string appendString:indent]; [string appendFormat:@" %@ = %@;\n", key, description]; }]; [string appendString:indent]; [string appendString:@"}"]; return string; } - (NSString *)XMLPlistString { return [NSString stringWithFormat:@"\n" "\n" "\n%@\n\n", [self XMLPlistStringWithIndent:@""]]; } - (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile { return [[self XMLPlistString] writeToFile:path atomically:useAuxiliaryFile encoding:NSUTF8StringEncoding error:NULL]; } - (BOOL)writeToURL:(NSURL *)url atomically:(BOOL)atomically { return [[self XMLPlistString] writeToURL:url atomically:atomically encoding:NSUTF8StringEncoding error:NULL]; } @end @implementation MutableOrderedDictionary #define _mutableValues ((NSMutableArray *)_values) #define _mutableKeys ((NSMutableOrderedSet *)_keys) + (instancetype)dictionaryWithCapacity:(NSUInteger)count { return [(MutableOrderedDictionary *)[self alloc] initWithCapacity:count]; } - (instancetype)initWithObjects:(const __unsafe_unretained id [])objects forKeys:(const __unsafe_unretained id [])keys count:(NSUInteger)count { if ((self = [super init])) { _values = [[NSMutableArray alloc] initWithObjects:objects count:count]; _keys = [[NSMutableOrderedSet alloc] initWithObjects:keys count:count]; } return self; } - (instancetype)initWithCapacity:(NSUInteger)capacity { if ((self = [super init])) { _values = [[NSMutableArray alloc] initWithCapacity:capacity]; _keys = [[NSMutableOrderedSet alloc] initWithCapacity:capacity]; } return self; } - (instancetype)init { return [self initWithCapacity:0]; } - (instancetype)initWithCoder:(NSCoder *)decoder { if ((self = [super init])) { _values = [decoder decodeObjectOfClass:[NSMutableArray class] forKey:@"values"]; _keys = [decoder decodeObjectOfClass:[NSMutableOrderedSet class] forKey:@"keys"]; } return self; } - (id)copyWithZone:(NSZone *)zone { return [[OrderedDictionary allocWithZone:zone] initWithDictionary:self]; } - (void)addEntriesFromDictionary:(NSDictionary *)otherDictionary { [otherDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop) { [self setObject:obj forKey:key]; }]; } - (void)insertObject:(id)object forKey:(id)key atIndex:(NSUInteger)index { [self removeObjectForKey:key]; [_mutableKeys insertObject:key atIndex:index]; [_mutableValues insertObject:object atIndex:index]; } - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)object { _mutableValues[index] = object; } - (void)setObject:(id)object atIndexedSubscript:(NSUInteger)index { _mutableValues[index] = object; } - (void)exchangeObjectAtIndex:(NSUInteger)idx1 withObjectAtIndex:(NSUInteger)idx2 { [_mutableKeys exchangeObjectAtIndex:idx1 withObjectAtIndex:idx2]; [_mutableValues exchangeObjectAtIndex:idx1 withObjectAtIndex:idx2]; } - (void)removeAllObjects { [_mutableKeys removeAllObjects]; [_mutableValues removeAllObjects]; } - (void)removeObjectAtIndex:(NSUInteger)index { [_mutableKeys removeObjectAtIndex:index]; [_mutableValues removeObjectAtIndex:index]; } - (void)removeObjectsAtIndexes:(NSIndexSet *)indexes { [_mutableKeys removeObjectsAtIndexes:indexes]; [_mutableValues removeObjectsAtIndexes:indexes]; } - (void)removeObjectForKey:(id)key { NSUInteger index = [self->_keys indexOfObject:key]; if (index != NSNotFound) { [self removeObjectAtIndex:index]; } } - (void)removeObjectsForKeys:(NSArray *)keyArray { for (id key in [keyArray copy]) { [self removeObjectForKey:key]; } } - (void)setDictionary:(NSDictionary *)otherDictionary { [_mutableKeys removeAllObjects]; [_mutableKeys addObjectsFromArray:otherDictionary.allKeys]; [_mutableValues setArray:otherDictionary.allValues]; } - (void)setObject:(id)object forKey:(id)key { NSUInteger index = [_keys indexOfObject:key]; if (index != NSNotFound) { _mutableValues[index] = object; } else { [_mutableKeys addObject:key]; [_mutableValues addObject:object]; } } - (void)setValue:(id)value forKey:(NSString *)key { if (value) { [self setObject:value forKey:key]; } else { [self removeObjectForKey:key]; } } - (void)setObject:(id)object forKeyedSubscript:(id)key { [self setObject:object forKey:key]; } @end @implementation NSObject (XMLPlistWriting) - (NSString *)XMLPlistStringWithIndent:(__unused NSString *)indent { NSLog(@"%@ is not a supported property list type.", self.classForCoder); [self doesNotRecognizeSelector:_cmd]; return nil; } @end @implementation NSString (XMLPlistWriting) - (NSString *)XMLEscapedString { return [[[self stringByReplacingOccurrencesOfString:@"&" withString:@"&"] stringByReplacingOccurrencesOfString:@"<" withString:@"<"] stringByReplacingOccurrencesOfString:@">" withString:@">"]; } - (NSString *)XMLPlistStringWithIndent:(__unused NSString *)indent { return [NSString stringWithFormat:@"%@", [self XMLEscapedString]]; } @end @implementation NSNumber (XMLPlistWriting) - (NSString *)XMLPlistStringWithIndent:(__unused NSString *)indent { if ((__bridge CFBooleanRef)self == kCFBooleanTrue) { return @""; } else if ((__bridge CFBooleanRef)self == kCFBooleanFalse) { return @""; } else if (self.doubleValue != (double)self.integerValue) { return [NSString stringWithFormat:@"%@", self]; } else { return [NSString stringWithFormat:@"%@", self]; } } @end @implementation NSDate (XMLPlistWriting) - (NSString *)XMLPlistStringWithIndent:(__unused NSString *)indent { NSDateFormatter *formatter = [[NSThread currentThread] XMLPlistDateFormatter]; return [NSString stringWithFormat:@"%@", [formatter stringFromDate:self]]; } @end @implementation NSData (XMLPlistWriting) - (NSString *)XMLPlistStringWithIndent:(NSString *)indent { NSString *base64 = [self base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; return [NSString stringWithFormat:@"\n%@%@\n%@", indent, base64, indent]; } @end @implementation NSArray (XMLPlistWriting) - (NSString *)XMLPlistStringWithIndent:(NSString *)indent { NSMutableString *xml = [NSMutableString string]; [xml appendString:@"\n"]; NSString *subindent = [indent stringByAppendingString:@"\t"]; for (id value in self) { [xml appendString:subindent]; [xml appendString:[value XMLPlistStringWithIndent:subindent]]; [xml appendString:@"\n"]; } [xml appendString:indent]; [xml appendString:@""]; return xml; } @end @implementation NSDictionary (XMLPlistWriting) - (NSString *)XMLPlistStringWithIndent:(NSString *)indent { NSMutableString *xml = [NSMutableString string]; [xml appendString:@"\n"]; NSString *subindent = [indent stringByAppendingString:@"\t"]; [self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, __unused BOOL *stop) { [xml appendString:subindent]; [xml appendFormat:@"%@\n", [[key description] XMLEscapedString]]; [xml appendString:subindent]; [xml appendString:[value XMLPlistStringWithIndent:subindent]]; [xml appendString:@"\n"]; }]; [xml appendString:indent]; [xml appendString:@""]; return xml; } @end @implementation OrderedDictionaryXMLPlistParser { NSDateFormatter *_formatter; NSMutableArray *_valueStack; NSMutableArray *_keyStack; NSString *_text; BOOL _failed; } - (instancetype)initWithData:(NSData *)data root:(OrderedDictionary *)root { if ((self = [super init])) { _root = [root isKindOfClass:[MutableOrderedDictionary class]] ? root : nil; _keyStack = [NSMutableArray array]; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; parser.delegate = self; [parser parse]; } return self; } - (void)failWithError:(NSString *)error { NSLog(@"OrderedDictionary XML parsing error: %@", error); _failed = YES; _root = nil; } - (void)parser:(NSXMLParser *)parser didStartElement:(nonnull NSString *)elementName namespaceURI:(__unused NSString *)namespaceURI qualifiedName:(__unused NSString *)qName attributes:(__unused NSDictionary *)attributeDict { if ([elementName isEqualToString:@"dict"]) { if (_valueStack == nil) { _valueStack = [NSMutableArray arrayWithObject:_root ?: [MutableOrderedDictionary dictionary]]; } else { [_valueStack addObject:[MutableOrderedDictionary dictionary]]; } } else if (![elementName isEqualToString:@"plist"] && _valueStack == nil) { [self failWithError:@"Root element was not a dictionary."]; [parser abortParsing]; return; } else if ([elementName isEqualToString:@"array"]) { [_valueStack addObject:[NSMutableArray array]]; } } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(__unused NSString *)namespaceURI qualifiedName:(__unused NSString *)qName { id value = [_text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; _text = nil; if ([elementName isEqualToString:@"key"]) { [_keyStack addObject:value ?: @""]; return; } if ([elementName isEqualToString:@"string"]) { // Do nothing } else if ([elementName isEqualToString:@"real"]) { value = @([value doubleValue]); } else if ([elementName isEqualToString:@"integer"]) { value = @([value integerValue]); } else if ([elementName isEqualToString:@"date"]) { if (!_formatter) { _formatter = [[NSThread currentThread] XMLPlistDateFormatter]; } NSString *dateString = value; if (!(value = [_formatter dateFromString:dateString])) { [self failWithError:[NSString stringWithFormat:@"Unabled to parse date string: %@", dateString]]; [parser abortParsing]; return; } } else if ([elementName isEqualToString:@"data"]) { NSData *data = [value dataUsingEncoding:NSUTF8StringEncoding]; if (!(value = [[NSData alloc] initWithBase64EncodedData:data options:NSDataBase64DecodingIgnoreUnknownCharacters])) { [self failWithError:@"Unabled to parse data."]; [parser abortParsing]; return; } } else if ([elementName isEqualToString:@"true"]) { value = @YES; } else if ([elementName isEqualToString:@"false"]) { value = @NO; } else if ([elementName isEqualToString:@"dict"] || [elementName isEqualToString:@"array"]) { value = [_valueStack.lastObject copy]; [_valueStack removeLastObject]; } else if ([elementName isEqualToString:@"plist"]) { return; } id top = _valueStack.lastObject; if ([top isKindOfClass:[MutableOrderedDictionary class]]) { NSString *key = _keyStack.lastObject; ((MutableOrderedDictionary *)top)[key] = value; [_keyStack removeLastObject]; } else if ([top isKindOfClass:[NSArray class]]) { [(NSMutableArray *)top addObject:value]; } else if (_root == nil && !_failed) { _root = [value copy]; } } - (void)parser:(__unused NSXMLParser *)parser foundCharacters:(NSString *)string { _text = [_text ?: @"" stringByAppendingString:string]; } @end