173 lines
6.5 KiB
Java
173 lines
6.5 KiB
Java
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.");
|
||
}
|
||
}
|