1//  Copyright © 2019 650 Industries. All rights reserved.
2
3#import <EXUpdates/EXUpdatesParameterParser.h>
4
5NS_ASSUME_NONNULL_BEGIN
6
7@implementation EXUpdatesParameterParser {
8  // String to be parsed.
9  NSString *_parameterString;
10
11  // Current position in the string.
12  int _currentPosition;
13
14  // Start of a token.
15  int _i1;
16
17  // End of a token.
18  int _i2;
19}
20
21/**
22 * A helper method to process the parsed token. This method removes
23 * leading and trailing blanks as well as enclosing quotation marks,
24 * when necessary.
25 */
26- (nullable NSString *)getTokenWithQuoted:(BOOL)quoted {
27  // Trim leading white spaces
28  while ((_i1 < _i2) && [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[_parameterString characterAtIndex:_i1]]) {
29    _i1++;
30  }
31
32  // Trim trailing white spaces
33  while ((_i2 > _i1) && [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[_parameterString characterAtIndex:(_i2 - 1)]]) {
34      _i2--;
35  }
36
37  // Strip away quotation marks if necessary
38  if (quoted
39      && ((_i2 - _i1) >= 2)
40      && ([_parameterString characterAtIndex:_i1] == '"')
41      && ([_parameterString characterAtIndex:(_i2 - 1)] == '"')) {
42      _i1++;
43      _i2--;
44  }
45
46  NSString *result = nil;
47  if (_i2 > _i1) {
48    result = [_parameterString substringWithRange:NSMakeRange(_i1, _i2 - _i1)];
49  }
50  return result;
51}
52
53/**
54 * Parses out a token until any of the given terminators
55 * is encountered.
56 */
57- (NSString *)parseTokenWithTerminators:(NSCharacterSet *)terminators {
58  unichar ch;
59  _i1 = _currentPosition;
60  _i2 = _currentPosition;
61  while ([self hasChar]) {
62    ch = [_parameterString characterAtIndex:_currentPosition];
63    if ([terminators characterIsMember:ch]) {
64      break;
65    }
66    _i2++;
67    _currentPosition++;
68  }
69  return [self getTokenWithQuoted:false];
70}
71
72/**
73 * Parses out a token until any of the given terminators
74 * is encountered outside the quotation marks.
75 */
76- (NSString *)parseQuotedTokenWithTerminators:(NSCharacterSet *)terminators {
77  unichar ch;
78  _i1 = _currentPosition;
79  _i2 = _currentPosition;
80  BOOL quoted = false;
81  BOOL charEscaped = false;
82
83  while ([self hasChar]) {
84    ch = [_parameterString characterAtIndex:_currentPosition];
85    if (!quoted && [terminators characterIsMember:ch]) {
86      break;
87    }
88    if (!charEscaped && ch == '"') {
89      quoted = !quoted;
90    }
91    charEscaped = (!charEscaped && ch == '\\');
92    _i2++;
93    _currentPosition++;
94  }
95
96  return [self getTokenWithQuoted:true];
97}
98
99/**
100 * Are there any characters left to parse?
101 */
102- (BOOL)hasChar {
103  return _currentPosition < _parameterString.length;
104}
105
106/**
107 * Extracts a map of name/value pairs from the given string. Names are expected to be unique.
108 */
109- (NSDictionary *)parseParameterString:(NSString *)parameterString withDelimiter:(unichar)delimiter {
110  _parameterString = parameterString;
111
112  NSMutableDictionary *params = [NSMutableDictionary new];
113
114  NSString *paramName;
115  NSString *paramValue;
116
117  NSCharacterSet *charSetDelimiter = [NSCharacterSet characterSetWithCharactersInString:[NSString stringWithFormat:@"%C", delimiter]];
118  NSCharacterSet *charSetDelimiterAndEquals = [NSCharacterSet characterSetWithCharactersInString:[NSString stringWithFormat:@"%C=", delimiter]];
119
120  while ([self hasChar]) {
121    paramName = [self parseTokenWithTerminators:charSetDelimiterAndEquals];
122    paramValue = nil;
123
124    if ([self hasChar] && [_parameterString characterAtIndex:_currentPosition] == '=') {
125      _currentPosition++; // skip '='
126      paramValue = [self parseQuotedTokenWithTerminators:charSetDelimiter];
127    }
128
129    if ([self hasChar] && ([_parameterString characterAtIndex:_currentPosition] == delimiter)) {
130      _currentPosition++; // skip separator
131    }
132
133    if (paramName != nil && paramName.length > 0) {
134      [params setValue:(paramValue ?: [NSNull null]) forKey:paramName];
135    }
136  }
137
138  return params;
139}
140
141@end
142
143NS_ASSUME_NONNULL_END
144