1 /* 2 * Copyright (C) 2019 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.security.cts; 18 19 import java.util.concurrent.TimeoutException; 20 import java.util.regex.Pattern; 21 import java.util.regex.Matcher; 22 import com.android.ddmlib.Log.LogLevel; 23 import com.android.tradefed.log.LogUtil.CLog; 24 25 import static org.junit.Assert.*; 26 27 public class RegexUtils { 28 private static final int TIMEOUT_DURATION = 20 * 60_000; // 20 minutes 29 private static final int WARNING_THRESHOLD = 1000; // 1 second 30 private static final int CONTEXT_RANGE = 100; // chars before/after matched input string 31 assertContains(String pattern, String input)32 public static void assertContains(String pattern, String input) throws Exception { 33 assertFind(pattern, input, false, false); 34 } 35 assertContainsMultiline(String pattern, String input)36 public static void assertContainsMultiline(String pattern, String input) throws Exception { 37 assertFind(pattern, input, false, true); 38 } 39 assertNotContains(String pattern, String input)40 public static void assertNotContains(String pattern, String input) throws Exception { 41 assertFind(pattern, input, true, false); 42 } 43 assertNotContainsMultiline(String pattern, String input)44 public static void assertNotContainsMultiline(String pattern, String input) throws Exception { 45 assertFind(pattern, input, true, true); 46 } 47 assertFind( String pattern, String input, boolean shouldFind, boolean multiline)48 private static void assertFind( 49 String pattern, String input, boolean shouldFind, boolean multiline) { 50 // The input string throws an error when used after the timeout 51 TimeoutCharSequence timedInput = new TimeoutCharSequence(input, TIMEOUT_DURATION); 52 Matcher matcher = null; 53 if (multiline) { 54 // DOTALL lets .* match line separators 55 // MULTILINE lets ^ and $ match line separators instead of input start and end 56 matcher = Pattern.compile( 57 pattern, Pattern.DOTALL|Pattern.MULTILINE).matcher(timedInput); 58 } else { 59 matcher = Pattern.compile(pattern).matcher(timedInput); 60 } 61 62 try { 63 long start = System.currentTimeMillis(); 64 boolean found = matcher.find(); 65 long duration = System.currentTimeMillis() - start; 66 67 if (duration > WARNING_THRESHOLD) { 68 // Provide a warning to the test developer that their regex should be optimized. 69 CLog.logAndDisplay(LogLevel.WARN, "regex match took " + duration + "ms."); 70 } 71 72 if (found && shouldFind) { // failed notContains 73 String substring = input.substring(matcher.start(), matcher.end()); 74 String context = getInputContext(input, matcher.start(), matcher.end(), 75 CONTEXT_RANGE, CONTEXT_RANGE); 76 fail("Pattern found: '" + pattern + "' -> '" + substring + "' for input:\n..." + 77 context + "..."); 78 } else if (!found && !shouldFind) { // failed contains 79 fail("Pattern not found: '" + pattern + "' for input:\n..." + input + "..."); 80 } 81 } catch (TimeoutCharSequence.CharSequenceTimeoutException e) { 82 // regex match has taken longer than the timeout 83 // this usually means the input is extremely long or the regex is catastrophic 84 fail("Regex timeout with pattern: '" + pattern + "' for input:\n..." + input + "..."); 85 } 86 } 87 88 /* 89 * Helper method to grab the nearby chars for a subsequence. Similar to the -A and -B flags for 90 * grep. 91 */ getInputContext(String input, int start, int end, int before, int after)92 private static String getInputContext(String input, int start, int end, int before, int after) { 93 start = Math.max(0, start - before); 94 end = Math.min(input.length(), end + after); 95 return input.substring(start, end); 96 } 97 98 /* 99 * Wrapper for a given CharSequence. When charAt() is called, the current time is compared 100 * against the timeout. If the current time is greater than the expiration time, an exception is 101 * thrown. The expiration time is (time of object construction) + (timeout in milliseconds). 102 */ 103 private static class TimeoutCharSequence implements CharSequence { 104 long expireTime = 0; 105 CharSequence chars = null; 106 TimeoutCharSequence(CharSequence chars, long timeout)107 TimeoutCharSequence(CharSequence chars, long timeout) { 108 this.chars = chars; 109 expireTime = System.currentTimeMillis() + timeout; 110 } 111 112 @Override charAt(int index)113 public char charAt(int index) { 114 if (System.currentTimeMillis() > expireTime) { 115 throw new CharSequenceTimeoutException( 116 "TimeoutCharSequence was used after the expiration time."); 117 } 118 return chars.charAt(index); 119 } 120 121 @Override length()122 public int length() { 123 return chars.length(); 124 } 125 126 @Override subSequence(int start, int end)127 public CharSequence subSequence(int start, int end) { 128 return new TimeoutCharSequence(chars.subSequence(start, end), 129 expireTime - System.currentTimeMillis()); 130 } 131 132 @Override toString()133 public String toString() { 134 return chars.toString(); 135 } 136 137 private static class CharSequenceTimeoutException extends RuntimeException { CharSequenceTimeoutException(String message)138 public CharSequenceTimeoutException(String message) { 139 super(message); 140 } 141 } 142 } 143 } 144