package unit; import java.util.*; /** * Unit test for nodejs-0001: CWE-407 in Module._resolveFilename paths dedup. * * nodejs-0001 (MEDIUM): * File: lib/internal/modules/cjs/loader.js ~line 1408 * Symbol: Module._resolveFilename — ArrayPrototypeIncludes(paths, lookupPaths[j]) * Defect: When require.resolve(id, { paths: [...] }) is called with P explicit * paths, each generating L lookup paths, the dedup accumulator is a plain Array. * Every ArrayPrototypeIncludes() scans the entire growing accumulator: * O(P*L) per check × P*L checks = O(P²×L²) total. * Fix: Use a Set for the accumulator dedup: O(1) per check, O(P*L) total. * * Modeled here in Java: * JS Array + includes ≡ List + contains (defective) * JS Set + has ≡ java.util.Set + contains (fixed) * Comparison counts tracked at the membership-test site. * * Expected at P=50, L=10: * defective comparisons ~ P²×L²/2 ≈ 125,000 * fixed comparisons ~ P×L = 500 * ratio > 50× */ public class NodejsResolvePathsTest { // ========================================================================= // Model: _resolveFilename paths dedup // ========================================================================= /** * Defective: accumulator is a plain List; every dedup check is a linear scan. * * Simulates: for each of P input paths, generate L synthetic lookup paths. * Dedup into accumulator with List.contains() (O(size) per call). * All P*L generated paths are unique (worst-case: no early exit). */ static long deduplicateDefective(int P, int L) { long comparisons = 0; List paths = new ArrayList<>(); for (int i = 0; i < P; i++) { // Simulate _resolveLookupPaths: L unique paths per input for (int j = 0; j < L; j++) { String candidate = "path_" + i + "_" + j; // ArrayPrototypeIncludes(paths, candidate) — O(paths.size()) scan boolean found = false; for (String existing : paths) { comparisons++; if (existing.equals(candidate)) { found = true; break; } } if (!found) { paths.add(candidate); } } } return comparisons; } /** * Fixed: companion Set for O(1) membership; same result, far fewer comparisons. */ static long deduplicateFixed(int P, int L) { long comparisons = 0; List paths = new ArrayList<>(); Set pathsSet = new HashSet<>(); for (int i = 0; i < P; i++) { for (int j = 0; j < L; j++) { String candidate = "path_" + i + "_" + j; // Set.contains(candidate) — O(1) comparisons++; if (!pathsSet.contains(candidate)) { pathsSet.add(candidate); paths.add(candidate); } } } return comparisons; } // ========================================================================= // Tests // ========================================================================= /** Test 1 — Correctness: both produce the same deduplicated path list. */ static void testCorrectnessMatch() { int P = 10, L = 5; List defPaths = new ArrayList<>(); List fixPaths = new ArrayList<>(); Set fixSet = new HashSet<>(); // Rebuild lists without counting (just for correctness check) for (int i = 0; i < P; i++) { for (int j = 0; j < L; j++) { String s = "path_" + i + "_" + j; if (!defPaths.contains(s)) defPaths.add(s); if (!fixSet.contains(s)) { fixSet.add(s); fixPaths.add(s); } } } assert defPaths.size() == P * L : "expected " + (P * L) + " unique paths; got " + defPaths.size(); assert new HashSet<>(defPaths).equals(fixSet) : "defective and fixed path sets differ"; System.out.println("PASS testCorrectnessMatch"); } /** Test 2 — nodejs-0001: defective is O(P²×L²), fixed is O(P×L). */ static void testRatioAtScale() { int P = 50; int L = 10; long defComp = deduplicateDefective(P, L); long fixComp = deduplicateFixed(P, L); double ratio = (double) defComp / fixComp; // Defective: accumulator grows 0, 1, 2, ..., (P*L - 1) — sum = P*L*(P*L-1)/2 long total = (long) P * L; long expectedDef = total * (total - 1) / 2; assert defComp == expectedDef : "defective comparisons should be P*L*(P*L-1)/2=" + expectedDef + "; got " + defComp; // Fixed: exactly P*L probes (one Set.contains per candidate) assert fixComp == total : "fixed comparisons should be P*L=" + total + "; got " + fixComp; assert ratio >= 50.0 : "ratio should be >=50x at P=50, L=10; got " + ratio; System.out.printf( "PASS testRatioAtScale (defective=%d, fixed=%d, ratio=%.1fx)%n", defComp, fixComp, ratio); } /** Test 3 — Duplicate input paths: dedup is idempotent (same output either way). */ static void testWithDuplicateInputPaths() { // P paths but only P/2 unique base dirs — half of the lookupPaths overlap int P = 20, L = 5; List defPaths = new ArrayList<>(); Set fixSet = new HashSet<>(); List fixPaths = new ArrayList<>(); for (int i = 0; i < P; i++) { int base = i % (P / 2); // duplicate base dirs for (int j = 0; j < L; j++) { String s = "path_" + base + "_" + j; if (!defPaths.contains(s)) defPaths.add(s); if (!fixSet.contains(s)) { fixSet.add(s); fixPaths.add(s); } } } assert new HashSet<>(defPaths).equals(fixSet) : "duplicate-input path sets differ"; assert defPaths.size() == (P / 2) * L : "expected " + ((P / 2) * L) + " unique paths with duplicates; got " + defPaths.size(); System.out.println("PASS testWithDuplicateInputPaths"); } // ========================================================================= public static void main(String[] args) { testCorrectnessMatch(); testRatioAtScale(); testWithDuplicateInputPaths(); System.out.println("All nodejs-0001 tests passed."); } }