Skip to content

Commit deeee73

Browse files
committed
Address review comments
1 parent 843326b commit deeee73

6 files changed

Lines changed: 268 additions & 53 deletions

File tree

prelude/bootstrap-shared.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,75 @@ function installDiagnostic(snapshotPrefix) {
622622
}
623623
}
624624

625+
// /////////////////////////////////////////////////////////////////
626+
// SYMLINK PROCESSING //////////////////////////////////////////////
627+
// /////////////////////////////////////////////////////////////////
628+
629+
// Matches the typical Linux SYMLOOP_MAX. Bounds the symlink resolution
630+
// loop so a manifest cycle (or a corrupt manifest) cannot hang startup.
631+
var MAX_SYMLINK_DEPTH = 40;
632+
633+
function resolveSymlink(p, sep, symlinks, cache) {
634+
// Cache symlink resolution results to avoid re-walking the same path.
635+
// The cache is keyed by the original path, not the resolved path, so that
636+
// repeated calls with the same input path hit the cache. Only paths that
637+
// actually traverse a symlink get cached (see below) — the vast majority
638+
// of lookups are non-symlinked files, and most of those are looked up
639+
// once (module resolution tries many one-off candidate paths), so
640+
// memoizing them would grow the cache unboundedly for no benefit and add
641+
// Map overhead to every miss without amortizing it. Bounding the cache to
642+
// real hits keeps it both fast and small.
643+
var cached = cache.get(p);
644+
if (cached !== undefined) return cached;
645+
646+
var original = p;
647+
var matched = false;
648+
for (var i = 0; i < MAX_SYMLINK_DEPTH; i++) {
649+
// Exact match first (e.g. the path itself is the symlink).
650+
var target = symlinks[p];
651+
if (!target) {
652+
// Walk the path front-to-back (POSIX-style): resolve the shallowest
653+
// symlinked component first. This is O(path depth) hash lookups,
654+
// independent of how many symlinks exist in the manifest. Symlinks
655+
// (e.g. a package manager's node_modules entries) sit near the root
656+
// while the remainder of the path can be arbitrarily deep, so this
657+
// finds a hit in far fewer lookups than scanning from the leaf
658+
// backwards would.
659+
var pos = p.indexOf(sep, 1);
660+
while (pos > 0) {
661+
var prefix = p.slice(0, pos);
662+
var t = symlinks[prefix];
663+
if (t) {
664+
// If the symlink target ends with a separator, we need to skip
665+
// the leading separator of the remainder to avoid a double
666+
// separator. Otherwise, we can just append the remainder as-is.
667+
target = t.endsWith(sep) ? t + p.slice(pos + 1) : t + p.slice(pos);
668+
break;
669+
}
670+
pos = p.indexOf(sep, pos + 1);
671+
}
672+
}
673+
674+
if (!target) {
675+
// No symlink found in the path, so the current path is fully resolved.
676+
if (matched) cache.set(original, p);
677+
return p;
678+
}
679+
680+
matched = true;
681+
p = target;
682+
}
683+
684+
var err = new Error(
685+
"ELOOP: too many symbolic links encountered, '" + original + "'",
686+
);
687+
err.code = 'ELOOP';
688+
err.errno = -40;
689+
err.syscall = 'stat';
690+
err.path = original;
691+
throw err;
692+
}
693+
625694
module.exports = {
626695
patchDlopen: patchDlopen,
627696
patchChildProcess: patchChildProcess,
@@ -631,4 +700,5 @@ module.exports = {
631700
COMPRESS_NONE: COMPRESS_NONE,
632701
pickDecompressorSync: pickDecompressorSync,
633702
pickDecompressorAsync: pickDecompressorAsync,
703+
resolveSymlink: resolveSymlink,
634704
};

prelude/bootstrap.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -231,25 +231,16 @@ function toOriginal(fShort) {
231231
.join(path.sep);
232232
}
233233

234-
const symlinksEntries = Object.entries(SYMLINKS);
234+
const hasSymlinks = Object.keys(SYMLINKS).length > 0;
235+
const symlinkCache = new Map();
235236

236237
// separator for substitution depends on platform;
237238
const sepsep = DOCOMPRESS ? separator : path.sep;
238239

239240
function findVirtualFileSystemKeyAndFollowLinks(path_) {
240241
let vfsKey = findVirtualFileSystemKey(path_, path.sep);
241-
let needToSubstitute = true;
242-
while (needToSubstitute) {
243-
needToSubstitute = false;
244-
for (const [k, v] of symlinksEntries) {
245-
if (vfsKey.startsWith(`${k}${sepsep}`) || vfsKey === k) {
246-
vfsKey = vfsKey.replace(k, v);
247-
needToSubstitute = true;
248-
break;
249-
}
250-
}
251-
}
252-
return vfsKey;
242+
if (!hasSymlinks) return vfsKey;
243+
return REQUIRE_SHARED.resolveSymlink(vfsKey, sepsep, SYMLINKS, symlinkCache);
253244
}
254245

255246
function realpathFromSnapshot(path_) {

prelude/sea-vfs-setup.js

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ try {
2424
var VirtualFileSystem = vfsModule.VirtualFileSystem;
2525
var MemoryProvider = vfsModule.MemoryProvider;
2626

27-
// Matches the typical Linux SYMLOOP_MAX. Bounds the symlink resolution
28-
// loop so a manifest cycle (or a corrupt manifest) cannot hang startup.
29-
var MAX_SYMLINK_DEPTH = 40;
30-
3127
// /////////////////////////////////////////////////////////////////
3228
// PERFORMANCE INSTRUMENTATION /////////////////////////////////////
3329
// /////////////////////////////////////////////////////////////////
@@ -151,6 +147,7 @@ var perf = {
151147
'statSync calls',
152148
'existsSync calls',
153149
'readdirSync calls',
150+
'_resolveSymlink calls',
154151
];
155152
counterOrder.forEach(function (label) {
156153
var v = self._counters[label];
@@ -283,13 +280,21 @@ function _makeStats(meta) {
283280
*
284281
* Performance design:
285282
*
286-
* - internalModuleStat() O(1) manifest hash lookup (no tree walk).
283+
* - internalModuleStat() Symlink resolution (no-op O(1) if the manifest has
284+
* no symlinks; O(path depth) for any path that isn't itself symlinked,
285+
* paid on every call — not memoised, since most lookups are one-off
286+
* candidate paths and caching them would grow the cache unboundedly for
287+
* no benefit; O(1) amortized only for paths that actually traverse a
288+
* symlink, via a Map keyed by the original path — see resolveSymlink()
289+
* in bootstrap-shared.js) + O(1) manifest lookup.
287290
* This is the hottest path (~30K calls for large projects).
288291
*
289-
* - statSync() O(1) manifest lookup + lightweight stat allocation.
292+
* - statSync() Same symlink resolution as above + O(1) manifest
293+
* lookup + lightweight stat allocation.
290294
* Not on the module resolution hot path. Returns a fresh object each call.
291295
*
292-
* - existsSync() O(1) manifest lookup.
296+
* - existsSync() Same symlink resolution as above + O(1) manifest
297+
* lookup.
293298
*
294299
* - readFileSync() Zero-copy subarray from the archive with a Map
295300
* cache. Bypasses the MemoryProvider tree entirely. Returns a Buffer
@@ -307,9 +312,8 @@ class SEAProvider extends MemoryProvider {
307312
this._manifest = seaManifest;
308313
this._fileCache = new Map();
309314

310-
// Precompute whether the manifest has any symlinks.
311-
// If a project has no symlinks, there is also no need to resolve them.
312-
this._hasSymlinks = Object.keys(seaManifest.symlinks).length > 0;
315+
this._hasSymlinks = Object.keys(seaManifest.symlinks || {}).length > 0;
316+
this._symlinkCache = new Map();
313317

314318
// Pick the per-file decompressor once at construction time. Absent or 0 =
315319
// uncompressed archive (backward compat with pre-#250 SEA binaries). The
@@ -341,37 +345,14 @@ class SEAProvider extends MemoryProvider {
341345
}
342346

343347
_resolveSymlink(p) {
344-
// Fast path: if the manifest has no symlinks, skip the loop entirely.
348+
perf.count('_resolveSymlink calls');
345349
if (!this._hasSymlinks) return p;
346-
var symlinks = this._manifest.symlinks;
347-
var original = p;
348-
for (var i = 0; i < MAX_SYMLINK_DEPTH; i++) {
349-
// First check the full path, then walk up the directory tree to find a symlink.
350-
var target = symlinks[p];
351-
if (!target) {
352-
var parentIdx = p.lastIndexOf('/');
353-
while (parentIdx > 0) {
354-
var parent = p.slice(0, parentIdx);
355-
target = symlinks[parent];
356-
if (target) {
357-
// Resolve the symlink and append the remainder of the original path.
358-
target = target + p.slice(parentIdx);
359-
break;
360-
}
361-
parentIdx = parent.lastIndexOf('/');
362-
}
363-
}
364-
if (!target) return p;
365-
p = target;
366-
}
367-
var err = new Error(
368-
"ELOOP: too many symbolic links encountered, '" + original + "'",
350+
return shared.resolveSymlink(
351+
p,
352+
'/',
353+
this._manifest.symlinks,
354+
this._symlinkCache,
369355
);
370-
err.code = 'ELOOP';
371-
err.errno = -40;
372-
err.syscall = 'stat';
373-
err.path = original;
374-
throw err;
375356
}
376357

377358
get fileCacheSize() {
@@ -459,6 +440,10 @@ class SEAProvider extends MemoryProvider {
459440
}
460441

461442
readlinkSync(filePath) {
443+
// readlinkSync must return the symlink target verbatim, without resolving
444+
// it. If the path is not a symlink, fall back to the super method (which throws
445+
// ENOENT for non-existent paths). The manifest's symlinks map is keyed by
446+
// the symlink path and contains the target path, so we can look it up directly.
462447
var p = toManifestKey(filePath);
463448
var target = this._manifest.symlinks[p];
464449
if (target) return target;

test/test-99-#295/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "test-99-#295",
2+
"name": "test-99-295",
33
"version": "1.0.0",
44
"main": "index.js",
55
"bin": "index.js"

test/test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const npmTests = [
8585
'test-91-sea-esm-entry',
8686
'test-92-sea-tla',
8787
'test-94-sea-esm-import-meta',
88+
'test-99-#295',
8889
];
8990

9091
if (testFilter) {

0 commit comments

Comments
 (0)