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 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 objects) { final Set seen = new HashSet<>(); for (final Object object : objects) { if (!seen.add(object)) return false; } return true; } // Instrumented: count equality comparisons static long defectiveIsSimpleOps(List 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 objects) { long ops = 0; final Set seen = new HashSet<>(); for (final Object object : objects) { ops++; if (!seen.add(object)) break; } return ops; } static List makePath(int n) { // Simple path: n unique vertices (all distinct) List 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 simple = Arrays.asList("a", "b", "c", "d"); List 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 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 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"); } }