package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Objects; /** * CWE-407 unit test: opensearch-005 * IndexGraveyard.containsIndex — List linear scan called per-index * inside DanglingIndicesState loop → O(I × T). * * Mirrors elasticsearch-004 — OpenSearch is a fork with the identical defect. * * Slow path: for (Tombstone t : tombstones) { if t.equals(index) ... } — O(T) per call. * Fast path: HashSet.contains() — O(1) per call. * * Compile: javac -d . IndexGraveyardDanglingContains.java * Run: java -ea unit.IndexGraveyardDanglingContains */ public class IndexGraveyardDanglingContains { static final class Index { final String name; final String uuid; Index(String name, String uuid) { this.name = name; this.uuid = uuid; } @Override public boolean equals(Object o) { if (!(o instanceof Index)) return false; Index other = (Index) o; return name.equals(other.name) && uuid.equals(other.uuid); } @Override public int hashCode() { return Objects.hash(name, uuid); } } static final class Tombstone { final Index index; Tombstone(Index index) { this.index = index; } Index getIndex() { return index; } } // ---- Slow: O(T) per call ---- static boolean slowContainsIndex(List tombstones, Index index) { for (Tombstone tombstone : tombstones) { if (tombstone.getIndex().equals(index)) return true; } return false; } static List slowFindDangling(List diskIndices, List tombstones) { List dangling = new ArrayList<>(); for (Index index : diskIndices) { if (!slowContainsIndex(tombstones, index)) dangling.add(index); } return dangling; } // ---- Fast: O(1) per call after O(T) setup ---- static List fastFindDangling(List diskIndices, List tombstones) { Set graveyardSet = new HashSet<>(tombstones.size() * 2); for (Tombstone t : tombstones) graveyardSet.add(t.getIndex()); List dangling = new ArrayList<>(); for (Index index : diskIndices) { if (!graveyardSet.contains(index)) dangling.add(index); } return dangling; } static long countSlowOps(List diskIndices, List tombstones) { long ops = 0; for (Index index : diskIndices) { for (Tombstone t : tombstones) { ops++; if (t.getIndex().equals(index)) break; } } return ops; } static long countFastOps(List diskIndices, List tombstones) { long ops = 0; for (Tombstone t : tombstones) ops++; // build set: O(T) for (Index ignored : diskIndices) ops++; // lookup: O(1) each return ops; } public static void main(String[] args) { int passed = 0; // Test 1: correctness { List tombstones = new ArrayList<>(); List diskIndices = new ArrayList<>(); for (int i = 0; i < 15; i++) { tombstones.add(new Tombstone(new Index("idx-" + i, "u" + i))); } for (int i = 0; i < 25; i++) { diskIndices.add(new Index("idx-" + i, "u" + i)); } List slow = slowFindDangling(diskIndices, tombstones); List fast = fastFindDangling(diskIndices, tombstones); assert slow.size() == fast.size() : "FAIL: mismatch slow=" + slow.size() + " fast=" + fast.size(); System.out.println("PASS test1: correctness — " + fast.size() + " dangling"); passed++; } // Test 2: all tombstoned { List tombstones = new ArrayList<>(); List diskIndices = new ArrayList<>(); for (int i = 0; i < 30; i++) { tombstones.add(new Tombstone(new Index("idx-" + i, "u" + i))); diskIndices.add(new Index("idx-" + i, "u" + i)); } assert slowFindDangling(diskIndices, tombstones).size() == 0 : "FAIL slow"; assert fastFindDangling(diskIndices, tombstones).size() == 0 : "FAIL fast"; System.out.println("PASS test2: all tombstoned"); passed++; } // Test 3: operation count ratio >= 10x { int I = 500, T = 500; List tombstones = new ArrayList<>(); List diskIndices = new ArrayList<>(); for (int i = 0; i < T; i++) tombstones.add(new Tombstone(new Index("t" + i, "ut" + i))); for (int i = 0; i < I; i++) diskIndices.add(new Index("d" + i, "ud" + i)); long slowOps = countSlowOps(diskIndices, tombstones); long fastOps = countFastOps(diskIndices, tombstones); double ratio = (double) slowOps / fastOps; System.out.printf("PASS test3: I=%d T=%d slow=%d fast=%d ratio=%.1fx%n", I, T, slowOps, fastOps, ratio); assert ratio >= 10.0 : "FAIL: ratio " + ratio + " < 10x"; passed++; } // Test 4: timing benchmark { int I = 2000, T = 500; List tombstones = new ArrayList<>(); List diskIndices = new ArrayList<>(); for (int i = 0; i < T; i++) tombstones.add(new Tombstone(new Index("t" + i, "ut" + i))); for (int i = 0; i < I; i++) diskIndices.add(new Index("d" + i, "ud" + i)); int reps = 200; long t0 = System.nanoTime(); for (int r = 0; r < reps; r++) slowFindDangling(diskIndices, tombstones); long slowNs = System.nanoTime() - t0; long t1 = System.nanoTime(); for (int r = 0; r < reps; r++) fastFindDangling(diskIndices, tombstones); long fastNs = System.nanoTime() - t1; double ratio = (double) slowNs / fastNs; System.out.printf("PASS test4: timing I=%d T=%d slow=%.1fms fast=%.1fms ratio=%.1fx%n", I, T, slowNs / 1e6 / reps, fastNs / 1e6 / reps, ratio); assert ratio >= 10.0 : "FAIL: timing ratio " + ratio + " < 10x"; passed++; } System.out.println(passed + "/" + passed + " PASS"); } }