@@ -97,25 +97,29 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan<char> filePath)
9797 {
9898 Debug . Assert ( MasterMegArchive is not null ) ;
9999
100- if ( filePath . Length > PGConstants . MaxMegEntryPathLength )
100+ var sb = new ValueStringBuilder ( stackalloc char [ Math . Max ( filePath . Length , PGConstants . MaxMegEntryPathLength ) ] ) ;
101+ sb . Append ( filePath ) ;
102+ NormalizePath ( ref sb ) ;
103+
104+ if ( sb . Length > PGConstants . MaxMegEntryPathLength )
101105 {
102- Logger . LogWarning ( "Trying to open a MEG entry which is longer than 259 characters: '{FilePath}'" , filePath . ToString ( ) ) ;
106+ Logger . LogWarning ( "Trying to open a MEG entry which is longer than 259 characters: '{FileName}'" , sb . ToString ( ) ) ;
107+ sb . Dispose ( ) ;
103108 return default ;
104109 }
105110
106111 Span < char > fileNameSpan = stackalloc char [ PGConstants . MaxMegEntryPathLength ] ;
107112
108- if ( ! _megPathNormalizer . TryNormalize ( filePath , fileNameSpan , out var length ) )
109- return default ;
110-
111- var fileName = fileNameSpan . Slice ( 0 , length ) ;
112-
113- if ( fileName . Length > PGConstants . MaxMegEntryPathLength )
113+ if ( ! _megPathNormalizer . TryNormalize ( sb . AsSpan ( ) , fileNameSpan , out var length ) )
114114 {
115- Logger . LogWarning ( "Trying to open a MEG entry which is longer than 259 characters after normalization: '{FileName}'" , fileName . ToString ( ) ) ;
115+ sb . Dispose ( ) ;
116116 return default ;
117117 }
118118
119+ sb . Dispose ( ) ;
120+
121+ var fileName = fileNameSpan . Slice ( 0 , length ) ;
122+
119123 var crc = _crc32HashingService . GetCrc32 ( fileName , MegFileConstants . MegDataEntryPathEncoding ) ;
120124
121125 var entry = MasterMegArchive ! . EntriesWithCrc ( crc ) . FirstOrDefault ( ) ;
@@ -133,13 +137,15 @@ protected FileFoundInfo FindFileCore(ReadOnlySpan<char> filePath, ref ValueStrin
133137 stringBuilder . Append ( filePath ) ;
134138 else
135139 FileSystem . Path . Join ( GameDirectory . AsSpan ( ) , filePath , ref stringBuilder ) ;
140+
136141
137- var actualFilePath = stringBuilder . AsSpan ( ) ;
138-
139- // We accept a *possible* difference here between platforms,
140- // unless it's proven the differences are too significant.
141142 if ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
142- exists = FileSystem . File . Exists ( actualFilePath . ToString ( ) ) ;
143+ {
144+ NormalizePath ( ref stringBuilder ) ;
145+
146+ var actualFilePath = stringBuilder . AsSpan ( ) ;
147+ exists = FileSystemPathExistsCaseInsensitive ( actualFilePath , ref stringBuilder ) ;
148+ }
143149 else
144150 {
145151 // We *could* also use the slightly faster GetFileAttributesA.
@@ -157,7 +163,7 @@ in stringBuilder.GetPinnableReference(true),
157163
158164 exists = IsValidAndClose ( fileHandle ) ;
159165 }
160- return ! exists ? new FileFoundInfo ( ) : new FileFoundInfo ( actualFilePath ) ;
166+ return ! exists ? new FileFoundInfo ( ) : new FileFoundInfo ( stringBuilder . AsSpan ( ) ) ;
161167 }
162168
163169 protected FileFoundInfo FileFromAltExists ( ReadOnlySpan < char > filePath , IList < string > fallbackPaths , ref ValueStringBuilder pathStringBuilder )
@@ -183,6 +189,79 @@ protected FileFoundInfo FileFromAltExists(ReadOnlySpan<char> filePath, IList<str
183189
184190 return default ;
185191 }
192+
193+ private static int NormalizePath ( Span < char > path )
194+ {
195+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
196+ return path . Length ;
197+
198+ var writePos = 0 ;
199+ var lastWasSeparator = false ;
200+ for ( var i = 0 ; i < path . Length ; i ++ )
201+ {
202+ var c = path [ i ] ;
203+ var isSeparator = c is '\\ ' or '/' ;
204+ if ( isSeparator && lastWasSeparator )
205+ continue ;
206+ path [ writePos ++ ] = isSeparator ? '/' : c ;
207+ lastWasSeparator = isSeparator ;
208+ }
209+
210+ return writePos ;
211+ }
212+
213+ private static void NormalizePath ( ref ValueStringBuilder stringBuilder )
214+ {
215+ stringBuilder . Length = NormalizePath ( stringBuilder . RawChars . Slice ( 0 , stringBuilder . Length ) ) ;
216+ }
217+
218+ private bool FileSystemPathExistsCaseInsensitive ( ReadOnlySpan < char > filePath , ref ValueStringBuilder stringBuilder )
219+ {
220+ var pathString = filePath . ToString ( ) ;
221+ if ( FileSystem . File . Exists ( pathString ) )
222+ return true ;
223+
224+ var directory = FileSystem . Path . GetDirectoryName ( pathString ) ;
225+ var fileName = FileSystem . Path . GetFileName ( pathString ) ;
226+
227+ if ( string . IsNullOrEmpty ( directory ) || string . IsNullOrEmpty ( fileName ) )
228+ return false ;
229+
230+ if ( ! FileSystem . Directory . Exists ( directory ) )
231+ {
232+ if ( ! FileSystemPathExistsCaseInsensitive ( directory . AsSpan ( ) , ref stringBuilder ) )
233+ return false ;
234+
235+ directory = stringBuilder . AsSpan ( ) . ToString ( ) ;
236+ }
237+
238+ var files = FileSystem . Directory . GetFiles ( directory ) ;
239+ var directories = FileSystem . Directory . GetDirectories ( directory ) ;
240+
241+ foreach ( var file in files )
242+ {
243+ var name = FileSystem . Path . GetFileName ( file ) ;
244+ if ( name . Equals ( fileName , StringComparison . OrdinalIgnoreCase ) )
245+ {
246+ stringBuilder . Length = 0 ;
247+ stringBuilder . Append ( file ) ;
248+ return true ;
249+ }
250+ }
251+
252+ foreach ( var dir in directories )
253+ {
254+ var name = FileSystem . Path . GetFileName ( dir ) ;
255+ if ( name . Equals ( fileName , StringComparison . OrdinalIgnoreCase ) )
256+ {
257+ stringBuilder . Length = 0 ;
258+ stringBuilder . Append ( dir ) ;
259+ return true ;
260+ }
261+ }
262+
263+ return false ;
264+ }
186265
187266 private static bool PathStartsWithDataDirectory ( ReadOnlySpan < char > path , out int cutoffLength )
188267 {
0 commit comments