2424var VirtualFileSystem = vfsModule . VirtualFileSystem ;
2525var 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 ;
0 commit comments