java-topology/defects/spring/unit/SpringVersionResourceResolverTest.java

134 lines
5.5 KiB
Java

package unit;
/**
* Regression test for spring-0006: CWE-407 O(N²) patternsList.contains() inside
* addFixedVersionStrategy() loop in VersionResourceResolver.
*
* File: spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
* Lines: 130-141
*
* Defect: patternsList = Arrays.asList(pathPatterns) then for each pattern calls
* patternsList.contains(versionPrefix + pattern) — O(N) inside O(N) loop = O(N²).
*
* Fix: use HashSet<String> for O(1) membership check.
*/
public class SpringVersionResourceResolverTest {
// ---- Defective model (mirrors VersionResourceResolver before fix) ----
static java.util.List<String> addFixedVersionStrategyDefective(String version, String... pathPatterns) {
java.util.List<String> patternsList = java.util.Arrays.asList(pathPatterns); // plain List — O(N) contains
java.util.List<String> prefixedPatterns = new java.util.ArrayList<>(pathPatterns.length);
String versionPrefix = "/" + version;
long ops = 0;
for (String pattern : patternsList) {
prefixedPatterns.add(pattern);
ops++;
for (String p : patternsList) { ops++; } // simulate contains() scan
if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) {
prefixedPatterns.add(versionPrefix + pattern);
}
}
slowOps = ops;
return prefixedPatterns;
}
// ---- Fixed model (mirrors VersionResourceResolver after fix) ----
static java.util.List<String> addFixedVersionStrategyFixed(String version, String... pathPatterns) {
java.util.Set<String> patternsSet = new java.util.HashSet<>(java.util.Arrays.asList(pathPatterns));
java.util.List<String> prefixedPatterns = new java.util.ArrayList<>(pathPatterns.length * 2);
String versionPrefix = "/" + version;
long ops = 0;
for (String pattern : pathPatterns) {
prefixedPatterns.add(pattern);
ops++;
ops++; // HashSet.contains() = O(1)
if (!pattern.startsWith(versionPrefix) && !patternsSet.contains(versionPrefix + pattern)) {
prefixedPatterns.add(versionPrefix + pattern);
}
}
fastOps = ops;
return prefixedPatterns;
}
static long slowOps = 0;
static long fastOps = 0;
// ---- Tests ----
public static void main(String[] args) {
testCorrectness();
testNoFalseDuplicates();
testPerformance();
System.out.println("2/2 PASS");
}
static void testCorrectness() {
// A version prefix already in the patterns list should NOT be duplicated
java.util.List<String> result = addFixedVersionStrategyFixed("1.0.0",
"/js/**", "/css/**", "/1.0.0/js/**");
// "/js/**" already has versioned form in list — should not add "/1.0.0/js/**" again
long count = result.stream().filter(p -> p.equals("/1.0.0/js/**")).count();
if (count != 1) {
System.err.println("FAIL testCorrectness: expected 1 occurrence of /1.0.0/js/**, got " + count);
System.exit(1);
}
// "/css/**" has no versioned form yet — should add "/1.0.0/css/**"
count = result.stream().filter(p -> p.equals("/1.0.0/css/**")).count();
if (count != 1) {
System.err.println("FAIL testCorrectness: expected 1 occurrence of /1.0.0/css/**, got " + count);
System.exit(1);
}
System.out.println(" [PASS] correctness: versioned patterns deduped correctly");
}
static void testNoFalseDuplicates() {
// Both implementations should produce same result
String[] patterns = {"/a/**", "/b/**", "/c/**", "/1.5/a/**"};
java.util.List<String> defective = addFixedVersionStrategyDefective("1.5", patterns);
java.util.List<String> fixed = addFixedVersionStrategyFixed("1.5", patterns);
if (defective.size() != fixed.size()) {
System.err.println("FAIL testNoFalseDuplicates: defective=" + defective.size()
+ " fixed=" + fixed.size() + " — results differ");
System.exit(1);
}
System.out.println(" [PASS] no false duplicates: defective=" + defective.size()
+ " fixed=" + fixed.size());
}
static void testPerformance() {
final int N = 2000;
String[] patterns = new String[N];
for (int i = 0; i < N; i++) {
patterns[i] = "/path" + i + "/**";
}
// Defective: O(N²) operations
long t0 = System.nanoTime();
addFixedVersionStrategyDefective("v1.0", patterns);
long slowTime = System.nanoTime() - t0;
long slowCount = slowOps;
// Fixed: O(N) operations
t0 = System.nanoTime();
addFixedVersionStrategyFixed("v1.0", patterns);
long fastTime = System.nanoTime() - t0;
long fastCount = fastOps;
double opRatio = (double) slowCount / fastCount;
double timeRatio = slowTime > 0 ? (double) slowTime / fastTime : 1.0;
System.out.printf(" [PERF] N=%d slow_ops=%d fast_ops=%d op_ratio=%.1fx time_ratio=%.1fx%n",
N, slowCount, fastCount, opRatio, timeRatio);
if (opRatio < 5.0) {
System.err.println("FAIL testPerformance: op ratio " + opRatio + "x < 5x minimum");
System.exit(1);
}
System.out.println(" [PASS] performance: " + opRatio + "x op ratio (>= 5x required)");
}
}