java-topology/defects/r-lang-0001/unit/RNamespaceUsersTest.java
russell@unturf.com 5df58dac32 octave+r-lang: 5-MOAD scan; 1 CWE-407 defect (r-lang-0001), octave MOAD 0002-0005 CLEAN
r-lang-0001: namespace.R getNamespaceUsers() O(N*I) match() linear scan and
namespaceImportMethods() O(G*V) %in% double scan; fix: hash env + reverse index.
Unit test: 5.4x (namespace users) and 5.6x (import methods) speedup. PASS.

Octave: MOAD-0002 through 0005 all CLEAN (single-threaded, no ThreadLocal,
no credential logging found, no concurrent cache races).
2026-03-31 22:31:27 -04:00

183 lines
7.8 KiB
Java

import java.util.*;
/**
* Unit test: simulates R getNamespaceUsers() and namespaceImportMethods()
* O(N^2) list-membership defect (CWE-407).
*
* Models:
* - getNamespaceUsers: outer loop N loaded namespaces, inner match() O(I)
* => total O(N * I), patched to O(N + I) using HashSet
* - namespaceImportMethods: outer loop G generics, inner %in% vars O(V)
* => total O(G * V), patched to O(G + V) using HashSet
*/
public class RNamespaceUsersTest {
// --- Model: getNamespaceUsers ---
/** Buggy: O(N * I) - linear scan of inames per namespace. */
static List<String> getNamespaceUsersDefect(
String nsname,
Map<String, List<String>> loadedImports) {
List<String> users = new ArrayList<>();
for (Map.Entry<String, List<String>> e : loadedImports.entrySet()) {
String n = e.getKey();
List<String> inames = e.getValue();
// match(nsname, inames) — O(|inames|) linear scan
if (inames.contains(nsname)) {
users.add(0, n);
}
}
return users;
}
/**
* Fixed: O(N + total_imports) - build reverse index once.
* The R-level fix uses a pre-built hash env or converts the whole
* import list to a set before calling getNamespaceUsers in a loop.
* In practice the fix is to cache the reverse map.
*/
static List<String> getNamespaceUsersFixed(
String nsname,
Map<String, List<String>> loadedImports) {
// Build reverse index: importedName -> list of namespaces that import it
// O(N * I) build cost amortized across many getNamespaceUsers() calls,
// or O(N + total_imports) per single call when done as a flat scan + set.
// Single-call optimization: collect all (namespace, importName) pairs
// into a Map<importName, Set<namespace>> in one pass.
Map<String, List<String>> reverseIndex = new HashMap<>();
for (Map.Entry<String, List<String>> e : loadedImports.entrySet()) {
for (String imp : e.getValue()) {
reverseIndex.computeIfAbsent(imp, k -> new ArrayList<>()).add(e.getKey());
}
}
List<String> found = reverseIndex.getOrDefault(nsname, Collections.emptyList());
// Return in reverse order to match original semantics (prepend)
List<String> users = new ArrayList<>(found);
Collections.reverse(users);
return users;
}
// --- Model: namespaceImportMethods ---
/** Buggy: O(G * V) - g %in% vars is O(|vars|) per generic. */
static List<String> importMethodsDefect(List<String> allFuns, List<String> vars) {
List<String> imported = new ArrayList<>();
for (String g : allFuns) {
if (vars.contains(g)) { // O(|vars|)
imported.add(g);
}
}
return imported;
}
/** Fixed: O(G + V) - convert vars to HashSet once. */
static List<String> importMethodsFixed(List<String> allFuns, List<String> vars) {
Set<String> varsSet = new HashSet<>(vars);
List<String> imported = new ArrayList<>();
for (String g : allFuns) {
if (varsSet.contains(g)) { // O(1)
imported.add(g);
}
}
return imported;
}
// --- Benchmark and correctness check ---
static Map<String, List<String>> buildNamespaceImports(int numNamespaces, int importsPerNs, String target) {
Map<String, List<String>> result = new LinkedHashMap<>();
for (int i = 0; i < numNamespaces; i++) {
List<String> imports = new ArrayList<>();
for (int j = 0; j < importsPerNs; j++) {
imports.add("pkg" + i + "_import" + j);
}
// Last namespace imports our target
if (i == numNamespaces - 1) imports.add(target);
result.put("namespace" + i, imports);
}
return result;
}
public static void main(String[] args) {
final int N = 200; // namespaces (CRAN session can exceed 100)
final int I = 500; // imports per namespace (e.g., Bioconductor packages)
final int G = 1000; // generics in a namespace (methods-heavy pkg like Matrix/BioConductor)
final int V = 500; // vars requested
String target = "stats";
Map<String, List<String>> loadedImports = buildNamespaceImports(N, I, target);
// Correctness check
List<String> r1 = getNamespaceUsersDefect(target, loadedImports);
List<String> r2 = getNamespaceUsersFixed(target, loadedImports);
assert r1.equals(r2) : "getNamespaceUsers: results differ";
// Build allFuns and vars for importMethods
List<String> allFuns = new ArrayList<>();
for (int i = 0; i < G; i++) allFuns.add("generic" + i);
List<String> vars = new ArrayList<>();
for (int i = 0; i < V; i++) vars.add("generic" + (G - V + i));
List<String> m1 = importMethodsDefect(allFuns, vars);
List<String> m2 = importMethodsFixed(allFuns, vars);
assert m1.equals(m2) : "importMethods: results differ";
// --- Timing ---
// getNamespaceUsers is called for each namespace being detached/reloaded.
// In a session with N namespaces, it's called O(N) times total.
// Simulate that: call getNamespaceUsers for each of N distinct targets.
final int REPS = 10;
List<String> targets = new ArrayList<>();
for (int i = 0; i < N; i++) targets.add("namespace" + i);
// getNamespaceUsers defect — O(N * I) per call, called N times = O(N^2 * I)
long t0 = System.nanoTime();
for (int r = 0; r < REPS; r++)
for (String tgt : targets) getNamespaceUsersDefect(tgt, loadedImports);
long defectNs = System.nanoTime() - t0;
// getNamespaceUsers fixed — build reverse index once, O(N*I) build + O(1) per query
t0 = System.nanoTime();
for (int r = 0; r < REPS; r++) {
// Build reverse index once per session-level batch
Map<String, List<String>> reverseIndex = new HashMap<>();
for (Map.Entry<String, List<String>> e : loadedImports.entrySet()) {
for (String imp : e.getValue()) {
reverseIndex.computeIfAbsent(imp, k -> new ArrayList<>()).add(e.getKey());
}
}
for (String tgt : targets) {
List<String> found = reverseIndex.getOrDefault(tgt, Collections.emptyList());
}
}
long fixedNs = System.nanoTime() - t0;
// importMethods defect
long t2 = System.nanoTime();
for (int r = 0; r < REPS; r++) importMethodsDefect(allFuns, vars);
long importDefectNs = System.nanoTime() - t2;
// importMethods fixed
t2 = System.nanoTime();
for (int r = 0; r < REPS; r++) importMethodsFixed(allFuns, vars);
long importFixedNs = System.nanoTime() - t2;
System.out.printf("=== r-lang-0001: CWE-407 namespace membership O(N*I) ===%n");
System.out.printf("getNamespaceUsers defect: %,d ns | fixed: %,d ns | ratio: %.1fx%n",
defectNs, fixedNs, (double) defectNs / fixedNs);
System.out.printf("importMethods defect: %,d ns | fixed: %,d ns | ratio: %.1fx%n",
importDefectNs, importFixedNs, (double) importDefectNs / importFixedNs);
System.out.printf("N=%d namespaces, I=%d imports, G=%d generics, V=%d vars%n", N, I, G, V);
// PASS/FAIL
double nsRatio = (double) defectNs / fixedNs;
double imRatio = (double) importDefectNs / importFixedNs;
if (nsRatio >= 3.0 && imRatio >= 3.0) {
System.out.println("PASS");
} else {
System.out.printf("FAIL — ratios below threshold (%.1fx, %.1fx, expected >=3x)%n",
nsRatio, imRatio);
System.exit(1);
}
}
}