@@ -100,6 +100,127 @@ public void EnumerateFiles_TwoLinksToSameDirectory_VisitsItOnce()
100100 Assert . AreEqual ( 1 , files . Count ) ;
101101 }
102102
103+ [ DataTestMethod ]
104+ [ DataRow ( true ) ]
105+ [ DataRow ( false ) ]
106+ public void EnumerateFiles_RealDirAlreadyClaimedAsLinkTarget_IsVisitedOnce ( bool xyLinkIsDeeper )
107+ {
108+ // ext/X/Y/y.json is reachable two ways: through a link to ext/X/Y, and as a real subfolder
109+ // of a link to ext/X. Sibling enumeration order is file-system dependent, so nesting the
110+ // links at different depths pins the walk order instead of relying on names: the deeper
111+ // link is always popped first (draining real dirs first pushes it last, and the link stack
112+ // pops last-in first-out).
113+ var root = CreateDir ( "root" ) ;
114+ var sub = CreateDir ( "root" , "sub" ) ;
115+ CreateFile ( "ext" , "X" , "Y" , "y.json" ) ;
116+
117+ var x = Path . Combine ( tempDir , "ext" , "X" ) ;
118+ var xy = Path . Combine ( x , "Y" ) ;
119+
120+ // Deep link -> ext/X/Y, shallow link -> ext/X. When the deep link is the one targeting
121+ // ext/X/Y, it is walked first and the shallower ext/X link then reaches that same real
122+ // folder again through its "Y" child.
123+ var xyLink = Path . Combine ( xyLinkIsDeeper ? sub : root , "inner" ) ;
124+ var xLink = Path . Combine ( xyLinkIsDeeper ? root : sub , "outer" ) ;
125+ TempFiles . CreateDirectoryLink ( xyLink , xy ) ;
126+ TempFiles . CreateDirectoryLink ( xLink , x ) ;
127+
128+ var files = LinkSafeFileSystem . EnumerateFiles ( root , "*.json" ) . ToList ( ) ;
129+
130+ Assert . AreEqual ( 1 , files . Count , $ "Expected one file, got: { string . Join ( ", " , files ) } ") ;
131+ }
132+
133+ [ DataTestMethod ]
134+ [ DataRow ( "a_inner" , "b_outer" ) ]
135+ [ DataRow ( "b_inner" , "a_outer" ) ]
136+ public void EnumerateFiles_NestedLinkTarget_SiblingLinkOrder_IsVisitedOnce (
137+ string innerName ,
138+ string outerName
139+ )
140+ {
141+ if ( ! Compat . IsWindows )
142+ {
143+ Assert . Inconclusive (
144+ "Needs NTFS, which enumerates sibling directories in stored name order; "
145+ + "EnumerateFiles_RealDirAlreadyClaimedAsLinkTarget_IsVisitedOnce covers the same "
146+ + "bug portably by varying depth instead."
147+ ) ;
148+ return ;
149+ }
150+
151+ // The maintainer's original repro: innerName -> ext/X/Y, outerName -> ext/X, so the inner
152+ // link's target is also reached as a real subfolder of the outer link. NTFS yields siblings
153+ // in name order, so the two rows walk the links in opposite orders.
154+ var root = CreateDir ( "root" ) ;
155+ CreateFile ( "ext" , "X" , "Y" , "y.json" ) ;
156+
157+ var x = Path . Combine ( tempDir , "ext" , "X" ) ;
158+ var xy = Path . Combine ( x , "Y" ) ;
159+
160+ TempFiles . CreateDirectoryLink ( Path . Combine ( root , innerName ) , xy ) ;
161+ TempFiles . CreateDirectoryLink ( Path . Combine ( root , outerName ) , x ) ;
162+
163+ var files = LinkSafeFileSystem . EnumerateFiles ( root , "*.json" ) . ToList ( ) ;
164+
165+ Assert . AreEqual ( 1 , files . Count , $ "Expected one file, got: { string . Join ( ", " , files ) } ") ;
166+ }
167+
168+ [ DataTestMethod ]
169+ [ DataRow ( "diffusion_models" ) ]
170+ [ DataRow ( "sub" , "alias" ) ]
171+ public void EnumerateFiles_RealFolderShadowedByLink_KeepsRealFolderPaths ( params string [ ] linkSegments )
172+ {
173+ var root = CreateDir ( "root" ) ;
174+ CreateFile ( "root" , "DiffusionModels" , "a.json" ) ;
175+ CreateFile ( "root" , "DiffusionModels" , "b.json" ) ;
176+
177+ var linkPath = Path . Combine ( [ root , .. linkSegments ] ) ;
178+ Directory . CreateDirectory ( Path . GetDirectoryName ( linkPath ) ! ) ;
179+ TempFiles . CreateDirectoryLink ( linkPath , Path . Combine ( root , "DiffusionModels" ) ) ;
180+
181+ var files = LinkSafeFileSystem . EnumerateFiles ( root , "*.json" ) . ToList ( ) ;
182+
183+ CollectionAssert . AreEquivalent (
184+ new [ ]
185+ {
186+ Path . Combine ( root , "DiffusionModels" , "a.json" ) ,
187+ Path . Combine ( root , "DiffusionModels" , "b.json" ) ,
188+ } ,
189+ files
190+ ) ;
191+ }
192+
193+ [ TestMethod ]
194+ public void EnumerateFiles_JunctionTargetCaseMismatch_KeepsRealFolderPaths ( )
195+ {
196+ if ( ! Compat . IsWindows )
197+ {
198+ Assert . Inconclusive ( "Junctions with a differently-cased stored target are Windows-only." ) ;
199+ return ;
200+ }
201+
202+ var root = CreateDir ( "root" ) ;
203+ CreateFile ( "root" , "DiffusionModels" , "a.json" ) ;
204+ CreateFile ( "root" , "DiffusionModels" , "b.json" ) ;
205+
206+ // Store the junction target with different casing than the real folder on disk.
207+ TempFiles . CreateDirectoryLink (
208+ Path . Combine ( root , "diffusion_models" ) ,
209+ Path . Combine ( root . ToUpperInvariant ( ) , "DIFFUSIONMODELS" )
210+ ) ;
211+
212+ var files = LinkSafeFileSystem . EnumerateFiles ( root , "*.json" ) . ToList ( ) ;
213+
214+ CollectionAssert . AreEquivalent (
215+ new [ ]
216+ {
217+ Path . Combine ( root , "DiffusionModels" , "a.json" ) ,
218+ Path . Combine ( root , "DiffusionModels" , "b.json" ) ,
219+ } ,
220+ files
221+ ) ;
222+ }
223+
103224 [ TestMethod ]
104225 public void EnumerateFiles_DeeperThanMaxDepth_IsSkipped ( )
105226 {
0 commit comments