diff --git a/src/Lumina/GameData.cs b/src/Lumina/GameData.cs
index 1fa740d5..94d03f46 100644
--- a/src/Lumina/GameData.cs
+++ b/src/Lumina/GameData.cs
@@ -122,29 +122,45 @@ public GameData( string dataPath, ILogger logger, LuminaOptions? options = null!
Logger = logger ?? throw new ArgumentNullException( nameof( logger ) );
}
+ ///
+ public static ParsedFilePath? ParseFilePath( string path )
+ => ParseFilePath( path.AsSpan() );
+
///
/// Parses a game filesystem path and extracts information and hashes the path provided.
///
/// A game filesystem path
/// A which contains extracted info from the path, along with the hashes used to access the file index
- public static ParsedFilePath? ParseFilePath( string path )
+ public static ParsedFilePath? ParseFilePath( ReadOnlySpan path )
{
- if( string.IsNullOrWhiteSpace( path ) )
+ if( path.IsWhiteSpace() )
return null;
// validate path slightly
if( path[ ^1 ] == '/' )
return null;
- path = path.ToLowerInvariant().Trim();
-
- var pathParts = path.Split( '/' );
- var category = pathParts.First();
+ // Game paths can not be longer than MAX_PATH.
+ if( path.Length >= 260 )
+ return null;
- var hash = GetFileHash( path );
- var hash2 = Crc32.Get( path );
+ // Has to have at least one folder.
+ var directorySplit = path.LastIndexOf( '/' );
+ if( directorySplit < 0 )
+ return null;
+
+ Span lowerPath = stackalloc char[260];
+ var length = path.ToLowerInvariant( lowerPath );
+ lowerPath[length] = '\0';
+ lowerPath = lowerPath[ ..length ];
+ lowerPath = lowerPath.Trim();
- var repo = pathParts[ 1 ];
+ var hash = GetFileHash( lowerPath[..directorySplit], lowerPath[(directorySplit + 1)..] );
+ var hash2 = Crc32.Get( lowerPath );
+
+ var pathParts = lowerPath.Split( '/' );
+ ReadOnlySpan category = pathParts.MoveNext() ? lowerPath[pathParts.Current] : [];
+ ReadOnlySpan repo = pathParts.MoveNext() ? lowerPath[pathParts.Current] : [];
// todo: supports up to ex9, so we've got another ~11 years before this breaks
if( repo[ 0 ] != 'e' || repo[ 1 ] != 'x' || !char.IsDigit( repo[ 2 ] ) )
{
@@ -153,11 +169,11 @@ public GameData( string dataPath, ILogger logger, LuminaOptions? options = null!
return new ParsedFilePath
{
- Category = category,
+ Category = category.ToString(),
IndexHash = hash,
Index2Hash = hash2,
- Repository = repo,
- Path = path
+ Repository = repo.ToString(),
+ Path = lowerPath.ToString(),
};
}
@@ -254,12 +270,27 @@ public T GetFileFromDisk< T >( string path, string? origPath = null ) where T :
return null;
}
+ ///
+ public bool FileExists( string path )
+ {
+ var parsedPath = ParseFilePath( path );
+ if( parsedPath == null )
+ return false;
+
+ if( Repositories.TryGetValue( parsedPath.Repository, out var repo ) )
+ {
+ return repo.FileExists( parsedPath.Category, parsedPath );
+ }
+
+ return false;
+ }
+
///
/// Check if a file exists anywhere by checking whether the hash exists in any index
///
/// The full path of the file
/// True if the file exists
- public bool FileExists( string path )
+ public bool FileExists( ReadOnlySpan path )
{
var parsedPath = ParseFilePath( path );
if( parsedPath == null )
@@ -273,18 +304,33 @@ public bool FileExists( string path )
return false;
}
+ ///
+ public static UInt64 GetFileHash( string path )
+ => GetFileHash( path.AsSpan() );
+
///
/// Returns the index variant of a file hash
///
/// The full path of the file
/// A U64 containing a split hash of the folder and file CRC32s
- public static UInt64 GetFileHash( string path )
+ public static UInt64 GetFileHash( ReadOnlySpan path )
{
- var pathParts = path.Split( '/' );
- var filename = pathParts[ ^1 ];
- var folder = path.Substring( 0, path.LastIndexOf( '/' ) );
+ var directorySplit = path.LastIndexOf( '/' );
+ if( directorySplit < 0 )
+ return Crc32.Get( path );
+
+ return GetFileHash( path[..directorySplit] ) << 32 | Crc32.Get( path[(directorySplit + 1)..] );
+ }
- return (UInt64) Crc32.Get( folder ) << 32 | Crc32.Get( filename );
+ ///
+ /// Returns the index variant of a file hash
+ ///
+ /// The full path of the directory the file is in without trailing '/'.
+ /// The file name with extension.
+ /// A U64 containing a split hash of the folder and file CRC32s
+ public static UInt64 GetFileHash( ReadOnlySpan folder, ReadOnlySpan filename )
+ {
+ return (UInt64)Crc32.Get( folder ) << 32 | Crc32.Get( filename );
}
/// Loads an . Returns if the sheet does not exist, has an invalid column hash or unsupported variant, or was requested with an unsupported language.
diff --git a/src/Lumina/Misc/Crc32.cs b/src/Lumina/Misc/Crc32.cs
index b9553ee5..bf66dae3 100644
--- a/src/Lumina/Misc/Crc32.cs
+++ b/src/Lumina/Misc/Crc32.cs
@@ -41,9 +41,24 @@ static Crc32()
/// The CRC32 of the input data
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public static uint Get( string value, uint crc = CrcInitialSeed )
+ => Get( value.AsSpan(), crc );
+
+ ///
+ /// Calculate the CRC32 of the given string.
+ ///
+ /// The value to hash
+ /// The initial seed/value
+ /// The CRC32 of the input data
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public static uint Get( ReadOnlySpan value, uint crc = CrcInitialSeed )
{
- var data = Encoding.UTF8.GetBytes( value );
- return Get( data, 0, data.Length, crc );
+ Span< byte > bytes = stackalloc byte[260];
+ int length;
+ while( !Encoding.UTF8.TryGetBytes( value, bytes, out length ) )
+ bytes = new byte[bytes.Length * 4];
+ bytes[length] = 0;
+ bytes = bytes[ ..length ];
+ return Get( bytes, 0, length, crc );
}
///