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 ); } ///