From 1378b87ea67c902e764bec5e61c34491d06de12b Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sun, 2 Nov 2025 21:11:39 -0500 Subject: [PATCH] Directly include StormLibSharp code --- .../_EXTERNAL/CascLibSharp/CascApi.cs | 211 ++++++++ .../_EXTERNAL/CascLibSharp/CascException.cs | 56 ++ .../_EXTERNAL/CascLibSharp/CascFileStream.cs | 168 ++++++ .../_EXTERNAL/CascLibSharp/CascFoundFile.cs | 62 +++ .../CascLibSharp/CascStorageContext.cs | 403 ++++++++++++++ .../Native/CascFileEnumerationSafeHandle.cs | 32 ++ .../Native/CascStorageFileSafeHandle.cs | 36 ++ .../Native/CascStorageSafeHandle.cs | 36 ++ .../CascLibSharp/Native/CoTaskMem.cs | 64 +++ .../CascLibSharp/Native/NativeMethods.cs | 124 +++++ SabreTools.Serialization/_EXTERNAL/README.MD | 5 +- .../_EXTERNAL/StormLibSharp/MpqArchive.cs | 414 +++++++++++++++ .../MpqArchiveCompactingEventArgs.cs | 49 ++ .../_EXTERNAL/StormLibSharp/MpqFileStream.cs | 185 +++++++ .../StormLibSharp/Native/Callbacks.cs | 17 + .../Native/MpqArchiveSafeHandle.cs | 26 + .../StormLibSharp/Native/MpqFileSafeHandle.cs | 27 + .../StormLibSharp/Native/NativeMethods.cs | 497 ++++++++++++++++++ .../StormLibSharp/Native/SFileInfoClass.cs | 70 +++ .../Native/SFileOpenArchiveFlags.cs | 26 + .../StormLibSharp/Native/Win32Methods.cs | 60 +++ 21 files changed, 2567 insertions(+), 1 deletion(-) create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascApi.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascException.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFileStream.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFoundFile.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascStorageContext.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascFileEnumerationSafeHandle.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageFileSafeHandle.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageSafeHandle.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CoTaskMem.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/NativeMethods.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchive.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchiveCompactingEventArgs.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqFileStream.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Callbacks.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqArchiveSafeHandle.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqFileSafeHandle.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/NativeMethods.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileInfoClass.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileOpenArchiveFlags.cs create mode 100644 SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Win32Methods.cs diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascApi.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascApi.cs new file mode 100644 index 00000000..d3e1390b --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascApi.cs @@ -0,0 +1,211 @@ +using CascLibSharp.Native; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp +{ + internal sealed class CascApi + { + #region Imported method signature type definitions + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascOpenStorage( + [MarshalAs(UnmanagedType.LPTStr)] string szDataPath, + uint dwFlags, + out CascStorageSafeHandle phStorage); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascGetStorageInfo( + CascStorageSafeHandle hStorage, + CascStorageInfoClass infoClass, + ref uint pvStorageInfo, + IntPtr cbStorageInfo, + ref uint pcbLengthNeeded); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascCloseStorage( IntPtr hStorage); + + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascOpenFileByIndexKey( + CascStorageSafeHandle hStorage, + ref QueryKey pIndexKey, + uint dwFlags, + out CascStorageFileSafeHandle phFile); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascOpenFileByEncodingKey( + CascStorageSafeHandle hStorage, + ref QueryKey pEncodingKey, + uint dwFlags, + out CascStorageFileSafeHandle phFile); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascOpenFile( + CascStorageSafeHandle hStorage, + [MarshalAs(UnmanagedType.LPStr)] string szFileName, + uint dwLocale, + uint dwFlags, + out CascStorageFileSafeHandle phFile); + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate uint FnCascGetFileSize( + CascStorageFileSafeHandle hFile, + out uint pdwFileSizeHigh); + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate uint FnCascSetFilePointer( + CascStorageFileSafeHandle hFile, + uint lFilePos, + ref uint plFilePosHigh, + SeekOrigin dwMoveMethod); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascReadFile( + CascStorageFileSafeHandle hFile, + IntPtr lpBuffer, + uint dwToRead, + out uint dwRead); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascCloseFile( IntPtr hFile); + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate CascFileEnumerationSafeHandle FnCascFindFirstFile( + CascStorageSafeHandle hStorage, + [MarshalAs(UnmanagedType.LPStr)] string szMask, + ref CascFindData pFindData, + [MarshalAs(UnmanagedType.LPTStr)] string szListFile); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascFindNextFile( + CascFileEnumerationSafeHandle hFind, + ref CascFindData pFindData); + [return: MarshalAs(UnmanagedType.I1)] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate bool FnCascFindClose( IntPtr hFind); + #endregion + + #region Field and method defs + public readonly FnCascOpenStorage CascOpenStorage; + public readonly FnCascGetStorageInfo CascGetStorageInfo; + public readonly FnCascCloseStorage CascCloseStorage; + + public readonly FnCascOpenFileByIndexKey CascOpenFileByIndexKey; + public readonly FnCascOpenFileByEncodingKey CascOpenFileByEncodingKey; + public readonly FnCascOpenFile CascOpenFile; + + public readonly FnCascGetFileSize CascGetFileSizeBase; + public long CascGetFileSize(CascStorageFileSafeHandle hFile) + { + uint high; + uint low = CascGetFileSizeBase(hFile, out high); + long result = (high << 32) | low; + + return result; + } + public readonly FnCascSetFilePointer CascSetFilePointerBase; + public long CascSetFilePointer(CascStorageFileSafeHandle hFile, long filePos, SeekOrigin moveMethod) + { + uint low, high; + unchecked + { + low = (uint)(filePos & 0xffffffff); + high = (uint)(((ulong)filePos & 0xffffffff00000000) >> 32); + } + + low = CascSetFilePointerBase(hFile, low, ref high, moveMethod); + + long result = (high << 32) | low; + + return result; + } + public readonly FnCascReadFile CascReadFile; + public readonly FnCascCloseFile CascCloseFile; + + public readonly FnCascFindFirstFile CascFindFirstFile; + public readonly FnCascFindNextFile CascFindNextFile; + public readonly FnCascFindClose CascFindClose; + #endregion + + internal CascApi(IntPtr hModule) + { + SetFn(ref CascOpenStorage, hModule, "CascOpenStorage"); + SetFn(ref CascGetStorageInfo, hModule, "CascGetStorageInfo"); + SetFn(ref CascCloseStorage, hModule, "CascCloseStorage"); + + SetFn(ref CascOpenFileByIndexKey, hModule, "CascOpenFileByIndexKey"); + SetFn(ref CascOpenFileByEncodingKey, hModule, "CascOpenFileByEncodingKey"); + SetFn(ref CascOpenFile, hModule, "CascOpenFile"); + SetFn(ref CascGetFileSizeBase, hModule, "CascGetFileSize"); + SetFn(ref CascSetFilePointerBase, hModule, "CascSetFilePointer"); + SetFn(ref CascReadFile, hModule, "CascReadFile"); + SetFn(ref CascCloseFile, hModule, "CascCloseFile"); + + SetFn(ref CascFindFirstFile, hModule, "CascFindFirstFile"); + SetFn(ref CascFindNextFile, hModule, "CascFindNextFile"); + SetFn(ref CascFindClose, hModule, "CascFindClose"); + } + + private static void SetFn(ref T target, IntPtr hModule, string procName) + where T : class + { + IntPtr procAddr = NativeMethods.GetProcAddress(hModule, procName); + if (procAddr == IntPtr.Zero) + throw new Win32Exception(); + target = Marshal.GetDelegateForFunctionPointer(procAddr, typeof(T)) as T; + } + + private static CascApi Load() + { + string myPath = Assembly.GetExecutingAssembly().Location; + string directory = Path.GetDirectoryName(myPath); + string arch = IntPtr.Size == 8 ? "x64" : "x86"; +#if DEBUG + string build = "dbg"; +#else + string build = "fre"; +#endif + + string mainPath = Path.Combine(directory, "CascLib", build, arch, "CascLib.dll"); + if (File.Exists(mainPath)) + return FromFile(mainPath); + + string alternatePath = Path.Combine(directory, "CascLib", arch, "CascLib.dll"); + if (File.Exists(mainPath)) + return FromFile(alternatePath); + + string localPath = Path.Combine(directory, "CascLib.dll"); + if (File.Exists(localPath)) + return FromFile(localPath); + + throw new FileNotFoundException(string.Format("Could not locate a copy of CascLib.dll to load. The following paths were tried:\n\t{0}\n\t{1}\n\t{2}\n\nEnsure that an architecture-appropriate copy of CascLib.dll is included in your project.", mainPath, alternatePath, localPath)); + } + + private static Lazy _sharedInstance = new Lazy(Load); + public static CascApi Instance + { + get + { + return _sharedInstance.Value; + } + } + + public static CascApi FromFile(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException("The CascLib.dll library could not be loaded at the specified path.", filePath); + + IntPtr hMod = NativeMethods.LoadLibraryEx(filePath, IntPtr.Zero, 0); + if (hMod == IntPtr.Zero) + throw new Win32Exception(); + + return new CascApi(hMod); + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascException.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascException.cs new file mode 100644 index 00000000..d6a3dec9 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascException.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp +{ + /// + /// Represents an exception raised as part of a CASC operation. + /// + public class CascException : Win32Exception + { + /// + /// Creates a new CascException based on the last Win32 error. + /// + public CascException() + : base(Marshal.GetLastWin32Error()) + { + + } + + /// + /// Creates a new CascException based on the specified Win32 error code. + /// + /// A Win32 error code. + public CascException(int errorCode) + : base(errorCode) + { + + } + + /// + /// Creates a new CascException based on the specified message. + /// + /// The error message. + public CascException(string message) + : base(message) + { + + } + + /// + /// Creates a new CascException based on the specified Win32 error code and custom error message. + /// + /// A Win32 error code. + /// The error message. + public CascException(int errorCode, string message) + : base(errorCode, message) + { + + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFileStream.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFileStream.cs new file mode 100644 index 00000000..2a197720 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFileStream.cs @@ -0,0 +1,168 @@ +using CascLibSharp.Native; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp +{ + /// + /// Provides a view of the data in a file contained within CASC storage. + /// + public class CascFileStream : Stream + { + private CascStorageFileSafeHandle _handle; + private CascApi _api; + + internal CascFileStream(CascStorageFileSafeHandle handle, CascApi api) + { + Debug.Assert(handle != null); + Debug.Assert(!handle.IsInvalid); + Debug.Assert(api != null); + + _api = api; + _handle = handle; + } + + private void AssertValidHandle() + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascFileStream"); + } + + /// + /// Gets whether this Stream may be read. Always returns true as long as the Stream has not been disposed. + /// + public override bool CanRead + { + get { return _handle != null && !_handle.IsInvalid; } + } + + /// + /// Gets whether this Stream may seek. Always returns true as long as the Stream has not been disposed. + /// + public override bool CanSeek + { + get { return _handle != null && !_handle.IsInvalid; } + } + + /// + /// Gets whether this Stream may write. Always returns false. + /// + public override bool CanWrite + { + get { return false; } + } + + /// + /// Flushes writes to the backing store. This method always throws because writing is not supported. + /// + /// Always thrown. + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + /// Gets the length of the Stream. + /// + /// Thrown if the Stream has been disposed. + public override long Length + { + get + { + AssertValidHandle(); + return _api.CascGetFileSize(_handle); + } + } + + /// + /// Gets or sets the current position within the Stream. + /// + /// Thrown if the Stream has been disposed. + /// Thrown if the attempt to seek fails. + public override long Position + { + get + { + AssertValidHandle(); + return _api.CascSetFilePointer(_handle, 0, SeekOrigin.Current); + } + set + { + AssertValidHandle(); + long result = _api.CascSetFilePointer(_handle, value, SeekOrigin.Begin); + if (result != value) + throw new CascException(); + } + } + + /// + /// Reads data into the buffer. + /// + /// The destination into which the data should be read. + /// The offset into the buffer to read from the current position of the Stream. + /// The number of bytes to read. + /// The number of bytes actually read. + /// Thrown if the Stream has been disposed. + /// Thrown if the CASC provider fails to read. + public override unsafe int Read(byte[] buffer, int offset, int count) + { + AssertValidHandle(); + long len = Length; + if (offset < 0 || offset > len) + throw new ArgumentException("offset"); + if (count < 0) + throw new ArgumentException("count"); + if (offset + count > len) + throw new ArgumentException("count"); + + uint read = 0; + fixed (byte* pBuffer = &buffer[0]) + { + if (!_api.CascReadFile(_handle, new IntPtr((void*)pBuffer), unchecked((uint)count), out read)) + throw new CascException(); + } + + return unchecked((int)read); + } + + /// + /// Seeks to a specific position within the Stream. + /// + /// The offset from the specified origin to seek. + /// The relative location (beginning, current, or end) from which to seek. + /// The new position in the stream. + /// Thrown if the Stream has been disposed. + public override long Seek(long offset, SeekOrigin origin) + { + AssertValidHandle(); + return _api.CascSetFilePointer(_handle, offset, origin); + } + + /// + /// Sets the length of the Stream. Always throws because writing is not supported. + /// + /// The new length of the Stream. + /// Always thrown. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes the specified data to the Stream. Always throws because writing is not supported. + /// + /// The data to write. + /// The starting position in the buffer. + /// The number of bytes. + /// Always thrown. + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFoundFile.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFoundFile.cs new file mode 100644 index 00000000..af3594e4 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFoundFile.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp +{ + /// + /// Represents a file found in a CASC container search. + /// + public class CascFoundFile + { + private WeakReference _ownerContext; + + internal CascFoundFile(string fileName, IntPtr plainName, byte[] encodingKey, CascLocales locales, long fileSize, CascStorageContext ownerContext) + { + FileName = fileName; + PlainFileName = Marshal.PtrToStringAnsi(plainName); + EncodingKey = encodingKey; + Locales = locales; + FileSize = fileSize; + + _ownerContext = new WeakReference(ownerContext); + } + + /// + /// Gets the full path to this file. + /// + public string FileName { get; private set; } + /// + /// Gets the plain (no directory-qualified) file name of this file. + /// + public string PlainFileName { get; private set; } + /// + /// Gets the CASC encoding key for this file. + /// + public byte[] EncodingKey { get; private set; } + /// + /// Gets the locales supported by this resource. + /// + public CascLocales Locales { get; private set; } + /// + /// Gets the length of the file in bytes. + /// + public long FileSize { get; private set; } + + /// + /// Opens the found file for reading. + /// + /// A CascFileStream, which acts as a Stream for a CASC stored file. + public CascFileStream Open() + { + CascStorageContext context; + if (!_ownerContext.TryGetTarget(out context)) + throw new ObjectDisposedException("The owning context has been closed."); + + return context.OpenFileByEncodingKey(EncodingKey); + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascStorageContext.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascStorageContext.cs new file mode 100644 index 00000000..7dbae534 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascStorageContext.cs @@ -0,0 +1,403 @@ +using CascLibSharp.Native; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp +{ + /// + /// Represents a CASC storage directory. + /// + public class CascStorageContext : IDisposable + { + private CascApi _api; + private CascStorageSafeHandle _handle; + private Lazy _hasListfile; + private Lazy _clientType; + private Lazy _fileCount; + private Lazy _gameBuild; + + /// + /// Creates a new CascStorageContext for the specified path. + /// + /// The path to a game's data directory. + /// An example directory is c:\Program Files (x86)\Heroes of the Storm\HeroesData. + public CascStorageContext(string dataPath) + { + _api = CascApi.Instance; + + if (!_api.CascOpenStorage(dataPath, 0, out _handle) || _handle.IsInvalid) + throw new CascException(); + _handle.Api = _api; + + _hasListfile = new Lazy(CheckHasListfile); + _clientType = new Lazy(GetClient); + _fileCount = new Lazy(GetFileCount); + _gameBuild = new Lazy(GetGameBuild); + } + + /// + /// Opens a file by its fully-qualified name. + /// + /// The name of the file. + /// The file's locale (defaults to English-United States). + /// A CascFileStream, which implements a Stream. + /// Thrown if the CascStorageContext has been disposed. + /// Thrown if the file does not exist within the CASC storage container. + public CascFileStream OpenFile(string fileName, CascLocales locale = CascLocales.EnUs) + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + CascStorageFileSafeHandle hFile; + if (!_api.CascOpenFile(_handle, fileName, (uint)locale, 0, out hFile)) + throw new CascException(); + + hFile.Api = _api; + + return new CascFileStream(hFile, _api); + } + + /// + /// Opens a file by its index key. + /// + /// + /// An index key is a binary representation of a file. I do not know what it comes from; I know that it's used to identify files inside of CASC, + /// but I don't know how someone obtains the index key. They are not produced in the public API of CascLib. + /// + /// The index key to search. + /// A CascFileStream, which implements a Stream. + /// Thrown if the CascStorageContext has been disposed. + /// Thrown if the file does not exist within the CASC storage container. + public CascFileStream OpenFileByIndexKey(byte[] indexKey) + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + using (CoTaskMem mem = CoTaskMem.FromBytes(indexKey)) + { + QueryKey qk = new QueryKey(); + qk.cbData = unchecked((uint)indexKey.Length); + qk.pbData = mem.Pointer; + + CascStorageFileSafeHandle hFile; + if (!_api.CascOpenFileByIndexKey(_handle, ref qk, 0, out hFile)) + throw new CascException(); + + hFile.Api = _api; + + return new CascFileStream(hFile, _api); + } + } + + /// + /// Opens a file by its encoding key. + /// + /// A 16-byte key representing the file. Encoding keys may be obtained via . + /// A CascFileStream, which implements a Stream. + /// Thrown if the CascStorageContext has been disposed. + /// Thrown if the file does not exist within the CASC storage container. + public CascFileStream OpenFileByEncodingKey(byte[] encodingKey) + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + using (CoTaskMem mem = CoTaskMem.FromBytes(encodingKey)) + { + QueryKey qk = new QueryKey(); + qk.cbData = unchecked((uint)encodingKey.Length); + qk.pbData = mem.Pointer; + + CascStorageFileSafeHandle hFile; + if (!_api.CascOpenFileByEncodingKey(_handle, ref qk, 0, out hFile)) + throw new CascException(); + + hFile.Api = _api; + + return new CascFileStream(hFile, _api); + } + } + + /// + /// Searches the files in the CASC container for files that match the specified pattern. + /// + /// The mask to search. * and ? are valid tokens for substitution. + /// A path to a listfile. Required if the CASC container is for World of Warcraft. + /// An enumeration of matching file references in the CASC container. + public IEnumerable SearchFiles(string mask, string listFilePath = null) + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + if (this.GameClient == CascKnownClient.WorldOfWarcraft && string.IsNullOrWhiteSpace(listFilePath)) + throw new ArgumentNullException("listFilePath"); + + CascFindData cfd = new CascFindData(); + using (var handle = _api.CascFindFirstFile(_handle, mask, ref cfd, listFilePath)) + { + if (handle.IsInvalid) + yield break; + + handle.Api = _api; + + yield return cfd.ToFoundFile(this); + + while (_api.CascFindNextFile(handle, ref cfd)) + { + yield return cfd.ToFoundFile(this); + } + } + } + + private bool CheckHasListfile() + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + uint storageInfo = 0, lengthNeeded = 4; + if (!_api.CascGetStorageInfo(_handle, CascStorageInfoClass.Features, ref storageInfo, new IntPtr(4), ref lengthNeeded)) + throw new CascException(); + + CascStorageFeatures features = (CascStorageFeatures)storageInfo; + if (features.HasFlag(CascStorageFeatures.HasListfile)) + return true; + + return false; + } + + private CascKnownClient GetClient() + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + uint storageInfo = 0, lengthNeeded = 4; + if (!_api.CascGetStorageInfo(_handle, CascStorageInfoClass.GameInfo, ref storageInfo, new IntPtr(4), ref lengthNeeded)) + throw new CascException(); + + CascGameId gameId = (CascGameId)storageInfo; + + return gameId.ToKnownClient(); + } + + private long GetFileCount() + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + uint storageInfo = 0, lengthNeeded = 4; + if (!_api.CascGetStorageInfo(_handle, CascStorageInfoClass.Features, ref storageInfo, new IntPtr(4), ref lengthNeeded)) + throw new CascException(); + + return storageInfo; + } + + private int GetGameBuild() + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("CascStorageContext"); + + uint storageInfo = 0, lengthNeeded = 4; + if (!_api.CascGetStorageInfo(_handle, CascStorageInfoClass.Features, ref storageInfo, new IntPtr(4), ref lengthNeeded)) + throw new CascException(); + + return unchecked((int)storageInfo); + } + + /// + /// Gets whether the CASC container has a listfile or if one must be supplied while searching. + /// + public bool HasListfile + { + get + { + return _hasListfile.Value; + } + } + + /// + /// Gets the number of files in the CASC container. + /// + public long FileCount + { + get + { + return _fileCount.Value; + } + } + + /// + /// Gets the build number of the game. + /// + public int GameBuild + { + get + { + return _gameBuild.Value; + } + } + + /// + /// Gets the game client of the container, if it can be determined. + /// + public CascKnownClient GameClient + { + get + { + return _clientType.Value; + } + } + + #region IDisposable implementation + /// + /// Finalizes the storage context. + /// + ~CascStorageContext() + { + Dispose(false); + } + + /// + /// Disposes the object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the object, cleaning up the unmanaged objects. + /// + /// True if this is being called via the Dispose() method; false if it's being called by the finalizer. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (!_handle.IsInvalid) + { + _handle.Close(); + _handle = null; + } + } + } + #endregion + } + + /// + /// Known locales supported by CASC. + /// + [Flags] + public enum CascLocales + { + /// + /// All available locales. + /// + All = -1, + /// + /// No locales. + /// + None = 0, + /// + /// Unknown + /// + Unknown1 = 1, + /// + /// English, United States + /// + EnUs = 2, + /// + /// Korean, South Korea + /// + KoKr = 4, + /// + /// Reserved (unknown) + /// + Reserved = 8, + /// + /// French, France + /// + FrFr = 0x10, + /// + /// German, Germany + /// + DeDe = 0x20, + /// + /// Chinese, China + /// + ZhCn = 0x40, + /// + /// Spanish, Spain + /// + EsEs = 0x80, + /// + /// Chinese, Taiwan + /// + ZhTw = 0x100, + /// + /// English, Great Britain + /// + EnGb = 0x200, + /// + /// English, China + /// + EnCn = 0x400, + /// + /// English, Taiwan + /// + EnTw = 0x800, + /// + /// Spanish, Mexico + /// + EsMx = 0x1000, + /// + /// Russian, Russia + /// + RuRu = 0x2000, + /// + /// Portuguese, Brazil + /// + PtBr = 0x4000, + /// + /// Italian, Italy + /// + ItIt = 0x8000, + /// + /// Portuguese, Portugal + /// + PtPt = 0x10000, + } + + /// + /// Known clients supporting CASC + /// + public enum CascKnownClient + { + /// + /// The game client was unrecognized. + /// + Unknown = -1, + /// + /// Heroes of the Storm + /// + HeroesOfTheStorm = 0, + /// + /// Diablo 3 + /// + Diablo3 = 1, + /// + /// World of Warcraft + /// + WorldOfWarcraft = 2, + /// + /// Overwatch + /// + Overwatch = 3, + /// + /// Starcraft 2 + /// + Starcraft2 = 4, + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascFileEnumerationSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascFileEnumerationSafeHandle.cs new file mode 100644 index 00000000..a673cf70 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascFileEnumerationSafeHandle.cs @@ -0,0 +1,32 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp.Native +{ + internal class CascFileEnumerationSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public CascFileEnumerationSafeHandle() + : base(true) { } + public CascFileEnumerationSafeHandle(IntPtr handle) + : this() + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + var api = Api ?? CascApi.Instance; + return api.CascFindClose(DangerousGetHandle()); + } + + internal CascApi Api + { + get; + set; + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageFileSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageFileSafeHandle.cs new file mode 100644 index 00000000..b6d2cc67 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageFileSafeHandle.cs @@ -0,0 +1,36 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp.Native +{ + internal class CascStorageFileSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public CascStorageFileSafeHandle() + : base(true) + { + + } + + public CascStorageFileSafeHandle(IntPtr handle) + : this() + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + var api = Api ?? CascApi.Instance; + return api.CascCloseFile(DangerousGetHandle()); + } + + internal CascApi Api + { + get; + set; + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageSafeHandle.cs new file mode 100644 index 00000000..83361901 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageSafeHandle.cs @@ -0,0 +1,36 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp.Native +{ + internal class CascStorageSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public CascStorageSafeHandle() + : base(true) + { + + } + + public CascStorageSafeHandle(IntPtr handle) + : this() + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + var api = Api ?? CascApi.Instance; + return api.CascCloseStorage(DangerousGetHandle()); + } + + internal CascApi Api + { + get; + set; + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CoTaskMem.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CoTaskMem.cs new file mode 100644 index 00000000..78ddf58c --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CoTaskMem.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp.Native +{ + internal sealed class CoTaskMem : IDisposable + { + private IntPtr _mem; + private int _size; + + public CoTaskMem(int size) + { + _mem = Marshal.AllocCoTaskMem(size); + if (_mem == IntPtr.Zero) + throw new OutOfMemoryException(); + + _size = size; + } + + public static CoTaskMem FromBytes(byte[] b) + { + CoTaskMem result = new CoTaskMem(b.Length); + Marshal.Copy(b, 0, result._mem, b.Length); + + return result; + } + + public IntPtr Pointer + { + get + { + if (_mem == IntPtr.Zero) + throw new ObjectDisposedException("CoTaskMem"); + + return _mem; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_mem != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(_mem); + _mem = IntPtr.Zero; + _size = 0; + } + } + + ~CoTaskMem() + { + Dispose(false); + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/NativeMethods.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/NativeMethods.cs new file mode 100644 index 00000000..9bdf9409 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/NativeMethods.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace CascLibSharp.Native +{ + internal static class NativeMethods + { + [DllImport("kernel32", SetLastError = true)] + public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport("kernel32", SetLastError = true)] + public static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); + } + + internal enum CascStorageInfoClass + { + FileCount, + Features, + GameInfo, + GameBuild, + } + + internal enum CascStorageFeatures + { + None = 0x0, + HasListfile = 0x1, + } + + /* + * #define CASC_GAME_HOTS 0x00010000 // Heroes of the Storm +#define CASC_GAME_WOW6 0x00020000 // World of Warcraft - Warlords of Draenor +#define CASC_GAME_DIABLO3 0x00030000 // Diablo 3 since PTR 2.2.0 +#define CASC_GAME_OVERWATCH 0x00040000 // Overwatch since PTR 24919* + */ + internal enum CascGameId + { + Hots = 0x00010000, + Wow6 = 0x00020000, + Diablo3 = 0x00030000, + Overwatch = 0x00040000, + Starcraft2 = 0x00050000, + } + + internal static class GameConverterExtensions + { + private static Dictionary GameClientMap = new Dictionary + { + { CascGameId.Hots, CascKnownClient.HeroesOfTheStorm }, + { CascGameId.Wow6, CascKnownClient.WorldOfWarcraft }, + { CascGameId.Diablo3, CascKnownClient.Diablo3 }, + { CascGameId.Overwatch, CascKnownClient.Overwatch }, + { CascGameId.Starcraft2, CascKnownClient.Starcraft2 }, + }; + + private static Dictionary ClientGameMap = new Dictionary() + { + { CascKnownClient.HeroesOfTheStorm, CascGameId.Hots }, + { CascKnownClient.WorldOfWarcraft, CascGameId.Wow6 }, + { CascKnownClient.Diablo3, CascGameId.Diablo3 }, + { CascKnownClient.Overwatch, CascGameId.Overwatch }, + { CascKnownClient.Starcraft2, CascGameId.Starcraft2 }, + }; + + public static CascKnownClient ToKnownClient(this CascGameId gameId) + { + CascKnownClient result; + if (!GameClientMap.TryGetValue(gameId, out result)) + result = CascKnownClient.Unknown; + + return result; + } + + public static CascGameId ToGameId(this CascKnownClient knownClient) + { + CascGameId result; + if (!ClientGameMap.TryGetValue(knownClient, out result)) + throw new ArgumentException("Invalid client."); + + return result; + } + } + +#pragma warning disable 649 + internal struct QueryKey + { + public IntPtr pbData; + public uint cbData; + } + + internal unsafe struct CascFindData + { + const int MAX_PATH = 260; + + public fixed byte szFileName[MAX_PATH]; + public IntPtr szPlainName; + //public ulong FileNameHash; + public fixed byte EncodingKey[16]; + //public uint dwPackageIndex; + public uint dwLocaleFlags; + public uint dwFileSize; + + public unsafe CascFoundFile ToFoundFile(CascStorageContext ownerContext) + { + string fileName = null; + fixed (void* pFileName = szFileName) + { + fileName = Marshal.PtrToStringAnsi(new IntPtr(pFileName)); + } + byte[] encodingKey = new byte[16]; + fixed (void* pEncodingKey = EncodingKey) + { + Marshal.Copy(new IntPtr(pEncodingKey), encodingKey, 0, 16); + } + + return new CascFoundFile(fileName, szPlainName, encodingKey, (CascLocales)dwLocaleFlags, dwFileSize, ownerContext); + } + } +#pragma warning restore 649 +} diff --git a/SabreTools.Serialization/_EXTERNAL/README.MD b/SabreTools.Serialization/_EXTERNAL/README.MD index 74cbca55..06c3af9a 100644 --- a/SabreTools.Serialization/_EXTERNAL/README.MD +++ b/SabreTools.Serialization/_EXTERNAL/README.MD @@ -4,4 +4,7 @@ This directory contains multiple external libraries. Here is the status of each: | Directory | Library | Status | | --------- | ------- | ------ | -| stormlibsharp | [stormlibsharp](https://github.com/robpaveza/stormlibsharp) | External submodule | +| CascLibSharp | [stormlibsharp](https://github.com/robpaveza/stormlibsharp) | Direct code duplicate | +| StormLibSharp | [stormlibsharp](https://github.com/robpaveza/stormlibsharp) | Direct code duplicate | + +**CascLibSharp** and **StormLibSharp** have multiple changes made to accomodate modern .NET versions. The original code is MIT licensed. diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchive.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchive.cs new file mode 100644 index 00000000..4585dc50 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchive.cs @@ -0,0 +1,414 @@ +using StormLibSharp.Native; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace StormLibSharp +{ + public class MpqArchive : IDisposable + { + private MpqArchiveSafeHandle _handle; + private List _openFiles = new List(); + private FileAccess _accessType; + private List _compactCallbacks = new List(); + private SFILE_COMPACT_CALLBACK _compactCallback; + + #region Constructors / Factories + public MpqArchive(string filePath, FileAccess accessType) + { + _accessType = accessType; + SFileOpenArchiveFlags flags = SFileOpenArchiveFlags.TypeIsFile; + if (accessType == FileAccess.Read) + flags |= SFileOpenArchiveFlags.AccessReadOnly; + else + flags |= SFileOpenArchiveFlags.AccessReadWriteShare; + + // constant 2 = SFILE_OPEN_HARD_DISK_FILE + if (!NativeMethods.SFileOpenArchive(filePath, 2, flags, out _handle)) + throw new Win32Exception(); // Implicitly calls GetLastError + } + + public MpqArchive(MemoryMappedFile file, FileAccess accessType) + { + _accessType = accessType; + string fileName = Win32Methods.GetFileNameOfMemoryMappedFile(file); + if (fileName == null) + throw new ArgumentException("Could not retrieve the name of the file to initialize."); + + SFileOpenArchiveFlags flags = SFileOpenArchiveFlags.TypeIsMemoryMapped; + if (accessType == FileAccess.Read) + flags |= SFileOpenArchiveFlags.AccessReadOnly; + else + flags |= SFileOpenArchiveFlags.AccessReadWriteShare; + + // constant 2 = SFILE_OPEN_HARD_DISK_FILE + if (!NativeMethods.SFileOpenArchive(fileName, 2, flags, out _handle)) + throw new Win32Exception(); // Implicitly calls GetLastError + } + + private MpqArchive(string filePath, MpqArchiveVersion version, MpqFileStreamAttributes listfileAttributes, MpqFileStreamAttributes attributesFileAttributes, int maxFileCount) + { + if (maxFileCount < 0) + throw new ArgumentException("maxFileCount"); + + SFileOpenArchiveFlags flags = SFileOpenArchiveFlags.TypeIsFile | SFileOpenArchiveFlags.AccessReadWriteShare; + flags |= (SFileOpenArchiveFlags)version; + + //SFILE_CREATE_MPQ create = new SFILE_CREATE_MPQ() + //{ + // cbSize = unchecked((uint)Marshal.SizeOf(typeof(SFILE_CREATE_MPQ))), + // dwMaxFileCount = unchecked((uint)maxFileCount), + // dwMpqVersion = (uint)version, + // dwFileFlags1 = (uint)listfileAttributes, + // dwFileFlags2 = (uint)attributesFileAttributes, + // dwStreamFlags = (uint)flags, + //}; + + //if (!NativeMethods.SFileCreateArchive2(filePath, ref create, out _handle)) + // throw new Win32Exception(); + if (!NativeMethods.SFileCreateArchive(filePath, (uint)flags, int.MaxValue, out _handle)) + throw new Win32Exception(); + } + + public static MpqArchive CreateNew(string mpqPath, MpqArchiveVersion version) + { + return CreateNew(mpqPath, version, MpqFileStreamAttributes.None, MpqFileStreamAttributes.None, int.MaxValue); + } + + public static MpqArchive CreateNew(string mpqPath, MpqArchiveVersion version, MpqFileStreamAttributes listfileAttributes, + MpqFileStreamAttributes attributesFileAttributes, int maxFileCount) + { + return new MpqArchive(mpqPath, version, listfileAttributes, attributesFileAttributes, maxFileCount); + } + #endregion + + #region Properties + // TODO: Move to common location. + // This is a global setting, not per-archive setting. + + //public int Locale + //{ + // get + // { + // throw new NotImplementedException(); + // } + // set + // { + // throw new NotImplementedException(); + // } + //} + + public long MaxFileCount + { + get + { + VerifyHandle(); + return NativeMethods.SFileGetMaxFileCount(_handle); + } + set + { + if (value < 0 || value > uint.MaxValue) + throw new ArgumentException("value"); + VerifyHandle(); + + if (!NativeMethods.SFileSetMaxFileCount(_handle, unchecked((uint)value))) + throw new Win32Exception(); + } + } + + private void VerifyHandle() + { + if (_handle == null || _handle.IsInvalid) + throw new ObjectDisposedException("MpqArchive"); + } + + public bool IsPatchedArchive + { + get + { + VerifyHandle(); + return NativeMethods.SFileIsPatchedArchive(_handle); + } + } + #endregion + + public void Flush() + { + VerifyHandle(); + if (!NativeMethods.SFileFlushArchive(_handle)) + throw new Win32Exception(); + } + + public int AddListFile(string listfileContents) + { + VerifyHandle(); + return NativeMethods.SFileAddListFile(_handle, listfileContents); + } + + public void AddFileFromDisk(string filePath, string archiveName) + { + VerifyHandle(); + + if (!NativeMethods.SFileAddFile(_handle, filePath, archiveName, 0)) + throw new Win32Exception(); + } + + public void Compact(string listfile) + { + VerifyHandle(); + if (!NativeMethods.SFileCompactArchive(_handle, listfile, false)) + throw new Win32Exception(); + } + + private void _OnCompact(IntPtr pvUserData, uint dwWorkType, ulong bytesProcessed, ulong totalBytes) + { + MpqArchiveCompactingEventArgs args = new MpqArchiveCompactingEventArgs(dwWorkType, bytesProcessed, totalBytes); + OnCompacting(args); + } + + protected virtual void OnCompacting(MpqArchiveCompactingEventArgs e) + { + foreach (var cb in _compactCallbacks) + { + cb(this, e); + } + } + + public event MpqArchiveCompactingEventHandler Compacting + { + add + { + VerifyHandle(); + _compactCallback = _OnCompact; + if (!NativeMethods.SFileSetCompactCallback(_handle, _compactCallback, IntPtr.Zero)) + throw new Win32Exception(); + + _compactCallbacks.Add(value); + } + remove + { + _compactCallbacks.Remove(value); + + VerifyHandle(); + if (_compactCallbacks.Count == 0) + { + if (!NativeMethods.SFileSetCompactCallback(_handle, null, IntPtr.Zero)) + { + // Don't do anything here. Remove shouldn't fail hard. + } + } + } + } + + // TODO: Determine if SFileGetAttributes/SFileSetAttributes/SFileUpdateFileAttributes deserves a projection. + // It's unclear - these seem to affect the (attributes) file but I can't figure out exactly what that means. + + public void AddPatchArchive(string patchPath) + { + VerifyHandle(); + + if (!NativeMethods.SFileOpenPatchArchive(_handle, patchPath, null, 0)) + throw new Win32Exception(); + } + + public void AddPatchArchives(IEnumerable patchPaths) + { + if (patchPaths == null) + throw new ArgumentNullException("patchPaths"); + + VerifyHandle(); + + foreach (string path in patchPaths) + { + // Don't sublet to AddPatchArchive to avoid having to repeatedly call VerifyHandle() + if (!NativeMethods.SFileOpenPatchArchive(_handle, path, null, 0)) + throw new Win32Exception(); + } + } + + public bool HasFile(string fileToFind) + { + VerifyHandle(); + + return NativeMethods.SFileHasFile(_handle, fileToFind); + } + + public MpqFileStream OpenFile(string fileName) + { + VerifyHandle(); + + MpqFileSafeHandle fileHandle; + if (!NativeMethods.SFileOpenFileEx(_handle, fileName, 0, out fileHandle)) + throw new Win32Exception(); + + MpqFileStream fs = new MpqFileStream(fileHandle, _accessType, this); + _openFiles.Add(fs); + return fs; + } + + public void ExtractFile(string fileToExtract, string destinationPath) + { + VerifyHandle(); + + if (!NativeMethods.SFileExtractFile(_handle, fileToExtract, destinationPath, 0)) + throw new Win32Exception(); + } + + public MpqFileVerificationResults VerifyFile(string fileToVerify) + { + VerifyHandle(); + + return (MpqFileVerificationResults)NativeMethods.SFileVerifyFile(_handle, fileToVerify, 0); + } + + // TODO: Consider SFileVerifyRawData + + public MpqArchiveVerificationResult VerifyArchive() + { + VerifyHandle(); + + return (MpqArchiveVerificationResult)NativeMethods.SFileVerifyArchive(_handle); + } + + + #region IDisposable implementation + public void Dispose() + { + Dispose(true); + } + + ~MpqArchive() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Release owned files first. + if (_openFiles != null) + { + foreach (var file in _openFiles) + { + file.Dispose(); + } + + _openFiles.Clear(); + _openFiles = null; + } + + // Release + if (_handle != null && !_handle.IsInvalid) + { + _handle.Close(); + _handle = null; + } + } + } + + internal void RemoveOwnedFile(MpqFileStream file) + { + _openFiles.Remove(file); + } + + #endregion + } + + public enum MpqArchiveVersion + { + Version1 = 0, + Version2 = 0x01000000, + Version3 = 0x02000000, + Version4 = 0x03000000, + } + + [Flags] + public enum MpqFileStreamAttributes + { + None = 0x0, + } + + [Flags] + public enum MpqFileVerificationResults + { + /// + /// There were no errors with the file. + /// + Verified = 0, + /// + /// Failed to open the file + /// + Error = 0x1, + /// + /// Failed to read all data from the file + /// + ReadError = 0x2, + /// + /// File has sector CRC + /// + HasSectorCrc = 0x4, + /// + /// Sector CRC check failed + /// + SectorCrcError = 0x8, + /// + /// File has CRC32 + /// + HasChecksum = 0x10, + /// + /// CRC32 check failed + /// + ChecksumError = 0x20, + /// + /// File has data MD5 + /// + HasMd5 = 0x40, + /// + /// MD5 check failed + /// + Md5Error = 0x80, + /// + /// File has raw data MD5 + /// + HasRawMd5 = 0x100, + /// + /// Raw MD5 check failed + /// + RawMd5Error = 0x200, + } + + public enum MpqArchiveVerificationResult + { + /// + /// There is no signature in the MPQ + /// + NoSignature = 0, + /// + /// There was an error during verifying signature (like no memory) + /// + VerificationFailed = 1, + /// + /// There is a weak signature and sign check passed + /// + WeakSignatureVerified = 2, + /// + /// There is a weak signature but sign check failed + /// + WeakSignatureFailed = 3, + /// + /// There is a strong signature and sign check passed + /// + StrongSignatureVerified = 4, + /// + /// There is a strong signature but sign check failed + /// + StrongSignatureFailed = 5, + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchiveCompactingEventArgs.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchiveCompactingEventArgs.cs new file mode 100644 index 00000000..c2a2ee7b --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchiveCompactingEventArgs.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace StormLibSharp +{ + public delegate void MpqArchiveCompactingEventHandler(MpqArchive sender, MpqArchiveCompactingEventArgs e); + + public class MpqArchiveCompactingEventArgs : EventArgs + { + internal MpqArchiveCompactingEventArgs(uint dwWorkType, ulong processed, ulong total) + { + unchecked + { + WorkType = (MpqCompactingWorkType)dwWorkType; + BytesProcessed = (long)processed; + TotalBytes = (long)total; + } + } + + public MpqCompactingWorkType WorkType + { + get; + private set; + } + + public long BytesProcessed + { + get; + private set; + } + + public long TotalBytes + { + get; + private set; + } + } + + public enum MpqCompactingWorkType + { + CheckingFiles = 1, + CheckingHashTable = 2, + CopyingNonMpqData = 3, + CompactingFiles = 4, + ClosingArchive = 5, + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqFileStream.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqFileStream.cs new file mode 100644 index 00000000..18f6de25 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqFileStream.cs @@ -0,0 +1,185 @@ +using StormLibSharp.Native; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +namespace StormLibSharp +{ + public class MpqFileStream : Stream + { + private MpqFileSafeHandle _handle; + private FileAccess _accessType; + private MpqArchive _owner; + + internal MpqFileStream(MpqFileSafeHandle handle, FileAccess accessType, MpqArchive owner) + { + _handle = handle; + _accessType = accessType; + _owner = owner; + } + + private void VerifyHandle() + { + if (_handle == null || _handle.IsInvalid || _handle.IsClosed) + throw new ObjectDisposedException("MpqFileStream"); + } + + public override bool CanRead + { + get { VerifyHandle(); return true; } + } + + public override bool CanSeek + { + get { VerifyHandle(); return true; } + } + + public override bool CanWrite + { + get { VerifyHandle(); return _accessType != FileAccess.Read; } + } + + public override void Flush() + { + VerifyHandle(); + + _owner.Flush(); + } + + public override long Length + { + get + { + VerifyHandle(); + + uint high = 0; + uint low = NativeMethods.SFileGetFileSize(_handle, ref high); + + ulong val = (high << 32) | low; + return unchecked((long)val); + } + } + + public override long Position + { + get + { + VerifyHandle(); + + return NativeMethods.SFileGetFilePointer(_handle); + } + set + { + Seek(value, SeekOrigin.Begin); + } + } + + public override unsafe int Read(byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + if (offset > buffer.Length || (offset + count) > buffer.Length) + throw new ArgumentException(); + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + VerifyHandle(); + + bool success; + uint read; + fixed (byte* pb = &buffer[offset]) + { + NativeOverlapped overlapped = default(NativeOverlapped); + success = NativeMethods.SFileReadFile(_handle, new IntPtr(pb), unchecked((uint)count), out read, ref overlapped); + } + + if (!success) + { + int lastError = Win32Methods.GetLastError(); + if (lastError != 38) // EOF + throw new Win32Exception(lastError); + } + + return unchecked((int)read); + } + + public override long Seek(long offset, SeekOrigin origin) + { + VerifyHandle(); + + uint low, high; + low = unchecked((uint)(offset & 0xffffffffu)); + high = unchecked((uint)(offset >> 32)); + return NativeMethods.SFileSetFilePointer(_handle, low, ref high, (uint)origin); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override unsafe void Write(byte[] buffer, int offset, int count) + { + VerifyHandle(); + + if (buffer == null) + throw new ArgumentNullException("buffer"); + if (offset > buffer.Length || (offset + count) > buffer.Length) + throw new ArgumentException(); + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + VerifyHandle(); + + bool success; + fixed (byte* pb = &buffer[offset]) + { + success = NativeMethods.SFileWriteFile(_handle, new IntPtr(pb), unchecked((uint)count), 0u); + } + + if (!success) + throw new Win32Exception(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (_handle != null && !_handle.IsInvalid) + { + _handle.Close(); + _handle = null; + } + + if (_owner != null) + { + _owner.RemoveOwnedFile(this); + _owner = null; + } + } + } + + // TODO: Seems like the right place for SFileGetFileInfo, but will need to determine + // what value add these features have except for sophisticated debugging purposes + // (like in Ladis' MPQ Editor app). + + public int ChecksumCrc32 + { + get + { + throw new NotImplementedException(); + } + } + + public byte[] GetMd5Hash() + { + throw new NotImplementedException(); + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Callbacks.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Callbacks.cs new file mode 100644 index 00000000..58e7fa5e --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Callbacks.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace StormLibSharp.Native +{ + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate void SFILE_DOWNLOAD_CALLBACK(IntPtr pvUserData, ulong byteOffset, uint dwTotalBytes); + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate void SFILE_COMPACT_CALLBACK(IntPtr pvUserData, uint dwWorkType, ulong bytesProcessed, ulong totalBytes); + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate void SFILE_ADDFILE_CALLBACK(IntPtr pvUserData, uint dwBytesWritte, uint dwTotalBytes, bool bFinalCall); +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqArchiveSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqArchiveSafeHandle.cs new file mode 100644 index 00000000..5e2df9c4 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqArchiveSafeHandle.cs @@ -0,0 +1,26 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace StormLibSharp.Native +{ + internal sealed class MpqArchiveSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public MpqArchiveSafeHandle(IntPtr handle) + : base(true) + { + this.SetHandle(handle); + } + + public MpqArchiveSafeHandle() + : base(true) { } + + protected override bool ReleaseHandle() + { + return NativeMethods.SFileCloseArchive(this.handle); + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqFileSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqFileSafeHandle.cs new file mode 100644 index 00000000..495dd129 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqFileSafeHandle.cs @@ -0,0 +1,27 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace StormLibSharp.Native +{ + internal sealed class MpqFileSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public MpqFileSafeHandle(IntPtr handle) + : base(true) + { + this.SetHandle(handle); + } + + public MpqFileSafeHandle() + : base(true) + { + } + + protected override bool ReleaseHandle() + { + return NativeMethods.SFileCloseFile(this.handle); + } + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/NativeMethods.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/NativeMethods.cs new file mode 100644 index 00000000..acef5fbc --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/NativeMethods.cs @@ -0,0 +1,497 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace StormLibSharp.Native +{ + internal static class NativeMethods + { + private const string STORMLIB = "stormlib.dll"; + + #region Functions for manipulation with StormLib global flags + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileGetLocale(); + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileSetLocale(uint lcNewLocale); + #endregion + + #region Functions for archive manipulation + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileOpenArchive( + [MarshalAs(UnmanagedType.LPTStr)] string szMpqName, + uint dwPriority, + SFileOpenArchiveFlags dwFlags, + out MpqArchiveSafeHandle phMpq + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCreateArchive( + [MarshalAs(UnmanagedType.LPTStr)] string szMpqName, + uint dwCreateFlags, + uint dwMaxFileCount, + out MpqArchiveSafeHandle phMpq + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCreateArchive2( + [MarshalAs(UnmanagedType.LPTStr)] string szMpqName, + ref SFILE_CREATE_MPQ pCreateInfo, + out MpqArchiveSafeHandle phMpq + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileSetDownloadCallback( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.FunctionPtr)] SFILE_DOWNLOAD_CALLBACK pfnCallback, + IntPtr pvUserData + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileFlushArchive(MpqArchiveSafeHandle hMpq); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCloseArchive(IntPtr hMpq); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCloseArchive(MpqArchiveSafeHandle hMpq); + #endregion + + #region Adds another listfile into MPQ. + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SFileAddListFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szListFile + ); + #endregion + + #region Archive compacting + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileSetCompactCallback( + MpqArchiveSafeHandle hMpq, + SFILE_COMPACT_CALLBACK compactCB, + IntPtr pvUserData + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCompactArchive( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szListFile, + bool bReserved + ); + #endregion + + #region Maximum file count + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileGetMaxFileCount(MpqArchiveSafeHandle hMpq); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileSetMaxFileCount(MpqArchiveSafeHandle hMpq, uint dwMaxFileCount); + #endregion + + #region Changing (attributes) file + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileGetAttributes(MpqArchiveSafeHandle hMpq); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileSetAttributes(MpqArchiveSafeHandle hMpq, uint dwFlags); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileUpdateFileAttributes( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szFileName + ); + #endregion + + #region Functions for manipulation with patch archives + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileOpenPatchArchive( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPTStr)] string szPatchMpqName, + [MarshalAs(UnmanagedType.LPStr)] string szPatchPathPrefix, + uint dwFlags + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileIsPatchedArchive(MpqArchiveSafeHandle hMpq); + #endregion + + #region Functions for file manipulation + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileHasFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szFileName + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileOpenFileEx( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szFileName, + uint dwSearchScope, + out MpqFileSafeHandle phFile + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileGetFileSize(MpqFileSafeHandle hFile, ref uint pdwFileSizeHigh); + + public static unsafe uint SFileGetFilePointer( + MpqFileSafeHandle hFile + ) + { + if (hFile.IsInvalid || hFile.IsClosed) + throw new InvalidOperationException(); + + IntPtr handle = hFile.DangerousGetHandle(); + _TMPQFileHeader* header = (_TMPQFileHeader*)handle.ToPointer(); + return header->dwFilePos; + } + + //public static unsafe uint SFileGetFileSize( + // MpqFileSafeHandle hFile + // ) + //{ + // if (hFile.IsInvalid || hFile.IsClosed) + // throw new InvalidOperationException(); + + // IntPtr handle = hFile.DangerousGetHandle(); + // _TMPQFileHeader* header = (_TMPQFileHeader*)handle.ToPointer(); + // return header->pFileEntry->dwFileSize; + //} + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileSetFilePointer( + MpqFileSafeHandle hFile, + uint lFilePos, + ref uint plFilePosHigh, + uint dwMoveMethod + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileReadFile( + MpqFileSafeHandle hFile, + IntPtr lpBuffer, + uint dwToRead, + out uint pdwRead, + ref System.Threading.NativeOverlapped lpOverlapped + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCloseFile(IntPtr hFile); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCloseFile(MpqFileSafeHandle hFile); + + #region Retrieving info about a file in the archive + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileGetFileInfo( + IntPtr hMpqOrFile, + SFileInfoClass InfoClass, + IntPtr pvFileInfo, + uint cbFileInfoSize, + out uint pcbLengthNeeded + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileGetFileInfo( + MpqArchiveSafeHandle hMpqOrFile, + SFileInfoClass InfoClass, + IntPtr pvFileInfo, + uint cbFileInfoSize, + out uint pcbLengthNeeded + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileGetFileInfo( + MpqFileSafeHandle hMpqOrFile, + SFileInfoClass InfoClass, + IntPtr pvFileInfo, + uint cbFileInfoSize, + out uint pcbLengthNeeded + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileGetFileName( + MpqFileSafeHandle hFile, + [MarshalAs(UnmanagedType.LPStr)] out string szFileName + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileFreeFileInfo( + IntPtr pvFileInfo, + SFileInfoClass infoClass + ); + #endregion + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileExtractFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szToExtract, + [MarshalAs(UnmanagedType.LPTStr)] string szExtracted, + uint dwSearchScope + ); + + #endregion + + #region Functions for file and archive verification + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileGetFileChecksums( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szFileName, + out uint pdwCrc32, + IntPtr pMD5 + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileVerifyFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szFileName, + uint dwFlags + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SFileVerifyRawData( + MpqArchiveSafeHandle hMpq, + uint dwWhatToVerify, + [MarshalAs(UnmanagedType.LPStr)] string szFileName + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern uint SFileVerifyArchive(MpqArchiveSafeHandle hMpq); + #endregion + + #region Functions for file searching + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern IntPtr SFileFindFirstFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szMask, + out _SFILE_FIND_DATA lpFindFileData, + [MarshalAs(UnmanagedType.LPStr)] string szListFile + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileFindNextFile( + IntPtr hFind, + [In, Out] ref _SFILE_FIND_DATA lpFindFileData + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileFindClose(IntPtr hFind); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern IntPtr SListFileFindFirstFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szListFile, + [MarshalAs(UnmanagedType.LPStr)] string szMask, + [In, Out] ref _SFILE_FIND_DATA lpFindFileData + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SListFileFindNextFile( + IntPtr hFind, + [In, Out] ref _SFILE_FIND_DATA lpFindFileData + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SListFileFindClose(IntPtr hFind); + #endregion + + #region Locale support + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SFileEnumLocales( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szFileName, + IntPtr plcLocales, + ref uint pdwMaxLocales, + uint dwSearchScope + ); + #endregion + + #region Support for adding files to the MPQ + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileCreateFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szArchiveName, + ulong fileTime, + uint dwFileSize, + uint lcLocale, + uint dwFlags, + out IntPtr phFile + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileWriteFile( + MpqFileSafeHandle hFile, + IntPtr pvData, + uint dwSize, + uint dwCompression + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileFinishFile(MpqFileSafeHandle hFile); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileAddFileEx( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPTStr)] string szFileName, + [MarshalAs(UnmanagedType.LPStr)] string szArchivedName, + uint dwFlags, + uint dwCompression, + uint dwCompressionNext + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileAddFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPTStr)] string szFileName, + [MarshalAs(UnmanagedType.LPStr)] string szArchivedName, + uint dwFlags + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileAddWave( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPTStr)] string szFileName, + [MarshalAs(UnmanagedType.LPStr)] string szArchivedName, + uint dwFlags, + uint dwQuality + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileRemoveFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szFileName, + uint dwSearchScope + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileRenameFile( + MpqArchiveSafeHandle hMpq, + [MarshalAs(UnmanagedType.LPStr)] string szOldFileName, + [MarshalAs(UnmanagedType.LPStr)] string szNewFileName + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileSetFileLocale( + MpqFileSafeHandle hFile, + uint lcNewLocale + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileSetDataCompression(uint DataCompression); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern bool SFileSetAddFileCallback( + MpqArchiveSafeHandle hMpq, + SFILE_ADDFILE_CALLBACK AddFileCB, + IntPtr pvUserData + ); + #endregion + + #region Compression and decompression + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SCompImplode( + IntPtr pvOutBuffer, + ref int pcbOutBuffer, + IntPtr pvInBuffer, + int cbInBuffer + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SCompExplode( + IntPtr pvOutBuffer, + ref int pcbOutBuffer, + IntPtr pvInBuffer, + int cbInBuffer + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SCompCompress( + IntPtr pvOutBuffer, + ref int pcbOutBuffer, + IntPtr pvInBuffer, + int cbInBuffer, + uint uCompressionMask, + int nCmpType, + int nCmpLevel + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SCompDecompress( + IntPtr pvOutBuffer, + ref int pcbOutBuffer, + IntPtr pvInBuffer, + int cbInBuffer + ); + + [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)] + public static extern int SCompDecompress2( + IntPtr pvOutBuffer, + ref int pcbOutBuffer, + IntPtr pvInBuffer, + int cbInBuffer + ); + + + #endregion + } + +#pragma warning disable 0169,0649 + internal struct SFILE_CREATE_MPQ + { + public uint cbSize; + public uint dwMpqVersion; + private IntPtr pvUserData; + private uint cbUserData; + public uint dwStreamFlags; + public uint dwFileFlags1; + public uint dwFileFlags2; + public uint dwAttrFlags; + public uint dwSectorSize; + public uint dwRawChunkSize; + public uint dwMaxFileCount; + } + + internal unsafe struct _SFILE_FIND_DATA + { + public fixed char cFileName[260]; // Full name of the found file + + public IntPtr szPlainName; // Plain name of the found file + public uint dwHashIndex; // Hash table index for the file + public uint dwBlockIndex; // Block table index for the file + public uint dwFileSize; // File size in bytes + public uint dwFileFlags; // MPQ file flags + public uint dwCompSize; // Compressed file size + public uint dwFileTimeLo; // Low 32-bits of the file time (0 if not present) + public uint dwFileTimeHi; // High 32-bits of the file time (0 if not present) + public uint lcLocale; // Locale version + } + + internal unsafe struct _TFileEntry + { + public ulong FileNameHash; + public ulong ByteOffset; + public ulong FileTime; + public uint dwHashIndex; + public uint dwFileSize; + public uint dwCmpSize; + public uint dwFlags; + public ushort lcLocale; + public ushort wPlatform; + public uint dwCrc32; + public fixed byte md5[16]; + public IntPtr szFileName; + } + + // Provides enough of _TMPQFile to get to the file size and current position. + internal unsafe struct _TMPQFileHeader + { + public IntPtr pStream; + public IntPtr ha; + public _TFileEntry* pFileEntry; + public uint dwFileKey; + public uint dwFilePos; + } +#pragma warning restore 0169,0649 + +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileInfoClass.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileInfoClass.cs new file mode 100644 index 00000000..f8ef7a92 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileInfoClass.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace StormLibSharp.Native +{ + internal enum SFileInfoClass + { + // Info classes for archives + SFileMpqFileName, // Name of the archive file (TCHAR []) + SFileMpqStreamBitmap, // Array of bits, each bit means availability of one block (BYTE []) + SFileMpqUserDataOffset, // Offset of the user data header (ULONGLONG) + SFileMpqUserDataHeader, // Raw (unfixed) user data header (TMPQUserData) + SFileMpqUserData, // MPQ USer data, without the header (BYTE []) + SFileMpqHeaderOffset, // Offset of the MPQ header (ULONGLONG) + SFileMpqHeaderSize, // Fixed size of the MPQ header + SFileMpqHeader, // Raw (unfixed) archive header (TMPQHeader) + SFileMpqHetTableOffset, // Offset of the HET table, relative to MPQ header (ULONGLONG) + SFileMpqHetTableSize, // Compressed size of the HET table (ULONGLONG) + SFileMpqHetHeader, // HET table header (TMPQHetHeader) + SFileMpqHetTable, // HET table as pointer. Must be freed using SFileFreeFileInfo + SFileMpqBetTableOffset, // Offset of the BET table, relative to MPQ header (ULONGLONG) + SFileMpqBetTableSize, // Compressed size of the BET table (ULONGLONG) + SFileMpqBetHeader, // BET table header, followed by the flags (TMPQBetHeader + DWORD[]) + SFileMpqBetTable, // BET table as pointer. Must be freed using SFileFreeFileInfo + SFileMpqHashTableOffset, // Hash table offset, relative to MPQ header (ULONGLONG) + SFileMpqHashTableSize64, // Compressed size of the hash table (ULONGLONG) + SFileMpqHashTableSize, // Size of the hash table, in entries (DWORD) + SFileMpqHashTable, // Raw (unfixed) hash table (TMPQBlock []) + SFileMpqBlockTableOffset, // Block table offset, relative to MPQ header (ULONGLONG) + SFileMpqBlockTableSize64, // Compressed size of the block table (ULONGLONG) + SFileMpqBlockTableSize, // Size of the block table, in entries (DWORD) + SFileMpqBlockTable, // Raw (unfixed) block table (TMPQBlock []) + SFileMpqHiBlockTableOffset, // Hi-block table offset, relative to MPQ header (ULONGLONG) + SFileMpqHiBlockTableSize64, // Compressed size of the hi-block table (ULONGLONG) + SFileMpqHiBlockTable, // The hi-block table (USHORT []) + SFileMpqSignatures, // Signatures present in the MPQ (DWORD) + SFileMpqStrongSignatureOffset, // Byte offset of the strong signature, relative to begin of the file (ULONGLONG) + SFileMpqStrongSignatureSize, // Size of the strong signature (DWORD) + SFileMpqStrongSignature, // The strong signature (BYTE []) + SFileMpqArchiveSize64, // Archive size from the header (ULONGLONG) + SFileMpqArchiveSize, // Archive size from the header (DWORD) + SFileMpqMaxFileCount, // Max number of files in the archive (DWORD) + SFileMpqFileTableSize, // Number of entries in the file table (DWORD) + SFileMpqSectorSize, // Sector size (DWORD) + SFileMpqNumberOfFiles, // Number of files (DWORD) + SFileMpqRawChunkSize, // Size of the raw data chunk for MD5 + SFileMpqStreamFlags, // Stream flags (DWORD) + SFileMpqIsReadOnly, // Nonzero if the MPQ is read only (DWORD) + + // Info classes for files + SFileInfoPatchChain, // Chain of patches where the file is (TCHAR []) + SFileInfoFileEntry, // The file entry for the file (TFileEntry) + SFileInfoHashEntry, // Hash table entry for the file (TMPQHash) + SFileInfoHashIndex, // Index of the hash table entry (DWORD) + SFileInfoNameHash1, // The first name hash in the hash table (DWORD) + SFileInfoNameHash2, // The second name hash in the hash table (DWORD) + SFileInfoNameHash3, // 64-bit file name hash for the HET/BET tables (ULONGLONG) + SFileInfoLocale, // File locale (DWORD) + SFileInfoFileIndex, // Block index (DWORD) + SFileInfoByteOffset, // File position in the archive (ULONGLONG) + SFileInfoFileTime, // File time (ULONGLONG) + SFileInfoFileSize, // Size of the file (DWORD) + SFileInfoCompressedSize, // Compressed file size (DWORD) + SFileInfoFlags, // File flags from (DWORD) + SFileInfoEncryptionKey, // File encryption key + SFileInfoEncryptionKeyRaw, // Unfixed value of the file key + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileOpenArchiveFlags.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileOpenArchiveFlags.cs new file mode 100644 index 00000000..d444f2be --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileOpenArchiveFlags.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace StormLibSharp.Native +{ + [Flags] + internal enum SFileOpenArchiveFlags : uint + { + None = 0, + TypeIsFile = None, + TypeIsMemoryMapped = 1, + TypeIsHttp = 2, + + AccessReadOnly = 0x100, + AccessReadWriteShare = 0x200, + AccessUseBitmap = 0x400, + + DontOpenListfile = 0x10000, + DontOpenAttributes = 0x20000, + DontSearchHeader = 0x40000, + ForceVersion1 = 0x80000, + CheckSectorCRC = 0x100000, + } +} diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Win32Methods.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Win32Methods.cs new file mode 100644 index 00000000..15596c07 --- /dev/null +++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Win32Methods.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO.MemoryMappedFiles; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace StormLibSharp.Native +{ + internal static class Win32Methods + { + [DllImport("kernel32", ExactSpelling = false, SetLastError = true)] + public static extern uint GetMappedFileName( + IntPtr hProcess, + IntPtr fileHandle, + IntPtr lpFilename, + uint nSize + ); + + [DllImport("kernel32", ExactSpelling = false, SetLastError = true)] + public static extern uint GetFinalPathNameByHandle( + IntPtr hFile, + IntPtr lpszFilePath, + uint cchFilePath, + uint dwFlags + ); + + [DllImport("kernel32", SetLastError = false, ExactSpelling = false)] + public static extern int GetLastError(); + + public static string GetFileNameOfMemoryMappedFile(MemoryMappedFile file) + { + const uint size = 522; + IntPtr path = Marshal.AllocCoTaskMem(unchecked((int)size)); // MAX_PATH + 1 char + + string result = null; + try + { + // constant 0x2 = VOLUME_NAME_NT + uint test = GetFinalPathNameByHandle(file.SafeMemoryMappedFileHandle.DangerousGetHandle(), path, size, 0x2); + if (test != 0) + throw new Win32Exception(); + + result = Marshal.PtrToStringAuto(path); + } + catch + { + uint test = GetMappedFileName(Process.GetCurrentProcess().Handle, file.SafeMemoryMappedFileHandle.DangerousGetHandle(), path, size); + if (test != 0) + throw new Win32Exception(); + + result = Marshal.PtrToStringAuto(path); + } + + return result; + } + } +}