java-topology/defects/tinkerpop/unit/TinkerPopPathTest.java
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

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");
}
}