FLEXRuntimeKeyPathTokenizer.m 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. //
  2. // FLEXRuntimeKeyPathTokenizer.m
  3. // FLEX
  4. //
  5. // Created by Tanner on 3/22/17.
  6. // Copyright © 2017 Tanner Bennett. All rights reserved.
  7. //
  8. #import "FLEXRuntimeKeyPathTokenizer.h"
  9. #define TBCountOfStringOccurence(target, str) ([target componentsSeparatedByString:str].count - 1)
  10. @implementation FLEXRuntimeKeyPathTokenizer
  11. #pragma mark Initialization
  12. static NSCharacterSet *firstAllowed = nil;
  13. static NSCharacterSet *identifierAllowed = nil;
  14. static NSCharacterSet *filenameAllowed = nil;
  15. static NSCharacterSet *keyPathDisallowed = nil;
  16. static NSCharacterSet *methodAllowed = nil;
  17. + (void)initialize {
  18. if (self == [self class]) {
  19. NSString *_methodFirstAllowed = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
  20. NSString *_identifierAllowed = [_methodFirstAllowed stringByAppendingString:@"1234567890"];
  21. NSString *_methodAllowedSansType = [_identifierAllowed stringByAppendingString:@":"];
  22. NSString *_filenameNameAllowed = [_identifierAllowed stringByAppendingString:@"-+?!"];
  23. firstAllowed = [NSCharacterSet characterSetWithCharactersInString:_methodFirstAllowed];
  24. identifierAllowed = [NSCharacterSet characterSetWithCharactersInString:_identifierAllowed];
  25. filenameAllowed = [NSCharacterSet characterSetWithCharactersInString:_filenameNameAllowed];
  26. methodAllowed = [NSCharacterSet characterSetWithCharactersInString:_methodAllowedSansType];
  27. NSString *_kpDisallowed = [_identifierAllowed stringByAppendingString:@"-+:\\.*"];
  28. keyPathDisallowed = [NSCharacterSet characterSetWithCharactersInString:_kpDisallowed].invertedSet;
  29. }
  30. }
  31. #pragma mark Public
  32. + (FLEXRuntimeKeyPath *)tokenizeString:(NSString *)userInput {
  33. if (!userInput.length) {
  34. return nil;
  35. }
  36. NSUInteger tokens = [self tokenCountOfString:userInput];
  37. if (tokens == 0) {
  38. return nil;
  39. }
  40. if ([userInput containsString:@"**"]) {
  41. @throw NSInternalInconsistencyException;
  42. }
  43. NSNumber *instance = nil;
  44. NSScanner *scanner = [NSScanner scannerWithString:userInput];
  45. FLEXSearchToken *bundle = [self scanToken:scanner allowed:filenameAllowed first:filenameAllowed];
  46. FLEXSearchToken *cls = [self scanToken:scanner allowed:identifierAllowed first:firstAllowed];
  47. FLEXSearchToken *method = tokens > 2 ? [self scanMethodToken:scanner instance:&instance] : nil;
  48. return [FLEXRuntimeKeyPath bundle:bundle
  49. class:cls
  50. method:method
  51. isInstance:instance
  52. string:userInput];
  53. }
  54. + (BOOL)allowedInKeyPath:(NSString *)text {
  55. if (!text.length) {
  56. return YES;
  57. }
  58. return [text rangeOfCharacterFromSet:keyPathDisallowed].location == NSNotFound;
  59. }
  60. #pragma mark Private
  61. + (NSUInteger)tokenCountOfString:(NSString *)userInput {
  62. NSUInteger escapedCount = TBCountOfStringOccurence(userInput, @"\\.");
  63. NSUInteger tokenCount = TBCountOfStringOccurence(userInput, @".") - escapedCount + 1;
  64. return tokenCount;
  65. }
  66. + (FLEXSearchToken *)scanToken:(NSScanner *)scanner allowed:(NSCharacterSet *)allowedChars first:(NSCharacterSet *)first {
  67. if (scanner.isAtEnd) {
  68. if ([scanner.string hasSuffix:@"."] && ![scanner.string hasSuffix:@"\\."]) {
  69. return [FLEXSearchToken string:nil options:TBWildcardOptionsAny];
  70. }
  71. return nil;
  72. }
  73. TBWildcardOptions options = TBWildcardOptionsNone;
  74. NSMutableString *token = [NSMutableString new];
  75. // Token cannot start with '.'
  76. if ([scanner scanString:@"." intoString:nil]) {
  77. @throw NSInternalInconsistencyException;
  78. }
  79. if ([scanner scanString:@"*." intoString:nil]) {
  80. return [FLEXSearchToken string:nil options:TBWildcardOptionsAny];
  81. } else if ([scanner scanString:@"*" intoString:nil]) {
  82. if (scanner.isAtEnd) {
  83. return FLEXSearchToken.any;
  84. }
  85. options |= TBWildcardOptionsPrefix;
  86. }
  87. NSString *tmp = nil;
  88. BOOL stop = NO, didScanDelimiter = NO, didScanFirstAllowed = NO;
  89. NSCharacterSet *disallowed = allowedChars.invertedSet;
  90. while (!stop && ![scanner scanString:@"." intoString:&tmp] && !scanner.isAtEnd) {
  91. // Scan word chars
  92. // In this block, we have not scanned anything yet, except maybe leading '\' or '\.'
  93. if (!didScanFirstAllowed) {
  94. if ([scanner scanCharactersFromSet:first intoString:&tmp]) {
  95. [token appendString:tmp];
  96. didScanFirstAllowed = YES;
  97. } else if ([scanner scanString:@"\\" intoString:nil]) {
  98. if (options == TBWildcardOptionsPrefix && [scanner scanString:@"." intoString:nil]) {
  99. [token appendString:@"."];
  100. } else if (scanner.isAtEnd && options == TBWildcardOptionsPrefix) {
  101. // Only allow standalone '\' if prefixed by '*'
  102. return FLEXSearchToken.any;
  103. } else {
  104. // Token starts with a number, period, or something else not allowed,
  105. // or token is a standalone '\' with no '*' prefix
  106. @throw NSInternalInconsistencyException;
  107. }
  108. } else {
  109. // Token starts with a number, period, or something else not allowed
  110. @throw NSInternalInconsistencyException;
  111. }
  112. } else if ([scanner scanCharactersFromSet:allowedChars intoString:&tmp]) {
  113. [token appendString:tmp];
  114. }
  115. // Scan '\.' or trailing '\'
  116. else if ([scanner scanString:@"\\" intoString:nil]) {
  117. if ([scanner scanString:@"." intoString:nil]) {
  118. [token appendString:@"."];
  119. } else if (scanner.isAtEnd) {
  120. // Ignore forward slash not followed by period if at end
  121. return [FLEXSearchToken string:token options:options | TBWildcardOptionsSuffix];
  122. } else {
  123. // Only periods can follow a forward slash
  124. @throw NSInternalInconsistencyException;
  125. }
  126. }
  127. // Scan '*.'
  128. else if ([scanner scanString:@"*." intoString:nil]) {
  129. options |= TBWildcardOptionsSuffix;
  130. stop = YES;
  131. didScanDelimiter = YES;
  132. }
  133. // Scan '*' not followed by .
  134. else if ([scanner scanString:@"*" intoString:nil]) {
  135. if (!scanner.isAtEnd) {
  136. // Invalid token, wildcard in middle of token
  137. @throw NSInternalInconsistencyException;
  138. }
  139. } else if ([scanner scanCharactersFromSet:disallowed intoString:nil]) {
  140. // Invalid token, invalid characters
  141. @throw NSInternalInconsistencyException;
  142. }
  143. }
  144. // Did we scan a trailing, un-escsaped '.'?
  145. if ([tmp isEqualToString:@"."]) {
  146. didScanDelimiter = YES;
  147. }
  148. if (!didScanDelimiter) {
  149. options |= TBWildcardOptionsSuffix;
  150. }
  151. return [FLEXSearchToken string:token options:options];
  152. }
  153. + (FLEXSearchToken *)scanMethodToken:(NSScanner *)scanner instance:(NSNumber **)instance {
  154. if (scanner.isAtEnd) {
  155. if ([scanner.string hasSuffix:@"."]) {
  156. return [FLEXSearchToken string:nil options:TBWildcardOptionsAny];
  157. }
  158. return nil;
  159. }
  160. if ([scanner.string hasSuffix:@"."] && ![scanner.string hasSuffix:@"\\."]) {
  161. // Methods cannot end with '.' except for '\.'
  162. @throw NSInternalInconsistencyException;
  163. }
  164. if ([scanner scanString:@"-" intoString:nil]) {
  165. *instance = @YES;
  166. } else if ([scanner scanString:@"+" intoString:nil]) {
  167. *instance = @NO;
  168. } else {
  169. if ([scanner scanString:@"*" intoString:nil]) {
  170. // Just checking... It has to start with one of these three!
  171. scanner.scanLocation--;
  172. } else {
  173. @throw NSInternalInconsistencyException;
  174. }
  175. }
  176. // -*foo not allowed
  177. if (*instance && [scanner scanString:@"*" intoString:nil]) {
  178. @throw NSInternalInconsistencyException;
  179. }
  180. if (scanner.isAtEnd) {
  181. return [FLEXSearchToken string:@"" options:TBWildcardOptionsSuffix];
  182. }
  183. return [self scanToken:scanner allowed:methodAllowed first:firstAllowed];
  184. }
  185. @end