From 8a61361eae582669d163e1cdd032a314a012a365 Mon Sep 17 00:00:00 2001 From: XmiliaH <45106915+XmiliaH@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:41:55 +0100 Subject: Add fuzzer (#58) * Add fuzzer * Add fuzzer --------- Co-authored-by: Cliff Click --- .../java/com/seaofnodes/simple/Fuzzer08Test.java | 154 +++++++++++ .../test/java/com/seaofnodes/simple/Reducer.java | 119 ++++++++ .../com/seaofnodes/simple/ScriptGenerator.java | 305 +++++++++++++++++++++ 3 files changed, 578 insertions(+) create mode 100644 chapter08/src/test/java/com/seaofnodes/simple/Fuzzer08Test.java create mode 100644 chapter08/src/test/java/com/seaofnodes/simple/Reducer.java create mode 100644 chapter08/src/test/java/com/seaofnodes/simple/ScriptGenerator.java diff --git a/chapter08/src/test/java/com/seaofnodes/simple/Fuzzer08Test.java b/chapter08/src/test/java/com/seaofnodes/simple/Fuzzer08Test.java new file mode 100644 index 00000000..d8f10eef --- /dev/null +++ b/chapter08/src/test/java/com/seaofnodes/simple/Fuzzer08Test.java @@ -0,0 +1,154 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Random; + +import static org.junit.Assert.assertTrue; + +public class Fuzzer08Test { + + private final ArrayList exceptions = new ArrayList<>(); + + public static StopNode parse(String script, boolean disablePeeps) { + var parser = new Parser(script); + Node._disablePeephole = disablePeeps; + return parser.parse(); + } + + private static boolean sameException(Throwable e1, Throwable e2) { + if (e1 == null || e2 == null) return e1 == e2; + if (e1.getClass() != e2.getClass()) return false; + if (e1.getMessage() != null && e2.getMessage() != null && !e1.getMessage().equals(e2.getMessage())) return false; + var s1 = e1.getStackTrace(); + var s2 = e2.getStackTrace(); + if (s1 != null && s1.length > 0 && s2 != null && s2.length > 0 && !s1[0].equals(s2[0])) return false; + if (!sameException(e1.getCause(), e2.getCause())) return false; + var o1 = e1.getSuppressed(); + var o2 = e2.getSuppressed(); + if (o1 == null || o2 == null || o1.length != o2.length) return o1 == o2; + for (int i=0; i=(])--"), ""), + new Replace(Pattern.compile("\\b--"), "+"), + new Replace(Pattern.compile("\\b\\+-"), "-"), + new Replace(Pattern.compile("\\bfalse\\b"), "0"), + new Replace(Pattern.compile("\\btrue\\b"), "1"), + new Replace(Pattern.compile("-"), ""), + new Replace(Pattern.compile("(?=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"), + new Replace(Pattern.compile("\\{(?:[^{}]|\\{})+}"), "{}"), + new Replace(Pattern.compile("\\{((?:[^{}]|\\{})+)}"), "$1"), + new Replace(Pattern.compile("\\(([^()]+)\\)"), "$1"), + new Replace(Pattern.compile("\\b\\w+\\b"), "1"), + new Replace(Pattern.compile("\\b\\w+\\b"), "0"), + new Replace(Pattern.compile("\\d+\\+"), ""), + new Replace(Pattern.compile("\\+\\d+"), ""), + new Replace(Pattern.compile("(?<=\\n|^)[^\\n]*(?=\\n|$)"), "\n"), + new Replace(Pattern.compile("\\n+"), "\n"), + new Replace(Pattern.compile("^\\n+|\\n+$"), ""), + new Replace(Pattern.compile("else "), ""), + new Replace(Pattern.compile("\\bif\\([^()]+\\) *"), ""), + new Replace(Pattern.compile("\\bwhile\\([^()]+\\) *"), ""), + }; + + private static boolean sameException(Throwable e1, Throwable e2) { + if (e1 == null || e2 == null) return e1 == e2; + if (e1.getClass() != e2.getClass()) return false; + if (e1.getMessage() != null && e2.getMessage() != null && !e1.getMessage().equals(e2.getMessage())) return false; + var s1 = e1.getStackTrace(); + var s2 = e2.getStackTrace(); + if (s1 == null || s1.length == 0 || s2 == null || s2.length == 0) return true; + return s1[0].equals(s2[0]); + } + + private final boolean disablePeeps; + + private Reducer(boolean disablePeeps) { + this.disablePeeps = disablePeeps; + } + + private Throwable parse(String script) { + try { + var parser = new Parser(script); + Node._disablePeephole = disablePeeps; + parser.parse(); + return null; + } catch (Throwable e) { + return e; + } + } + + private String doReplacement(String script, Throwable ex, Pattern pattern, String with) { + var matcher = pattern.matcher(script); + var sb = new StringBuilder(); + int last = 0; + while (matcher.find()) { + var pos = sb.length(); + matcher.appendReplacement(sb, with); + var tail = sb.length(); + matcher.appendTail(sb); + var ex2 = parse(sb.toString()); + if (sameException(ex, ex2)) { + sb.setLength(tail); + } else { + sb.setLength(pos); + sb.append(script, last, matcher.end()); + } + last = matcher.end(); + } + matcher.appendTail(sb); + return sb.toString(); + } + + private String cleanVariables(String script, Throwable ex) { + var matcher = Pattern.compile("\\bint ([0-9a-zA-Z_]+)").matcher(script); + int num = 0; + while (matcher.find()) { + var var = matcher.group(1); + if (var.matches("v\\d+")) continue; + while (script.contains("v"+num)) num++; + var n = script.replaceAll("\\b"+var+"\\b", "v" + num); + var ex2 = parse(n); + if (sameException(ex, ex2)) { + script = n; + } + } + return script; + } + + private String doAllReplacements(String script, Throwable ex) { + for (var replace: REPLACEMENTS) { + script = doReplacement(script, ex, replace.p, replace.r); + } + return script; + } + + public static String reduce(String script, boolean disablePeeps) { + var reducer = new Reducer(disablePeeps); + var ex = reducer.parse(script); + if (ex == null) return "return 0;"; + String old; + do { + old = script; + script = reducer.doAllReplacements(script, ex); + script = reducer.cleanVariables(script, ex); + } while (!script.equals(old)); + return script; + } + +} diff --git a/chapter08/src/test/java/com/seaofnodes/simple/ScriptGenerator.java b/chapter08/src/test/java/com/seaofnodes/simple/ScriptGenerator.java new file mode 100644 index 00000000..8c51089c --- /dev/null +++ b/chapter08/src/test/java/com/seaofnodes/simple/ScriptGenerator.java @@ -0,0 +1,305 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.ScopeNode; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Random; + +public class ScriptGenerator { + + @SuppressWarnings("unchecked") + private static HashSet getKeywords() { + try { + var field = Parser.class.getDeclaredField("KEYWORDS"); + field.setAccessible(true); + return (HashSet) field.get(new Parser("")); + } catch (Throwable e) { + throw new ExceptionInInitializerError(e); + } + } + + private static final int INDENTATION = 4; + private static final String VAR_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + private static final HashSet KEYWORDS = getKeywords(); + + private static final int FLAG_STOP = 0x01; + private static final int FLAG_IF_WITHOUT_ELSE = 0x02; + + private final Random random; + private int indentation = 0; + private final StringBuilder sb; + private int loopDepth = 0; + private int currScopeStart = 0; + private int depth = 0; + private int exprDepth = 3; + private final ArrayList variables = new ArrayList<>(); + private final boolean generateValid; + private boolean maybeInvalid = false; + + public ScriptGenerator(Random random, StringBuilder sb, boolean generateValid) { + this.random = random; + this.sb = sb; + this.generateValid = generateValid; + } + + private int randLog(int max) { + max--; + int n = 1 << max; + var num = random.nextInt(n); + for(int i=max-1; i>=0; i--) { + if ((num & (1 << i))!=0) return max-i-1; + } + return max; + } + + private StringBuilder printIndentation() { + return sb.repeat(' ', indentation); + } + + public boolean genProgram() { + this.maybeInvalid = false; + variables.add(ScopeNode.ARG0); + depth = random.nextInt(9) + 1; + currScopeStart = variables.size(); + if ((genStatements() & FLAG_STOP) != 0) + genReturn(); + return !this.maybeInvalid; + } + + public int genStatements() { + var num = random.nextInt(10); + for (int i=0; i genBlock(); + case 2 -> genIf(); + case 3 -> genWhile(); + case 4, 5 -> genDecl(); + case 6, 7, 8 -> genAssignment(); + default -> genExit(); + }; + } + + public int genStatementBlock() { + indentation += INDENTATION; + int res; + if (depth == 0) { + res = random.nextBoolean() ? genAssignment() : genExit(); + } else { + depth--; + res = switch (random.nextInt(10)) { + case 1, 2, 3, 4, 5 -> { + depth++; + indentation -= INDENTATION; + var ret = genBlock(); + indentation += INDENTATION; + depth--; + yield ret; + } + case 6 -> genIf(); + case 7 -> genWhile(); + case 8 -> genAssignment(); + default -> genExit(); + }; + depth++; + } + indentation -= INDENTATION; + return res; + } + + public int genExit() { + if (loopDepth == 0 && generateValid) return genReturn(); + return switch (random.nextInt(7)) { + case 0,1,2 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("continue;"); + yield FLAG_STOP; + } + case 3,4,5 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("break;"); + yield FLAG_STOP; + } + default -> genReturn(); + }; + } + + public int genBlock() { + if (depth == 0) { + sb.append("{}"); + return 0; + } + depth--; + int oldCSS = currScopeStart; + currScopeStart = variables.size(); + sb.append("{\n"); + indentation += INDENTATION; + var stop = genStatements(); + indentation -= INDENTATION; + printIndentation().append("}"); + while (variables.size() > currScopeStart) + variables.removeLast(); + currScopeStart = oldCSS; + depth++; + return stop & ~FLAG_IF_WITHOUT_ELSE; + } + + public int genIf() { + sb.append("if("); + genExpression(); + sb.append(") "); + var stop = genStatementBlock(); + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + stop &= genStatementBlock(); + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + return stop; + } + + public int genWhile() { + sb.append("while("); + genExpression(); + sb.append(") "); + loopDepth++; + var stop = genStatementBlock(); + loopDepth--; + return stop & FLAG_IF_WITHOUT_ELSE; + } + + private StringBuilder getRandomName() { + int len = random.nextInt(10) + 1; + StringBuilder sb = new StringBuilder(len); + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()-10))); + for (int i=1; i 0 && random.nextInt(10) > 7) { + var v = variables.get(random.nextInt(currScopeStart)); + if (variables.lastIndexOf(v) < currScopeStart) return v; + if (!generateValid) { + maybeInvalid = true; + return v; + } + } + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart)) { + maybeInvalid = true; + return v; + } + while (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart) { + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + v = sb.toString(); + } + return v; + } + + public int genDecl() { + String name = getVarName(); + sb.append("int ").append(name).append("="); + genExpression(); + sb.append(";"); + variables.add(name); + return 0; + } + + public int genAssignment() { + if (variables.isEmpty()) return genDecl(); + genVariable(); + sb.append("="); + genExpression(); + sb.append(";"); + return 0; + } + + public int genReturn() { + sb.append("return "); + genExpression(); + sb.append(";"); + return FLAG_STOP; + } + + private static final String[] EXPRESSIONS = {"==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"}; + private static final String[] UNARY = {"-"}; + + public void genExpression() { + var num = randLog(10); + while(num-->0) { + genUnary(); + sb.append(EXPRESSIONS[random.nextInt(EXPRESSIONS.length)]); + } + genUnary(); + } + + public void genUnary() { + var num = randLog(6); + while(num-->0) { + sb.append(UNARY[random.nextInt(UNARY.length)]); + } + genPrimary(); + } + + public void genPrimary() { + switch (random.nextInt(10)) { + case 0: + sb.append("true"); + break; + case 1: + sb.append("false"); + break; + case 2: + if (exprDepth == 0) { + if (random.nextBoolean()) { + genNumber(); + } else { + genVariable(); + } + } else { + sb.append("("); + exprDepth--; + genExpression(); + exprDepth++; + sb.append(")"); + } + break; + case 3, 4, 5, 6: + genNumber(); + break; + default: + genVariable(); + } + } + + public void genNumber() { + sb.append(random.nextInt(100)); + } + + public void genVariable() { + if (!generateValid && random.nextInt(100)>97) { + sb.append(getRandomName()); + maybeInvalid = true; + return; + } + if (variables.isEmpty()) { + genNumber(); + } else { + sb.append(variables.get(random.nextInt(variables.size()))); + } + } + +} -- cgit v1.2.3