package unit; import java.util.*; /** * Box2DAlgorithm — unit test for Box2D CWE-407 defect. * * box2d-0001: b2BroadPhase b2UnBufferMove() linear scan of moveArray. * * File: src/broad_phase.c lines 71–88 * Defect: b2UnBufferMove() first removes proxyKey from moveSet in O(1), * then scans moveArray linearly to find the array index for RemoveSwap. * With N buffered proxies, destroying all bodies triggers O(N²) comparisons. * * for ( int i = 0; i < count; ++i ) * { * if ( bp->moveArray.data[i] == proxyKey ) // linear scan * { * b2IntArray_RemoveSwap( &bp->moveArray, i ); * break; * } * } * * Fix: maintain a parallel HashMap (moveIndex). * On bufferMove: record index. On unBufferMove: O(1) lookup, then * RemoveSwap with displaced-element index fix-up. * * The dev comment reads: "Purge from move buffer. Linear search." * and a TODO: "todo if I can iterate the move set then I don't need the moveArray" * * Tests: correctness + performance ratio >= 5x at N=500-1000. * Prints: N/N PASS */ public class Box2DAlgorithm { static int passed = 0; static int total = 0; static void pass(String name) { passed++; total++; System.out.println(" PASS " + name); } static void fail(String name, String reason) { total++; System.out.println(" FAIL " + name + " — " + reason); } // ----------------------------------------------------------------------- // Slow path: mirrors b2UnBufferMove (linear scan of moveArray) // ----------------------------------------------------------------------- static class SlowMoveBuffer { Set moveSet = new HashSet<>(); List moveArray = new ArrayList<>(); void bufferMove(int proxyKey) { if (moveSet.add(proxyKey)) { moveArray.add(proxyKey); } } /** Returns number of comparisons made during the linear scan. */ long unBufferMove(int proxyKey) { long ops = 0; if (moveSet.remove(proxyKey)) { for (int i = 0; i < moveArray.size(); i++) { ops++; if (moveArray.get(i).equals(proxyKey)) { int last = moveArray.size() - 1; moveArray.set(i, moveArray.get(last)); moveArray.remove(last); break; } } } return ops; } List snapshot() { return new ArrayList<>(moveArray); } } // ----------------------------------------------------------------------- // Fast path: O(1) removal via index map (the proposed fix) // ----------------------------------------------------------------------- static class FastMoveBuffer { Set moveSet = new HashSet<>(); List moveArray = new ArrayList<>(); Map indexMap = new HashMap<>(); // proxyKey → array index void bufferMove(int proxyKey) { if (moveSet.add(proxyKey)) { indexMap.put(proxyKey, moveArray.size()); moveArray.add(proxyKey); } } /** Returns 1 (one map lookup) per call regardless of array size. */ long unBufferMove(int proxyKey) { if (moveSet.remove(proxyKey)) { Integer idx = indexMap.remove(proxyKey); if (idx != null) { int last = moveArray.size() - 1; if (idx != last) { Integer displaced = moveArray.get(last); moveArray.set(idx, displaced); indexMap.put(displaced, idx); } moveArray.remove(last); } return 1; } return 0; } List snapshot() { return new ArrayList<>(moveArray); } } // ----------------------------------------------------------------------- // Test helpers // ----------------------------------------------------------------------- static long runSlowDestroyAll(int n) { SlowMoveBuffer buf = new SlowMoveBuffer(); for (int i = 0; i < n; i++) buf.bufferMove(i); long ops = 0; for (int i = 0; i < n; i++) ops += buf.unBufferMove(i); return ops; } static long runFastDestroyAll(int n) { FastMoveBuffer buf = new FastMoveBuffer(); for (int i = 0; i < n; i++) buf.bufferMove(i); long ops = 0; for (int i = 0; i < n; i++) ops += buf.unBufferMove(i); return ops; } // ----------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------- static void testCorrectness() { // Buffer N proxies, remove half, check both paths have identical final arrays. int N = 50; SlowMoveBuffer slow = new SlowMoveBuffer(); FastMoveBuffer fast = new FastMoveBuffer(); for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); } // Remove even keys for (int i = 0; i < N; i += 2) { slow.unBufferMove(i); fast.unBufferMove(i); } List slowSnap = slow.snapshot(); List fastSnap = fast.snapshot(); Collections.sort(slowSnap); Collections.sort(fastSnap); if (!slowSnap.equals(fastSnap)) { fail("box2d-0001 correctness N=50 half-remove", "snapshots differ: slow=" + slowSnap + " fast=" + fastSnap); return; } pass("box2d-0001 correctness N=50 half-remove (snapshots match)"); } static void testCorrectnessWithDups() { // Duplicate bufferMove calls should be idempotent in both paths. int N = 20; SlowMoveBuffer slow = new SlowMoveBuffer(); FastMoveBuffer fast = new FastMoveBuffer(); for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); } // buffer the same keys again — should not double-add for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); } if (slow.moveArray.size() != N) { fail("box2d-0001 dup-bufferMove slow dedup", "size=" + slow.moveArray.size()); return; } if (fast.moveArray.size() != N) { fail("box2d-0001 dup-bufferMove fast dedup", "size=" + fast.moveArray.size()); return; } pass("box2d-0001 dup-bufferMove N=20 (both deduplicate correctly)"); } static void testPerformanceDestroyAll() { // N=1000: all proxies buffered then all destroyed. // Slow: sum of array sizes scanned — O(N²/2). // Fast: exactly N ops — O(N). int N = 1000; long slowOps = runSlowDestroyAll(N); long fastOps = runFastDestroyAll(N); // Slow expected: N + (N-1) + ... + 1 = N*(N+1)/2 (worst case ordering) // Actual may vary by removal order; just verify ratio. double ratio = (double) slowOps / fastOps; System.out.printf(" box2d-0001 N=%d slow_ops=%,d fast_ops=%,d ratio=%.0fx%n", N, slowOps, fastOps, ratio); if (fastOps != N) { fail("box2d-0001 performance N=1000", "fast_ops=" + fastOps + " expected=" + N); return; } if (ratio < 5.0) { fail("box2d-0001 performance N=1000", "ratio=" + ratio + " < 5x"); return; } pass("box2d-0001 performance ratio >= 5x at N=1000 (destroy-all)"); } static void testPerformanceHalfFill() { // N=800 buffered, N/2 removed (mixed static/dynamic pattern). int N = 800; SlowMoveBuffer slow = new SlowMoveBuffer(); FastMoveBuffer fast = new FastMoveBuffer(); for (int i = 0; i < N; i++) { slow.bufferMove(i); fast.bufferMove(i); } long slowOps = 0, fastOps = 0; for (int i = 0; i < N / 2; i++) { slowOps += slow.unBufferMove(i); fastOps += fast.unBufferMove(i); } double ratio = (double) slowOps / fastOps; System.out.printf(" box2d-0001 N=%d half-remove slow_ops=%,d fast_ops=%,d ratio=%.0fx%n", N, slowOps, fastOps, ratio); if (ratio < 5.0) { fail("box2d-0001 performance N=800 half-remove", "ratio=" + ratio + " < 5x"); return; } pass("box2d-0001 performance ratio >= 5x at N=800 (half-remove)"); } public static void main(String[] args) { System.out.println("Box2D CWE-407 unit tests"); System.out.println("=".repeat(60)); System.out.println(); System.out.println("[box2d-0001] b2BroadPhase::b2UnBufferMove linear scan"); testCorrectness(); testCorrectnessWithDups(); testPerformanceDestroyAll(); testPerformanceHalfFill(); System.out.println(); System.out.printf("%d/%d PASS%n", passed, total); if (passed != total) System.exit(1); } }