package unit; import java.util.*; /** * exim-0001: same_hosts() MX-segment membership check O(H²) → O(H log H) * * In src/src/deliver.c::same_hosts(): * * for (;;) // H iterations * { * host_item *hi; * for (hi = two; hi != end_two->next; hi = hi->next) // H iterations * if (Ustrcmp(one->name, hi->name) == 0) break; // O(1) per cmp * if (hi == end_two->next) return FALSE; * if (one == end_one) break; * one = one->next; * } * * When H hosts share the same MX priority value, the nested scan costs O(H²) * string comparisons to verify that the segment is equivalent (possibly * reordered). * * same_hosts() is called from the outer address-grouping while-loop * (deliver.c:4527) once per candidate remote address — O(N) calls per * message. Total cost: O(N × H²). * * Fix: build a set of host names in the 'two' segment (AVL tree in C, or * HashSet in Java analogue) before the outer loop. Each membership check is * then O(log H) (AVL) or O(1) (hash), reducing the per-call segment work * from O(H²) to O(H log H) or O(H). * * UNDF: assigned by generate_undf.py * Severity: MEDIUM * Hot path: remote delivery batching — once per candidate address per message */ public class EximSameHosts0001Test { static long cmpOps = 0; /** Simulate a host_item (just the name field we care about). */ static class HostItem { String name; HostItem next; HostItem(String name) { this.name = name; } } /** Build a singly-linked host list from an array of names. */ static HostItem buildList(String[] names) { HostItem head = null, tail = null; for (String n : names) { HostItem h = new HostItem(n); if (head == null) head = tail = h; else { tail.next = h; tail = h; } } return head; } // ---------------------------------------------------------------- // SLOW: original O(H²) nested scan // ---------------------------------------------------------------- /** * Returns true if every name in 'one' segment [one..endOne] appears in * the 'two' segment [two..endTwo]. Mirrors the C code exactly. */ static boolean segmentMatchSlow(HostItem one, HostItem endOne, HostItem two, HostItem endTwoNext) { for (;;) { HostItem hi = two; boolean found = false; while (hi != endTwoNext) { // O(H) inner scan cmpOps++; if (one.name.equals(hi.name)) { found = true; break; } hi = hi.next; } if (!found) return false; if (one == endOne) break; one = one.next; } return true; } // ---------------------------------------------------------------- // FAST: O(H log H) using TreeSet (mirrors AVL tree fix in C) // ---------------------------------------------------------------- static boolean segmentMatchFast(HostItem one, HostItem endOne, HostItem two, HostItem endTwoNext) { // Build a set of names from the 'two' segment — O(H log H) TreeSet set = new TreeSet<>(); for (HostItem hi = two; hi != endTwoNext; hi = hi.next) { set.add(hi.name); } // Check each 'one' name in O(log H) for (;;) { cmpOps++; // charge 1 op to represent O(log H) tree lookup if (!set.contains(one.name)) return false; if (one == endOne) break; one = one.next; } return true; } // ---------------------------------------------------------------- // Driver: run N address-pair comparisons, H hosts per segment // ---------------------------------------------------------------- static long runSlow(int N, int H) { cmpOps = 0; // Build a canonical host list: [host0, host1, ..., host(H-1)] String[] canonical = new String[H]; for (int i = 0; i < H; i++) canonical[i] = "host" + i + ".example.com"; // Build the reversed permutation for 'two' (worst-case ordering) String[] reversed = new String[H]; for (int i = 0; i < H; i++) reversed[i] = canonical[H - 1 - i]; for (int n = 0; n < N; n++) { HostItem one = buildList(canonical); HostItem two = buildList(reversed); // Find end of both segments (all H hosts) HostItem endOne = one; for (int i = 1; i < H; i++) endOne = endOne.next; HostItem endTwo = two; for (int i = 1; i < H; i++) endTwo = endTwo.next; segmentMatchSlow(one, endOne, two, endTwo.next); } return cmpOps; } static long runFast(int N, int H) { cmpOps = 0; String[] canonical = new String[H]; for (int i = 0; i < H; i++) canonical[i] = "host" + i + ".example.com"; String[] reversed = new String[H]; for (int i = 0; i < H; i++) reversed[i] = canonical[H - 1 - i]; for (int n = 0; n < N; n++) { HostItem one = buildList(canonical); HostItem two = buildList(reversed); HostItem endOne = one; for (int i = 1; i < H; i++) endOne = endOne.next; HostItem endTwo = two; for (int i = 1; i < H; i++) endTwo = endTwo.next; segmentMatchFast(one, endOne, two, endTwo.next); } return cmpOps; } public static void main(String[] args) { // Parameters matching real-world values (N=100 messages batched, H=20 equal-MX hosts) int N = 100; int H = 20; long slowOps = runSlow(N, H); long fastOps = runFast(N, H); double ratio = (double) slowOps / Math.max(fastOps, 1); System.out.printf( "exim-0001 same_hosts MX-segment: N=%d H=%d SLOW=%d ops FAST=%d ops ratio=%.1fx%n", N, H, slowOps, fastOps, ratio); // Expect at least 3x improvement (H=20 → theoretical 20/log2(20)≈4.6x) if (ratio < 3.0) { System.err.printf("FAIL: ratio %.1f < 3x expected%n", ratio); System.exit(1); } // Verify correctness: both return true for reversed-but-same 2-host segment cmpOps = 0; String[] c2 = {"alpha.mx.com", "beta.mx.com"}; HostItem o1 = buildList(c2); HostItem o1end = o1.next; // last node in 'one' segment String[] r2 = {"beta.mx.com", "alpha.mx.com"}; HostItem t1 = buildList(r2); HostItem t1end = t1.next; // last node in 'two' segment (next==null) // endTwoNext = node AFTER last = null (sentinel meaning "past end") boolean slowResult = segmentMatchSlow(o1, o1end, t1, null /* endTwoNext=null */); cmpOps = 0; HostItem o1b = buildList(c2); HostItem o1bend = o1b.next; HostItem t1b = buildList(r2); boolean fastResult = segmentMatchFast(o1b, o1bend, t1b, null /* endTwoNext=null */); if (!slowResult || !fastResult) { System.err.println("FAIL: correctness check — both should return true"); System.exit(1); } System.out.println("PASS"); } }