203 lines
8.2 KiB
Java
203 lines
8.2 KiB
Java
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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> slowResult = new ArrayList<>();
|
||
Set<Integer> 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<Integer> slowList = new ArrayList<>();
|
||
Set<Integer> 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);
|
||
}
|
||
}
|
||
}
|