package unit; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; /** * Unit test for Spring CWE-407 defects: * spring-0001: AnnotationTypeMapping.processAliases — aliases.contains (ArrayList) in nested loops * spring-0002: AbstractApplicationEventMulticaster — allListeners.contains (ArrayList) in loop * * Op counting model: each contains() call costs as many ops as there are elements scanned. * - ArrayList.contains(x): scans up to N elements → N ops * - HashSet.contains(x): O(1) → 1 op * * No JUnit. No external deps. Compile and run: * javac -d . *.java && java -ea unit.SpringTest */ public class SpringTest { // ---- spring-0001 simulation ---- // Simulates processAliases: for each attribute (A), for each mapping depth (M), // for each attribute, call aliases.contains → O(A² × M) total inner scan ops static long slowAliasProcessing(int attributeCount, int mappingDepth) { long innerScanOps = 0; List aliases = new ArrayList<>(); for (int i = 0; i < attributeCount; i++) { aliases.clear(); aliases.add(i); // collectAliases: for each k, scan the ArrayList for (int k = 0; k < attributeCount; k++) { // ArrayList.contains scans all current elements innerScanOps += aliases.size(); if (!aliases.contains(k)) { aliases.add(k); } } // processAliases(i, aliases): while(mapping depth) + for(each attribute) for (int depth = 0; depth < mappingDepth; depth++) { for (int j = 0; j < attributeCount; j++) { // ArrayList.contains scans all aliases innerScanOps += aliases.size(); // (don't call aliases.contains again — cost already counted above) } } } return innerScanOps; } static long fastAliasProcessing(int attributeCount, int mappingDepth) { long innerScanOps = 0; Set aliases = new LinkedHashSet<>(); for (int i = 0; i < attributeCount; i++) { aliases.clear(); aliases.add(i); // collectAliases: HashSet.contains is O(1) = 1 op per call for (int k = 0; k < attributeCount; k++) { innerScanOps += 1; // O(1) lookup aliases.add(k); // Set.add handles dedup } // processAliases: HashSet.contains O(1) per call for (int depth = 0; depth < mappingDepth; depth++) { for (int j = 0; j < attributeCount; j++) { innerScanOps += 1; // O(1) } } } return innerScanOps; } // ---- spring-0002 simulation ---- // Simulates retrieveApplicationListeners: for each listenerBean, call allListeners.contains. // allListeners is ArrayList → O(L) scan per call; three calls per bean. static long slowListenerDedup(int listenerCount) { long innerScanOps = 0; List allListeners = new ArrayList<>(); // Pre-populate with half the listeners (programmatic registration) for (int i = 0; i < listenerCount / 2; i++) { allListeners.add(i); } // Iterate bean-name listeners (the other half) for (int i = listenerCount / 2; i < listenerCount; i++) { // filteredListeners.contains(unwrappedListener) — scan cost innerScanOps += allListeners.size(); // allListeners.contains(unwrappedListener) — scan cost innerScanOps += allListeners.size(); // allListeners.contains(listener) — scan cost innerScanOps += allListeners.size(); if (!allListeners.contains(i)) { allListeners.add(i); } } return innerScanOps; } static long fastListenerDedup(int listenerCount) { long innerScanOps = 0; Set allListeners = new LinkedHashSet<>(); for (int i = 0; i < listenerCount / 2; i++) { allListeners.add(i); } for (int i = listenerCount / 2; i < listenerCount; i++) { // Each contains on LinkedHashSet: O(1) = 1 op innerScanOps += 1; innerScanOps += 1; innerScanOps += 1; allListeners.add(i); } return innerScanOps; } public static void main(String[] args) { int pass = 0; int total = 0; // --- spring-0001 tests --- { total++; long slow = slowAliasProcessing(20, 5); long fast = fastAliasProcessing(20, 5); // With A=20, M=5: slow has O(A^2 * M) scan ops = ~2000+, fast has O(A * (A + A*M)) = ~2400 // The key difference is in collectAliases: slow scans growing list (avg A/2), fast is 1 boolean ok = slow > fast; System.out.println("[spring-0001] A=20 M=5: slow_ops=" + slow + " fast_ops=" + fast + " ratio=" + String.format("%.1f", (double)slow/Math.max(fast,1)) + "x " + (ok ? "PASS" : "FAIL")); if (ok) pass++; } { total++; long slow = slowAliasProcessing(50, 10); long fast = fastAliasProcessing(50, 10); boolean ok = slow > fast * 3; System.out.println("[spring-0001] A=50 M=10: slow_ops=" + slow + " fast_ops=" + fast + " ratio=" + String.format("%.1f", (double)slow/Math.max(fast,1)) + "x " + (ok ? "PASS" : "FAIL")); if (ok) pass++; } { total++; // Correctness: both paths discover same unique attributes List slowResult = new ArrayList<>(); Set fastResult = new LinkedHashSet<>(); int A = 15; for (int i = 0; i < A; i++) { if (!slowResult.contains(i)) slowResult.add(i); fastResult.add(i); } // Also add some duplicates for (int i = 0; i < A / 2; i++) { if (!slowResult.contains(i)) slowResult.add(i); fastResult.add(i); } boolean ok = slowResult.size() == fastResult.size() && new ArrayList<>(fastResult).equals(slowResult); System.out.println("[spring-0001] correctness A=15: slow_size=" + slowResult.size() + " fast_size=" + fastResult.size() + " " + (ok ? "PASS" : "FAIL")); if (ok) pass++; } // --- spring-0002 tests --- { total++; long slow = slowListenerDedup(200); long fast = fastListenerDedup(200); boolean ok = slow > fast * 10; System.out.println("[spring-0002] L=200: slow_ops=" + slow + " fast_ops=" + fast + " ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL")); if (ok) pass++; } { total++; long slow = slowListenerDedup(500); long fast = fastListenerDedup(500); boolean ok = slow > fast * 50; System.out.println("[spring-0002] L=500: slow_ops=" + slow + " fast_ops=" + fast + " ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL")); if (ok) pass++; } { total++; // Correctness: dedup result must be same size List slowList = new ArrayList<>(); Set fastSet = new LinkedHashSet<>(); // Add 100 items including duplicates for (int i = 0; i < 100; i++) { int val = i % 80; if (!slowList.contains(val)) slowList.add(val); fastSet.add(val); } boolean ok = slowList.size() == fastSet.size(); System.out.println("[spring-0002] dedup correctness: slow=" + slowList.size() + " fast=" + fastSet.size() + " " + (ok ? "PASS" : "FAIL")); if (ok) pass++; } System.out.println("\n" + pass + "/" + total + " PASS"); if (pass != total) { System.exit(1); } } }