package unit; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * MavenReactorManagerTest * * Models two CWE-407 defects in Apache Maven: * * maven-0006: ReactorManager.blackList * File: impl/maven-core/src/main/java/org/apache/maven/execution/ReactorManager.java * Defective: private List blackList = new ArrayList<>() * blackList(String id) calls blackList.contains(id) — O(N) linear scan. * Recursive cascade over N projects: contains() at step k costs k comparisons * → total O(N²) comparisons. * Fixed: private Set blackList = new HashSet<>() * blackList.add(id) is O(1) per call; total O(N). * * maven-0007: DefaultMavenExecutionRequest.addPluginGroup / addPluginGroups * File: impl/maven-core/src/main/java/org/apache/maven/execution/DefaultMavenExecutionRequest.java * Defective: private List pluginGroups = new ArrayList<>() * addPluginGroup() calls ArrayList.contains() — O(G) per call. * addPluginGroups() loops over G groups → O(G²) total comparisons. * Fixed: private Set pluginGroups = new LinkedHashSet<>() * Each add() is O(1); total O(G). * * Comparison counts (not wall-clock) are measured by simulating the ArrayList * linear scan cost explicitly: contains() on a list of size k costs k comparisons. */ public class MavenReactorManagerTest { // ========================================================================= // Models for maven-0006: ReactorManager ArrayList blackList // ========================================================================= /** * Defective: ArrayList.contains() requires scanning the list linearly. * We count comparisons explicitly: checking a list of size k costs k ops. */ static class DefectiveReactorManager { final List blackList = new ArrayList<>(); final Map> dependents; long comparisons = 0; DefectiveReactorManager(Map> dependents) { this.dependents = dependents; } boolean listContains(String id) { // Simulate ArrayList.contains() scan cost: O(size) comparisons comparisons += blackList.size(); return blackList.contains(id); } void blackList(String id) { if (!listContains(id)) { blackList.add(id); List deps = dependents.getOrDefault(id, List.of()); for (String dep : deps) { blackList(dep); } } } } /** * Fixed: HashSet.contains() / add() is O(1) — 1 comparison per call. */ static class FixedReactorManager { final Set blackList = new HashSet<>(); final Map> dependents; long comparisons = 0; FixedReactorManager(Map> dependents) { this.dependents = dependents; } void blackList(String id) { comparisons++; // HashSet.add() is O(1) if (blackList.add(id)) { List deps = dependents.getOrDefault(id, List.of()); for (String dep : deps) { blackList(dep); } } } } /** * Build a linear dependency chain: p0 -> p1 -> p2 -> ... -> p(N-1). * Blacklisting p0 triggers a recursive cascade over all N projects. */ static Map> buildLinearChain(int n) { Map> deps = new HashMap<>(); for (int i = 0; i < n - 1; i++) { deps.put("p" + i, List.of("p" + (i + 1))); } return deps; } static long[] runBlacklistComparisons(int n) { Map> deps = buildLinearChain(n); DefectiveReactorManager defective = new DefectiveReactorManager(deps); defective.blackList("p0"); long defectiveCmp = defective.comparisons; FixedReactorManager fixed = new FixedReactorManager(deps); fixed.blackList("p0"); long fixedCmp = fixed.comparisons; // Defective: at step k the blackList has k elements, so contains() costs k. // For N projects: sum(0, 1, ..., N-1) = N*(N-1)/2 comparisons → O(N²). // Fixed: N calls each costing 1 → O(N). return new long[]{ defectiveCmp, fixedCmp }; } // ========================================================================= // Models for maven-0007: DefaultMavenExecutionRequest ArrayList pluginGroups // ========================================================================= static class DefectivePluginGroupRequest { List pluginGroups = new ArrayList<>(); long comparisons = 0; void addPluginGroup(String group) { // Simulate ArrayList.contains() cost = list size comparisons += pluginGroups.size(); if (!pluginGroups.contains(group)) { pluginGroups.add(group); } } void addPluginGroups(List groups) { for (String g : groups) addPluginGroup(g); } } static class FixedPluginGroupRequest { LinkedHashSet pluginGroups = new LinkedHashSet<>(); long comparisons = 0; void addPluginGroup(String group) { comparisons++; // HashSet.add() is O(1) pluginGroups.add(group); } void addPluginGroups(List groups) { for (String g : groups) addPluginGroup(g); } } static long[] runPluginGroupComparisons(int g) { List groups = new ArrayList<>(); for (int i = 0; i < g; i++) groups.add("org.apache.plugin" + i); DefectivePluginGroupRequest defective = new DefectivePluginGroupRequest(); defective.addPluginGroups(groups); long defCmp = defective.comparisons; FixedPluginGroupRequest fixed = new FixedPluginGroupRequest(); fixed.addPluginGroups(groups); long fixCmp = fixed.comparisons; // Defective: at step k list has k elements, contains() costs k. // sum(0..G-1) = G*(G-1)/2 → O(G²). // Fixed: G calls each O(1) → O(G). return new long[]{ defCmp, fixCmp }; } // ========================================================================= // Test runner // ========================================================================= public static void main(String[] args) { int pass = 0; int fail = 0; System.out.println("=== maven-0006: ReactorManager ArrayList blackList ==="); for (int n : new int[]{ 10, 50, 100, 200 }) { long[] r = runBlacklistComparisons(n); long slow = r[0], fast = r[1]; long expectedSlow = (long) n * (n - 1) / 2; // N*(N-1)/2 boolean slowMatchesQuadratic = slow == expectedSlow; boolean fastMatchesLinear = fast == n; boolean slowWorse = slow > fast; boolean ok = slowMatchesQuadratic && fastMatchesLinear && slowWorse; if (ok) pass++; else fail++; String s = ok ? "PASS" : "FAIL"; System.out.printf( " N=%-3d | slow=%6d cmp (expect %6d=N²/2) | fast=%3d cmp (expect %3d=N) | %s%n", n, slow, expectedSlow, fast, n, s); } // Verify ratio grows quadratically long[] r10 = runBlacklistComparisons(10); long[] r100 = runBlacklistComparisons(100); // At N=10: slow/fast = 45/10 = 4.5; at N=100: 4950/100 = 49.5 → ratio*10 at 100 boolean ratioGrows = (r100[0] * r10[1]) > (r10[0] * r100[1]); if (ratioGrows) pass++; else fail++; System.out.printf( " ratio@N=10=%.1f ratio@N=100=%.1f — grows with N: %s%n", (double) r10[0] / r10[1], (double) r100[0] / r100[1], ratioGrows ? "PASS" : "FAIL"); System.out.println("\n=== maven-0007: DefaultMavenExecutionRequest ArrayList pluginGroups ==="); for (int g : new int[]{ 10, 50, 100, 200 }) { long[] r = runPluginGroupComparisons(g); long slow = r[0], fast = r[1]; long expectedSlow = (long) g * (g - 1) / 2; boolean slowMatchesQuadratic = slow == expectedSlow; boolean fastMatchesLinear = fast == g; boolean slowWorse = slow > fast; boolean ok = slowMatchesQuadratic && fastMatchesLinear && slowWorse; if (ok) pass++; else fail++; String s = ok ? "PASS" : "FAIL"; System.out.printf( " G=%-3d | slow=%6d cmp (expect %6d=G²/2) | fast=%3d (expect %3d=G) | %s%n", g, slow, expectedSlow, fast, g, s); } // Verify deduplication still works in fixed version FixedPluginGroupRequest dedup = new FixedPluginGroupRequest(); List dupes = new ArrayList<>(); for (int i = 0; i < 50; i++) dupes.add("org.apache.plugin" + (i % 10)); dedup.addPluginGroups(dupes); boolean dedupCorrect = dedup.pluginGroups.size() == 10; if (dedupCorrect) pass++; else fail++; System.out.printf(" Dedup: 50 adds (10 unique) → size=%d == 10: %s%n", dedup.pluginGroups.size(), dedupCorrect ? "PASS" : "FAIL"); // Verify insertion order preserved in fixed version FixedPluginGroupRequest ordered = new FixedPluginGroupRequest(); ordered.addPluginGroup("alpha"); ordered.addPluginGroup("beta"); ordered.addPluginGroup("gamma"); ordered.addPluginGroup("alpha"); // duplicate — must be ignored List orderedList = new ArrayList<>(ordered.pluginGroups); boolean orderCorrect = orderedList.equals(List.of("alpha", "beta", "gamma")); if (orderCorrect) pass++; else fail++; System.out.printf(" Order [alpha, beta, gamma]: %s%n", orderCorrect ? "PASS" : "FAIL"); System.out.println("\n=== Summary ==="); System.out.printf(" %d/%d PASS%n", pass, pass + fail); if (fail > 0) { throw new AssertionError(fail + " test(s) FAILED"); } } }