1// Copyright © 2021 650 Industries. All rights reserved. 2 3#import <EXStructuredHeaders/EXStructuredHeadersParser.h> 4 5NS_ASSUME_NONNULL_BEGIN 6 7typedef NS_ENUM(NSInteger, EXStructuredHeadersParserNumberType) { 8 EXStructuredHeadersParserNumberTypeInteger, 9 EXStructuredHeadersParserNumberTypeDecimal 10}; 11 12static NSString * const EXStructuredHeadersParserErrorDomain = @"EXStructuredHeadersParser"; 13 14@interface EXStructuredHeadersParser () 15 16@property (nonatomic, strong) NSString *raw; 17@property (nonatomic, assign) NSUInteger position; 18 19@property (nonatomic, assign) EXStructuredHeadersParserFieldType fieldType; 20@property (nonatomic, assign) BOOL shouldIgnoreParameters; 21 22@end 23 24@implementation EXStructuredHeadersParser 25 26- (instancetype)initWithRawInput:(NSString *)raw fieldType:(EXStructuredHeadersParserFieldType)fieldType 27{ 28 return [self initWithRawInput:raw fieldType:fieldType ignoringParameters:NO]; 29} 30 31- (instancetype)initWithRawInput:(NSString *)raw fieldType:(EXStructuredHeadersParserFieldType)fieldType ignoringParameters:(BOOL)shouldIgnoreParameters 32{ 33 if (self = [super init]) { 34 _raw = raw; 35 _position = 0; 36 _fieldType = fieldType; 37 _shouldIgnoreParameters = shouldIgnoreParameters; 38 } 39 return self; 40} 41 42- (nullable id)parseStructuredFieldsWithError:(NSError ** _Nullable)error 43{ 44 // 4.2 45 46 // check for non-ASCII characters 47 for (int i = 0; i < _raw.length; i++) { 48 unichar ch = [_raw characterAtIndex:i]; 49 if (ch < 0x00 || ch > 0x7F) { 50 if (error) *error = [self errorWithMessage:[NSString stringWithFormat:@"Invalid character at index %i", i]]; 51 return nil; 52 } 53 } 54 55 [self removeLeadingSP]; 56 57 id output; 58 switch (_fieldType) { 59 case EXStructuredHeadersParserFieldTypeList: 60 output = [self _parseAListWithError:error]; 61 break; 62 case EXStructuredHeadersParserFieldTypeDictionary: 63 output = [self _parseADictionaryWithError:error]; 64 break; 65 case EXStructuredHeadersParserFieldTypeItem: 66 output = [self _parseAnItemWithError:error]; 67 break; 68 default: 69 if (error) *error = [self errorWithMessage:@"Invalid field type given to parser"]; 70 break; 71 } 72 if (!output) return nil; 73 74 [self removeLeadingSP]; 75 if ([self hasRemaining]) { 76 if (error) *error = [self errorWithMessage:@"Failed to parse structured fields; unexpected trailing characters found"]; 77 return nil; 78 } 79 80 return output; 81} 82 83- (nullable NSArray *)_parseAListWithError:(NSError ** _Nullable)error 84{ 85 // 4.2.1 86 NSMutableArray *members = [NSMutableArray new]; 87 while ([self hasRemaining]) { 88 id itemOrInnerList = [self _parseAnItemOrInnerListWithError:error]; 89 if (!itemOrInnerList) return nil; 90 [members addObject:itemOrInnerList]; 91 92 [self removeLeadingOWS]; 93 if (![self hasRemaining]) { 94 return [members copy]; 95 } 96 97 if ([self consume] != ',') { 98 if (error) *error = [self errorWithMessage:@"Failed to parse list; invalid character after list member"]; 99 return nil; 100 } 101 102 [self removeLeadingOWS]; 103 if (![self hasRemaining]) { 104 if (error) *error = [self errorWithMessage:@"List cannot have a trailing comma"]; 105 return nil; 106 } 107 } 108 109 return [members copy]; 110} 111 112- (nullable id)_parseAnItemOrInnerListWithError:(NSError ** _Nullable)error 113{ 114 // 4.2.1.1 115 if ([self compareNextChar:'(']) { 116 return [self _parseAnInnerListWithError:error]; 117 } else { 118 return [self _parseAnItemWithError:error]; 119 } 120} 121 122- (nullable NSArray *)_parseAnInnerListWithError:(NSError ** _Nullable)error 123{ 124 // 4.2.1.2 125 if ([self consume] != '(') { 126 if (error) *error = [self errorWithMessage:@"Inner list must start with '('"]; 127 return nil; 128 } 129 130 NSMutableArray *innerList = [NSMutableArray new]; 131 while ([self hasRemaining]) { 132 [self removeLeadingSP]; 133 134 if ([self compareNextChar:')']) { 135 [self advance]; 136 NSDictionary *parameters = [self _parseParametersWithError:error]; 137 if (!parameters) return nil; 138 return [self _memberEntryWithValue:innerList.copy parameters:parameters]; 139 } 140 141 id item = [self _parseAnItemWithError:error]; 142 if (!item) return nil; 143 [innerList addObject:item]; 144 145 if (![self compareNextCharWithSet:@" )"]) { 146 if (error) *error = [self errorWithMessage:@"Failed to parse inner list; invalid character after item"]; 147 return nil; 148 } 149 } 150 151 if (error) *error = [self errorWithMessage:@"Failed to parse inner list; end of list not found"]; 152 return nil; 153} 154 155- (nullable NSDictionary *)_parseADictionaryWithError:(NSError ** _Nullable)error 156{ 157 // 4.2.2 158 NSMutableDictionary *dictionary = [NSMutableDictionary new]; 159 while ([self hasRemaining]) { 160 NSString *key = [self _parseAKeyWithError:error]; 161 if (!key) return nil; 162 163 id member; 164 if ([self compareNextChar:'=']) { 165 [self advance]; 166 member = [self _parseAnItemOrInnerListWithError:error]; 167 if (!member) return nil; 168 } else { 169 NSDictionary *parameters = [self _parseParametersWithError:error]; 170 if (!parameters) return nil; 171 member = [self _memberEntryWithValue:@(YES) parameters:parameters]; 172 } 173 174 dictionary[key] = member; 175 [self removeLeadingOWS]; 176 if (![self hasRemaining]) { 177 return [dictionary copy]; 178 } 179 180 if ([self consume] != ',') { 181 if (error) *error = [self errorWithMessage:@"Failed to parse dictionary; invalid character after member"]; 182 return nil; 183 } 184 185 [self removeLeadingOWS]; 186 if (![self hasRemaining]) { 187 if (error) *error = [self errorWithMessage:@"Dictionary cannot have a trailing comma"]; 188 return nil; 189 } 190 } 191 192 return [dictionary copy]; 193} 194 195- (nullable id)_parseAnItemWithError:(NSError ** _Nullable)error 196{ 197 // 4.2.3 198 id bareItem = [self _parseABareItemWithError:error]; 199 if (!bareItem) return nil; 200 NSDictionary *parameters = [self _parseParametersWithError:error]; 201 if (!parameters) return nil; 202 return [self _memberEntryWithValue:bareItem parameters:parameters]; 203} 204 205- (nullable id)_parseABareItemWithError:(NSError ** _Nullable)error 206{ 207 // 4.2.3.1 208 unichar firstChar = [self peek]; 209 if ([self isDigit:firstChar] || firstChar == '-') { 210 return [self _parseAnIntegerOrDecimalWithError:error]; 211 } else if (firstChar == '"') { 212 return [self _parseAStringWithError:error]; 213 } else if ([self isAlpha:firstChar] || firstChar == '*') { 214 return [self _parseATokenWithError:error]; 215 } else if (firstChar == ':') { 216 return [self _parseAByteSequenceWithError:error]; 217 } else if (firstChar == '?') { 218 return [self _parseABooleanWithError:error]; 219 } else { 220 if (error) *error = [self errorWithMessage:@"Unrecognized item type"]; 221 return nil; 222 } 223} 224 225- (nullable NSDictionary *)_parseParametersWithError:(NSError ** _Nullable)error 226{ 227 // 4.2.3.2 228 NSMutableDictionary *parameters = [NSMutableDictionary new]; 229 while ([self hasRemaining]) { 230 if (![self compareNextChar:';']) { 231 break; 232 } 233 [self advance]; 234 [self removeLeadingSP]; 235 NSString *key = [self _parseAKeyWithError:error]; 236 if (!key) return nil; 237 id value = @(YES); 238 if ([self compareNextChar:'=']) { 239 [self advance]; 240 value = [self _parseABareItemWithError:error]; 241 if (!value) return nil; 242 } 243 parameters[key] = value; 244 } 245 return [parameters copy]; 246} 247 248- (nullable NSString *)_parseAKeyWithError:(NSError ** _Nullable)error 249{ 250 // 4.2.3.3 251 unichar firstChar = [self peek]; 252 if (![self isLowercaseAlpha:firstChar] && firstChar != '*') { 253 if (error) *error = [self errorWithMessage:@"Key must begin with a lowercase letter or '*'"]; 254 return nil; 255 } 256 257 NSMutableString *outputString = [NSMutableString stringWithCapacity:[self remainingLength]]; 258 while ([self hasRemaining]) { 259 unichar nextChar = [self peek]; 260 if (![self isLowercaseAlpha:nextChar] && ![self isDigit:nextChar] && ![self compareChar:nextChar withSet:@"_-.*"]) { 261 return [outputString copy]; 262 } else { 263 [outputString appendFormat:@"%c", nextChar]; 264 [self advance]; 265 } 266 } 267 268 return [outputString copy]; 269} 270 271- (nullable NSNumber *)_parseAnIntegerOrDecimalWithError:(NSError ** _Nullable)error 272{ 273 // 4.2.4 274 EXStructuredHeadersParserNumberType type = EXStructuredHeadersParserNumberTypeInteger; 275 NSInteger sign = 1; 276 NSMutableString *inputNumber = [NSMutableString stringWithCapacity:20]; 277 278 if ([self compareNextChar:'-']) { 279 [self advance]; 280 sign = -1; 281 } 282 283 if (![self hasRemaining]) { 284 if (error) *error = [self errorWithMessage:@"Integer or decimal cannot be empty"]; 285 return nil; 286 } 287 288 if (![self isDigit:[self peek]]) { 289 if (error) *error = [self errorWithMessage:@"Integer or decimal must begin with a digit"]; 290 return nil; 291 } 292 293 while ([self hasRemaining]) { 294 unichar nextChar = [self consume]; 295 if ([self isDigit:nextChar]) { 296 [inputNumber appendFormat:@"%c", nextChar]; 297 } else if (type == EXStructuredHeadersParserNumberTypeInteger && nextChar == '.') { 298 if (inputNumber.length > 12) { 299 if (error) *error = [self errorWithMessage:@"Decimal cannot have more than 12 digits before the decimal point"]; 300 return nil; 301 } 302 [inputNumber appendFormat:@"%c", nextChar]; 303 type = EXStructuredHeadersParserNumberTypeDecimal; 304 } else { 305 [self backout]; 306 break; 307 } 308 309 if (type == EXStructuredHeadersParserNumberTypeInteger && inputNumber.length > 15) { 310 if (error) *error = [self errorWithMessage:@"Integer cannot have more than 15 digits"]; 311 return nil; 312 } else if (type == EXStructuredHeadersParserNumberTypeDecimal && inputNumber.length > 16) { 313 if (error) *error = [self errorWithMessage:@"Decimal cannot have more than 16 characters"]; 314 return nil; 315 } 316 } 317 318 if (type == EXStructuredHeadersParserNumberTypeInteger) { 319 return @(inputNumber.longLongValue * sign); 320 } else { 321 if ([inputNumber hasSuffix:@"."]) { 322 if (error) *error = [self errorWithMessage:@"Decimal cannot end with the character '.'"]; 323 return nil; 324 } 325 if ([inputNumber rangeOfString:@"."].location + 3 < inputNumber.length - 1) { 326 if (error) *error = [self errorWithMessage:@"Decimal cannot have more than 3 digits after the decimal point"]; 327 return nil; 328 } 329 return @(inputNumber.doubleValue * sign); 330 } 331} 332 333- (nullable NSString *)_parseAStringWithError:(NSError ** _Nullable)error 334{ 335 // 4.2.5 336 NSMutableString *outputString = [NSMutableString stringWithCapacity:[self remainingLength]]; 337 338 if (![self compareNextChar:'"']) { 339 if (error) *error = [self errorWithMessage:@"String must begin with the character '\"'"]; 340 return nil; 341 } 342 343 [self advance]; 344 while ([self hasRemaining]) { 345 unichar nextChar = [self consume]; 346 if (nextChar == '\\') { 347 if (![self hasRemaining]) { 348 if (error) *error = [self errorWithMessage:@"String cannot end with the character '\\'"]; 349 return nil; 350 } 351 unichar followingChar = [self consume]; 352 if (![self compareChar:followingChar withSet:@"\"\\"]) { 353 if (error) *error = [self errorWithMessage:@"String cannot contain '\\' followed by an invalid character"]; 354 return nil; 355 } 356 [outputString appendFormat:@"%c", followingChar]; 357 } else if (nextChar == '"') { 358 return [outputString copy]; 359 } else if (nextChar < 0x20 || nextChar >= 0x7F) { 360 if (error) *error = [self errorWithMessage:@"Invalid character in string"]; 361 return nil; 362 } else { 363 [outputString appendFormat:@"%c", nextChar]; 364 } 365 } 366 367 if (error) *error = [self errorWithMessage:@"String must have a closing '\"'"]; 368 return nil; 369} 370 371- (nullable NSString *)_parseATokenWithError:(NSError ** _Nullable)error 372{ 373 // 4.2.6 374 unichar firstChar = [self peek]; 375 if (![self isAlpha:firstChar] && firstChar != '*') { 376 if (error) *error = [self errorWithMessage:@"Token must begin with an alphabetic character or '*'"]; 377 return nil; 378 } 379 380 NSMutableString *outputString = [NSMutableString stringWithCapacity:[self remainingLength]]; 381 while ([self hasRemaining]) { 382 // the only allowed characters are tchar, ':', and '/' 383 // check to see if nextChar is outside this set 384 unichar nextChar = [self peek]; 385 if (nextChar <= ' ' || nextChar >= 0x7F || [self compareChar:nextChar withSet:@"\"(),;<=>?@[\\]{}"]) { 386 return [outputString copy]; 387 } else { 388 [outputString appendFormat:@"%c", [self consume]]; 389 } 390 } 391 392 return [outputString copy]; 393} 394 395- (nullable NSData *)_parseAByteSequenceWithError:(NSError ** _Nullable)error 396{ 397 // 4.2.7 398 if (![self compareNextChar:':']) { 399 if (error) *error = [self errorWithMessage:@"Byte sequence must begin with ':'"]; 400 return nil; 401 } 402 403 [self advance]; 404 NSMutableString *inputByteSequence = [NSMutableString stringWithCapacity:[self remainingLength]]; 405 while ([self hasRemaining]) { 406 unichar nextChar = [self consume]; 407 if (nextChar == ':') { 408 return [[NSData alloc] initWithBase64EncodedString:inputByteSequence options:kNilOptions]; 409 } else if (![self isBase64:nextChar]) { 410 if (error) *error = [self errorWithMessage:@"Byte sequence can only contain valid base64 characters"]; 411 return nil; 412 } else { 413 [inputByteSequence appendFormat:@"%c", nextChar]; 414 } 415 } 416 417 if (error) *error = [self errorWithMessage:@"Byte sequence must have a closing ':'"]; 418 return nil; 419} 420 421- (nullable NSNumber *)_parseABooleanWithError:(NSError ** _Nullable)error 422{ 423 // 4.2.8 424 if (![self compareNextChar:'?']) { 425 if (error) *error = [self errorWithMessage:@"Boolean must begin with '?'"]; 426 return nil; 427 } 428 429 [self advance]; 430 unichar nextChar = [self peek]; 431 if (nextChar == '1') { 432 [self advance]; 433 return @(YES); 434 } else if (nextChar == '0') { 435 [self advance]; 436 return @(NO); 437 } else { 438 if (error) *error = [self errorWithMessage:@"Invalid value for boolean"]; 439 return nil; 440 } 441} 442 443# pragma mark - ignoring parameters 444 445- (nullable id)_memberEntryWithValue:(id)value parameters:(NSDictionary *)parameters 446{ 447 if (_shouldIgnoreParameters) { 448 return value; 449 } else { 450 return @[value, parameters]; 451 } 452} 453 454# pragma mark - utility methods 455 456- (BOOL)hasRemaining 457{ 458 return _position < _raw.length; 459} 460 461- (NSUInteger)remainingLength 462{ 463 return _raw.length - _position; 464} 465 466- (unichar)peek 467{ 468 return [self hasRemaining] ? [_raw characterAtIndex:_position] : (unichar) -1; 469} 470 471- (void)advance 472{ 473 _position++; 474} 475 476- (void)backout 477{ 478 _position--; 479} 480 481- (unichar)consume 482{ 483 unichar thisChar = [self peek]; 484 [self advance]; 485 return thisChar; 486} 487 488- (BOOL)compareNextChar:(unichar)match 489{ 490 return [self hasRemaining] ? [self peek] == match : NO; 491} 492 493- (BOOL)compareNextCharWithSet:(NSString *)charset 494{ 495 return [self hasRemaining] ? [self compareChar:[self peek] withSet:charset] : NO; 496} 497 498- (void)removeLeadingSP 499{ 500 while ([self compareNextChar:' ']) { 501 [self advance]; 502 } 503} 504 505- (void)removeLeadingOWS 506{ 507 while ([self compareNextChar:' '] || [self compareNextChar:'\t']) { 508 [self advance]; 509 } 510} 511 512- (BOOL)compareChar:(unichar)ch withSet:(NSString *)charset 513{ 514 return [charset containsString:[NSString stringWithFormat:@"%c", ch]]; 515} 516 517- (BOOL)isDigit:(unichar)ch 518{ 519 return ch >= '0' && ch <= '9'; 520} 521 522- (BOOL)isLowercaseAlpha:(unichar)ch 523{ 524 return ch >= 'a' && ch <= 'z'; 525} 526 527- (BOOL)isAlpha:(unichar)ch 528{ 529 return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); 530} 531 532- (BOOL)isBase64:(unichar)ch 533{ 534 return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '+' || ch == '/' || ch == '='; 535} 536 537- (NSError *)errorWithMessage:(NSString *)message 538{ 539 return [NSError errorWithDomain:EXStructuredHeadersParserErrorDomain code:1 userInfo:@{ 540 NSLocalizedDescriptionKey: message 541 }]; 542} 543 544@end 545 546NS_ASSUME_NONNULL_END 547