diff --git a/KSFileUtilities.xcodeproj/project.pbxproj b/KSFileUtilities.xcodeproj/project.pbxproj index 5398ed5..16f7508 100644 --- a/KSFileUtilities.xcodeproj/project.pbxproj +++ b/KSFileUtilities.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 27A90103165C003A00D23C4B /* nameprep.c in Sources */ = {isa = PBXBuildFile; fileRef = 27A90101165C003A00D23C4B /* nameprep.c */; }; 27A90106165C005700D23C4B /* puny.c in Sources */ = {isa = PBXBuildFile; fileRef = 27A90104165C005600D23C4B /* puny.c */; }; 27DA2577148E641100209E50 /* TestKSURLUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 27DA2576148E641100209E50 /* TestKSURLUtilities.m */; }; + 27DC29041859BC2C0089D717 /* KSURLQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = 27DC29031859BC2C0089D717 /* KSURLQuery.m */; }; 27F0501C178829220019FC07 /* KSURLComponents.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F0501B178829180019FC07 /* KSURLComponents.m */; }; 27F0501E1788399E0019FC07 /* TestKSURLComponents.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F0501D1788399E0019FC07 /* TestKSURLComponents.m */; }; 27F86FBF164FBA9D003608CC /* KSEncodeURLString.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F86FBE164FBA9D003608CC /* KSEncodeURLString.m */; }; @@ -50,6 +51,8 @@ 27A90104165C005600D23C4B /* puny.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = puny.c; path = IFUnicodeURL/IDNSDK/puny.c; sourceTree = ""; }; 27A90105165C005700D23C4B /* puny.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = puny.h; path = IFUnicodeURL/IDNSDK/puny.h; sourceTree = ""; }; 27DA2576148E641100209E50 /* TestKSURLUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestKSURLUtilities.m; sourceTree = ""; }; + 27DC29021859BC2C0089D717 /* KSURLQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KSURLQuery.h; sourceTree = SOURCE_ROOT; }; + 27DC29031859BC2C0089D717 /* KSURLQuery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSURLQuery.m; sourceTree = SOURCE_ROOT; }; 27F0501A178829180019FC07 /* KSURLComponents.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSURLComponents.h; sourceTree = SOURCE_ROOT; }; 27F0501B178829180019FC07 /* KSURLComponents.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSURLComponents.m; sourceTree = SOURCE_ROOT; }; 27F0501D1788399E0019FC07 /* TestKSURLComponents.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestKSURLComponents.m; sourceTree = ""; }; @@ -124,6 +127,8 @@ 27246F10165BF1D2001B638E /* IFUnicodeURL */, 27F0501A178829180019FC07 /* KSURLComponents.h */, 27F0501B178829180019FC07 /* KSURLComponents.m */, + 27DC29021859BC2C0089D717 /* KSURLQuery.h */, + 27DC29031859BC2C0089D717 /* KSURLQuery.m */, 27422A77180D609B00136F43 /* KSMailtoURLs.h */, 27422A78180D609B00136F43 /* KSMailtoURLs.m */, 27F86FBD164FBA9D003608CC /* KSEncodeURLString.h */, @@ -296,6 +301,7 @@ EE43C85213898D7B008ABD16 /* KSWebLocation.m in Sources */, EE43C85313898D7B008ABD16 /* KSWebLocationPasteboardUtilities.m in Sources */, 27F0501E1788399E0019FC07 /* TestKSURLComponents.m in Sources */, + 27DC29041859BC2C0089D717 /* KSURLQuery.m in Sources */, 27F0501C178829220019FC07 /* KSURLComponents.m in Sources */, EE43C85413898D7B008ABD16 /* KSWorkspaceUtilities.m in Sources */, 27DA2577148E641100209E50 /* TestKSURLUtilities.m in Sources */, diff --git a/KSMailtoURLs.m b/KSMailtoURLs.m index 752e250..7a4585f 100644 --- a/KSMailtoURLs.m +++ b/KSMailtoURLs.m @@ -26,7 +26,7 @@ #import "KSMailtoURLs.h" -#import "KSURLQueryUtilities.h" +#import "KSURLQuery.h" NSString *KSURLMailtoScheme = @"mailto"; @@ -52,7 +52,7 @@ + (NSURL *)ks_mailtoURLWithEmailAddress:(NSString *)address headerLines:(NSDicti if (headers) { - NSString *query = [self ks_queryWithParameters:headers]; + NSString *query = [KSURLQuery encodeParameters:headers]; if ([query length]) { string = [string stringByAppendingFormat:@"?%@", query]; @@ -72,7 +72,7 @@ - (NSDictionary *)ks_mailHeaderLines; if (queryIndicatorRange.location != NSNotFound) { NSString *query = [urlString substringFromIndex:NSMaxRange(queryIndicatorRange)]; - return [NSURL ks_parametersOfQuery:query]; + return [KSURLQuery decodeString:query options:0]; } return nil; diff --git a/KSURLComponents.h b/KSURLComponents.h index 070a494..d433c0a 100644 --- a/KSURLComponents.h +++ b/KSURLComponents.h @@ -26,6 +26,7 @@ #import + @interface KSURLComponents : NSObject { @private @@ -117,13 +118,12 @@ @property (nonatomic, copy) NSString *query; @property (nonatomic, copy) NSString *fragment; -// Getting these properties retains any percent encoding these components may have. Setting these properties is currently not supported as I am lazy and doing so is rarely useful. If you do have a use case, please send me a pull request or file an issue on GitHub. +// Getting these properties retains any percent encoding these components may have. Setting most of these properties is currently not supported as I am lazy and doing so is rarely useful. If you do have a use case, please send me a pull request or file an issue on GitHub. @property (nonatomic, copy, readonly) NSString *percentEncodedUser; @property (nonatomic, copy, readonly) NSString *percentEncodedPassword; @property (nonatomic, copy, readonly) NSString *percentEncodedHost; @property (nonatomic, copy, readonly) NSString *percentEncodedPath; -@property (nonatomic, copy, readonly) NSString *percentEncodedQuery; +@property (nonatomic, copy) NSString *percentEncodedQuery; @property (nonatomic, copy, readonly) NSString *percentEncodedFragment; - @end diff --git a/KSURLComponents.m b/KSURLComponents.m index 8af3003..c06206e 100644 --- a/KSURLComponents.m +++ b/KSURLComponents.m @@ -32,7 +32,6 @@ @interface KSURLComponents () @property (nonatomic, copy, readwrite) NSString *percentEncodedPassword; @property (nonatomic, copy, readwrite) NSString *percentEncodedHost; @property (nonatomic, copy, readwrite) NSString *percentEncodedPath; -@property (nonatomic, copy, readwrite) NSString *percentEncodedQuery; @property (nonatomic, copy, readwrite) NSString *percentEncodedFragment; @end @@ -447,6 +446,12 @@ - (void)setPath:(NSString *)path; } @synthesize percentEncodedQuery = _queryComponent; +- (void)setPercentEncodedQuery:(NSString *)percentEncodedQuery; +{ + // FIXME: Check the query is valid + percentEncodedQuery = [percentEncodedQuery copy]; + [_queryComponent release]; _queryComponent = percentEncodedQuery; +} - (NSString *)query; { return [self.percentEncodedQuery stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; diff --git a/KSURLQuery.h b/KSURLQuery.h new file mode 100644 index 0000000..51fc46a --- /dev/null +++ b/KSURLQuery.h @@ -0,0 +1,119 @@ +// +// KSURLQuery.h +// KSFileUtilities +// +// Created by Mike on 12/12/2013. +// Copyright (c) 2013 Karelia Software. All rights reserved. +// + +#import + + +typedef NS_OPTIONS(NSUInteger, KSURLQueryParameterDecodingOptions) { + KSURLQueryParameterDecodingPlusAsSpace = 1UL << 0, // + characters are interpreted as spaces, rather than regular + symbols +}; + + +@interface KSURLQuery : NSObject +{ + @private + NSString *_percentEncodedString; +} + +#pragma mark Convenience ++ (NSString *)encodeParameters:(NSDictionary *)parameters; ++ (NSDictionary *)decodeString:(NSString *)percentEncodedQuery options:(KSURLQueryParameterDecodingOptions)options; ++ (NSDictionary *)parametersFromURL:(NSURL *)url options:(KSURLQueryParameterDecodingOptions)options; + + +#pragma mark Creating a KSURLQuery ++ (instancetype)queryWithURL:(NSURL *)url; +- initWithPercentEncodedString:(NSString *)string; + + +#pragma mark Decoding Parameters + +/** + Converts the query into a dictionary representation. + + For example: + + http://example.com?key=value&foo=bar + + can be interpreted as: + + @{ @"key" : @"value", @"foo" : @"bar" } + + Keys and values are percent decoded according to `options`. + + If you have a query which doesn't match `NSDictionary`'s design, drop down to + the primitive `-enumerateParametersWithOptions:usingBlock:` method instead. + + @param options A mask that specifies options for parameter decoding. + @return `nil` if query doesn't neatly fit an `NSDictionary` representation + */ +- (NSDictionary *)parametersWithOptions:(KSURLQueryParameterDecodingOptions)options; + +/** + Enumerates the receiver's parameters, handling cases where an NSDictionary representation doesn't suffice. + + * Parameters are reported in the order they appear in the URL + * Keys and values are percent decoded for your convenience + * Parameters without a value are reported as `nil` + * Duplicate parameters are correctly reported too + + @param options A mask that specifies options for parameter decoding. + @param block A block called for each parameter of the query. + */ +- (void)enumerateParametersWithOptions:(KSURLQueryParameterDecodingOptions)options usingBlock:(void (^)(NSString *key, NSString *value, BOOL *stop))block __attribute((nonnull(2))); + + +#pragma mark Encoding Parameters + +/** + Replaces any existing query by encoding the `parameters` dictionary. + + For example: + + @{ @"key" : @"value", @"foo" : @"bar" } + + can be represented as: + + http://example.com?key=value&foo=bar + + See `-addParameter:value:` for full details on encoding of keys and values. + + Parameters are encoded in alphabetical order (of keys) for consistency across + platforms and OS releases. If ordering is important to your use case, or you + particularly need to eke out a little more performance, use `-addParameter:value:` + directly instead. + + @param parameters A dictionary to encode, whose keys and values are all strings. + */ +- (void)setParameters:(NSDictionary *)parameters; + +/** + Adds an extra parameter to end of the receiver. + + Enables the query to be built up piece-by-piece. This can be especially useful + when the ordering of parameters is critical, and/or parameters appear more than + once. + + Both the key and value are percent encoded. + + `value` is sent a `-description` message to obtain a string representation + suitable for encoding, enabling objects like `NSNumber`s to be easily encoded, + as well as strings. + */ +- (void)addParameter:(NSString *)key value:(id )value __attribute((nonnull(1))); + +/** + @result The encoded representation of the receiver. + + Generally you then pass the result of this method to `NSURLComponents.percentEncodedQuery` + (or `KSURLComponents`) to build up a full URL. + */ +@property(atomic, readonly, copy) NSString *percentEncodedString; + + +@end diff --git a/KSURLQuery.m b/KSURLQuery.m new file mode 100644 index 0000000..fb98afe --- /dev/null +++ b/KSURLQuery.m @@ -0,0 +1,197 @@ +// +// KSURLQuery.m +// KSFileUtilities +// +// Created by Mike on 12/12/2013. +// Copyright (c) 2013 Karelia Software. All rights reserved. +// + +#import "KSURLQuery.h" + + +@interface KSURLQuery () +@property(atomic, readwrite, copy) NSString *percentEncodedString; +@end + + +@implementation KSURLQuery + +#pragma mark Convenience + ++ (NSDictionary *)parametersFromURL:(NSURL *)url options:(KSURLQueryParameterDecodingOptions)options; +{ + return [[self queryWithURL:url] parametersWithOptions:options]; +} + ++ (NSDictionary *)decodeString:(NSString *)string options:(KSURLQueryParameterDecodingOptions)options; +{ + KSURLQuery *query = [[self alloc] initWithPercentEncodedString:string]; + NSDictionary *result = [query parametersWithOptions:options]; + [query release]; + return result; +} + ++ (NSString *)encodeParameters:(NSDictionary *)parameters; +{ + KSURLQuery *query = [[KSURLQuery alloc] init]; + [query setParameters:parameters]; + NSString *result = query.percentEncodedString; + [query release]; + return result; +} + +#pragma mark + ++ (instancetype)queryWithURL:(NSURL *)url; +{ + // Always resolve, since unlike paths there's no way for two queries to be in some way concatenated + CFURLRef cfURL = CFURLCopyAbsoluteURL((CFURLRef)url); + + NSString *string = (NSString *)CFURLCopyQueryString(cfURL, + NULL); // leave unescaped + + KSURLQuery *result = [[self alloc] initWithPercentEncodedString:string]; + [string release]; + CFRelease(cfURL); + return [result autorelease]; +} + +- initWithPercentEncodedString:(NSString *)string; +{ + if (self = [self init]) + { + _percentEncodedString = [string copy]; + } + return self; +} + +- (void)dealloc +{ + [_percentEncodedString release]; + [super dealloc]; +} + +@synthesize percentEncodedString = _percentEncodedString; + +- (NSDictionary *)parametersWithOptions:(KSURLQueryParameterDecodingOptions)options; +{ + __block NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + [self enumerateParametersWithOptions:options usingBlock:^(NSString *key, NSString *value, BOOL *stop) { + + // Bail if doesn't fit dictionary paradigm + if (!value || [result objectForKey:key]) + { + *stop = YES; + result = nil; + return; + } + + [result setObject:value forKey:key]; + }]; + + return result; +} + +- (void)enumerateParametersWithOptions:(KSURLQueryParameterDecodingOptions)options usingBlock:(void (^)(NSString *, NSString *, BOOL *))block; +{ + BOOL stop = NO; + + NSString *query = self.percentEncodedString; // we'll do our own decoding after separating components + NSRange searchRange = NSMakeRange(0, query.length); + + while (!stop) + { + NSRange keySeparatorRange = [query rangeOfString:@"=" options:NSLiteralSearch range:searchRange]; + if (keySeparatorRange.location == NSNotFound) keySeparatorRange = NSMakeRange(NSMaxRange(searchRange), 0); + + NSRange keyRange = NSMakeRange(searchRange.location, keySeparatorRange.location - searchRange.location); + NSString *key = [query substringWithRange:keyRange]; + + NSString *value = nil; + if (keySeparatorRange.length) // there might be no value, so report as nil + { + searchRange = NSMakeRange(NSMaxRange(keySeparatorRange), query.length - NSMaxRange(keySeparatorRange)); + + NSRange valueSeparatorRange = [query rangeOfString:@"&" options:NSLiteralSearch range:searchRange]; + if (valueSeparatorRange.location == NSNotFound) + { + valueSeparatorRange.location = NSMaxRange(searchRange); + stop = YES; + } + + NSRange valueRange = NSMakeRange(searchRange.location, valueSeparatorRange.location - searchRange.location); + value = [query substringWithRange:valueRange]; + + searchRange = NSMakeRange(NSMaxRange(valueSeparatorRange), query.length - NSMaxRange(valueSeparatorRange)); + } + else + { + stop = YES; + } + + if (options & KSURLQueryParameterDecodingPlusAsSpace) + { + key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + } + + block([key stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding], + [value stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding], + &stop); + } +} + +- (void)setParameters:(NSDictionary *)parameters; +{ + if (!parameters) + { + self.percentEncodedString = nil; + return; + } + + if (!parameters.count) + { + self.percentEncodedString = @""; + return; + } + + // Build the list of parameters as a string + for (NSString *aKey in [parameters.allKeys sortedArrayUsingSelector:@selector(compare:)]) + { + [self addParameter:aKey value:[parameters objectForKey:aKey]]; + } +} + +- (void)addParameter:(NSString *)key value:(id )value; +{ + CFStringRef escapedKey = CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)key, NULL, CFSTR("+=&#"), kCFStringEncodingUTF8); + // Escape + for safety as some backends interpret it as a space + // = indicates the start of value, so must be escaped + // & indicates the start of next parameter, so must be escaped + // # indicates the start of fragment, so must be escaped + + CFStringRef escapedValue = CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)value.description, NULL, CFSTR("+&#"), kCFStringEncodingUTF8); + // Escape + for safety as some backends interpret it as a space + // = is allowed in values, as there's no further value to indicate + // & indicates the start of next parameter, so must be escaped + // # indicates the start of fragment, so must be escaped + + // Append the parameter and its value to the full query string + NSString *query = self.percentEncodedString; + if (query) + { + query = [query stringByAppendingFormat:@"&%@=%@", escapedKey, escapedValue]; + } + else + { + query = [NSString stringWithFormat:@"%@=%@", escapedKey, escapedValue]; + } + + self.percentEncodedString = query; + + CFRelease(escapedKey); + CFRelease(escapedValue); +} + +@end diff --git a/KSURLQueryUtilities.h b/KSURLQueryUtilities.h index 05d6094..8334e14 100644 --- a/KSURLQueryUtilities.h +++ b/KSURLQueryUtilities.h @@ -30,14 +30,14 @@ @interface NSURL (KSURLQueryUtilities) // It's common to use the query part of a URL for a dictionary-like series of parameters. This method will decode that for you, including handling strings which were escaped to fit the scheme -- (NSDictionary *)ks_queryParameters; +- (NSDictionary *)ks_queryParameters __deprecated_msg("use KSURLQuery instead"); // To do the reverse, construct a dictonary for the query and pass into either of these methods. You can base the result off of an existing URL, or specify all the components. -- (NSURL *)ks_URLWithQueryParameters:(NSDictionary *)parameters; +- (NSURL *)ks_URLWithQueryParameters:(NSDictionary *)parameters __deprecated_msg("use KSURLQuery instead"); + (NSURL *)ks_URLWithScheme:(NSString *)scheme host:(NSString *)host path:(NSString *)path - queryParameters:(NSDictionary *)parameters; + queryParameters:(NSDictionary *)parameters __deprecated_msg("use KSURLQuery instead"); // Primitive methods for if you need tighter control over handling query dictionaries + (NSString *)ks_queryWithParameters:(NSDictionary *)parameters; @@ -49,7 +49,7 @@ @interface NSString (KSURLQueryUtilities) // Follows RFC2396, section 3.4 -- (NSString *)ks_stringByAddingQueryComponentPercentEscapes; -- (NSString *)ks_stringByReplacingQueryComponentPercentEscapes; +- (NSString *)ks_stringByAddingQueryComponentPercentEscapes __deprecated_msg("use KSURLQuery instead; keys and values have different encoding needs"); +- (NSString *)ks_stringByReplacingQueryComponentPercentEscapes __deprecated_msg("use KSURLQuery instead; not all servers use + to encode a space"); @end diff --git a/KSURLUtilities.m b/KSURLUtilities.m index f28fc6b..82f3625 100644 --- a/KSURLUtilities.m +++ b/KSURLUtilities.m @@ -266,13 +266,29 @@ - (NSString *)ks_stringRelativeToURL:(NSURL *)URL NSString *myHost = [self host]; if (!myHost) { + // Host-less file URLs get special treatment, as if they're localhost. Maybe that could/should + // be generalised but I'm not in a position to presently. + if (self.isFileURL) { + myHost = @"localhost"; + } + else { + // If self is an empty URL, there's no way to get to it. Falls through to here; return nil NSString *result = [self absoluteString]; return ([result length] ? result : nil); + } } NSString *otherHost = [URL host]; - if (!otherHost) BAIL; + if (!otherHost) { + // Host-less file URLs get special treatment, as if they're localhost + if (URL.isFileURL) { + otherHost = @"localhost"; + } + else { + BAIL; + } + } if ([myHost caseInsensitiveCompare:otherHost] != NSOrderedSame) BAIL; diff --git a/TestKSFileUtilities/TestKSURLComponents.m b/TestKSFileUtilities/TestKSURLComponents.m index 6b583dd..04f7bc8 100644 --- a/TestKSFileUtilities/TestKSURLComponents.m +++ b/TestKSFileUtilities/TestKSURLComponents.m @@ -9,6 +9,7 @@ #import #import "KSURLComponents.h" +#import "KSURLQuery.h" @interface TestKSURLComponents : SenTestCase @@ -665,4 +666,157 @@ - (void)testCopying; [components2 release]; } +#pragma mark Query Parameters + +- (void)testNilQuery; +{ + KSURLQuery *query = [[KSURLQuery alloc] init]; + NSDictionary *parameters = [query parametersWithOptions:0]; + + STAssertNil(parameters, nil); +} + +- (void)testEmptyQuery; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertNil(parameters, nil); + + __block BOOL blockCalled = NO; + [query enumerateParametersWithOptions:0 usingBlock:^(NSString *key, NSString *value, BOOL *stop) { + STAssertEqualObjects(key, @"", nil); + STAssertNil(value, nil); + blockCalled = YES; + }]; + STAssertTrue(blockCalled, nil); +} + +- (void)testNonParameterisedQuery; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?query"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertNil(parameters, nil); + + __block BOOL blockCalled = NO; + [query enumerateParametersWithOptions:0 usingBlock:^(NSString *key, NSString *value, BOOL *stop) { + STAssertEqualObjects(key, @"query", nil); + STAssertNil(value, nil); + blockCalled = YES; + }]; + STAssertTrue(blockCalled, nil); +} + +- (void)testSingleQueryParameter; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?key=value"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertEqualObjects(parameters, @{ @"key" : @"value" }, nil); +} + +- (void)testQueryParameters; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?key=value&foo=bar"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + NSDictionary *expected = @{ @"key" : @"value", @"foo" : @"bar" }; + STAssertEqualObjects(parameters, expected, nil); +} + +- (void)testEmptyQueryParameterKey; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?=value"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertEqualObjects(parameters, @{ @"" : @"value" }, nil); +} + +- (void)testEmptyQueryParameterValue; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?key="]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertEqualObjects(parameters, @{ @"key" : @"" }, nil); +} + +- (void)testRepeatedKeys; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?key=value&key=value2"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertNil(parameters, nil); + + __block int blockCalled = 0; + [query enumerateParametersWithOptions:0 usingBlock:^(NSString *key, NSString *value, BOOL *stop) { + STAssertEqualObjects(key, @"key", nil); + STAssertEqualObjects(value, (blockCalled ? @"value2" : @"value"), nil); + ++blockCalled; + }]; + STAssertEquals(blockCalled, 2, nil); +} + +- (void)testEqualsSignInQueryParameterValue; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?key=val=ue"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertEqualObjects(parameters, @{ @"key" : @"val=ue" }, nil); +} + +- (void)testQueryParameterUnescaping; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"scheme://host?k%2Fy=va%2Fue"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertEqualObjects(parameters, @{ @"k/y" : @"va/ue" }, nil); +} + +- (void)testPlusSymbolInQueryParameters; +{ + KSURLQuery *query = [KSURLQuery queryWithURL:[NSURL URLWithString:@"?size=%7B64%2C+64%7D"]]; + + NSDictionary *parameters = [query parametersWithOptions:0]; + STAssertEqualObjects(parameters, @{ @"size" : @"{64,+64}" }, nil); + + parameters = [query parametersWithOptions:KSURLQueryParameterDecodingPlusAsSpace]; + STAssertEqualObjects(parameters, @{ @"size" : @"{64, 64}" }, nil); +} + +- (void)testEncodeNilQueryParameters; +{ + KSURLQuery *query = [[KSURLQuery alloc] init]; + [query setParameters:nil]; + STAssertNil(query.percentEncodedString, nil); +} + +- (void)testEncodeEmptyQueryParameters; +{ + KSURLQuery *query = [[KSURLQuery alloc] init]; + [query setParameters:@{ }]; + STAssertEqualObjects(query.percentEncodedString, @"", nil); +} + +- (void)testEncodeQueryParameter; +{ + KSURLQuery *query = [[KSURLQuery alloc] init]; + [query setParameters:@{ @"key" : @"value" }]; + STAssertEqualObjects(query.percentEncodedString, @"key=value", nil); +} + +- (void)testEncodeQueryParameters; +{ + KSURLQuery *query = [[KSURLQuery alloc] init]; + [query setParameters:@{ @"key" : @"value", @"key2" : @"value2" }]; + STAssertEqualObjects(query.percentEncodedString, @"key=value&key2=value2", nil); +} + +- (void)testEncodeQueryParameterEscaping; +{ + KSURLQuery *query = [[KSURLQuery alloc] init]; + [query setParameters:@{ @"!*'();:@&=+$,/?#[]" : @"!*'();:@&=+$,/?#[]" }]; + STAssertEqualObjects(query.percentEncodedString, @"!*'();:@%26%3D%2B$,/?%23%5B%5D=!*'();:@%26=%2B$,/?%23%5B%5D", nil); +} + @end diff --git a/TestKSFileUtilities/TestKSURLUtilities.m b/TestKSFileUtilities/TestKSURLUtilities.m index 86ff9d4..b15da5e 100644 --- a/TestKSFileUtilities/TestKSURLUtilities.m +++ b/TestKSFileUtilities/TestKSURLUtilities.m @@ -142,6 +142,10 @@ - (void)testURLRelativeToURL [self checkURL:URL(@"http://example.com/foo/bar") relativeToURL:URL(@"http://example.com/bar/foo%2F/") againstExpectedResult:@"../../foo/bar"]; + // File URLs + [self checkURL:URL(@"file:///foo/bar/baz") relativeToURL:URL(@"file:///foo/bar/") againstExpectedResult:@"baz"]; + + // Crashed at one point STAssertEqualObjects([URL(@"") ks_stringRelativeToURL:URL(@"http://example.com/foo/")], nil, nil);