B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
112 lines
4.4 KiB
Java
112 lines
4.4 KiB
Java
package unit;
|
|
|
|
import java.util.*;
|
|
|
|
/**
|
|
* TinkerPopPathTest — CWE-407 in Apache TinkerPop Path.isSimple()
|
|
*
|
|
* Path.java:206-214 (default isSimple()) uses an O(n²) nested double-loop to check
|
|
* for duplicate vertices in a path. Triggered by PathFilterStep.java:60,62,79 via
|
|
* subPath() → MutablePath (which has no override), bypassing ImmutablePath's correct
|
|
* HashSet implementation. Every .simplePath() / .cyclicPath() Gremlin step is affected
|
|
* when from()/to() label scoping or by() modulators are used.
|
|
*
|
|
* Fix: replace O(n²) nested loop with O(n) HashSet membership test.
|
|
*/
|
|
public class TinkerPopPathTest {
|
|
|
|
// --- Defective: O(n²) nested loop (Path.java:206-214 default isSimple) ---
|
|
static boolean defectiveIsSimple(List<Object> objects) {
|
|
for (int i = 0; i < objects.size() - 1; i++) {
|
|
for (int j = i + 1; j < objects.size(); j++) {
|
|
if (Objects.equals(objects.get(i), objects.get(j)))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// --- Fixed: O(n) HashSet (matches ImmutablePath.isSimple() override) ---
|
|
static boolean fixedIsSimple(List<Object> objects) {
|
|
final Set<Object> seen = new HashSet<>();
|
|
for (final Object object : objects) {
|
|
if (!seen.add(object)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Instrumented: count equality comparisons
|
|
static long defectiveIsSimpleOps(List<Object> objects) {
|
|
long ops = 0;
|
|
for (int i = 0; i < objects.size() - 1; i++) {
|
|
for (int j = i + 1; j < objects.size(); j++) {
|
|
ops++;
|
|
if (Objects.equals(objects.get(i), objects.get(j)))
|
|
break;
|
|
}
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static long fixedIsSimpleOps(List<Object> objects) {
|
|
long ops = 0;
|
|
final Set<Object> seen = new HashSet<>();
|
|
for (final Object object : objects) {
|
|
ops++;
|
|
if (!seen.add(object)) break;
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static List<Object> makePath(int n) {
|
|
// Simple path: n unique vertices (all distinct)
|
|
List<Object> path = new ArrayList<>();
|
|
for (int i = 0; i < n; i++) path.add("v" + i);
|
|
return path;
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("=== TinkerPop tinkerpop-0001: Path.isSimple() O(n²)→O(n) ===");
|
|
System.out.println();
|
|
|
|
// Correctness
|
|
List<Object> simple = Arrays.asList("a", "b", "c", "d");
|
|
List<Object> cyclic = Arrays.asList("a", "b", "c", "a");
|
|
assert defectiveIsSimple(simple) == fixedIsSimple(simple) : "simple path mismatch";
|
|
assert defectiveIsSimple(cyclic) == fixedIsSimple(cyclic) : "cyclic path mismatch";
|
|
System.out.println("PASS correctness: simple=" + fixedIsSimple(simple) + " cyclic=" + fixedIsSimple(cyclic));
|
|
|
|
// Scaling tests
|
|
int[] sizes = {10, 25, 50, 100, 200};
|
|
System.out.printf("%-6s %-10s %-8s %-8s%n", "n", "speedup", "defect-ops", "fixed-ops");
|
|
|
|
double lastSpeedup = 1.0;
|
|
for (int n : sizes) {
|
|
List<Object> path = makePath(n);
|
|
long defOps = defectiveIsSimpleOps(path);
|
|
long fixOps = fixedIsSimpleOps(path);
|
|
double speedup = (double) defOps / fixOps;
|
|
lastSpeedup = speedup;
|
|
System.out.printf("%-6d %-10.1f %-8d %-8d%n", n, speedup, defOps, fixOps);
|
|
}
|
|
|
|
// Assert quadratic vs linear growth
|
|
// At n=200: defective does n*(n-1)/2 = 19900 ops; fixed does n = 200 ops → ~99.5x
|
|
List<Object> big = makePath(200);
|
|
long defOps = defectiveIsSimpleOps(big);
|
|
long fixOps = fixedIsSimpleOps(big);
|
|
assert defOps > fixOps * 50 :
|
|
"Expected defective to do 50x+ more ops at n=200, got defect=" + defOps + " fixed=" + fixOps;
|
|
System.out.println();
|
|
System.out.println("PASS: defective O(n²) does " + defOps + " ops at n=200");
|
|
System.out.println("PASS: fixed O(n) does " + fixOps + " ops at n=200");
|
|
System.out.printf("PASS: speedup=%.1fx at n=200 (>50x threshold)%n", (double)defOps/fixOps);
|
|
|
|
// Assert correctness still holds at scale
|
|
assert defectiveIsSimple(big) == fixedIsSimple(big) : "correctness mismatch at n=200";
|
|
System.out.println("PASS: correctness confirmed at n=200");
|
|
|
|
System.out.println();
|
|
System.out.println("5/5 PASS — tinkerpop-0001 confirmed: O(n²)→O(n), ~99.5x at n=200");
|
|
}
|
|
}
|