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 getNamespaceUsersDefect( String nsname, Map> loadedImports) { List users = new ArrayList<>(); for (Map.Entry> e : loadedImports.entrySet()) { String n = e.getKey(); List 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 getNamespaceUsersFixed( String nsname, Map> 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> in one pass. Map> reverseIndex = new HashMap<>(); for (Map.Entry> e : loadedImports.entrySet()) { for (String imp : e.getValue()) { reverseIndex.computeIfAbsent(imp, k -> new ArrayList<>()).add(e.getKey()); } } List found = reverseIndex.getOrDefault(nsname, Collections.emptyList()); // Return in reverse order to match original semantics (prepend) List 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 importMethodsDefect(List allFuns, List vars) { List 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 importMethodsFixed(List allFuns, List vars) { Set varsSet = new HashSet<>(vars); List imported = new ArrayList<>(); for (String g : allFuns) { if (varsSet.contains(g)) { // O(1) imported.add(g); } } return imported; } // --- Benchmark and correctness check --- static Map> buildNamespaceImports(int numNamespaces, int importsPerNs, String target) { Map> result = new LinkedHashMap<>(); for (int i = 0; i < numNamespaces; i++) { List 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> loadedImports = buildNamespaceImports(N, I, target); // Correctness check List r1 = getNamespaceUsersDefect(target, loadedImports); List r2 = getNamespaceUsersFixed(target, loadedImports); assert r1.equals(r2) : "getNamespaceUsers: results differ"; // Build allFuns and vars for importMethods List allFuns = new ArrayList<>(); for (int i = 0; i < G; i++) allFuns.add("generic" + i); List vars = new ArrayList<>(); for (int i = 0; i < V; i++) vars.add("generic" + (G - V + i)); List m1 = importMethodsDefect(allFuns, vars); List 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 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> reverseIndex = new HashMap<>(); for (Map.Entry> e : loadedImports.entrySet()) { for (String imp : e.getValue()) { reverseIndex.computeIfAbsent(imp, k -> new ArrayList<>()).add(e.getKey()); } } for (String tgt : targets) { List 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); } } }