import java.util.*; /** * Unit test for Forgejo CWE-407 defect: Results.RepoIDs() in * modules/indexer/code/search.go uses slices.Contains() for dedup, * creating O(N^2) complexity on code search results. * * Additionally, the original code uses make([]int64, len(res)) which * pre-fills the slice with N zeros, making Contains scan even more * data than necessary. * * Fix: replace slices.Contains with a map[int64]struct{} set lookup. */ public class ForgejoSearchRepoIDsDedupTest { // --- DEFECTIVE: slices.Contains O(N^2) --- static List repoIDsDefective(long[] repoIDs) { // Simulates: ids := make([]int64, len(res)) — pre-filled with zeros! List ids = new ArrayList<>(Collections.nCopies(repoIDs.length, 0L)); for (long repoID : repoIDs) { if (!ids.contains(repoID)) { ids.add(repoID); } } return ids; } // --- FIXED: map-based O(N) --- static List repoIDsFixed(long[] repoIDs) { Set seen = new HashSet<>(repoIDs.length); List ids = new ArrayList<>(repoIDs.length); for (long repoID : repoIDs) { if (seen.add(repoID)) { ids.add(repoID); } } return ids; } public static void main(String[] args) { // Simulate code search returning N results from N/2 distinct repos int[] sizes = {100, 500, 1000, 5000}; System.out.println("forgejo-0001: Results.RepoIDs() slices.Contains dedup"); System.out.println("N\tDefect(ms)\tFixed(ms)\tRatio"); for (int N : sizes) { long[] repoIDs = new long[N]; Random rng = new Random(42); for (int i = 0; i < N; i++) { repoIDs[i] = rng.nextInt(N / 2) + 1; } // Warmup for (int w = 0; w < 3; w++) { repoIDsDefective(repoIDs); repoIDsFixed(repoIDs); } int iters = Math.max(1, 200000 / N); long t0 = System.nanoTime(); for (int i = 0; i < iters; i++) { repoIDsDefective(repoIDs); } long defectNs = System.nanoTime() - t0; t0 = System.nanoTime(); for (int i = 0; i < iters; i++) { repoIDsFixed(repoIDs); } long fixedNs = System.nanoTime() - t0; double defectMs = defectNs / 1e6; double fixedMs = fixedNs / 1e6; double ratio = defectMs / fixedMs; System.out.printf("%d\t%.1f\t\t%.1f\t\t%.1fx%n", N, defectMs, fixedMs, ratio); // Correctness: both must produce same unique set List dResult = repoIDsDefective(repoIDs); List fResult = repoIDsFixed(repoIDs); // Remove the leading zeros from defective version dResult.removeIf(id -> id == 0L); Set dSet = new HashSet<>(dResult); Set fSet = new HashSet<>(fResult); assert dSet.equals(fSet) : "Results differ at N=" + N; assert ratio > 1.5 || N < 200 : "Expected speedup at N=" + N + " but got ratio=" + ratio; } System.out.println("ALL PASS"); } }