package com.seaofnodes.simple.fuzzer; import java.util.Arrays; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class is used to reduce test cases created by the fuzzer. * It applies rewrite rules to the test case and checks that the * test still produces the same exception. * This is done in a fixpoint iteration until every rewrite produces * no or a different exception. */ public class Reducer { private interface Replacer extends BiConsumer {} /** * Holder for the rewrite rules */ private record Replace(Pattern pattern, boolean iterate, Replacer... replacements) { Replace(Pattern p, Object... replacements) { this(p, replacements[0] == Boolean.TRUE, Arrays.stream(replacements).filter(o->!(o instanceof Boolean)).map(Replace::from).toArray(Replacer[]::new)); } private static Replacer from(Object o) { if (o instanceof Replacer r) return r; if (o instanceof Integer i) return (s, m)->s.append(m.group(i)); if (o instanceof String r) return (s, m)->s.append(r); throw new IllegalArgumentException(); } } /** * List of rewrite rules. */ private static final Replace[] REPLACEMENTS = { new Replace(Pattern.compile("\\b(?:[^\\W0]|\\w\\w+)\\b"), "0", "1"), new Replace(Pattern.compile("(?<=\\n|^)[^\\n]+(?=\\n|$)"), true, ""), new Replace(Pattern.compile("(?<=[+\\-*/<>=(])--"), ""), new Replace(Pattern.compile("\\b--"), "+"), new Replace(Pattern.compile("[-!]"), ""), new Replace(Pattern.compile("\\b\\+-"), "-"), new Replace(Pattern.compile("(?=&|])(?:-|\\*|/|>=|<=|>>>|>>|>|<<|<|!=|==|&&|\\|\\||&|\\||^)(?![+*/<>=&|])"), "+"), new Replace(Pattern.compile("\\?([^;]+):"), "+"), new Replace(Pattern.compile("\\{((?:[^{}]|\\{})+)(?:\\n[ \\t]*)?}"), "", "{}", 1), new Replace(Pattern.compile("(?<=\\n|^)([ \\t]*)\\{\\n(.*?)\\n\\1}", Pattern.DOTALL), 2), new Replace(Pattern.compile("\\(([^()]+)\\)"), 1), new Replace(Pattern.compile("\\d+ ?\\+ ?"), ""), new Replace(Pattern.compile(" ?\\+ ?\\d+"), ""), new Replace(Pattern.compile("\\n\\n+"), "\n"), new Replace(Pattern.compile("^\\n+|\\n+$"), ""), new Replace(Pattern.compile("else *"), ""), new Replace(Pattern.compile("\\bif\\([^()]+\\) *"), ""), new Replace(Pattern.compile("\\bwhile\\([^()]+\\) *"), ""), new Replace(Pattern.compile("\\bstruct \\w+ \\{[^{}]*}"), "") }; /** * Predicate to test if the reduced script generates the same error. */ private final Predicate tester; /** * Create a reducer with a test function. * @param tester Test function which returns true when the same issue is found in the script. */ private Reducer(Predicate tester) { this.tester = tester; } /** * Rewrites the script with one rewrite rule. */ private String doReplacement(String script, Pattern pattern, Replacer[] replacements) { var matcher = pattern.matcher(script); var sb = new StringBuilder(); var last = 0; var skip = 0; outer: while (matcher.find(last + skip)) { sb.append(script, last, matcher.start()); last = matcher.end(); skip = last == matcher.start() ? 1 : 0; var pos = sb.length(); for (var replacement : replacements) { replacement.accept(sb, matcher); var tail = sb.length(); sb.append(script, last, script.length()); if (tester.test(sb.toString())) { sb.setLength(tail); continue outer; } sb.setLength(pos); } last = matcher.start(); skip = 1; } sb.append(script, last, script.length()); return sb.toString(); } /** * Clean up variable names from the random ones generated by the fuzzer. */ private String cleanVariables(String script) { var matcher = Pattern.compile("\\b([0-9a-zA-Z_]+) !?([0-9a-zA-Z_]+)").matcher(script); int[] num = {0, 0}; while (matcher.find()) { var type = matcher.group(1); var var = matcher.group(2); if (type.equals("new")) continue; String prefix = "v"; int t = 0; if (type.equals("struct")) { t = 1; prefix = "s"; } if (var.matches(prefix+"\\d+")) continue; while (script.contains(prefix+num[t])) num[t]++; var n = script.replaceAll("\\b"+var+"\\b", prefix + num[t]); if (tester.test(n)) { script = n; } } return script; } /** * Run all rewrite rules once. */ private String doAllReplacements(String script) { for (var replace: REPLACEMENTS) { String old; do { old = script; script = doReplacement(script, replace.pattern, replace.replacements); } while (replace.iterate && !old.equals(script)); } return script; } /** * Reduce the script to a minimal version which produces the same problem with a test function. * @param tester Test function which returns true when the same issue is found in the script. */ public static String reduce(String script, Predicate tester) { var reducer = new Reducer(tester); String old; do { old = script; script = reducer.doAllReplacements(script); script = reducer.cleanVariables(script); } while (!script.equals(old)); return script; } /** * Reduce the script to a minimal version which produces the same problem with a reproducer. * @param ex The exception originally caused by the script * @param reproducer Test function which throws an exception which will be checked against ex. */ public static String reduce(String script, Throwable ex, Consumer reproducer) { return reduce(script, s->{ Throwable nex = null; try { reproducer.accept(s); } catch (Throwable e) { nex = e; } return FuzzerUtils.isExceptionFromSameCause(nex, ex); }); } }