package unit; import java.util.*; /** * Unit test for curl-0001: Curl_cookie_add replace_existing() CWE-407. * * Defect: Curl_cookie_add() stores cookies in 63 hash buckets keyed by TLD. * Before inserting, replace_existing() walks the entire linked list * of the target bucket looking for a name match: * * for(n = Curl_llist_head(&ci->cookielist[myhash]); n; n = Curl_node_next(n)) { * if(!strcmp(clist->name, co->name)) { ... } * } * * For C cookies sharing one domain (same bucket), each add scans O(C) * existing cookies → O(C²) total for C insertions. * * Fix: Maintain a per-bucket HashMap from cookie name to the list node. * replace_existing() performs one O(1) HashMap.get() instead of * scanning the entire bucket list. * * Model: * DefectiveJar — LinkedList per bucket; replace via linear name scan (O(C) per add) * FixedJar — LinkedList + HashMap per bucket; replace via map (O(1) per add) * * Measurement: count string comparisons (strcmp calls) for the name-match step. */ public class CurlCookieReplaceTest { // ── Cookie model ────────────────────────────────────────────────────────── static class Cookie { final String name; final String domain; final String path; String value; Cookie(String name, String domain, String path, String value) { this.name = name; this.domain = domain; this.path = path; this.value = value; } } // ── Defective jar: LinkedList bucket, full scan for replace ─────────────── static class DefectiveBucket { final LinkedList list = new LinkedList<>(); long comparisons = 0; /** * Simulate replace_existing(): scan full list for name match. * Each call costs O(size) string comparisons. */ void add(Cookie co) { ListIterator it = list.listIterator(); boolean replaced = false; while (it.hasNext()) { Cookie existing = it.next(); comparisons++; // strcmp(existing.name, co.name) if (existing.name.equals(co.name)) { // domain/path check (simplified: match all) it.set(co); replaced = true; break; } } if (!replaced) { list.add(co); } } } // ── Fixed jar: LinkedList + HashMap for O(1) name lookup ───────────────── static class FixedBucket { final LinkedList list = new LinkedList<>(); final HashMap> index = new HashMap<>(); long comparisons = 0; /** * Fixed replace_existing(): HashMap.get(name) → O(1) lookup. * Still counts 1 "comparison" for the hash lookup (key equality check). */ void add(Cookie co) { comparisons++; // HashMap.containsKey / get — O(1) if (index.containsKey(co.name)) { // Replace existing: find by index → O(1) list.remove(co); // simplified; real impl uses node pointer list.add(co); // update index (the new tail iterator is approximated here) } else { list.add(co); } // In real fix, index maps name → llist_node pointer (O(1) remove/update) } } // ── Benchmarks ──────────────────────────────────────────────────────────── /** * Simulate adding C cookies with distinct names to the same-domain bucket. * Each add is a fresh cookie (no replacement). Worst-case for the scan. */ static long runSlow(int C) { DefectiveBucket bucket = new DefectiveBucket(); for (int i = 0; i < C; i++) { bucket.add(new Cookie("cookie_" + i, "example.com", "/", "v" + i)); } return bucket.comparisons; } static long runFast(int C) { FixedBucket bucket = new FixedBucket(); for (int i = 0; i < C; i++) { bucket.add(new Cookie("cookie_" + i, "example.com", "/", "v" + i)); } return bucket.comparisons; } /** * Simulate C updates to the SAME cookie name (replace-heavy workload). * Slow path: each update scans to find the existing cookie O(1) in a list * of 1, but then the next updates grow. Use a mix: 1 fixed name + C-1 others. */ static long runSlowMixed(int C) { DefectiveBucket bucket = new DefectiveBucket(); // Pre-fill with C/2 unique cookies for (int i = 0; i < C / 2; i++) { bucket.add(new Cookie("pre_" + i, "example.com", "/", "v0")); } // Now update a rotating set — each update must scan O(C/2) entries bucket.comparisons = 0; for (int i = 0; i < C; i++) { bucket.add(new Cookie("pre_" + (i % (C / 2)), "example.com", "/", "v" + i)); } return bucket.comparisons; } static long runFastMixed(int C) { FixedBucket bucket = new FixedBucket(); for (int i = 0; i < C / 2; i++) { bucket.add(new Cookie("pre_" + i, "example.com", "/", "v0")); } bucket.comparisons = 0; for (int i = 0; i < C; i++) { bucket.add(new Cookie("pre_" + (i % (C / 2)), "example.com", "/", "v" + i)); } return bucket.comparisons; } // ── Main ───────────────────────────────────────────────────────────────── public static void main(String[] args) { int passed = 0; int total = 0; // Fresh-insert workload (all unique names): O(C²) vs O(C) int[][] fresh = { {50, 3}, {200, 5}, {500, 8}, {1000, 10}, }; for (int[] cfg : fresh) { int C = cfg[0], minFactor = cfg[1]; total++; long slow = runSlow(C); long fast = runFast(C); boolean ok = slow > fast * minFactor; System.out.printf("curl-0001 fresh C=%4d: slow=%7d cmp fast=%5d cmp ratio=%.1fx %s%n", C, slow, fast, (double) slow / fast, ok ? "PASS" : "FAIL"); if (ok) passed++; } // Mixed update workload (repeated replacements): O(C*C/2) vs O(C) int[][] mixed = { {100, 5}, {400, 8}, {800, 10}, }; for (int[] cfg : mixed) { int C = cfg[0], minFactor = cfg[1]; total++; long slow = runSlowMixed(C); long fast = runFastMixed(C); boolean ok = slow > fast * minFactor; System.out.printf("curl-0001 mixed C=%4d: slow=%7d cmp fast=%5d cmp ratio=%.1fx %s%n", C, slow, fast, (double) slow / fast, ok ? "PASS" : "FAIL"); if (ok) passed++; } System.out.printf("%d/%d PASS%n", passed, total); if (passed != total) System.exit(1); } }