From a2a16728230f8704d662efa9e60dc132f459326b Mon Sep 17 00:00:00 2001 From: Isaac Greenspan Date: Mon, 29 Dec 2014 15:50:02 -0600 Subject: [PATCH 1/6] Bumping the podspec version to 0.1.1. --- VOKBenkode.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VOKBenkode.podspec b/VOKBenkode.podspec index f18113f..e90cc63 100644 --- a/VOKBenkode.podspec +++ b/VOKBenkode.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "VOKBenkode" - s.version = "0.1.0" + s.version = "0.1.1" s.summary = "An Objective-C library for encoding/decoding objects using Bencoding." s.homepage = "https://github.com/vokalinteractive/VOKBenkode" s.license = { :type => 'MIT', :file => 'LICENSE' } From 83ce635c43aefa7f1a0da549015cd54665e21e2e Mon Sep 17 00:00:00 2001 From: Isaac Greenspan Date: Mon, 29 Dec 2014 15:50:34 -0600 Subject: [PATCH 2/6] Ensuring that string decoding errors surface an NSError. --- Pod/Classes/VOKBenkode.h | 2 ++ Pod/Classes/VOKBenkode.m | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Pod/Classes/VOKBenkode.h b/Pod/Classes/VOKBenkode.h index b0ad261..65d7785 100644 --- a/Pod/Classes/VOKBenkode.h +++ b/Pod/Classes/VOKBenkode.h @@ -29,6 +29,8 @@ enum { VOKBenkodeErrorStringLengthMalformed, /// The length of the string indicates more data than was passed in. VOKBenkodeErrorStringLengthExceedsData, + /// The data for a particular string could not be decoded. + VOKBenkodeErrorStringDataInvalid, /// A dictionary key is not a string. VOKBenkodeErrorDictionaryKeyNotString, /// A dictionary has the same key more than once. diff --git a/Pod/Classes/VOKBenkode.m b/Pod/Classes/VOKBenkode.m index be067c2..8caf021 100644 --- a/Pod/Classes/VOKBenkode.m +++ b/Pod/Classes/VOKBenkode.m @@ -472,8 +472,18 @@ + (NSString *)decodeString:(NSData *)data if (length) { *length = localLength; } - return [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(index, stringLength)] - encoding:stringEncoding]; + NSData *stringData = [data subdataWithRange:NSMakeRange(index, stringLength)]; + NSString *result = [[NSString alloc] initWithData:stringData + encoding:stringEncoding]; + if (!result) { + if (error) { + *error = [NSError errorWithDomain:VOKBenkodeErrorDomain + code:VOKBenkodeErrorStringDataInvalid + userInfo:nil]; + } + return nil; + } + return result; } @end From 87df8319f0122f3e102565717020f37a4b357f7c Mon Sep 17 00:00:00 2001 From: Isaac Greenspan Date: Mon, 29 Dec 2014 16:38:31 -0600 Subject: [PATCH 3/6] Adding tests of dictionary key ordering. --- Example/Tests/VOKBenkodeDictionaryOrdering.m | 82 ++++++++++++++++++++ Example/VOKBenkode.xcodeproj/project.pbxproj | 4 + 2 files changed, 86 insertions(+) create mode 100644 Example/Tests/VOKBenkodeDictionaryOrdering.m diff --git a/Example/Tests/VOKBenkodeDictionaryOrdering.m b/Example/Tests/VOKBenkodeDictionaryOrdering.m new file mode 100644 index 0000000..8b126bd --- /dev/null +++ b/Example/Tests/VOKBenkodeDictionaryOrdering.m @@ -0,0 +1,82 @@ +// +// VOKBenkodeDictionaryOrdering.m +// VOKBenkode +// +// Created by Isaac Greenspan on 12/29/14. +// Copyright (c) 2014 VOKAL Interactive. All rights reserved. +// + +#import + +#import + +#import "VOKBenkodeAssert.h" + +@interface VOKBenkodeDictionaryOrdering : XCTestCase + +@end + +@implementation VOKBenkodeDictionaryOrdering + +- (void)testVariantsOfA +{ + /* ```python + from bencode import bencode + bencode({ 'a':1, 'A':1, 'ä':1, 'Ä':1, 'å':1, 'â':1, 'Å':1, 'Â':1, 'á':1, 'Á':1, }) + ``` */ + NSString *pythonOutput = @"d1:Ai1e1:ai1e2:\xc3\x81i1e2:\xc3\x82i1e2:\xc3\x84i1e2:\xc3\x85i1e2:\xc3\xa1i1e2:\xc3\xa2i1e2:\xc3\xa4i1e2:\xc3\xa5i1ee"; + + AssertOriginalMatchesEncodedString((@{ @"a": @1, @"A": @1, @"ä": @1, @"Ä": @1, @"å": @1, @"â": @1, @"Å": @1, @"Â": @1, @"á": @1, @"Á": @1, }), + pythonOutput); +} + +- (void)testAllFrom1To128 +{ + /* ```python + from bencode import bencode + bencode(dict((chr(x), 1) for x in xrange(1, 128))) + ``` */ + NSString *pythonOutput = @"d1:\x01i1e1:\x02i1e1:\x03i1e1:\x04i1e1:\x05i1e1:\x06i1e1:\x07i1e1:\x08i1e1:\ti1e1:\ni1e1:\x0bi1e1:\x0ci1e1:\ri1e1:\x0ei1e1:\x0fi1e1:\x10i1e1:\x11i1e1:\x12i1e1:\x13i1e1:\x14i1e1:\x15i1e1:\x16i1e1:\x17i1e1:\x18i1e1:\x19i1e1:\x1ai1e1:\x1bi1e1:\x1ci1e1:\x1di1e1:\x1ei1e1:\x1fi1e1: i1e1:!i1e1:\"i1e1:#i1e1:$i1e1:%i1e1:&i1e1:\'i1e1:(i1e1:)i1e1:*i1e1:+i1e1:,i1e1:-i1e1:.i1e1:/i1e1:0i1e1:1i1e1:2i1e1:3i1e1:4i1e1:5i1e1:6i1e1:7i1e1:8i1e1:9i1e1::i1e1:;i1e1:i1e1:?i1e1:@i1e1:Ai1e1:Bi1e1:Ci1e1:Di1e1:Ei1e1:Fi1e1:Gi1e1:Hi1e1:Ii1e1:Ji1e1:Ki1e1:Li1e1:Mi1e1:Ni1e1:Oi1e1:Pi1e1:Qi1e1:Ri1e1:Si1e1:Ti1e1:Ui1e1:Vi1e1:Wi1e1:Xi1e1:Yi1e1:Zi1e1:[i1e1:\\i1e1:]i1e1:^i1e1:_i1e1:`i1e1:ai1e1:bi1e1:ci1e1:di1e1:ei1e1:fi1e1:gi1e1:hi1e1:ii1e1:ji1e1:ki1e1:li1e1:mi1e1:ni1e1:oi1e1:pi1e1:qi1e1:ri1e1:si1e1:ti1e1:ui1e1:vi1e1:wi1e1:xi1e1:yi1e1:zi1e1:{i1e1:|i1e1:}i1e1:~i1e1:\x7fi1ee"; + + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:127]; + for (NSUInteger index = 1; index < 128; index++) { + dict[[NSString stringWithFormat:@"%c", (char)index]] = @1; + } + AssertOriginalMatchesEncodedString(dict, pythonOutput); +} + +- (void)testMultiLengthKeys +{ + /* ```python + from bencode import bencode + bencode(dict((chr(x) * y, 1) for x in xrange(60, 120) for y in xrange(1, 5))) + ``` */ + NSString *pythonOutput = @"d1:i1e2:>>i1e3:>>>i1e4:>>>>i1e1:?i1e2:??i1e3:???i1e4:????i1e1:@i1e2:@@i1e3:@@@i1e4:@@@@i1e1:Ai1e2:AAi1e3:AAAi1e4:AAAAi1e1:Bi1e2:BBi1e3:BBBi1e4:BBBBi1e1:Ci1e2:CCi1e3:CCCi1e4:CCCCi1e1:Di1e2:DDi1e3:DDDi1e4:DDDDi1e1:Ei1e2:EEi1e3:EEEi1e4:EEEEi1e1:Fi1e2:FFi1e3:FFFi1e4:FFFFi1e1:Gi1e2:GGi1e3:GGGi1e4:GGGGi1e1:Hi1e2:HHi1e3:HHHi1e4:HHHHi1e1:Ii1e2:IIi1e3:IIIi1e4:IIIIi1e1:Ji1e2:JJi1e3:JJJi1e4:JJJJi1e1:Ki1e2:KKi1e3:KKKi1e4:KKKKi1e1:Li1e2:LLi1e3:LLLi1e4:LLLLi1e1:Mi1e2:MMi1e3:MMMi1e4:MMMMi1e1:Ni1e2:NNi1e3:NNNi1e4:NNNNi1e1:Oi1e2:OOi1e3:OOOi1e4:OOOOi1e1:Pi1e2:PPi1e3:PPPi1e4:PPPPi1e1:Qi1e2:QQi1e3:QQQi1e4:QQQQi1e1:Ri1e2:RRi1e3:RRRi1e4:RRRRi1e1:Si1e2:SSi1e3:SSSi1e4:SSSSi1e1:Ti1e2:TTi1e3:TTTi1e4:TTTTi1e1:Ui1e2:UUi1e3:UUUi1e4:UUUUi1e1:Vi1e2:VVi1e3:VVVi1e4:VVVVi1e1:Wi1e2:WWi1e3:WWWi1e4:WWWWi1e1:Xi1e2:XXi1e3:XXXi1e4:XXXXi1e1:Yi1e2:YYi1e3:YYYi1e4:YYYYi1e1:Zi1e2:ZZi1e3:ZZZi1e4:ZZZZi1e1:[i1e2:[[i1e3:[[[i1e4:[[[[i1e1:\\i1e2:\\\\i1e3:\\\\\\i1e4:\\\\\\\\i1e1:]i1e2:]]i1e3:]]]i1e4:]]]]i1e1:^i1e2:^^i1e3:^^^i1e4:^^^^i1e1:_i1e2:__i1e3:___i1e4:____i1e1:`i1e2:``i1e3:```i1e4:````i1e1:ai1e2:aai1e3:aaai1e4:aaaai1e1:bi1e2:bbi1e3:bbbi1e4:bbbbi1e1:ci1e2:cci1e3:ccci1e4:cccci1e1:di1e2:ddi1e3:dddi1e4:ddddi1e1:ei1e2:eei1e3:eeei1e4:eeeei1e1:fi1e2:ffi1e3:fffi1e4:ffffi1e1:gi1e2:ggi1e3:gggi1e4:ggggi1e1:hi1e2:hhi1e3:hhhi1e4:hhhhi1e1:ii1e2:iii1e3:iiii1e4:iiiii1e1:ji1e2:jji1e3:jjji1e4:jjjji1e1:ki1e2:kki1e3:kkki1e4:kkkki1e1:li1e2:lli1e3:llli1e4:lllli1e1:mi1e2:mmi1e3:mmmi1e4:mmmmi1e1:ni1e2:nni1e3:nnni1e4:nnnni1e1:oi1e2:ooi1e3:oooi1e4:ooooi1e1:pi1e2:ppi1e3:pppi1e4:ppppi1e1:qi1e2:qqi1e3:qqqi1e4:qqqqi1e1:ri1e2:rri1e3:rrri1e4:rrrri1e1:si1e2:ssi1e3:sssi1e4:ssssi1e1:ti1e2:tti1e3:ttti1e4:tttti1e1:ui1e2:uui1e3:uuui1e4:uuuui1e1:vi1e2:vvi1e3:vvvi1e4:vvvvi1e1:wi1e2:wwi1e3:wwwi1e4:wwwwi1ee"; + + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:240]; + for (NSUInteger index = 60; index < 120; index++) { + for (NSUInteger count = 1; count < 5; count++) { + NSString *key = @""; + for (NSUInteger innerCount = 0; innerCount < count; innerCount++) { + key = [key stringByAppendingFormat:@"%c", (char)index]; + } + dict[key] = @1; + } + } + AssertOriginalMatchesEncodedString(dict, pythonOutput); + + // Swap the construction loop order and make sure it doesn't change anything. + dict = [NSMutableDictionary dictionaryWithCapacity:240]; + for (NSUInteger count = 1; count < 5; count++) { + for (NSUInteger index = 60; index < 120; index++) { + NSString *key = @""; + for (NSUInteger innerCount = 0; innerCount < count; innerCount++) { + key = [key stringByAppendingFormat:@"%c", (char)index]; + } + dict[key] = @1; + } + } + AssertOriginalMatchesEncodedString(dict, pythonOutput); +} + +@end diff --git a/Example/VOKBenkode.xcodeproj/project.pbxproj b/Example/VOKBenkode.xcodeproj/project.pbxproj index b4376fb..7582236 100644 --- a/Example/VOKBenkode.xcodeproj/project.pbxproj +++ b/Example/VOKBenkode.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; 6003F5BC195388D20070C39A /* VOKBenkodeBasicTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5BB195388D20070C39A /* VOKBenkodeBasicTests.m */; }; + 71E1AADC1A51FD7A004311E5 /* VOKBenkodeDictionaryOrdering.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E1AADB1A51FD7A004311E5 /* VOKBenkodeDictionaryOrdering.m */; }; 71FB774D1A4C967500A68427 /* VOKBenkodeS3rvacCppBencodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71FB774C1A4C967500A68427 /* VOKBenkodeS3rvacCppBencodingTests.m */; }; 71FB77501A4DDF0900A68427 /* VOKBenkodeAristotlePagaltzisTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71FB774F1A4DDF0900A68427 /* VOKBenkodeAristotlePagaltzisTests.m */; }; 71FB77521A4DF5F600A68427 /* VOKBenkodeLibTransmissionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71FB77511A4DF5F500A68427 /* VOKBenkodeLibTransmissionTests.m */; }; @@ -64,6 +65,7 @@ 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 6003F5BB195388D20070C39A /* VOKBenkodeBasicTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeBasicTests.m; sourceTree = ""; }; 606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = ""; }; + 71E1AADB1A51FD7A004311E5 /* VOKBenkodeDictionaryOrdering.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeDictionaryOrdering.m; sourceTree = ""; }; 71FB774C1A4C967500A68427 /* VOKBenkodeS3rvacCppBencodingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeS3rvacCppBencodingTests.m; sourceTree = ""; }; 71FB774E1A4C9AC400A68427 /* VOKBenkodeAssert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VOKBenkodeAssert.h; sourceTree = ""; }; 71FB774F1A4DDF0900A68427 /* VOKBenkodeAristotlePagaltzisTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeAristotlePagaltzisTests.m; sourceTree = ""; }; @@ -181,6 +183,7 @@ 71FB774F1A4DDF0900A68427 /* VOKBenkodeAristotlePagaltzisTests.m */, 71FB77511A4DF5F500A68427 /* VOKBenkodeLibTransmissionTests.m */, 71FB774C1A4C967500A68427 /* VOKBenkodeS3rvacCppBencodingTests.m */, + 71E1AADB1A51FD7A004311E5 /* VOKBenkodeDictionaryOrdering.m */, 6003F5B6195388D20070C39A /* Supporting Files */, ); path = Tests; @@ -386,6 +389,7 @@ 6003F5BC195388D20070C39A /* VOKBenkodeBasicTests.m in Sources */, 71FB77521A4DF5F600A68427 /* VOKBenkodeLibTransmissionTests.m in Sources */, 71FB774D1A4C967500A68427 /* VOKBenkodeS3rvacCppBencodingTests.m in Sources */, + 71E1AADC1A51FD7A004311E5 /* VOKBenkodeDictionaryOrdering.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 36054045345d9a084dab587908818000fedf5687 Mon Sep 17 00:00:00 2001 From: Isaac Greenspan Date: Mon, 29 Dec 2014 16:39:07 -0600 Subject: [PATCH 4/6] Leveraging NSData encoding for NSString encoding (which also fixes string length issues for UTF-8 encoding). --- Pod/Classes/VOKBenkode.m | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Pod/Classes/VOKBenkode.m b/Pod/Classes/VOKBenkode.m index 8caf021..c108852 100644 --- a/Pod/Classes/VOKBenkode.m +++ b/Pod/Classes/VOKBenkode.m @@ -30,10 +30,9 @@ + (NSData *)encode:(id)obj return result; } if ([obj isKindOfClass:[NSString class]]) { - NSMutableData *result = [[[NSString stringWithFormat:@"%@%c", @([obj length]), StringLengthDataSeparator] - dataUsingEncoding:NSASCIIStringEncoding] mutableCopy]; - [result appendData:[obj dataUsingEncoding:stringEncoding]]; - return result; + return [self encode:[obj dataUsingEncoding:stringEncoding] + stringEncoding:stringEncoding + error:error]; } if ([obj isKindOfClass:[NSNumber class]]) { return [[NSString stringWithFormat:@"%c%ld%c", NumberStartDelimiter, [obj longValue], EndDelimiter] From 2c9c89d532d526a134bf874229148715066cee30 Mon Sep 17 00:00:00 2001 From: Isaac Greenspan Date: Mon, 29 Dec 2014 16:39:36 -0600 Subject: [PATCH 5/6] Sort dictionary keys as NSData/binary, for spec compliance. --- Pod/Classes/VOKBenkode.m | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Pod/Classes/VOKBenkode.m b/Pod/Classes/VOKBenkode.m index c108852..96fec7d 100644 --- a/Pod/Classes/VOKBenkode.m +++ b/Pod/Classes/VOKBenkode.m @@ -57,11 +57,14 @@ + (NSData *)encode:(id)obj return result; } if ([obj isKindOfClass:[NSDictionary class]]) { - NSMutableData *result = [NSMutableData dataWithBytes:&DictionaryStartDelimiter length:1]; - NSArray *sortedKeys = [[obj allKeys] sortedArrayUsingSelector:@selector(compare:)]; - for (id key in sortedKeys) { - if (!([key isKindOfClass:[NSString class]] - || [key isKindOfClass:[NSData class]])) { + // Construct a dictionary mapping each key to its NSData representation. + NSMutableDictionary *keyDataByKey = [NSMutableDictionary dictionaryWithCapacity:[obj count]]; + for (id key in obj) { + if ([key isKindOfClass:[NSData class]]) { + keyDataByKey[key] = key; + } else if ([key isKindOfClass:[NSString class]]) { + keyDataByKey[key] = [key dataUsingEncoding:stringEncoding]; + } else { // The bencode spec says dictionary keys must be strings (NSData ~= bytestring, so...). if (error) { *error = [NSError errorWithDomain:VOKBenkodeErrorDomain @@ -70,6 +73,27 @@ + (NSData *)encode:(id)obj } return nil; } + } + + // Produce an array of the keys, sorted by their binary NSData representation. + NSArray *keys = [keyDataByKey.allKeys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) { + NSData *obj1 = keyDataByKey[key1]; + NSData *obj2 = keyDataByKey[key2]; + int result = memcmp(obj1.bytes, obj2.bytes, MIN(obj1.length, obj2.length)); + if (result < 0 + || (result == 0 && obj1.length < obj2.length)) { + return NSOrderedAscending; + } + if (result > 0 + || (result == 0 && obj1.length > obj2.length)) { + return NSOrderedDescending; + } + return NSOrderedSame; + }]; + + NSMutableData *result = [NSMutableData dataWithBytes:&DictionaryStartDelimiter length:1]; + + for (id key in keys) { NSError *innerError; NSData *data = [self encode:key stringEncoding:stringEncoding From 101919228b550a4b64893841dc4e631c74e9f08a Mon Sep 17 00:00:00 2001 From: Isaac Greenspan Date: Mon, 29 Dec 2014 17:17:14 -0600 Subject: [PATCH 6/6] Refactoring to move to separate internal encoding methods for each type and a static function for NSData comparison. --- Pod/Classes/VOKBenkode.m | 228 ++++++++++++++++++++++++--------------- 1 file changed, 142 insertions(+), 86 deletions(-) diff --git a/Pod/Classes/VOKBenkode.m b/Pod/Classes/VOKBenkode.m index 96fec7d..c566449 100644 --- a/Pod/Classes/VOKBenkode.m +++ b/Pod/Classes/VOKBenkode.m @@ -15,6 +15,20 @@ static char const EndDelimiter = 'e'; static char const StringLengthDataSeparator = ':'; +static NSComparisonResult CompareNSData(NSData *obj1, NSData *obj2) +{ + int result = memcmp(obj1.bytes, obj2.bytes, MIN(obj1.length, obj2.length)); + if (result < 0 + || (result == 0 && obj1.length < obj2.length)) { + return NSOrderedAscending; + } + if (result > 0 + || (result == 0 && obj1.length > obj2.length)) { + return NSOrderedDescending; + } + return NSOrderedSame; +} + @implementation VOKBenkode #pragma mark - Encoding @@ -24,100 +38,29 @@ + (NSData *)encode:(id)obj error:(NSError **)error { if ([obj isKindOfClass:[NSData class]]) { - NSMutableData *result = [[[NSString stringWithFormat:@"%@%c", @([obj length]), StringLengthDataSeparator] - dataUsingEncoding:NSASCIIStringEncoding] mutableCopy]; - [result appendData:obj]; - return result; + return [self encodeData:obj + stringEncoding:stringEncoding + error:error]; } if ([obj isKindOfClass:[NSString class]]) { - return [self encode:[obj dataUsingEncoding:stringEncoding] - stringEncoding:stringEncoding - error:error]; + return [self encodeString:obj + stringEncoding:stringEncoding + error:error]; } if ([obj isKindOfClass:[NSNumber class]]) { - return [[NSString stringWithFormat:@"%c%ld%c", NumberStartDelimiter, [obj longValue], EndDelimiter] - dataUsingEncoding:NSASCIIStringEncoding]; + return [self encodeNumber:obj + stringEncoding:stringEncoding + error:error]; } if ([obj isKindOfClass:[NSArray class]]) { - NSMutableData *result = [NSMutableData dataWithBytes:&ArrayStartDelimiter length:1]; - for (id innerObj in obj) { - NSError *innerError; - NSData *data = [self encode:innerObj - stringEncoding:stringEncoding - error:&innerError]; - if (!data) { - if (error) { - *error = innerError; - } - return nil; - } - [result appendData:data]; - } - [result appendBytes:&EndDelimiter length:1]; - return result; + return [self encodeArray:obj + stringEncoding:stringEncoding + error:error]; } if ([obj isKindOfClass:[NSDictionary class]]) { - // Construct a dictionary mapping each key to its NSData representation. - NSMutableDictionary *keyDataByKey = [NSMutableDictionary dictionaryWithCapacity:[obj count]]; - for (id key in obj) { - if ([key isKindOfClass:[NSData class]]) { - keyDataByKey[key] = key; - } else if ([key isKindOfClass:[NSString class]]) { - keyDataByKey[key] = [key dataUsingEncoding:stringEncoding]; - } else { - // The bencode spec says dictionary keys must be strings (NSData ~= bytestring, so...). - if (error) { - *error = [NSError errorWithDomain:VOKBenkodeErrorDomain - code:VOKBenkodeErrorDictionaryKeyNotString - userInfo:nil]; - } - return nil; - } - } - - // Produce an array of the keys, sorted by their binary NSData representation. - NSArray *keys = [keyDataByKey.allKeys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) { - NSData *obj1 = keyDataByKey[key1]; - NSData *obj2 = keyDataByKey[key2]; - int result = memcmp(obj1.bytes, obj2.bytes, MIN(obj1.length, obj2.length)); - if (result < 0 - || (result == 0 && obj1.length < obj2.length)) { - return NSOrderedAscending; - } - if (result > 0 - || (result == 0 && obj1.length > obj2.length)) { - return NSOrderedDescending; - } - return NSOrderedSame; - }]; - - NSMutableData *result = [NSMutableData dataWithBytes:&DictionaryStartDelimiter length:1]; - - for (id key in keys) { - NSError *innerError; - NSData *data = [self encode:key - stringEncoding:stringEncoding - error:&innerError]; - if (!data) { - if (error) { - *error = innerError; - } - return nil; - } - [result appendData:data]; - data = [self encode:obj[key] - stringEncoding:stringEncoding - error:&innerError]; - if (!data) { - if (error) { - *error = innerError; - } - return nil; - } - [result appendData:data]; - } - [result appendBytes:&EndDelimiter length:1]; - return result; + return [self encodeDictionary:obj + stringEncoding:stringEncoding + error:error]; } // Unknown data type. @@ -151,6 +94,119 @@ + (NSData *)encode:(id)obj error:NULL]; } +#pragma mark Encoding Primitives + ++ (NSData *)encodeDictionary:(NSDictionary *)dictionary + stringEncoding:(NSStringEncoding)stringEncoding + error:(NSError **)error +{ + NSAssert([dictionary isKindOfClass:[NSDictionary class]], @"Input is not a dictionary."); + // Construct a dictionary mapping each key to its NSData representation. + NSMutableDictionary *keyDataByKey = [NSMutableDictionary dictionaryWithCapacity:[dictionary count]]; + for (id key in dictionary) { + if ([key isKindOfClass:[NSData class]]) { + keyDataByKey[key] = key; + } else if ([key isKindOfClass:[NSString class]]) { + keyDataByKey[key] = [key dataUsingEncoding:stringEncoding]; + } else { + // The bencode spec says dictionary keys must be strings (NSData ~= bytestring, so...). + if (error) { + *error = [NSError errorWithDomain:VOKBenkodeErrorDomain + code:VOKBenkodeErrorDictionaryKeyNotString + userInfo:nil]; + } + return nil; + } + } + + // Produce an array of the keys, sorted by their binary NSData representation. + NSArray *keys = [keyDataByKey.allKeys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) { + return CompareNSData(keyDataByKey[key1], keyDataByKey[key2]); + }]; + + NSMutableData *result = [NSMutableData dataWithBytes:&DictionaryStartDelimiter length:1]; + + for (id key in keys) { + NSError *innerError; + NSData *data = [self encode:key + stringEncoding:stringEncoding + error:&innerError]; + if (!data) { + if (error) { + *error = innerError; + } + return nil; + } + [result appendData:data]; + data = [self encode:dictionary[key] + stringEncoding:stringEncoding + error:&innerError]; + if (!data) { + if (error) { + *error = innerError; + } + return nil; + } + [result appendData:data]; + } + [result appendBytes:&EndDelimiter length:1]; + return result; +} + ++ (NSData *)encodeArray:(NSArray *)array + stringEncoding:(NSStringEncoding)stringEncoding + error:(NSError **)error +{ + NSAssert([array isKindOfClass:[NSArray class]], @"Input is not an array."); + NSMutableData *result = [NSMutableData dataWithBytes:&ArrayStartDelimiter length:1]; + for (id innerObj in array) { + NSError *innerError; + NSData *data = [self encode:innerObj + stringEncoding:stringEncoding + error:&innerError]; + if (!data) { + if (error) { + *error = innerError; + } + return nil; + } + [result appendData:data]; + } + [result appendBytes:&EndDelimiter length:1]; + return result; +} + ++ (NSData *)encodeNumber:(NSNumber *)number + stringEncoding:(NSStringEncoding)stringEncoding + error:(NSError **)error +{ + NSAssert([number isKindOfClass:[NSNumber class]], @"Input is not a number."); + return [[NSString stringWithFormat:@"%c%ld%c", NumberStartDelimiter, [number longValue], EndDelimiter] + dataUsingEncoding:NSASCIIStringEncoding]; +} + ++ (NSData *)encodeString:(NSString *)string + stringEncoding:(NSStringEncoding)stringEncoding + error:(NSError **)error +{ + NSAssert([string isKindOfClass:[NSString class]], @"Input is not a string."); + return [self encode:[string dataUsingEncoding:stringEncoding] + stringEncoding:stringEncoding + error:error]; +} + ++ (NSData *)encodeData:(NSData *)data + stringEncoding:(NSStringEncoding)stringEncoding + error:(NSError **)error +{ + NSAssert([data isKindOfClass:[NSData class]], @"Input is not data."); + NSMutableData *result = [[[NSString stringWithFormat:@"%@%c", @([data length]), StringLengthDataSeparator] + dataUsingEncoding:NSASCIIStringEncoding] + mutableCopy]; + [result appendData:data]; + return result; +} + #pragma mark - Decoding + (id)decode:(NSData *)data