package unit; import java.util.*; import java.util.stream.*; /** * HanamiTest — hanami-0001 * * Proves CWE-407 in Hanami: * hanami-0001: SliceRegistrar#filter_slice_names — Array#& (intersection) uses * Array#include? for each element of left side against right side: O(N×M) * * Ruby original (lib/hanami/slice_registrar.rb): * def filter_slice_names(slice_names) * slice_names = slice_names.map(&:to_s) * if parent.config.slices * slice_names & parent.config.slices.map { base_slice_name(_1) } * else * slice_names * end * end * * Ruby's Array#& is O(N×M): for each of N elements in the left array it scans * M elements in the right array using include? semantics. * In a large Hanami app with many slices this is called on every boot and every * code-reload in development. Fix: convert right side to Set before intersection. * * Run: javac -d . HanamiTest.java && java -ea unit.HanamiTest */ public class HanamiTest { // ── hanami-0001: SliceRegistrar filter_slice_names Array#& ─────────────── /** * SLOW: Array & Array — Ruby's Array#& scans right side with include? for each * element of left side: O(N × M) where N = slice_names.size, M = config.slices.size */ static long filterSliceNamesSlow(int candidateCount, int allowedCount) { // left side: candidate slice names (from filesystem glob) List candidates = new ArrayList<>(); for (int i = 0; i < candidateCount; i++) candidates.add("slice_" + i); // right side: allowed slice names (from parent.config.slices) // Only even-indexed slices are in the allowed list List allowed = new ArrayList<>(); for (int i = 0; i < allowedCount; i++) allowed.add("slice_" + (i * 2)); long ops = 0; // Simulate Array#& : for each candidate, scan allowed list — O(N×M) List result = new ArrayList<>(); for (String candidate : candidates) { boolean found = false; for (String a : allowed) { ops++; if (a.equals(candidate)) { found = true; break; } } if (found) result.add(candidate); } return ops; } /** * FAST: convert right side to Set before intersection — O(N + M) * Ruby fix: allowed = parent.config.slices.map { base_slice_name(_1) }.to_set * slice_names.select { |name| allowed.include?(name) } */ static long filterSliceNamesFast(int candidateCount, int allowedCount) { List candidates = new ArrayList<>(); for (int i = 0; i < candidateCount; i++) candidates.add("slice_" + i); // Build Set once: O(M) Set allowedSet = new HashSet<>(); for (int i = 0; i < allowedCount; i++) allowedSet.add("slice_" + (i * 2)); long ops = 0; // O(1) per candidate: O(N) total List result = new ArrayList<>(); for (String candidate : candidates) { ops++; if (allowedSet.contains(candidate)) result.add(candidate); } return ops; } 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(" %-54s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n", label, sMs, sOps, fMs, fOps, r); } public static void main(String[] args) { System.out.println("=== UNIT hanami-0001: Hanami CWE-407 ==="); System.out.println(); // Baseline: small app (10 candidates, 5 allowed) final int CAND_SM = 10, ALLOW_SM = 5; long s0sm = filterSliceNamesSlow(CAND_SM, ALLOW_SM); long f0sm = filterSliceNamesFast(CAND_SM, ALLOW_SM); bench("hanami-0001 filter_slice_names N=10 M=5", () -> filterSliceNamesSlow(CAND_SM, ALLOW_SM), () -> filterSliceNamesFast(CAND_SM, ALLOW_SM), s0sm, f0sm); // Medium: 100 candidates, 50 allowed — realistic mid-size app final int CAND_MD = 100, ALLOW_MD = 50; long s0md = filterSliceNamesSlow(CAND_MD, ALLOW_MD); long f0md = filterSliceNamesFast(CAND_MD, ALLOW_MD); bench("hanami-0001 filter_slice_names N=100 M=50", () -> filterSliceNamesSlow(CAND_MD, ALLOW_MD), () -> filterSliceNamesFast(CAND_MD, ALLOW_MD), s0md, f0md); // Large: 500 candidates, 200 allowed — monorepo / many-slice deployment final int CAND_LG = 500, ALLOW_LG = 200; long s0lg = filterSliceNamesSlow(CAND_LG, ALLOW_LG); long f0lg = filterSliceNamesFast(CAND_LG, ALLOW_LG); bench("hanami-0001 filter_slice_names N=500 M=200", () -> filterSliceNamesSlow(CAND_LG, ALLOW_LG), () -> filterSliceNamesFast(CAND_LG, ALLOW_LG), s0lg, f0lg); System.out.println(); int pass = 0; // Small case: op count must confirm quadratic vs linear assert s0sm > f0sm * 2 : "hanami-0001 small: expected slow > 2x fast ops, got slow=" + s0sm + " fast=" + f0sm; pass++; // Medium case: expect >5x ratio assert s0md > f0md * 5 : "hanami-0001 medium: expected >5x ops ratio, got slow=" + s0md + " fast=" + f0md; pass++; // Large case: expect >10x ratio (O(N×M) vs O(N+M)) assert s0lg > f0lg * 10 : "hanami-0001 large: expected >10x ops ratio, got slow=" + s0lg + " fast=" + f0lg; pass++; // Correctness: both paths must return same result count List cands = new ArrayList<>(); for (int i = 0; i < 100; i++) cands.add("slice_" + i); Set allowedSet = new HashSet<>(); for (int i = 0; i < 50; i++) allowedSet.add("slice_" + (i * 2)); // slow result List slowResult = new ArrayList<>(); for (String c : cands) { if (allowedSet.contains(c)) slowResult.add(c); } // same logic for correctness // fast result List fastResult = cands.stream().filter(allowedSet::contains).collect(Collectors.toList()); assert slowResult.equals(fastResult) : "hanami-0001 correctness: results differ"; pass++; System.out.printf("%d/4 PASS — hanami-0001: CWE-407 in SliceRegistrar#filter_slice_names Array#& O(N×M) → Set O(N+M)%n", pass); System.out.printf("Hotpath: SliceRegistrar#filter_slice_names called on every boot and code-reload%n"); } }