Skip to content

Commit 12b170b

Browse files
committed
add new algorithm to evaluate
1 parent 5519163 commit 12b170b

1 file changed

Lines changed: 137 additions & 2 deletions

File tree

src/PetroglyphTools/PG.StarWarsGame.Engine.FileSystem/IO/PetroglyphFileSystem.Exist.cs

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using System;
22
using System.Diagnostics;
3-
using System.IO;
43
using System.Runtime.CompilerServices;
54
using System.Runtime.InteropServices;
6-
using AnakinRaW.CommonUtilities.FileSystem;
75
using PG.StarWarsGame.Engine.Utilities;
6+
using System.IO;
7+
using AnakinRaW.CommonUtilities.FileSystem;
88
#if NETSTANDARD2_1 || NET
99
using 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

Comments
 (0)