package unit; import java.util.*; /** * Unit test for tor-0002: nodelist_add_node_and_family() CWE-407. * * Defect: nodelist_add_node_and_family() (nodelist.c:2337) iterates all_nodes * (size N) and for each node2 calls nodes_have_common_family_id(node, node2). * nodes_have_common_family_id iterates ids_a and for each id calls * smartlist_contains_string(ids_b, id) — a full linear scan of ids_b. * Total cost when no match: O(N * |ids_a| * |ids_b|) = O(N * F^2). * * Fix: Before the outer loop, build a HashSet from node's own family IDs. * For each node2, iterate node2's ids and do O(1) HashSet.contains(). * Total cost: O(N * F). * * Model: * DefectiveMatcher — for each node2: nested loop scan of ids_b per id in ids_a (O(F^2)) * FixedMatcher — HashSet built once from ids_a; per node2: one pass over ids_b (O(F)) * * Worst-case scenario: source node's IDs are disjoint from all candidate nodes' IDs. * This is the common case (most relays are NOT in the same family). * The full scan always runs in the defective path; the fixed path exits early if found. */ public class TorNodelistFamilyTest { // ── Membership implementations ──────────────────────────────────────────── /** * Defective: simulates nodes_have_common_family_id() — * outer loop over ids_a, inner smartlist_contains_string scan of ids_b. * Returns total comparison count. No short-circuit on IDs in different families. */ static long defectiveCheck(List ids_a, List ids_b) { long ops = 0; for (String id : ids_a) { for (String candidate : ids_b) { ops++; if (id.equals(candidate)) { return ops; // short-circuit on match (mirrors C code) } } } return ops; } /** * Fixed: build a HashSet from ids_a once (caller does this before outer loop). * Per call: iterate ids_b and call HashSet.contains() — O(1) per check. */ static long fixedCheck(Set id_set, List ids_b) { long ops = 0; for (String id : ids_b) { ops++; // one O(1) hash lookup per id if (id_set.contains(id)) { return ops; } } return ops; } // ── Test data generation ────────────────────────────────────────────────── /** * Generate F family IDs for a node. IDs are globally unique (no sharing), * modelling the worst case: no family members, full scan required every time. */ static List makeDisjointIds(int nodeIndex, int F) { List ids = new ArrayList<>(); for (int j = 0; j < F; j++) { ids.add("fam:" + nodeIndex + ":" + j); } return ids; } /** * Generate IDs where the last ID of ids_a matches the last ID of ids_b. * Models the worst-case scan depth: match only found at end of both lists. */ static List makeLastMatchIds(int nodeIndex, int F, String sharedId) { List ids = new ArrayList<>(); for (int j = 0; j < F - 1; j++) { ids.add("fam:" + nodeIndex + ":" + j); } ids.add(sharedId); // shared at end — maximises scan depth return ids; } // ── Benchmark ──────────────────────────────────────────────────────────── /** * All-disjoint case: every pair has no match — maximum scan for slow path. * Slow: O(N * F^2). Fast: O(N * F). */ static long[] runDisjoint(int N, int F) { List sourceIds = makeDisjointIds(0, F); List> allNodes = new ArrayList<>(); for (int i = 1; i <= N; i++) { allNodes.add(makeDisjointIds(i, F)); } long slowOps = 0; for (List otherIds : allNodes) { slowOps += defectiveCheck(sourceIds, otherIds); } Set id_set = new HashSet<>(sourceIds); long fastOps = 0; for (List otherIds : allNodes) { fastOps += fixedCheck(id_set, otherIds); } return new long[]{slowOps, fastOps}; } /** * Last-match case: match only at the end of both id lists. * Slow: O(N * F^2) worst-case depth. Fast: O(N * F). */ static long[] runLastMatch(int N, int F) { String shared = "shared:family:id"; List sourceIds = makeLastMatchIds(0, F, shared); List> allNodes = new ArrayList<>(); for (int i = 1; i <= N; i++) { allNodes.add(makeLastMatchIds(i, F, shared)); } long slowOps = 0; for (List otherIds : allNodes) { slowOps += defectiveCheck(sourceIds, otherIds); } Set id_set = new HashSet<>(sourceIds); long fastOps = 0; for (List otherIds : allNodes) { fastOps += fixedCheck(id_set, otherIds); } return new long[]{slowOps, fastOps}; } // ── Main ───────────────────────────────────────────────────────────────── public static void main(String[] args) { int passed = 0; int total = 0; // Disjoint scenario: no matches, full scan always. // Ratio = F (slow scans F ids per pair; fast does 1 hash check per id in ids_b = F total). // slow = N*F*F, fast = N*F → ratio = F. int[][] disjointCfg = { {50, 3, 2}, // ratio=3 {200, 5, 4}, // ratio=5 {500, 5, 4}, // ratio=5 {500, 10, 9}, // ratio=10 }; for (int[] cfg : disjointCfg) { int N = cfg[0], F = cfg[1], minFactor = cfg[2]; total++; long[] ops = runDisjoint(N, F); long slowOps = ops[0], fastOps = ops[1]; boolean ok = slowOps > fastOps * minFactor; System.out.printf("tor-0002 disjoint N=%4d F=%2d: slow=%7d fast=%5d ratio=%.1fx %s%n", N, F, slowOps, fastOps, (double) slowOps / fastOps, ok ? "PASS" : "FAIL"); if (ok) passed++; } // Last-match scenario: match at end of both lists int[][] lastMatchCfg = { {50, 3, 2}, {200, 5, 4}, {500, 8, 7}, }; for (int[] cfg : lastMatchCfg) { int N = cfg[0], F = cfg[1], minFactor = cfg[2]; total++; long[] ops = runLastMatch(N, F); long slowOps = ops[0], fastOps = ops[1]; boolean ok = slowOps > fastOps * minFactor; System.out.printf("tor-0002 lastmatch N=%4d F=%2d: slow=%7d fast=%5d ratio=%.1fx %s%n", N, F, slowOps, fastOps, (double) slowOps / fastOps, ok ? "PASS" : "FAIL"); if (ok) passed++; } System.out.printf("%d/%d PASS%n", passed, total); if (passed != total) System.exit(1); } }