package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Unit test for CWE-407 jetty-0001: * HttpFields.formatCsvExcludingExisting() calls existing.getValues().contains() * inside a loop — List.contains() is O(M) per call, total O(V×M). * * Models the exact defect and fix without Jetty dependencies. * * Run: java -ea -cp . unit.JettyHttpFieldsCsvTest */ public class JettyHttpFieldsCsvTest { // ----------------------------------------------------------------------- // Model of QuotedCSV.getValues() — returns ArrayList // ----------------------------------------------------------------------- static final class QuotedCsvValues { final List values; QuotedCsvValues(List values) { this.values = new ArrayList<>(values); } public List getValues() { return values; } public boolean isEmpty() { return values.isEmpty(); } } // ----------------------------------------------------------------------- // Defective: mirrors HttpFields.formatCsvExcludingExisting() as-is // ----------------------------------------------------------------------- static FilterResult formatCsvDefective(QuotedCsvValues existing, String... values) { long probes = 0; boolean add = true; // Work on a copy so we can null out entries String[] vals = values.clone(); if (existing != null && !existing.isEmpty()) { add = false; for (int i = vals.length; i-- > 0; ) { String unquoted = vals[i]; probes += existing.getValues().size(); // O(M) per iteration if (existing.getValues().contains(unquoted)) // ArrayList.contains() vals[i] = null; else add = true; } } return new FilterResult(add, vals, probes); } // ----------------------------------------------------------------------- // Fixed: build HashSet once before the loop // ----------------------------------------------------------------------- static FilterResult formatCsvFixed(QuotedCsvValues existing, String... values) { long probes = 0; boolean add = true; String[] vals = values.clone(); if (existing != null && !existing.isEmpty()) { add = false; Set existingSet = new HashSet<>(existing.getValues()); // O(M) once for (int i = vals.length; i-- > 0; ) { String unquoted = vals[i]; probes += 1; // O(1) per lookup if (existingSet.contains(unquoted)) vals[i] = null; else add = true; } } return new FilterResult(add, vals, probes); } static final class FilterResult { final boolean add; final String[] filtered; final long probes; FilterResult(boolean add, String[] filtered, long probes) { this.add = add; this.filtered = filtered; this.probes = probes; } } // ----------------------------------------------------------------------- // Test 1 — correctness: null-out matching values, keep new ones // ----------------------------------------------------------------------- static void test1_correctness() { List existingValues = List.of("gzip", "br", "deflate"); QuotedCsvValues existing = new QuotedCsvValues(existingValues); // "gzip" already present, "zstd" is new String[] incoming = {"gzip", "zstd"}; FilterResult def = formatCsvDefective(existing, incoming); FilterResult fix = formatCsvFixed(existing, incoming); assert def.add == fix.add : "jetty-0001 correctness: add flag differs — defective=" + def.add + " fixed=" + fix.add; assert def.filtered.length == fix.filtered.length : "jetty-0001 correctness: filtered array length differs"; for (int i = 0; i < def.filtered.length; i++) { assert java.util.Objects.equals(def.filtered[i], fix.filtered[i]) : "jetty-0001 correctness: filtered[" + i + "] differs: def=" + def.filtered[i] + " fix=" + fix.filtered[i]; } // "gzip" should be nulled out (already in existing), "zstd" should survive assert fix.filtered[0] == null : "jetty-0001 correctness: 'gzip' should be nulled (already existing)"; assert "zstd".equals(fix.filtered[1]) : "jetty-0001 correctness: 'zstd' should survive (new value)"; assert fix.add : "jetty-0001 correctness: add should be true because 'zstd' is new"; System.out.println("PASS test1_correctness: gzip nulled, zstd kept, add=true"); } // ----------------------------------------------------------------------- // Test 2 — correctness: all values already present // ----------------------------------------------------------------------- static void test2_correctness_all_existing() { List existingValues = List.of("gzip", "br", "deflate"); QuotedCsvValues existing = new QuotedCsvValues(existingValues); String[] incoming = {"gzip", "br"}; FilterResult def = formatCsvDefective(existing, incoming); FilterResult fix = formatCsvFixed(existing, incoming); assert !def.add && !fix.add : "jetty-0001 correctness: add should be false when all values already present"; for (int i = 0; i < fix.filtered.length; i++) { assert fix.filtered[i] == null : "jetty-0001 correctness: value at " + i + " should be null (already exists)"; } System.out.println("PASS test2_correctness_all_existing: all nulled, add=false"); } // ----------------------------------------------------------------------- // Test 3 — complexity: O(V×M) probes vs O(V) probes // ----------------------------------------------------------------------- static void test3_complexity_ratio() { // Small: M=10 existing, V=10 incoming // Large: M=100 existing, V=100 incoming int small = 10; int large = 100; List smallExisting = new ArrayList<>(); List largeExisting = new ArrayList<>(); for (int i = 0; i < small; i++) smallExisting.add("exist-" + i); for (int i = 0; i < large; i++) largeExisting.add("exist-" + i); // Incoming values are all new (no matches) — worst case for defective (always scan to end) String[] smallIncoming = new String[small]; String[] largeIncoming = new String[large]; for (int i = 0; i < small; i++) smallIncoming[i] = "new-" + i; for (int i = 0; i < large; i++) largeIncoming[i] = "new-" + i; FilterResult defSmall = formatCsvDefective(new QuotedCsvValues(smallExisting), smallIncoming); FilterResult defLarge = formatCsvDefective(new QuotedCsvValues(largeExisting), largeIncoming); FilterResult fixSmall = formatCsvFixed(new QuotedCsvValues(smallExisting), smallIncoming); FilterResult fixLarge = formatCsvFixed(new QuotedCsvValues(largeExisting), largeIncoming); double defRatio = (double) defLarge.probes / defSmall.probes; double fixRatio = (double) fixLarge.probes / fixSmall.probes; System.out.printf(" defective probes: small=%d large=%d ratio=%.1fx%n", defSmall.probes, defLarge.probes, defRatio); System.out.printf(" fixed probes: small=%d large=%d ratio=%.1fx%n", fixSmall.probes, fixLarge.probes, fixRatio); // 10x input → ~100x probes (O(V×M)) assert defRatio > 50.0 : "jetty-0001 complexity: defective ratio should be >50x at 10x scale, got " + defRatio; // 10x input → ~10x probes (O(V)) assert fixRatio < 20.0 : "jetty-0001 complexity: fixed ratio should be <20x at 10x scale, got " + fixRatio; assert defRatio > fixRatio * 3 : "jetty-0001 complexity: defective should scale much worse, def=" + defRatio + " fix=" + fixRatio; System.out.printf("PASS test3_complexity_ratio: defective=%.0fx fixed=%.0fx%n", defRatio, fixRatio); } // ----------------------------------------------------------------------- // Test 4 — absolute probe counts at V=M=50 // ----------------------------------------------------------------------- static void test4_absolute_counts() { int N = 50; List existingValues = new ArrayList<>(); for (int i = 0; i < N; i++) existingValues.add("val-" + i); // All incoming values are new (worst case: full scan per value) String[] incoming = new String[N]; for (int i = 0; i < N; i++) incoming[i] = "new-" + i; FilterResult def = formatCsvDefective(new QuotedCsvValues(existingValues), incoming); FilterResult fix = formatCsvFixed(new QuotedCsvValues(existingValues), incoming); // Defective: N values × N existing = N² probes (worst case, no early exit) long expectedDefMin = (long) N * N; assert def.probes >= expectedDefMin : "jetty-0001 counts: defective probes=" + def.probes + " expected>=" + expectedDefMin; // Fixed: exactly N probes (one per value, O(1) HashSet lookup) assert fix.probes == N : "jetty-0001 counts: fixed probes=" + fix.probes + " expected=" + N; long speedup = def.probes / fix.probes; System.out.printf("PASS test4_absolute_counts: defective=%d fixed=%d speedup=%dx%n", def.probes, fix.probes, speedup); } // ----------------------------------------------------------------------- // Test 5 — edge case: null/empty existing → no loop entered // ----------------------------------------------------------------------- static void test5_empty_existing() { FilterResult def = formatCsvDefective(null, "gzip", "br"); FilterResult fix = formatCsvFixed(null, "gzip", "br"); assert def.probes == 0 && fix.probes == 0 : "jetty-0001 edge: null existing should produce 0 probes"; assert def.add && fix.add : "jetty-0001 edge: null existing means all values should be added"; System.out.println("PASS test5_empty_existing: null existing → 0 probes, add=true"); } // ----------------------------------------------------------------------- // main // ----------------------------------------------------------------------- public static void main(String[] args) { System.out.println("=== JettyHttpFieldsCsvTest — CWE-407 jetty-0001 ==="); int passed = 0; int failed = 0; Runnable[] tests = { JettyHttpFieldsCsvTest::test1_correctness, JettyHttpFieldsCsvTest::test2_correctness_all_existing, JettyHttpFieldsCsvTest::test3_complexity_ratio, JettyHttpFieldsCsvTest::test4_absolute_counts, JettyHttpFieldsCsvTest::test5_empty_existing, }; for (Runnable test : tests) { try { test.run(); passed++; } catch (AssertionError e) { System.out.println("FAIL: " + e.getMessage()); failed++; } } System.out.println("---"); System.out.println("Results: " + passed + " passed, " + failed + " failed"); if (failed > 0) System.exit(1); } }