Source/TNLURLCoding.m (329 lines of code) (raw):

// // TNLURLCoding.m // TwitterNetworkLayer // // Created on 7/28/14. // Copyright © 2020 Twitter. All rights reserved. // #import "NSNumber+TNLURLCoding.h" #import "TNL_Project.h" #import "TNLURLCoding.h" NS_ASSUME_NONNULL_BEGIN static NSString * __nullable TNLStringValue(id object, TNLURLEncodingOptions options, NSString * __nullable contextKey); static NSString *TNLNumberStringValue(NSNumber *number); static id __nullable TNLURLEncodableValue(id value, TNLURLEncodableDictionaryOptions options, NSString * __nullable contextKey); static void TNLAppendKeyValuePairToMutableString(NSMutableString *string, NSString *key, NSString *value, TNLURLEncodingOptions options); static void TNLAppendParameterDelimeterIfNecessary(NSMutableString *parameterString, BOOL *inoutIsFirstEntry); static void TNLAppendEncodedKeyValuePair(NSMutableString *parameterString, NSString *encodedKey, NSString *encodedValue, TNLURLEncodingOptions options, BOOL *inoutIsFirstEntry); static void TNLAppendArrayOfParameterValues(NSMutableString *parameterString, NSString *encodedKey, NSArray *values, TNLURLEncodingOptions options, BOOL *inoutIsFirstEntry); static void TNLAppendParameterValue(NSMutableString *parameterString, NSString *encodedKey, id value, TNLURLEncodingOptions options, BOOL *inoutIsFirstEntry); static NSArray *TNLURLConvertArrayToArrayOfEncodableStrings(NSArray * __nullable sourceArray, TNLURLEncodableDictionaryOptions options, NSString * __nullable contextKey); static NSDictionary *TNLURLConvertDictionaryToDictionaryOfEncodableStrings(NSDictionary * __nullable sourceDict, TNLURLEncodableDictionaryOptions options); // See TNLURLStringCoding.m // NSString *TNLURLEncodeString(NSString *string) // See TNLURLStringCoding.m // NSString *TNLURLDecodeString(NSString *string, BOOL replacePlussesWithSpaces) static void TNLAppendKeyValuePairToMutableString(NSMutableString *string, NSString *key, NSString *value, TNLURLEncodingOptions options) { TNLAssert(string != nil); TNLAssert(key != nil); TNLAssert(value != nil); if (key && string) { [string appendString:key]; if ((value.length > 0) || TNL_BITMASK_EXCLUDES_FLAGS(options, TNLURLEncodingOptionTrimEmptyValueDelimiter)) { [string appendString:@"="]; if (value) { [string appendString:value]; } } } } static void TNLAppendParameterDelimeterIfNecessary(NSMutableString *parameterString, BOOL *inoutIsFirstEntry) { if (!(*inoutIsFirstEntry)) { [parameterString appendString:@"&"]; } else { *inoutIsFirstEntry = NO; } } static void TNLAppendEncodedKeyValuePair(NSMutableString *parameterString, NSString *encodedKey, NSString *encodedValue, TNLURLEncodingOptions options, BOOL *inoutIsFirstEntry) { if (TNL_BITMASK_EXCLUDES_FLAGS(options, TNLURLEncodingOptionDiscardEmptyValues) || (encodedValue.length > 0)) { TNLAppendParameterDelimeterIfNecessary(parameterString, inoutIsFirstEntry); TNLAppendKeyValuePairToMutableString(parameterString, encodedKey, encodedValue, options); } } static void TNLAppendArrayOfParameterValues(NSMutableString *parameterString, NSString *encodedKey, NSArray *values, TNLURLEncodingOptions options, BOOL *inoutIsFirstEntry) { NSMutableArray *encodedValues = [NSMutableArray arrayWithCapacity:[values count]]; for (id subvalue in values) { NSString *stringValue = TNLStringValue(subvalue, options, encodedKey); if (stringValue) { stringValue = TNLURLEncodeString(stringValue); if (stringValue) { [encodedValues addObject:stringValue]; } } } if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionStableOrder)) { [encodedValues sortUsingSelector:@selector(compare:)]; } for (NSString *encodedValue in encodedValues) { TNLAppendEncodedKeyValuePair(parameterString, encodedKey, encodedValue, options, inoutIsFirstEntry); } } static void TNLAppendParameterValue(NSMutableString *parameterString, NSString *encodedKey, id value, TNLURLEncodingOptions options, BOOL *inoutIsFirstEntry) { TNLAssert(encodedKey != nil); NSString *stringValue = TNLStringValue(value, options, encodedKey); if (stringValue) { NSString *encodedValue = TNLURLEncodeString(stringValue); if (!encodedValue) { TNLLogError(@"Could not encode value for encoded key '%@': '%@'", encodedKey, stringValue); TNLAssertMessage(encodedValue != nil, @"Could not encode value for encoded key '%@': '%@'", encodedKey, stringValue); // Handle the unexpected encoding of the value as an unsupported value if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionTreatUnsupportedValuesAsEmpty)) { encodedValue = @""; } else if (TNL_BITMASK_EXCLUDES_FLAGS(options, TNLURLEncodingOptionIgnoreUnsupportedValues)) { NSString *reason = [NSString stringWithFormat:@"parameter object cannot be URL Encoded (options=%@, object=%@, stringValue=%@, key=%@)", @(options), value, stringValue, encodedKey]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:@{ @"object" : (value) ?: [NSNull null], @"encodingOptions" : @(options) }]; } } if (encodedKey && encodedValue) { TNLAppendEncodedKeyValuePair(parameterString, encodedKey, encodedValue, options, inoutIsFirstEntry); } } } NSString *TNLURLEncodeDictionary(NSDictionary * __nullable params, TNLURLEncodingOptions options) { NSMutableString *parameterString = [NSMutableString string]; NSArray *allKeys = params.allKeys; if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionStableOrder)) { allKeys = [allKeys sortedArrayUsingSelector:@selector(compare:)]; } const BOOL specialCaseArrays = TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionDuplicateEntriesForArrayValues); BOOL firstEntry = YES; for (NSString *key in allKeys) { NSString *encodedKey = TNLURLEncodeString(key); if (encodedKey.length > 0) { id value = params[key]; if (specialCaseArrays && [value isKindOfClass:[NSArray class]] && [value count] > 0) { TNLAppendArrayOfParameterValues(parameterString, encodedKey, value, options, &firstEntry); } else { TNLAppendParameterValue(parameterString, encodedKey, value, options, &firstEntry); } } } return parameterString; } NSDictionary *TNLURLDecodeDictionary(NSString * __nullable encodedURLString, TNLURLDecodingOptions options) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSArray *pairs = [encodedURLString componentsSeparatedByString:@"&"]; const BOOL preserveEmptyValues = TNL_BITMASK_EXCLUDES_FLAGS(options, TNLURLDecodingOptionOmitEmptyValues); const BOOL replacePlusses = TNL_BITMASK_EXCLUDES_FLAGS(options, TNLURLDecodingOptionPreservePlusses); const BOOL combineRepeatingKeys = TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLDecodingOptionCombineRepeatingKeysIntoArray); for (NSString *pair in pairs) { if (pair.length) { const NSRange delimeterRange = [pair rangeOfString:@"="]; NSString *key = nil; NSString *value = nil; if (delimeterRange.location != NSNotFound) { key = [pair substringToIndex:delimeterRange.location]; value = [pair substringFromIndex:delimeterRange.location + delimeterRange.length]; if (value.length == 0) { value = preserveEmptyValues ? @"" : nil; } } else if (preserveEmptyValues) { key = pair; value = @""; } if (nil != value && nil != key) { value = TNLURLDecodeString(value, replacePlusses); key = TNLURLDecodeString(key, replacePlusses); if (nil != value && key.length > 0) { if (combineRepeatingKeys) { id oldValue = dict[key]; if ([oldValue isKindOfClass:[NSString class]]) { dict[key] = [NSMutableArray arrayWithObjects:oldValue, value, nil]; } else if ([oldValue isKindOfClass:[NSArray class]]) { [(NSMutableArray *)oldValue addObject:value]; } else { dict[key] = value; } } else { dict[key] = value; } } } } } return TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLDecodingOptionOutputMutableDictionary) ? dict : [dict copy]; } static NSArray *TNLURLConvertArrayToArrayOfEncodableStrings(NSArray * __nullable sourceArray, TNLURLEncodableDictionaryOptions options, NSString * __nullable contextKey) { NSMutableArray *array = [NSMutableArray arrayWithCapacity:sourceArray.count]; for (__strong id value in sourceArray) { value = TNLURLEncodableValue(value, options, contextKey); if (value) { [array addObject:value]; } } return [array copy]; } static NSDictionary *TNLURLConvertDictionaryToDictionaryOfEncodableStrings(NSDictionary * __nullable sourceDict, TNLURLEncodableDictionaryOptions options) { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:sourceDict.count]; for (NSString *key in sourceDict) { id value = sourceDict[key]; value = TNLURLEncodableValue(value, options, key); if (value) { dict[key] = value; } } return [dict copy]; } NSDictionary *TNLURLEncodableDictionary(NSDictionary * __nullable params, TNLURLEncodableDictionaryOptions options) { TNLStaticAssert(TNLURLEncodableDictionaryOptionDiscardEmptyValues == TNLURLEncodingOptionDiscardEmptyValues, DiscardEmptyValuesOptionsAreNotEqual); TNLStaticAssert(TNLURLEncodableDictionaryOptionIgnoreUnsupportedValues == TNLURLEncodingOptionIgnoreUnsupportedValues, IgnoreUnsupportedValuesOptionsAreNotEqual); TNLStaticAssert(TNLURLEncodableDictionaryOptionTreatUnsupportedValuesAsEmpty == TNLURLEncodingOptionTreatUnsupportedValuesAsEmpty, TreatUnsupportedValuesAsEmptyOptionsAreNotEqual); NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:params.count]; NSArray *allKeys = params.allKeys; for (NSString *key in allKeys) { id value = params[key]; value = TNLURLEncodableValue(value, options, key); if (value) { dict[key] = value; } } return TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodableDictionaryOptionOutputMutableDictionary) ? dict : [dict copy]; } // This function is ~15% more performant than `-[NSNumber stringValue]` -- which matters when encoding values as rapidly and often as TNL does static NSString *TNLNumberStringValue(NSNumber *number) { static NSString * __nonnull kSmallPositives[] = { @"0", @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9" }; const char *objCType = [number objCType]; if (objCType) { switch (*objCType) { case 'c': case 'i': case 's': case 'l': case 'q': { const long long value = [number longLongValue]; if (value < 10 && value >= 0) { return kSmallPositives[value]; } return [[NSString alloc] initWithFormat:@"%lli", value]; } case 'C': case 'I': case 'S': case 'L': case 'Q': { const unsigned long long value = [number unsignedLongLongValue]; if (value < 10) { return kSmallPositives[value]; } return [[NSString alloc] initWithFormat:@"%llu", value]; } case 'f': return [[NSString alloc] initWithFormat:@"%0.7g", [number floatValue]]; case 'd': return [[NSString alloc] initWithFormat:@"%0.16g", [number doubleValue]]; default: break; } } return [number descriptionWithLocale:nil]; } static NSString *TNLStringValue(id object, TNLURLEncodingOptions options, NSString * __nullable contextKey) { NSString *value = nil; if ([object isKindOfClass:[NSString class]]) { value = object; } else if ([object isKindOfClass:[NSNumber class]]) { if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionEncodeBooleanNumbersAsTrueOrFalse) && [object tnl_isBoolean]) { // use "true"/"false" instead of default "1"/"0" value = [object boolValue] ? @"true" : @"false"; } else { value = TNLNumberStringValue(object); } } else if ([object respondsToSelector:@selector(tnl_URLEncodableStringValue)]) { value = [object tnl_URLEncodableStringValue]; } if (!value && TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionTreatUnsupportedValuesAsEmpty)) { value = @""; } if (!value && TNL_BITMASK_EXCLUDES_FLAGS(options, TNLURLEncodingOptionIgnoreUnsupportedValues)) { NSString *reason = [NSString stringWithFormat:@"parameter object cannot be URL Encoded (options=%@, object=%@, key=%@)", @(options), value, contextKey]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:@{ @"object" : (object) ?: [NSNull null], @"encodingOptions" : @(options) }]; } return value; } static id TNLURLEncodableValue(id value, TNLURLEncodableDictionaryOptions options, NSString * __nullable contextKey) { id returnValue; if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodableDictionaryOptionReplaceArraysWithArraysOfEncodableStrings) && [value isKindOfClass:[NSArray class]]) { returnValue = TNLURLConvertArrayToArrayOfEncodableStrings(value, options, contextKey); } else if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodableDictionaryOptionReplaceDictionariesWithDictionariesOfEncodableStrings) && [value isKindOfClass:[NSDictionary class]]) { returnValue = TNLURLConvertDictionaryToDictionaryOfEncodableStrings(value, options); } else if ([value isKindOfClass:[NSNumber class]]) { // NSNumbers are always OK returnValue = value; } else { const TNLURLEncodingOptions encodingOptions = (options & (TNLURLEncodingOptionDiscardEmptyValues | TNLURLEncodingOptionIgnoreUnsupportedValues | TNLURLEncodingOptionTreatUnsupportedValuesAsEmpty)); NSString *valueString = TNLStringValue(value, encodingOptions, contextKey); if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodableDictionaryOptionDiscardEmptyValues) && (valueString.length == 0)) { valueString = nil; } returnValue = valueString; } return returnValue; } // Implemented in TNLURLCoding.m to utilize the static TNLNumberStringValue function @implementation NSNumber (TNLStringCoding) - (NSString *)tnl_quickStringValue { return TNLNumberStringValue(self); } @end NS_ASSUME_NONNULL_END