1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.os; 18 19 import android.util.proto.ProtoOutputStream; 20 21 import java.util.Arrays; 22 23 /** 24 * A simple pattern matcher, which is safe to use on untrusted data: it does 25 * not provide full reg-exp support, only simple globbing that can not be 26 * used maliciously. 27 */ 28 public class PatternMatcher implements Parcelable { 29 /** 30 * Pattern type: the given pattern must exactly match the string it is 31 * tested against. 32 */ 33 public static final int PATTERN_LITERAL = 0; 34 35 /** 36 * Pattern type: the given pattern must match the 37 * beginning of the string it is tested against. 38 */ 39 public static final int PATTERN_PREFIX = 1; 40 41 /** 42 * Pattern type: the given pattern is interpreted with a 43 * simple glob syntax for matching against the string it is tested against. 44 * In this syntax, you can use the '*' character to match against zero or 45 * more occurrences of the character immediately before. If the 46 * character before it is '.' it will match any character. The character 47 * '\' can be used as an escape. This essentially provides only the '*' 48 * wildcard part of a normal regexp. 49 */ 50 public static final int PATTERN_SIMPLE_GLOB = 2; 51 52 /** 53 * Pattern type: the given pattern is interpreted with a regular 54 * expression-like syntax for matching against the string it is tested 55 * against. Supported tokens include dot ({@code .}) and sets ({@code [...]}) 56 * with full support for character ranges and the not ({@code ^}) modifier. 57 * Supported modifiers include star ({@code *}) for zero-or-more, plus ({@code +}) 58 * for one-or-more and full range ({@code {...}}) support. This is a simple 59 * evaluation implementation in which matching is done against the pattern in 60 * real time with no backtracking support. 61 */ 62 public static final int PATTERN_ADVANCED_GLOB = 3; 63 64 // token types for advanced matching 65 private static final int TOKEN_TYPE_LITERAL = 0; 66 private static final int TOKEN_TYPE_ANY = 1; 67 private static final int TOKEN_TYPE_SET = 2; 68 private static final int TOKEN_TYPE_INVERSE_SET = 3; 69 70 // Return for no match 71 private static final int NO_MATCH = -1; 72 73 private static final String TAG = "PatternMatcher"; 74 75 // Parsed placeholders for advanced patterns 76 private static final int PARSED_TOKEN_CHAR_SET_START = -1; 77 private static final int PARSED_TOKEN_CHAR_SET_INVERSE_START = -2; 78 private static final int PARSED_TOKEN_CHAR_SET_STOP = -3; 79 private static final int PARSED_TOKEN_CHAR_ANY = -4; 80 private static final int PARSED_MODIFIER_RANGE_START = -5; 81 private static final int PARSED_MODIFIER_RANGE_STOP = -6; 82 private static final int PARSED_MODIFIER_ZERO_OR_MORE = -7; 83 private static final int PARSED_MODIFIER_ONE_OR_MORE = -8; 84 85 private final String mPattern; 86 private final int mType; 87 private final int[] mParsedPattern; 88 89 90 private static final int MAX_PATTERN_STORAGE = 2048; 91 // workspace to use for building a parsed advanced pattern; 92 private static final int[] sParsedPatternScratch = new int[MAX_PATTERN_STORAGE]; 93 PatternMatcher(String pattern, int type)94 public PatternMatcher(String pattern, int type) { 95 mPattern = pattern; 96 mType = type; 97 if (mType == PATTERN_ADVANCED_GLOB) { 98 mParsedPattern = parseAndVerifyAdvancedPattern(pattern); 99 } else { 100 mParsedPattern = null; 101 } 102 } 103 getPath()104 public final String getPath() { 105 return mPattern; 106 } 107 getType()108 public final int getType() { 109 return mType; 110 } 111 match(String str)112 public boolean match(String str) { 113 return matchPattern(str, mPattern, mParsedPattern, mType); 114 } 115 toString()116 public String toString() { 117 String type = "? "; 118 switch (mType) { 119 case PATTERN_LITERAL: 120 type = "LITERAL: "; 121 break; 122 case PATTERN_PREFIX: 123 type = "PREFIX: "; 124 break; 125 case PATTERN_SIMPLE_GLOB: 126 type = "GLOB: "; 127 break; 128 case PATTERN_ADVANCED_GLOB: 129 type = "ADVANCED: "; 130 break; 131 } 132 return "PatternMatcher{" + type + mPattern + "}"; 133 } 134 135 /** @hide */ writeToProto(ProtoOutputStream proto, long fieldId)136 public void writeToProto(ProtoOutputStream proto, long fieldId) { 137 long token = proto.start(fieldId); 138 proto.write(PatternMatcherProto.PATTERN, mPattern); 139 proto.write(PatternMatcherProto.TYPE, mType); 140 // PatternMatcherProto.PARSED_PATTERN is too much to dump, but the field is reserved to 141 // match the current data structure. 142 proto.end(token); 143 } 144 describeContents()145 public int describeContents() { 146 return 0; 147 } 148 writeToParcel(Parcel dest, int flags)149 public void writeToParcel(Parcel dest, int flags) { 150 dest.writeString(mPattern); 151 dest.writeInt(mType); 152 dest.writeIntArray(mParsedPattern); 153 } 154 PatternMatcher(Parcel src)155 public PatternMatcher(Parcel src) { 156 mPattern = src.readString(); 157 mType = src.readInt(); 158 mParsedPattern = src.createIntArray(); 159 } 160 161 public static final @android.annotation.NonNull Parcelable.Creator<PatternMatcher> CREATOR 162 = new Parcelable.Creator<PatternMatcher>() { 163 public PatternMatcher createFromParcel(Parcel source) { 164 return new PatternMatcher(source); 165 } 166 167 public PatternMatcher[] newArray(int size) { 168 return new PatternMatcher[size]; 169 } 170 }; 171 matchPattern(String match, String pattern, int[] parsedPattern, int type)172 static boolean matchPattern(String match, String pattern, int[] parsedPattern, int type) { 173 if (match == null) return false; 174 if (type == PATTERN_LITERAL) { 175 return pattern.equals(match); 176 } if (type == PATTERN_PREFIX) { 177 return match.startsWith(pattern); 178 } else if (type == PATTERN_SIMPLE_GLOB) { 179 return matchGlobPattern(pattern, match); 180 } else if (type == PATTERN_ADVANCED_GLOB) { 181 return matchAdvancedPattern(parsedPattern, match); 182 } 183 return false; 184 } 185 matchGlobPattern(String pattern, String match)186 static boolean matchGlobPattern(String pattern, String match) { 187 final int NP = pattern.length(); 188 if (NP <= 0) { 189 return match.length() <= 0; 190 } 191 final int NM = match.length(); 192 int ip = 0, im = 0; 193 char nextChar = pattern.charAt(0); 194 while ((ip<NP) && (im<NM)) { 195 char c = nextChar; 196 ip++; 197 nextChar = ip < NP ? pattern.charAt(ip) : 0; 198 final boolean escaped = (c == '\\'); 199 if (escaped) { 200 c = nextChar; 201 ip++; 202 nextChar = ip < NP ? pattern.charAt(ip) : 0; 203 } 204 if (nextChar == '*') { 205 if (!escaped && c == '.') { 206 if (ip >= (NP-1)) { 207 // at the end with a pattern match, so 208 // all is good without checking! 209 return true; 210 } 211 ip++; 212 nextChar = pattern.charAt(ip); 213 // Consume everything until the next character in the 214 // pattern is found. 215 if (nextChar == '\\') { 216 ip++; 217 nextChar = ip < NP ? pattern.charAt(ip) : 0; 218 } 219 do { 220 if (match.charAt(im) == nextChar) { 221 break; 222 } 223 im++; 224 } while (im < NM); 225 if (im == NM) { 226 // Whoops, the next character in the pattern didn't 227 // exist in the match. 228 return false; 229 } 230 ip++; 231 nextChar = ip < NP ? pattern.charAt(ip) : 0; 232 im++; 233 } else { 234 // Consume only characters matching the one before '*'. 235 do { 236 if (match.charAt(im) != c) { 237 break; 238 } 239 im++; 240 } while (im < NM); 241 ip++; 242 nextChar = ip < NP ? pattern.charAt(ip) : 0; 243 } 244 } else { 245 if (c != '.' && match.charAt(im) != c) return false; 246 im++; 247 } 248 } 249 250 if (ip >= NP && im >= NM) { 251 // Reached the end of both strings, all is good! 252 return true; 253 } 254 255 // One last check: we may have finished the match string, but still 256 // have a '.*' at the end of the pattern, which should still count 257 // as a match. 258 if (ip == NP-2 && pattern.charAt(ip) == '.' 259 && pattern.charAt(ip+1) == '*') { 260 return true; 261 } 262 263 return false; 264 } 265 266 /** 267 * Parses the advanced pattern and returns an integer array representation of it. The integer 268 * array treats each field as a character if positive and a unique token placeholder if 269 * negative. This method will throw on any pattern structure violations. 270 */ 271 synchronized static int[] parseAndVerifyAdvancedPattern(String pattern) { 272 int ip = 0; 273 final int LP = pattern.length(); 274 275 int it = 0; 276 277 boolean inSet = false; 278 boolean inRange = false; 279 boolean inCharClass = false; 280 281 boolean addToParsedPattern; 282 283 while (ip < LP) { 284 if (it > MAX_PATTERN_STORAGE - 3) { 285 throw new IllegalArgumentException("Pattern is too large!"); 286 } 287 288 char c = pattern.charAt(ip); 289 addToParsedPattern = false; 290 291 switch (c) { 292 case '[': 293 if (inSet) { 294 addToParsedPattern = true; // treat as literal or char class in set 295 } else { 296 if (pattern.charAt(ip + 1) == '^') { 297 sParsedPatternScratch[it++] = PARSED_TOKEN_CHAR_SET_INVERSE_START; 298 ip++; // skip over the '^' 299 } else { 300 sParsedPatternScratch[it++] = PARSED_TOKEN_CHAR_SET_START; 301 } 302 ip++; // move to the next pattern char 303 inSet = true; 304 continue; 305 } 306 break; 307 case ']': 308 if (!inSet) { 309 addToParsedPattern = true; // treat as literal outside of set 310 } else { 311 int parsedToken = sParsedPatternScratch[it - 1]; 312 if (parsedToken == PARSED_TOKEN_CHAR_SET_START || 313 parsedToken == PARSED_TOKEN_CHAR_SET_INVERSE_START) { 314 throw new IllegalArgumentException( 315 "You must define characters in a set."); 316 } 317 sParsedPatternScratch[it++] = PARSED_TOKEN_CHAR_SET_STOP; 318 inSet = false; 319 inCharClass = false; 320 } 321 break; 322 case '{': 323 if (!inSet) { 324 if (it == 0 || isParsedModifier(sParsedPatternScratch[it - 1])) { 325 throw new IllegalArgumentException("Modifier must follow a token."); 326 } 327 sParsedPatternScratch[it++] = PARSED_MODIFIER_RANGE_START; 328 ip++; 329 inRange = true; 330 } 331 break; 332 case '}': 333 if (inRange) { // only terminate the range if we're currently in one 334 sParsedPatternScratch[it++] = PARSED_MODIFIER_RANGE_STOP; 335 inRange = false; 336 } 337 break; 338 case '*': 339 if (!inSet) { 340 if (it == 0 || isParsedModifier(sParsedPatternScratch[it - 1])) { 341 throw new IllegalArgumentException("Modifier must follow a token."); 342 } 343 sParsedPatternScratch[it++] = PARSED_MODIFIER_ZERO_OR_MORE; 344 } 345 break; 346 case '+': 347 if (!inSet) { 348 if (it == 0 || isParsedModifier(sParsedPatternScratch[it - 1])) { 349 throw new IllegalArgumentException("Modifier must follow a token."); 350 } 351 sParsedPatternScratch[it++] = PARSED_MODIFIER_ONE_OR_MORE; 352 } 353 break; 354 case '.': 355 if (!inSet) { 356 sParsedPatternScratch[it++] = PARSED_TOKEN_CHAR_ANY; 357 } 358 break; 359 case '\\': // escape 360 if (ip + 1 >= LP) { 361 throw new IllegalArgumentException("Escape found at end of pattern!"); 362 } 363 c = pattern.charAt(++ip); 364 addToParsedPattern = true; 365 break; 366 default: 367 addToParsedPattern = true; 368 break; 369 } 370 if (inSet) { 371 if (inCharClass) { 372 sParsedPatternScratch[it++] = c; 373 inCharClass = false; 374 } else { 375 // look forward for character class 376 if (ip + 2 < LP 377 && pattern.charAt(ip + 1) == '-' 378 && pattern.charAt(ip + 2) != ']') { 379 inCharClass = true; 380 sParsedPatternScratch[it++] = c; // set first token as lower end of range 381 ip++; // advance past dash 382 } else { // literal 383 sParsedPatternScratch[it++] = c; // set first token as literal 384 sParsedPatternScratch[it++] = c; // set second set as literal 385 } 386 } 387 } else if (inRange) { 388 int endOfSet = pattern.indexOf('}', ip); 389 if (endOfSet < 0) { 390 throw new IllegalArgumentException("Range not ended with '}'"); 391 } 392 String rangeString = pattern.substring(ip, endOfSet); 393 int commaIndex = rangeString.indexOf(','); 394 try { 395 final int rangeMin; 396 final int rangeMax; 397 if (commaIndex < 0) { 398 int parsedRange = Integer.parseInt(rangeString); 399 rangeMin = rangeMax = parsedRange; 400 } else { 401 rangeMin = Integer.parseInt(rangeString.substring(0, commaIndex)); 402 if (commaIndex == rangeString.length() - 1) { // e.g. {n,} (n or more) 403 rangeMax = Integer.MAX_VALUE; 404 } else { 405 rangeMax = Integer.parseInt(rangeString.substring(commaIndex + 1)); 406 } 407 } 408 if (rangeMin > rangeMax) { 409 throw new IllegalArgumentException( 410 "Range quantifier minimum is greater than maximum"); 411 } 412 sParsedPatternScratch[it++] = rangeMin; 413 sParsedPatternScratch[it++] = rangeMax; 414 } catch (NumberFormatException e) { 415 throw new IllegalArgumentException("Range number format incorrect", e); 416 } 417 ip = endOfSet; 418 continue; // don't increment ip 419 } else if (addToParsedPattern) { 420 sParsedPatternScratch[it++] = c; 421 } 422 ip++; 423 } 424 if (inSet) { 425 throw new IllegalArgumentException("Set was not terminated!"); 426 } 427 return Arrays.copyOf(sParsedPatternScratch, it); 428 } 429 430 private static boolean isParsedModifier(int parsedChar) { 431 return parsedChar == PARSED_MODIFIER_ONE_OR_MORE || 432 parsedChar == PARSED_MODIFIER_ZERO_OR_MORE || 433 parsedChar == PARSED_MODIFIER_RANGE_STOP || 434 parsedChar == PARSED_MODIFIER_RANGE_START; 435 } 436 437 static boolean matchAdvancedPattern(int[] parsedPattern, String match) { 438 439 // create indexes 440 int ip = 0, im = 0; 441 442 // one-time length check 443 final int LP = parsedPattern.length, LM = match.length(); 444 445 // The current character being analyzed in the pattern 446 int patternChar; 447 448 int tokenType; 449 450 int charSetStart = 0, charSetEnd = 0; 451 452 while (ip < LP) { // we still have content in the pattern 453 454 patternChar = parsedPattern[ip]; 455 // get the match type of the next verb 456 457 switch (patternChar) { 458 case PARSED_TOKEN_CHAR_ANY: 459 tokenType = TOKEN_TYPE_ANY; 460 ip++; 461 break; 462 case PARSED_TOKEN_CHAR_SET_START: 463 case PARSED_TOKEN_CHAR_SET_INVERSE_START: 464 tokenType = patternChar == PARSED_TOKEN_CHAR_SET_START 465 ? TOKEN_TYPE_SET 466 : TOKEN_TYPE_INVERSE_SET; 467 charSetStart = ip + 1; // start from the char after the set start 468 while (++ip < LP && parsedPattern[ip] != PARSED_TOKEN_CHAR_SET_STOP); 469 charSetEnd = ip - 1; // we're on the set stop, end is the previous 470 ip++; // move the pointer to the next pattern entry 471 break; 472 default: 473 charSetStart = ip; 474 tokenType = TOKEN_TYPE_LITERAL; 475 ip++; 476 break; 477 } 478 479 final int minRepetition; 480 final int maxRepetition; 481 482 // look for a match length modifier 483 if (ip >= LP) { 484 minRepetition = maxRepetition = 1; 485 } else { 486 patternChar = parsedPattern[ip]; 487 switch (patternChar) { 488 case PARSED_MODIFIER_ZERO_OR_MORE: 489 minRepetition = 0; 490 maxRepetition = Integer.MAX_VALUE; 491 ip++; 492 break; 493 case PARSED_MODIFIER_ONE_OR_MORE: 494 minRepetition = 1; 495 maxRepetition = Integer.MAX_VALUE; 496 ip++; 497 break; 498 case PARSED_MODIFIER_RANGE_START: 499 minRepetition = parsedPattern[++ip]; 500 maxRepetition = parsedPattern[++ip]; 501 ip += 2; // step over PARSED_MODIFIER_RANGE_STOP and on to the next token 502 break; 503 default: 504 minRepetition = maxRepetition = 1; // implied literal 505 break; 506 } 507 } 508 if (minRepetition > maxRepetition) { 509 return false; 510 } 511 512 // attempt to match as many characters as possible 513 int matched = matchChars(match, im, LM, tokenType, minRepetition, maxRepetition, 514 parsedPattern, charSetStart, charSetEnd); 515 516 // if we found a conflict, return false immediately 517 if (matched == NO_MATCH) { 518 return false; 519 } 520 521 // move the match pointer the number of characters matched 522 im += matched; 523 } 524 return ip >= LP && im >= LM; // have parsed entire string and regex 525 } 526 527 private static int matchChars(String match, int im, final int lm, int tokenType, 528 int minRepetition, int maxRepetition, int[] parsedPattern, 529 int tokenStart, int tokenEnd) { 530 int matched = 0; 531 532 while(matched < maxRepetition 533 && matchChar(match, im + matched, lm, tokenType, parsedPattern, tokenStart, 534 tokenEnd)) { 535 matched++; 536 } 537 538 return matched < minRepetition ? NO_MATCH : matched; 539 } 540 541 private static boolean matchChar(String match, int im, final int lm, int tokenType, 542 int[] parsedPattern, int tokenStart, int tokenEnd) { 543 if (im >= lm) { // we've overrun the string, no match 544 return false; 545 } 546 switch (tokenType) { 547 case TOKEN_TYPE_ANY: 548 return true; 549 case TOKEN_TYPE_SET: 550 for (int i = tokenStart; i < tokenEnd; i += 2) { 551 char matchChar = match.charAt(im); 552 if (matchChar >= parsedPattern[i] && matchChar <= parsedPattern[i + 1]) { 553 return true; 554 } 555 } 556 return false; 557 case TOKEN_TYPE_INVERSE_SET: 558 for (int i = tokenStart; i < tokenEnd; i += 2) { 559 char matchChar = match.charAt(im); 560 if (matchChar >= parsedPattern[i] && matchChar <= parsedPattern[i + 1]) { 561 return false; 562 } 563 } 564 return true; 565 case TOKEN_TYPE_LITERAL: 566 return match.charAt(im) == parsedPattern[tokenStart]; 567 default: 568 return false; 569 } 570 } 571 }