java-topology/defects/hanami/unit/HanamiTest.java

153 lines
6.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> 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<String> 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<String> 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<String> candidates = new ArrayList<>();
for (int i = 0; i < candidateCount; i++) candidates.add("slice_" + i);
// Build Set once: O(M)
Set<String> 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<String> 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<String> cands = new ArrayList<>();
for (int i = 0; i < 100; i++) cands.add("slice_" + i);
Set<String> allowedSet = new HashSet<>();
for (int i = 0; i < 50; i++) allowedSet.add("slice_" + (i * 2));
// slow result
List<String> slowResult = new ArrayList<>();
for (String c : cands) { if (allowedSet.contains(c)) slowResult.add(c); } // same logic for correctness
// fast result
List<String> 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");
}
}