@@ -21,6 +21,7 @@ import type {
2121} from '../flow-types' ;
2222
2323import H from '../constants' ;
24+ import normalizePathSeparatorsToSystem from './normalizePathSeparatorsToSystem' ;
2425import { RootPathUtils } from './RootPathUtils' ;
2526import invariant from 'invariant' ;
2627import path from 'node:path' ;
@@ -37,12 +38,6 @@ function isRegularFile(node: FileNode): boolean {
3738 return node [ H . SYMLINK ] === 0 ;
3839}
3940
40- type NormalizedSymlinkTarget = {
41- ancestorOfRootIdx : ?number ,
42- normalPath : string ,
43- startOfBasenameIdx : number ,
44- } ;
45-
4641type DeserializedSnapshotInput = {
4742 rootDir : string ,
4843 fileSystemData : DirectoryNode ,
@@ -95,10 +90,16 @@ type MetadataIteratorOptions = Readonly<{
9590 *
9691 * SYMLINKS:
9792 *
98- * Symlinks are represented as nodes whose metadata contains their literal
99- * target. Literal targets are resolved to normal paths at runtime, and cached.
100- * If a symlink is encountered during traversal, we restart traversal at the
101- * root node targeting join(normal symlink target, remaining path suffix).
93+ * Symlinks are represented as nodes whose metadata contains their target,
94+ * lexically resolved to a normal path with POSIX separators when the node was
95+ * populated. Lexical resolution only interprets the literal target relative to
96+ * the symlink's directory: it does not follow symlinks in the target, and the
97+ * target need not exist, so it is a normalPath, not a canonicalPath (see
98+ * TERMINOLOGY). The target is the only path stored inside the tree, so it must
99+ * not use system separators, or the snapshot would not be portable between
100+ * operating systems. If a symlink is encountered during traversal, we restart
101+ * traversal at the root node targeting join(normal symlink target, remaining
102+ * path suffix).
102103 *
103104 * NODE TYPES:
104105 *
@@ -107,8 +108,8 @@ type MetadataIteratorOptions = Readonly<{
107108 * - A file is represented by an `Array` (tuple) of metadata, of which:
108109 * - A regular file has node[H.SYMLINK] === 0
109110 * - A symlink has node[H.SYMLINK] === 1 or
110- * typeof node[H.SYMLINK] === 'string', where a string is the literal
111- * content of the symlink (i.e. from readlink) , if known.
111+ * typeof node[H.SYMLINK] === 'string', where a string is the target
112+ * lexically resolved to a normal path with POSIX separators , if known.
112113 *
113114 * TERMINOLOGY:
114115 *
@@ -124,10 +125,6 @@ type MetadataIteratorOptions = Readonly<{
124125 * a trailing slash
125126 */
126127export default class TreeFS implements MutableFileSystem {
127- readonly #cachedNormalSymlinkTargets: WeakMap <
128- FileNode ,
129- NormalizedSymlinkTarget ,
130- > = new WeakMap ( ) ;
131128 readonly #pathUtils: RootPathUtils ;
132129 readonly #processFile: ProcessFileFunction ;
133130 readonly #rootDir: Path ;
@@ -723,11 +720,16 @@ export default class TreeFS implements MutableFileSystem {
723720 } ;
724721 }
725722
726- // Symlink in a directory path
727- const normalSymlinkTarget = this . #resolveSymlinkTargetToNormalPath(
728- segmentNode ,
729- currentPath ,
723+ // Symlink in a directory path. Targets are stored already lexically
724+ // resolved to a normal path, with POSIX separators so that the
725+ // snapshot is portable between operating systems.
726+ const storedSymlinkTarget = segmentNode [ H . SYMLINK ] ;
727+ invariant (
728+ typeof storedSymlinkTarget === 'string' ,
729+ 'Expected symlink target to be populated.' ,
730730 ) ;
731+ const normalSymlinkTarget =
732+ normalizePathSeparatorsToSystem ( storedSymlinkTarget ) ;
731733 if ( opts . collectLinkPaths ) {
732734 opts . collectLinkPaths . add (
733735 this . #pathUtils. normalToAbsolute ( currentPath ) ,
@@ -741,7 +743,7 @@ export default class TreeFS implements MutableFileSystem {
741743 // Append any subsequent path segments to the symlink target, and reset
742744 // with our new target.
743745 const joinedResult = this . #pathUtils. joinNormalToRelative (
744- normalSymlinkTarget . normalPath ,
746+ normalSymlinkTarget ,
745747 remainingTargetPath ,
746748 ) ;
747749
@@ -761,8 +763,7 @@ export default class TreeFS implements MutableFileSystem {
761763 collectAncestors &&
762764 ! isLastSegment &&
763765 // No-op optimisation to bail out the common case of nothing to do.
764- ( normalSymlinkTarget . ancestorOfRootIdx === 0 ||
765- joinedResult . collapsedSegments > 0 )
766+ ( normalSymlinkTarget === '' || joinedResult . collapsedSegments > 0 )
766767 ) {
767768 let node : MixedNode = this . #rootNode;
768769 let collapsedPath = '';
@@ -773,20 +774,12 @@ export default class TreeFS implements MutableFileSystem {
773774 /* for Flow, always true: */ isDirectory ( node ) ;
774775 i ++
775776 ) {
776- if (
777- // Add the root only if the target is the root or we have
778- // collapsed segments.
779- i > 0 ||
780- normalSymlinkTarget . ancestorOfRootIdx === 0 ||
781- joinedResult . collapsedSegments > 0
782- ) {
783- reverseAncestors . push ( {
784- ancestorOfRootIdx : i ,
785- node,
786- normalPath : collapsedPath ,
787- segmentName : this . #pathUtils. getBasenameOfNthAncestor ( i ) ,
788- } ) ;
789- }
777+ reverseAncestors . push ( {
778+ ancestorOfRootIdx : i ,
779+ node,
780+ normalPath : collapsedPath ,
781+ segmentName : this . #pathUtils. getBasenameOfNthAncestor ( i ) ,
782+ } ) ;
790783 node = node . get ( '..' ) ?? new Map ( ) ;
791784 collapsedPath =
792785 collapsedPath === '' ? '..' : collapsedPath + path . sep + '..' ;
@@ -800,7 +793,7 @@ export default class TreeFS implements MutableFileSystem {
800793 // the symlink target, and start collecting ancestors only
801794 // from the target itself (ie, the basename of the normal target path)
802795 // onwards.
803- unseenPathFromIdx = normalSymlinkTarget . startOfBasenameIdx ;
796+ unseenPathFromIdx = normalSymlinkTarget . lastIndexOf ( path . sep ) + 1 ;
804797
805798 if ( seen == null ) {
806799 // Optimisation: set this lazily only when we've encountered a symlink
@@ -1222,43 +1215,6 @@ export default class TreeFS implements MutableFileSystem {
12221215 }
12231216 }
12241217
1225- #resolveSymlinkTargetToNormalPath(
1226- symlinkNode : FileMetadata ,
1227- canonicalPathOfSymlink : Path ,
1228- ) : NormalizedSymlinkTarget {
1229- const cachedResult = this . #cachedNormalSymlinkTargets. get ( symlinkNode ) ;
1230- if ( cachedResult != null ) {
1231- return cachedResult ;
1232- }
1233-
1234- const literalSymlinkTarget = symlinkNode [ H . SYMLINK ] ;
1235- invariant (
1236- typeof literalSymlinkTarget === 'string' ,
1237- 'Expected symlink target to be populated.' ,
1238- ) ;
1239- let absoluteSymlinkTarget = path . resolve (
1240- this . #rootDir,
1241- canonicalPathOfSymlink ,
1242- '..' , // Symlink target is relative to its containing directory.
1243- literalSymlinkTarget , // May be absolute, in which case the above are ignored
1244- ) ;
1245- if ( absoluteSymlinkTarget . endsWith ( path . sep ) ) {
1246- absoluteSymlinkTarget = absoluteSymlinkTarget . slice ( 0 , - 1 ) ;
1247- }
1248- const normalSymlinkTarget = this . #pathUtils. absoluteToNormal (
1249- absoluteSymlinkTarget ,
1250- ) ;
1251-
1252- const result = {
1253- ancestorOfRootIdx :
1254- this . #pathUtils. getAncestorOfRootIdx ( normalSymlinkTarget ) ,
1255- normalPath : normalSymlinkTarget ,
1256- startOfBasenameIdx : normalSymlinkTarget . lastIndexOf ( path . sep ) + 1 ,
1257- } ;
1258- this . #cachedNormalSymlinkTargets. set ( symlinkNode , result ) ;
1259- return result ;
1260- }
1261-
12621218 #getFileData(
12631219 filePath : Path ,
12641220 opts : { followLeaf : boolean } = { followLeaf : true } ,
0 commit comments