11using System ;
22using System . Diagnostics ;
3- using System . IO ;
43using System . Runtime . CompilerServices ;
54using System . Runtime . InteropServices ;
6- using AnakinRaW . CommonUtilities . FileSystem ;
75using PG . StarWarsGame . Engine . Utilities ;
6+ using System . IO ;
7+ using AnakinRaW . CommonUtilities . FileSystem ;
88#if NETSTANDARD2_1 || NET
99using System . Diagnostics . CodeAnalysis ;
1010#endif
@@ -132,4 +132,139 @@ private static extern IntPtr CreateFile(
132132 [ DllImport ( "kernel32.dll" , SetLastError = true ) ]
133133 [ return : MarshalAs ( UnmanagedType . Bool ) ]
134134 private static extern bool CloseHandle ( IntPtr hObject ) ;
135+
136+
137+
138+
139+ /// <summary>
140+ /// Checks whether a file exists using case-insensitive path resolution.
141+ /// On success, <paramref name="stringBuilder"/> contains the actual on-disk path.
142+ /// </summary>
143+ /// <remarks>
144+ /// <para>
145+ /// Strategy:
146+ /// 1. Fast path: single stat() for exact-case match.
147+ /// 2. Find deepest existing directory prefix, starting from the hint position.
148+ /// With a correct hint this costs 1 stat. Without a hint (or bad hint),
149+ /// walks backward — graceful degradation, never throws.
150+ /// 3. Forward resolve: lazily enumerate only the mismatched components.
151+ /// </para>
152+ /// <para>
153+ /// No exceptions occur in normal flow: Directory.Exists returns bool,
154+ /// and we only enumerate directories whose existence has been confirmed.
155+ /// </para>
156+ /// </remarks>
157+ /// <param name="filePath">
158+ /// Normalized absolute path with forward slashes. May alias stringBuilder's buffer.
159+ /// </param>
160+ /// <param name="stringBuilder">
161+ /// On success, overwritten with the actual on-disk path.
162+ /// </param>
163+ /// <param name="knownGoodPrefixLength">
164+ /// Length of the path prefix known to exist with correct casing (typically gameDirectory.Length).
165+ /// Pass 0 if unknown — the method falls back to a backward walk.
166+ /// </param>
167+ private bool FileExistsCaseInsensitive ( ReadOnlySpan < char > filePath , ref ValueStringBuilder stringBuilder , int knownGoodPrefixLength )
168+ {
169+ Debug . Assert ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ) ;
170+
171+ var pathString = filePath . ToString ( ) ;
172+
173+ // Fast path: exact case match — single stat() syscall
174+ if ( _underlyingFileSystem . File . Exists ( pathString ) )
175+ return true ;
176+
177+ if ( pathString . Length == 0 )
178+ return false ;
179+
180+
181+ var path = pathString . AsSpan ( ) ;
182+
183+ var rootLen = path [ 0 ] == '/' ? 1 : 0 ;
184+ var resolvedEnd = rootLen ;
185+
186+ int searchEnd ;
187+ if ( knownGoodPrefixLength > 0 )
188+ {
189+ searchEnd = knownGoodPrefixLength ;
190+ while ( searchEnd > 1 && path [ searchEnd - 1 ] == '/' )
191+ searchEnd -- ;
192+ }
193+ else
194+ {
195+ var lastSlash = path . LastIndexOf ( '/' ) ;
196+ searchEnd = lastSlash >= 0 ? ( lastSlash == 0 ? 1 : lastSlash ) : 0 ;
197+ }
198+
199+ // Walk backward until we find an existing directory.
200+ // Save the successful prefix string to reuse as the first currentDir.
201+ string ? resolvedPrefix = null ;
202+ while ( searchEnd > resolvedEnd )
203+ {
204+ var prefix = pathString . Substring ( 0 , searchEnd ) ;
205+ if ( _underlyingFileSystem . Directory . Exists ( prefix ) )
206+ {
207+ resolvedEnd = searchEnd ;
208+ resolvedPrefix = prefix ;
209+ break ;
210+ }
211+
212+ var slash = path . Slice ( 0 , searchEnd ) . LastIndexOf ( '/' ) ;
213+ if ( slash < 0 )
214+ break ;
215+ searchEnd = slash == 0 ? 1 : slash ;
216+ }
217+
218+ if ( resolvedEnd == 0 )
219+ return false ;
220+
221+ // Reuse the prefix from Directory.Exists if available, otherwise allocate once.
222+ var currentDir = resolvedPrefix ?? pathString . Substring ( 0 , resolvedEnd ) ;
223+
224+ stringBuilder . Length = 0 ;
225+ stringBuilder . Append ( currentDir ) ;
226+
227+ var pos = resolvedEnd ;
228+ if ( pos < path . Length && path [ pos ] == '/' )
229+ pos ++ ;
230+
231+ while ( pos < path . Length )
232+ {
233+ var nextSlash = path . Slice ( pos ) . IndexOf ( '/' ) ;
234+ var componentEnd = nextSlash >= 0 ? pos + nextSlash : path . Length ;
235+ var component = path . Slice ( pos , componentEnd - pos ) ;
236+
237+ if ( component . IsEmpty )
238+ {
239+ pos = componentEnd + 1 ;
240+ continue ;
241+ }
242+
243+ var isLast = componentEnd >= path . Length ;
244+
245+ var entries = isLast
246+ ? _underlyingFileSystem . Directory . EnumerateFiles ( currentDir )
247+ : _underlyingFileSystem . Directory . EnumerateDirectories ( currentDir ) ;
248+
249+ var found = false ;
250+ foreach ( var entry in entries )
251+ {
252+ if ( _underlyingFileSystem . Path . GetFileName ( entry . AsSpan ( ) ) . Equals ( component , StringComparison . OrdinalIgnoreCase ) )
253+ {
254+ stringBuilder . Length = 0 ;
255+ stringBuilder . Append ( entry ) ;
256+ currentDir = entry ; // entry is already a string — reuse it
257+ found = true ;
258+ break ;
259+ }
260+ }
261+
262+ if ( ! found )
263+ return false ;
264+
265+ pos = componentEnd + 1 ;
266+ }
267+
268+ return true ;
269+ }
135270}
0 commit comments