Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PPather: MpqFileStream over SFileExtractFile #535

Merged
merged 2 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions PPather/StormDll/Archive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.IO;

namespace StormDll;

internal sealed class Archive
{
public const uint SFILE_INVALID_SIZE = 0xFFFFFFFF;

private readonly IntPtr handle;

private readonly HashSet<string> fileList = new(StringComparer.InvariantCultureIgnoreCase);

private static readonly bool Is64Bit = Environment.Is64BitProcess;

public Archive(string file, out bool open, uint prio, OpenArchive flags)
{
open = Is64Bit
? StormDllx64.SFileOpenArchive(file, prio, flags, out handle)
: StormDllx86.SFileOpenArchive(file, prio, flags, out handle);

if (!open)
return;

using MpqFileStream mpq = GetStream("(listfile)");
using MemoryStream ms = new(mpq.ReadAllBytes());
using StreamReader stream = new(ms);

while (!stream.EndOfStream)
{
fileList.Add(stream.ReadLine()!);
}

if (fileList.Count == 0)
throw new InvalidOperationException($"{nameof(fileList)} contains no elements!");
}

public bool IsOpen()
{
return handle != IntPtr.Zero;
}

public bool HasFile(string name)
{
return fileList.Contains(name);
}

public bool SFileCloseArchive()
{
fileList.Clear();
return Is64Bit
? StormDllx64.SFileCloseArchive(handle)
: StormDllx86.SFileCloseArchive(handle);
}

public MpqFileStream GetStream(string fileName)
{
return !SFileOpenFileEx(handle, fileName, OpenFile.SFILE_OPEN_FROM_MPQ, out IntPtr fileHandle)
? throw new IOException("SFileOpenFileEx failed")
: new MpqFileStream(fileHandle);
}

public static bool SFileReadFile(IntPtr fileHandle, Span<byte> buffer, long toRead, out long read)
{
return Is64Bit
? StormDllx64.SFileReadFile(fileHandle, buffer, toRead, out read)
: StormDllx86.SFileReadFile(fileHandle, buffer, toRead, out read);
}

public static bool SFileCloseFile(IntPtr fileHandle)
{
return Is64Bit
? StormDllx64.SFileCloseFile(fileHandle)
: StormDllx86.SFileCloseFile(fileHandle);
}

public static long SFileGetFileSize(IntPtr fileHandle, out long fileSizeHigh)
{
return Is64Bit
? StormDllx64.SFileGetFileSize(fileHandle, out fileSizeHigh)
: StormDllx86.SFileGetFileSize(fileHandle, out fileSizeHigh);
}

public static uint SFileSetFilePointer(IntPtr fileHandle,
long filePos,
ref uint plFilePosHigh,
SeekOrigin origin)
{
return Is64Bit
? StormDllx64.SFileSetFilePointer(fileHandle, filePos, ref plFilePosHigh, origin)
: StormDllx86.SFileSetFilePointer(fileHandle, filePos, ref plFilePosHigh, origin);
}

public static bool SFileOpenFileEx(
IntPtr archiveHandle,
string fileName,
OpenFile searchScope,
out IntPtr fileHandle)
{
return Is64Bit
? StormDllx64.SFileOpenFileEx(archiveHandle, fileName, searchScope, out fileHandle)
: StormDllx86.SFileOpenFileEx(archiveHandle, fileName, searchScope, out fileHandle);
}
}
51 changes: 51 additions & 0 deletions PPather/StormDll/ArchiveSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.IO;
using Microsoft.Extensions.Logging;

namespace StormDll;

public sealed class ArchiveSet
{
private readonly Archive[] archives;

public ArchiveSet(ILogger logger, string[] files)
{
archives = new Archive[files.Length];

for (int i = 0; i < files.Length; i++)
{
Archive a = new(files[i], out bool open, 0,
OpenArchive.MPQ_OPEN_NO_LISTFILE |
OpenArchive.MPQ_OPEN_NO_ATTRIBUTES |
OpenArchive.MPQ_OPEN_NO_HEADER_SEARCH |
OpenArchive.MPQ_OPEN_READ_ONLY);

if (open && a.IsOpen())
{
archives[i] = a;

if (logger.IsEnabled(LogLevel.Trace))
logger.LogTrace($"Archive[{i}] open {files[i]}");
}
else if (logger.IsEnabled(LogLevel.Trace))
logger.LogTrace($"Archive[{i}] openfail {files[i]}");
}
}

public MpqFileStream GetStream(string fileName)
{
for (int i = 0; i < archives.Length; i++)
{
Archive a = archives[i];
if (a.HasFile(fileName))
return a.GetStream(fileName);
}

throw new FileNotFoundException(nameof(fileName));
}

public void Close()
{
for (int i = 0; i < archives.Length; i++)
archives[i].SFileCloseArchive();
}
}
108 changes: 108 additions & 0 deletions PPather/StormDll/MpqFileStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;

namespace StormDll;

public sealed class MpqFileStream : Stream
{
private const int ERROR_HANDLE_EOF = 38;

private readonly long length;

private nint fileHandle;
private long position;

public MpqFileStream(nint fileHandle)
{
this.fileHandle = fileHandle;

long low = Archive.SFileGetFileSize(fileHandle, out long high);
length = (high << 32) | low;
}

public sealed override bool CanRead => true;
public sealed override bool CanSeek => true;
public sealed override bool CanWrite => false;

public sealed override long Length => length;

public sealed override long Position
{
get => position;
set => Seek(value, SeekOrigin.Begin);
}

public sealed override void Flush() { }

public sealed override int Read(byte[] buffer, int offset, int count)
{
if (offset > buffer.Length || (offset + count) > buffer.Length)
throw new ArgumentException();

if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));

