wave8b: 438/196 — rails-0017, hanami-0001, spark-0002 + PDF

This commit is contained in:
russell@unturf.com 2026-03-27 16:33:08 -04:00
parent 2d3e3d603e
commit c4330be5b0
18 changed files with 1069 additions and 8 deletions

View file

@ -0,0 +1,153 @@
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");
}
}