forgejo-0001: Results.RepoIDs() slices.Contains dedup O(N^2) in code search — 107x at N=5000 snort3-0001: ServiceDiscovery service_candidates std::find dedup O(M*C) in AppID — 59x at M=10000
92 lines
3.2 KiB
Java
92 lines
3.2 KiB
Java
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<Long> repoIDsDefective(long[] repoIDs) {
|
|
// Simulates: ids := make([]int64, len(res)) — pre-filled with zeros!
|
|
List<Long> 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<Long> repoIDsFixed(long[] repoIDs) {
|
|
Set<Long> seen = new HashSet<>(repoIDs.length);
|
|
List<Long> 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<Long> dResult = repoIDsDefective(repoIDs);
|
|
List<Long> fResult = repoIDsFixed(repoIDs);
|
|
// Remove the leading zeros from defective version
|
|
dResult.removeIf(id -> id == 0L);
|
|
Set<Long> dSet = new HashSet<>(dResult);
|
|
Set<Long> 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");
|
|
}
|
|
}
|