Skip to content

Commit

Permalink
Merge pull request #535 from Xian55/refactor/ppather-memory-stream-ov…
Browse files Browse the repository at this point in the history
…er-extract

PPather: `MpqFileStream` over `SFileExtractFile`
  • Loading branch information
Xian55 authored Sep 1, 2023
2 parents eb5c6f6 + 2e64f85 commit f5bcb5b
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 271 deletions.
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

0 comments on commit f5bcb5b

Please sign in to comment.