Span<byte> bufferSpan = buffer.AsSpan(offset);
bool success = Archive.SFileReadFile(fileHandle, bufferSpan, count, out long bytesRead);
position += bytesRead;

if (!success)
{
int lastError = Marshal.GetLastWin32Error();
if (lastError != ERROR_HANDLE_EOF)
throw new Win32Exception(lastError);
}

return unchecked((int)bytesRead);
}

public sealed override long Seek(long offset, SeekOrigin origin)
{
if (origin == SeekOrigin.Current && offset < 0)
{
offset = Position + offset;
origin = SeekOrigin.Begin;
}

uint low, high;
low = unchecked((uint)(offset & 0xffffffffu));
high = unchecked((uint)(offset >> 32));

uint result = Archive.SFileSetFilePointer(fileHandle, low, ref high, origin);
if (result == Archive.SFILE_INVALID_SIZE)
throw new IOException("SFileSetFilePointer failed");

return position = result;
}

public override void SetLength(long value)
{
throw new NotSupportedException();
}

public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}

public sealed override void Close()
{
base.Close();

if (fileHandle == nint.Zero)
return;

Archive.SFileCloseFile(fileHandle);
fileHandle = nint.Zero;
}

public byte[] ReadAllBytes()
{
byte[] bytes = new byte[Length];
Read(bytes, 0, (int)Length);

return bytes;
}
}
30 changes: 30 additions & 0 deletions PPather/StormDll/OpenArchive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

namespace StormDll;

[Flags]
internal enum OpenArchive : uint
{
BASE_PROVIDER_FILE = 0x00000000, // Base data source is a file
BASE_PROVIDER_MAP = 0x00000001, // Base data source is memory-mapped file
BASE_PROVIDER_HTTP = 0x00000002, // Base data source is a file on web server
BASE_PROVIDER_MASK = 0x0000000F, // Mask for base provider value
STREAM_PROVIDER_FLAT = 0x00000000, // Stream is linear with no offset mapping
STREAM_PROVIDER_PARTIAL = 0x00000010, // Stream is partial file (.part)
STREAM_PROVIDER_MPQE = 0x00000020, // Stream is an encrypted MPQ
STREAM_PROVIDER_BLOCK4 = 0x00000030, // = 0x4000 per block, text MD5 after each block, max = 0x2000 blocks per file
STREAM_PROVIDER_MASK = 0x000000F0, // Mask for stream provider value
STREAM_FLAG_READ_ONLY = 0x00000100, // Stream is read only
STREAM_FLAG_WRITE_SHARE = 0x00000200, // Allow write sharing when open for write
STREAM_FLAG_USE_BITMAP = 0x00000400, // If the file has a file bitmap, load it and use it
STREAM_OPTIONS_MASK = 0x0000FF00, // Mask for stream options
STREAM_PROVIDERS_MASK = 0x000000FF, // Mask to get stream providers
STREAM_FLAGS_MASK = 0x0000FFFF, // Mask for all stream flags (providers+options)
MPQ_OPEN_NO_LISTFILE = 0x00010000, // Don't load the internal listfile
MPQ_OPEN_NO_ATTRIBUTES = 0x00020000, // Don't open the attributes
MPQ_OPEN_NO_HEADER_SEARCH = 0x00040000, // Don't search for the MPQ header past the begin of the file
MPQ_OPEN_FORCE_MPQ_V1 = 0x00080000, // Always open the archive as MPQ v 1.00, ignore the "wFormatVersion" variable in the header
MPQ_OPEN_CHECK_SECTOR_CRC = 0x00100000, // On files with MPQ_FILE_SECTOR_CRC, the CRC will be checked when reading file
MPQ_OPEN_FORCE_LISTFILE = 0x00400000, // Force add listfile even if there is none at the moment of opening
MPQ_OPEN_READ_ONLY = STREAM_FLAG_READ_ONLY
};
8 changes: 8 additions & 0 deletions PPather/StormDll/OpenFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace StormDll;

internal enum OpenFile : uint
{
SFILE_OPEN_FROM_MPQ = 0x00000000, // Open the file from the MPQ archive
SFILE_OPEN_CHECK_EXISTS = 0xFFFFFFFC, // Only check whether the file exists
SFILE_OPEN_LOCAL_FILE = 0xFFFFFFFF // Open a local file
};
53 changes: 53 additions & 0 deletions PPather/StormDll/StormDllx64.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace StormDll;

internal sealed partial class StormDllx64
{
[LibraryImport("MPQ\\StormLib_x64.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SFileOpenArchive(
[MarshalAs(UnmanagedType.LPWStr)] string szMpqName,
uint dwPriority,
OpenArchive dwFlags,
out nint phMpq);

[LibraryImport("MPQ\\StormLib_x64.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SFileCloseArchive(nint hMpq);

[LibraryImport("MPQ\\StormLib_x64.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SFileReadFile(
nint fileHandle,
Span<byte> buffer,
[MarshalAs(UnmanagedType.I8)] long toRead,
out long read);

[LibraryImport("MPQ\\StormLib_x64.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SFileCloseFile(
nint fileHandle);

[LibraryImport("MPQ\\StormLib_x64.dll")]
public static partial uint SFileGetFileSize(
nint fileHandle,
out long fileSizeHigh);

[LibraryImport("MPQ\\StormLib_x64.dll")]
public static partial uint SFileSetFilePointer(
nint fileHandle,
long filePos,
ref uint plFilePosHigh,
SeekOrigin origin);

[LibraryImport("MPQ\\StormLib_x64.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SFileOpenFileEx(
nint archiveHandle,
[MarshalAs(UnmanagedType.LPStr)] string fileName,
OpenFile searchScope,
out nint fileHandle);
}
Loading