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