package unit; import java.util.*; /** * DovecotTest — CWE-407 benchmark for dovecot-0001 * * Models dsync_mail_change_have_keyword() in * src/doveadm/dsync/dsync-mailbox-import.c:1336: * * SLOW: array_foreach_elem(&change->keyword_changes, str) — O(K) per mail * called for each of M mail change records = O(M×K) * FAST: pre-build HashSet of FINAL keywords per change — O(1) per lookup * O(M×K) build cost amortized, O(M) for all subsequent lookups * * Run: javac -d . DovecotTest.java && java -ea unit.DovecotTest */ public class DovecotTest { // ── Keyword change model ────────────────────────────────────────────────── static final char KEYWORD_CHANGE_ADD = '+'; static final char KEYWORD_CHANGE_REMOVE = '-'; static final char KEYWORD_CHANGE_FINAL = '='; // "final" state for keyword static final char KEYWORD_CHANGE_ADD_FINAL = '!'; // add + final static class MailChange { final List keywordChanges; // e.g. "=\\Seen", "+\\Draft", "-\\Flagged" Map finalCache; // CWE-407 fix: lazy hash set MailChange(List kc) { this.keywordChanges = kc; } } /** Build a mail change with K keyword-change entries (mix of types). */ static MailChange buildChange(int numKeywords, int messageIdx) { List kc = new ArrayList<>(numKeywords); for (int i = 0; i < numKeywords; i++) { char type; switch (i % 4) { case 0: type = KEYWORD_CHANGE_FINAL; break; case 1: type = KEYWORD_CHANGE_ADD_FINAL; break; case 2: type = KEYWORD_CHANGE_ADD; break; default: type = KEYWORD_CHANGE_REMOVE; break; } kc.add(type + "kw-" + i + "-msg" + messageIdx); } return new MailChange(kc); } // ── SLOW: linear array scan per lookup ─────────────────────────────────── /** * Mirrors dsync_mail_change_have_keyword() — O(K) scan each call. * Returns total number of strcasecmp operations across all M×calls. */ static long haveKeywordSlow(List changes, String targetKeyword) { long ops = 0; for (MailChange change : changes) { for (String str : change.keywordChanges) { ops++; char type = str.charAt(0); if ((type == KEYWORD_CHANGE_FINAL || type == KEYWORD_CHANGE_ADD_FINAL) && str.substring(1).equalsIgnoreCase(targetKeyword)) { break; // found } } } return ops; } // ── FAST: lazy hash set, built once per MailChange ──────────────────────── /** * Models the CWE-407 fix: build a HashSet of FINAL keywords on first * access per change, then O(1) lookup. Returns total ops including the * amortized build cost. */ static long haveKeywordFast(List changes, String targetKeyword) { long ops = 0; for (MailChange change : changes) { // Build lazy cache if not present (amortized O(K) build) if (change.finalCache == null) { change.finalCache = new HashMap<>(); for (String str : change.keywordChanges) { ops++; // build cost (paid once per change) char type = str.charAt(0); if (type == KEYWORD_CHANGE_FINAL || type == KEYWORD_CHANGE_ADD_FINAL) { change.finalCache.put(str.substring(1).toLowerCase(), Boolean.TRUE); } } } ops++; // O(1) hash lookup change.finalCache.containsKey(targetKeyword.toLowerCase()); } return ops; } // ── bench harness ───────────────────────────────────────────────────────── static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) { slow.run(); fast.run(); long t0 = System.nanoTime(); slow.run(); long sMs = (System.nanoTime() - t0) / 1_000_000; long t1 = System.nanoTime(); fast.run(); long fMs = (System.nanoTime() - t1) / 1_000_000; double r = fOps > 0 ? (double) sOps / fOps : 0; System.out.printf(" %-56s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n", label, sMs, sOps, fMs, fOps, r); } // ── Multi-pass scan: test Q different keywords over same M changes ───────── /** * SLOW multi-pass: for each of Q sync passes (each with a different target * keyword), scan all M changes O(K) each. Total: O(Q × M × K). */ static long haveKeywordSlowMulti(List changes, List targets) { long ops = 0; for (String target : targets) { ops += haveKeywordSlow(changes, target); } return ops; } /** * FAST multi-pass: build the keyword hash set once per change on the first * pass; all Q subsequent passes do O(1) lookup. Total: O(M×K) build + O(Q×M). * For Q >= 2 and K > 1, fast is strictly cheaper. */ static long haveKeywordFastMulti(List changes, List targets) { long ops = 0; // Reset caches for a clean measurement for (MailChange c : changes) c.finalCache = null; for (String target : targets) { ops += haveKeywordFast(changes, target); } return ops; } // ── main ────────────────────────────────────────────────────────────────── public static void main(String[] args) { final int M = 10_000; // mail change records in a mailbox sync final int K = 20; // keyword-change entries per mail change final int Q = 10; // distinct keyword queries across the sync session // (e.g., importer checks \Seen, \Flagged, $Junk, etc.) List changesSlow = new ArrayList<>(M); List changesFast = new ArrayList<>(M); for (int i = 0; i < M; i++) { changesSlow.add(buildChange(K, i)); changesFast.add(buildChange(K, i)); } // Q distinct target keywords — each is checked against all M changes List targets = new ArrayList<>(Q); for (int q = 0; q < Q; q++) targets.add("kw-" + q + "-msg0"); System.out.println("DovecotTest — CWE-407"); System.out.println(); System.out.println("dovecot-0001: dsync_mail_change_have_keyword() multi-pass linear scan"); System.out.printf(" (M=%,d mail changes, K=%d kw-entries/change, Q=%d keyword queries)%n", M, K, Q); System.out.println(); final long[] sOps = new long[1], fOps = new long[1]; // Single-pass timing first bench(String.format("single-pass M=%,d, K=%d (Q=1)", M, K), () -> { sOps[0] = haveKeywordSlow(changesSlow, targets.get(0)); }, () -> { for (MailChange c : changesFast) c.finalCache = null; fOps[0] = haveKeywordFast(changesFast, targets.get(0)); }, haveKeywordSlow(changesSlow, targets.get(0)), haveKeywordFast(changesFast, targets.get(0))); // Multi-pass timing — the cache pays off here bench(String.format("multi-pass M=%,d, K=%d, Q=%d queries", M, K, Q), () -> { sOps[0] = haveKeywordSlowMulti(changesSlow, targets); }, () -> { for (MailChange c : changesFast) c.finalCache = null; fOps[0] = haveKeywordFastMulti(changesFast, targets); }, haveKeywordSlowMulti(changesSlow, targets), haveKeywordFastMulti(changesFast, targets)); // Multi-pass assertion: slow does Q × M × K, fast does M×K (build) + Q×M // Ratio ≈ Q*M*K / (M*K + Q*M) = Q*K / (K + Q) // With Q=10, K=20: ratio ≈ 200/30 ≈ 6.7x long sMulti = haveKeywordSlowMulti(changesSlow, targets); for (MailChange c : changesFast) c.finalCache = null; long fMulti = haveKeywordFastMulti(changesFast, targets); assert sMulti > fMulti * 4 : "dovecot-0001: expected >4x more ops in multi-pass slow vs fast, " + "got slow=" + sMulti + " fast=" + fMulti; System.out.println(); System.out.println("All assertions passed."); } }