package unit; import java.util.*; /** * Box2dTest — box2d-0001 * * Proves CWE-407 in Box2D v3 (erincatto/box2d) src/broad_phase.c: * box2d-0001: b2UnBufferMove — linear scan through moveArray when removing a * buffered proxy key → O(N²) when N proxies are destroyed. * * The code itself has a comment: "Purge from move buffer. Linear search." * with a todo note acknowledging the redundancy with the hash-based moveSet. * * Run: javac -d . Box2dTest.java && java -ea unit.Box2dTest */ public class Box2dTest { // ── box2d-0001: b2UnBufferMove linear scan ─────────────────────────────── /** * SLOW: simulates b2UnBufferMove — linear scan through moveArray. * For each destroy call: scan the array from index 0 to find the key, * then swap-remove (O(1) remove once found, but O(N) to find). * Destroys all N proxies → O(N²) total. */ static long bulkDestroySlow(int proxyCount) { // moveArray: list of buffered proxy keys List moveArray = new ArrayList<>(); for (int i = 0; i < proxyCount; i++) { moveArray.add(i); // b2BufferMove: add proxy to moveArray } long ops = 0; // Destroy all proxies in reverse order (worst-case scan direction) for (int proxyKey = proxyCount - 1; proxyKey >= 0; proxyKey--) { // b2UnBufferMove: linear scan to find proxyKey in moveArray int size = moveArray.size(); for (int i = 0; i < size; i++) { ops++; if (moveArray.get(i) == proxyKey) { // swap-remove: O(1) int last = moveArray.get(moveArray.size() - 1); moveArray.set(i, last); moveArray.remove(moveArray.size() - 1); break; } } } return ops; } /** * FAST: index map (proxyKey → slot in moveArray) → O(1) lookup per remove. * Simulates the fix: maintain a HashMap. */ static long bulkDestroyFast(int proxyCount) { List moveArray = new ArrayList<>(); Map moveIndex = new HashMap<>(); // proxyKey → array slot for (int i = 0; i < proxyCount; i++) { moveIndex.put(i, moveArray.size()); moveArray.add(i); } long ops = 0; for (int proxyKey = proxyCount - 1; proxyKey >= 0; proxyKey--) { ops++; // O(1) hash lookup Integer slot = moveIndex.remove(proxyKey); if (slot != null && slot < moveArray.size()) { // swap-remove: O(1) int last = moveArray.get(moveArray.size() - 1); moveArray.set(slot, last); moveIndex.put(last, slot); moveArray.remove(moveArray.size() - 1); } } return ops; } static void testBox2d0001() { System.out.println("=== box2d-0001: b2UnBufferMove linear scan on bulk destroy ==="); int[] sizes = {100, 500, 1000, 5000, 10000}; for (int N : sizes) { long slow = bulkDestroySlow(N); long fast = bulkDestroyFast(N); double ratio = (double) slow / fast; System.out.printf(" N=%6d slow_ops=%12d fast_ops=%8d ratio=%.1fx%n", N, slow, fast, ratio); } // Regression: at N=1000 slow >> fast long slow1000 = bulkDestroySlow(1000); long fast1000 = bulkDestroyFast(1000); double ratio1000 = (double) slow1000 / fast1000; assert ratio1000 > 50.0 : "box2d-0001 FAIL: expected ratio > 50x at N=1000, got " + ratio1000; System.out.println(" [PASS] box2d-0001: ratio=" + String.format("%.1f", ratio1000) + "x at N=1000"); // Verify O(N²) vs O(N): ratio at N=5000 should be >> ratio at N=1000 long slow5000 = bulkDestroySlow(5000); long fast5000 = bulkDestroyFast(5000); double ratio5000 = (double) slow5000 / fast5000; assert ratio5000 > ratio1000 * 3 : "box2d-0001 FAIL: O(N²) growth expected; ratio5000=" + ratio5000 + " ratio1000=" + ratio1000; System.out.println(" [PASS] box2d-0001: O(N²) growth confirmed (ratio5000=" + String.format("%.1f", ratio5000) + "x > 3×ratio1000=" + String.format("%.1f", ratio1000) + "x)"); } public static void main(String[] args) { testBox2d0001(); System.out.println("\nAll Box2D tests PASS."); } }