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