java-topology/defects/nodejs/unit/NodejsResolvePathsTest.java

173 lines
6.5 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.*;
/**
* 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<String> + 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<String> 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<String> paths = new ArrayList<>();
Set<String> 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<String> defPaths = new ArrayList<>();
List<String> fixPaths = new ArrayList<>();
Set<String> 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<String> defPaths = new ArrayList<>();
Set<String> fixSet = new HashSet<>();
List<String> 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.");
}
}