Skip to content

Commit

Permalink
Merge pull request #5 from vokal-isaac/master
Browse files Browse the repository at this point in the history
Dictionary Ordering
  • Loading branch information
chillpop committed Dec 30, 2014
2 parents 566251b + 1019192 commit 56fb841
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 66 deletions.
82 changes: 82 additions & 0 deletions Example/Tests/VOKBenkodeDictionaryOrdering.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// VOKBenkodeDictionaryOrdering.m
// VOKBenkode
//
// Created by Isaac Greenspan on 12/29/14.
// Copyright (c) 2014 VOKAL Interactive. All rights reserved.
//

#import <XCTest/XCTest.h>

#import <VOKBenkode.h>

#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:?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:?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
4 changes: 4 additions & 0 deletions Example/VOKBenkode.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -64,6 +65,7 @@
6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
6003F5BB195388D20070C39A /* VOKBenkodeBasicTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeBasicTests.m; sourceTree = "<group>"; };
606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = "<group>"; };
71E1AADB1A51FD7A004311E5 /* VOKBenkodeDictionaryOrdering.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeDictionaryOrdering.m; sourceTree = "<group>"; };
71FB774C1A4C967500A68427 /* VOKBenkodeS3rvacCppBencodingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeS3rvacCppBencodingTests.m; sourceTree = "<group>"; };
71FB774E1A4C9AC400A68427 /* VOKBenkodeAssert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VOKBenkodeAssert.h; sourceTree = "<group>"; };
71FB774F1A4DDF0900A68427 /* VOKBenkodeAristotlePagaltzisTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VOKBenkodeAristotlePagaltzisTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -181,6 +183,7 @@
71FB774F1A4DDF0900A68427 /* VOKBenkodeAristotlePagaltzisTests.m */,
71FB77511A4DF5F500A68427 /* VOKBenkodeLibTransmissionTests.m */,
71FB774C1A4C967500A68427 /* VOKBenkodeS3rvacCppBencodingTests.m */,
71E1AADB1A51FD7A004311E5 /* VOKBenkodeDictionaryOrdering.m */,
6003F5B6195388D20070C39A /* Supporting Files */,
);
path = Tests;
Expand Down Expand Up @@ -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;
};
Expand Down
2 changes: 2 additions & 0 deletions Pod/Classes/VOKBenkode.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
219 changes: 154 additions & 65 deletions Pod/Classes/VOKBenkode.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,77 +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]]) {
NSMutableData *result = [[[NSString stringWithFormat:@"%@%c", @([obj length]), StringLengthDataSeparator]
dataUsingEncoding:NSASCIIStringEncoding] mutableCopy];
[result appendData:[obj dataUsingEncoding:stringEncoding]];
return result;
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]]) {
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]])) {
// The bencode spec says dictionary keys must be strings (NSData ~= bytestring, so...).
if (error) {
*error = [NSError errorWithDomain:VOKBenkodeErrorDomain
code:VOKBenkodeErrorDictionaryKeyNotString
userInfo:nil];
}
return nil;
}
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.
Expand Down Expand Up @@ -128,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
Expand Down Expand Up @@ -472,8 +551,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
2 changes: 1 addition & 1 deletion VOKBenkode.podspec
Original file line number Diff line number Diff line change
@@ -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' }
Expand Down

0 comments on commit 56fb841

Please sign in to comment.