package unit; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; /** * Models cargo's Edges::add_edge() in src/cargo/ops/tree/graph.rs — * deduplicates outgoing edges from a dependency graph node. * * SLOW: Vec.contains() before each push — O(k) per insertion → O(E²) total. * FAST: LinkedHashSet (insertion-ordered, O(1) contains) — O(E) total. * * CWE-407: src/cargo/ops/tree/graph.rs:122-126 */ public class EdgesAddEdgeAlgorithm { // ------------------------------------------------------------------------- // Edge model // ------------------------------------------------------------------------- enum EdgeKind { DEP, FEATURE } static final class Edge { final EdgeKind kind; final int node; // NodeId (simplified as int) final boolean pub; Edge(EdgeKind kind, int node, boolean pub) { this.kind = kind; this.node = node; this.pub = pub; } @Override public boolean equals(Object o) { if (!(o instanceof Edge)) return false; Edge e = (Edge) o; return kind == e.kind && node == e.node && pub == e.pub; } @Override public int hashCode() { return Objects.hash(kind, node, pub); } } // ------------------------------------------------------------------------- // Slow (defective) implementation — mirrors current cargo Vec // ------------------------------------------------------------------------- static class SlowEdges { final Map> map = new HashMap<>(); long containsChecks = 0; /** O(k) membership test before push — quadratic when k grows. */ void addEdge(Edge edge) { List bucket = map.computeIfAbsent(edge.kind, k -> new ArrayList<>()); containsChecks += bucket.size(); // count comparisons (worst case = full scan) if (!bucket.contains(edge)) { bucket.add(edge); } } int edgeCount() { return map.values().stream().mapToInt(List::size).sum(); } List ofKind(EdgeKind k) { return map.getOrDefault(k, List.of()); } } // ------------------------------------------------------------------------- // Fast (fixed) implementation — LinkedHashSet preserves insertion order // ------------------------------------------------------------------------- static class FastEdges { final Map> map = new HashMap<>(); long insertCalls = 0; /** O(1) amortized insert — LinkedHashSet ignores duplicates. */ void addEdge(Edge edge) { LinkedHashSet bucket = map.computeIfAbsent(edge.kind, k -> new LinkedHashSet<>()); insertCalls++; bucket.add(edge); } int edgeCount() { return map.values().stream().mapToInt(LinkedHashSet::size).sum(); } List ofKind(EdgeKind k) { return new ArrayList<>(map.getOrDefault(k, new LinkedHashSet<>())); } } // ------------------------------------------------------------------------- // Test helpers // ------------------------------------------------------------------------- static int passed = 0; static int total = 0; static void check(String desc, boolean cond) { total++; if (cond) { passed++; System.out.printf(" PASS %s%n", desc); } else { System.out.printf(" FAIL %s%n", desc); } } public static void main(String[] args) { System.out.println("EdgesAddEdgeAlgorithm — CWE-407 unit test"); System.out.println("cargo-0002: Edges::add_edge Vec.contains() → LinkedHashSet"); System.out.println(); // --- Correctness: no duplicate edges --- { SlowEdges slow = new SlowEdges(); FastEdges fast = new FastEdges(); Edge e1 = new Edge(EdgeKind.DEP, 1, true); Edge e2 = new Edge(EdgeKind.DEP, 2, false); Edge e3 = new Edge(EdgeKind.FEATURE, 3, true); Edge e1dup = new Edge(EdgeKind.DEP, 1, true); // duplicate of e1 for (Edge e : List.of(e1, e2, e3, e1dup, e2, e3)) { slow.addEdge(e); fast.addEdge(e); } check("no-dup: slow drops duplicates", slow.edgeCount() == 3); check("no-dup: fast drops duplicates", fast.edgeCount() == 3); check("no-dup: same DEP edges", slow.ofKind(EdgeKind.DEP).equals(fast.ofKind(EdgeKind.DEP))); check("no-dup: same FEATURE edges", slow.ofKind(EdgeKind.FEATURE).equals(fast.ofKind(EdgeKind.FEATURE))); } // --- Correctness: insertion order preserved --- { SlowEdges slow = new SlowEdges(); FastEdges fast = new FastEdges(); for (int i = 0; i < 10; i++) { slow.addEdge(new Edge(EdgeKind.FEATURE, i, true)); fast.addEdge(new Edge(EdgeKind.FEATURE, i, true)); } check("order: slow preserves insertion order", slow.ofKind(EdgeKind.FEATURE).get(0).node == 0 && slow.ofKind(EdgeKind.FEATURE).get(9).node == 9); check("order: fast preserves insertion order", fast.ofKind(EdgeKind.FEATURE).get(0).node == 0 && fast.ofKind(EdgeKind.FEATURE).get(9).node == 9); } // --- Performance: O(E²) vs O(E) --- { // Simulate adding E unique FEATURE edges (no duplicates) — worst case for slow // because each insertion must scan the entire existing bucket. // Model: a package node with F feature edges, e.g. tokio with 50+ features. int F = 200; // feature edge count per node (--graph-features mode, large workspace) // Build edge list: F unique Feature edges List edges = new ArrayList<>(); for (int i = 0; i < F; i++) { edges.add(new Edge(EdgeKind.FEATURE, i, true)); } // Warm up JIT for (int w = 0; w < 50; w++) { SlowEdges s = new SlowEdges(); FastEdges f = new FastEdges(); for (Edge e : edges) { s.addEdge(e); f.addEdge(e); } } int RUNS = 2_000; long slowContains = 0; long fastInserts = 0; long t0 = System.nanoTime(); for (int r = 0; r < RUNS; r++) { SlowEdges s = new SlowEdges(); for (Edge e : edges) s.addEdge(e); slowContains += s.containsChecks; } long slowNs = System.nanoTime() - t0; long t1 = System.nanoTime(); for (int r = 0; r < RUNS; r++) { FastEdges f = new FastEdges(); for (Edge e : edges) f.addEdge(e); fastInserts += f.insertCalls; } long fastNs = System.nanoTime() - t1; double ratio = (double) slowNs / fastNs; long slowOpsPerRun = slowContains / RUNS; long fastOpsPerRun = fastInserts / RUNS; System.out.printf(" INFO F=%d slow_contains_checks=%d fast_inserts=%d ratio=%.1fx%n", F, slowOpsPerRun, fastOpsPerRun, ratio); // Slow: F unique edges → 0+1+2+...+(F-1) = F*(F-1)/2 contains checks total long expectedSlowTotal = (long) F * (F - 1) / 2; check("slow: contains checks = F*(F-1)/2 per run (triangular O(F²))", slowOpsPerRun >= expectedSlowTotal); // Fast: exactly F insert calls per run check("fast: insert calls = F per run (O(F))", fastOpsPerRun == F); // Op ratio matches O(F²)/O(F) = O(F) = 200/2 = 100x difference in ops check("slow op count >> fast op count (>= 50x)", slowOpsPerRun >= fastOpsPerRun * 50); } System.out.println(); System.out.printf("%d/%d PASS%n", passed, total); if (passed != total) System.exit(1); } }