using WhiteMagic.Hooking;
using WhiteMagic.Memory;
using WhiteMagic.Native;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace WhiteMagic;
///
/// Abstract base for all memory-access readers and writers. Provides typed
/// /, array IO, string IO, and
/// relative/absolute addressing. Subclasses implement the concrete
/// and methods.
///
public abstract class MemoryBase : IDisposable
{
/// Creates the shared hooking managers for this memory instance.
protected MemoryBase()
{
PatchManager = new PatchManager(this);
DetourManager = new DetourManager(this);
}
/// The base address of the target process's main module.
public abstract IntPtr ImageBase { get; }
/// The native handle to the target process.
public abstract SafeMemoryHandle Handle { get; }
/// if the target process is 64-bit.
public abstract bool Is64Bit { get; }
/// The operating-system process identifier of the target process.
public abstract int ProcessId { get; }
/// Named byte-patch manager; valid for in-process and external readers.
public PatchManager PatchManager { get; }
/// Inline-detour manager; valid only when operating in-process.
public DetourManager DetourManager { get; }
// ── Raw byte IO ────────────────────────────────────────────────────────
/// Reads a sequence of bytes from the target address.
public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false);
/// Writes a sequence of bytes to the target address.
/// The number of bytes written.
public abstract int WriteBytes(IntPtr address, ReadOnlySpan bytes, bool isRelative = false);
// ── Typed IO ───────────────────────────────────────────────────────────
/// Reads a value of type from the target address.
/// The value, or default(T) when the read fails or returns fewer bytes than
/// .
public T Read(IntPtr address, bool isRelative = false) where T : struct
{
if (isRelative)
address = GetAbsolute(address);
int size = MarshalCache.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.Size;
byte[] raw = ReadBytes(address, size);
if (raw.Length < size)
return default;
if (MarshalCache.TypeRequiresMarshal)
return MarshalByteArrayToStructure(raw);
return MemoryMarshal.Read(raw.AsSpan());
}
/// Writes a value of type to the target address.
/// if all bytes were written.
public bool Write(IntPtr address, T value, bool isRelative = false) where T : struct
{
if (isRelative)
address = GetAbsolute(address);
int size = MarshalCache.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.Size;
byte[] raw;
if (MarshalCache.TypeRequiresMarshal)
raw = StructureToByteArray(value, size);
else
{
raw = new byte[size];
MemoryMarshal.Write(raw.AsSpan(), in value);
}
int written = WriteBytes(address, raw, false);
return written == size;
}
/// Reads an array of values of type from the target address.
/// An array of at most elements. May be shorter when the read
/// returns fewer bytes than expected.
public T[] Read(IntPtr address, int count, bool isRelative = false) where T : struct
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
if (isRelative)
address = GetAbsolute(address);
int elementSize = MarshalCache.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.Size;
long totalSize = (long)elementSize * count;
ArgumentOutOfRangeException.ThrowIfGreaterThan(totalSize, int.MaxValue, nameof(count));
byte[] raw = ReadBytes(address, (int)totalSize);
int actualCount = Math.Min(count, raw.Length / elementSize);
var result = new T[actualCount];
if (actualCount == 0)
return result;
if (MarshalCache.TypeRequiresMarshal)
{
GCHandle pin = GCHandle.Alloc(raw, GCHandleType.Pinned);
try
{
IntPtr basePtr = pin.AddrOfPinnedObject();
for (int i = 0; i < actualCount; i++)
result[i] = Marshal.PtrToStructure(basePtr + (i * elementSize));
}
finally
{
pin.Free();
}
}
else
{
ReadOnlySpan span = raw;
for (int i = 0; i < actualCount; i++)
result[i] = MemoryMarshal.Read(span.Slice(i * elementSize, elementSize));
}
return result;
}
/// Writes an array of values of type to the target address.
/// if all bytes were written.
public bool Write(IntPtr address, T[] values, bool isRelative = false) where T : struct
{
if (isRelative)
address = GetAbsolute(address);
if (values is null || values.Length == 0)
return true;
int elementSize = MarshalCache.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.Size;
long total = (long)elementSize * values.Length;
ArgumentOutOfRangeException.ThrowIfGreaterThan(total, int.MaxValue, nameof(values));
int totalSize = (int)total;
byte[] raw = new byte[totalSize];
Span span = raw;
for (int i = 0; i < values.Length; i++)
{
Span slice = span.Slice(i * elementSize, elementSize);
if (MarshalCache.TypeRequiresMarshal)
StructureToByteArray(values[i], slice, elementSize);
else
MemoryMarshal.Write(slice, in values[i]);
}
int written = WriteBytes(address, raw, false);
return written == totalSize;
}
// ── String IO ──────────────────────────────────────────────────────────
/// Reads a null-terminated string from the target address by scanning in small
/// chunks. Stops at the null terminator, the maximum length, or the first page boundary
/// that fails to read (avoids an atomic failure when a 512-byte window crosses an unmapped
/// region).
/// The address to read from. For multi-byte encodings this must be
/// aligned to a code-unit boundary or the result is undefined.
/// The text encoding.
/// The maximum number of bytes to read.
/// If , is relative
/// to .
///
/// The scan is aligned to the encoding's code-unit width (1 byte for UTF-8/ASCII, 2 bytes
/// for UTF-16, 4 bytes for UTF-32). The trailing bytes of each chunk are merged with the
/// next chunk so a null terminator that straddles the chunk boundary is not missed.
///
public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false)
{
if (relative)
address = GetAbsolute(address);
// The encoded null terminator. For ASCII/UTF-8 this is a single 0x00 byte;
// for UTF-16 it is two zero bytes (0x00 0x00); for UTF-32 it is four.
byte[] nullTerminator = encoding.GetBytes("\0");
int nullLen = nullTerminator.Length;
const int chunkSize = 64;
int remaining = maxLength;
var accumulated = new System.Collections.Generic.List();
while (remaining > 0)
{
int take = Math.Min(chunkSize, remaining);
byte[] chunk = ReadBytes(address + accumulated.Count, take);
if (chunk.Length == 0)
break;
int previousLen = accumulated.Count;
accumulated.AddRange(chunk);
// Search the newly extended buffer at code-unit-aligned positions. A terminator
// can start as far back as (nullLen - 1) bytes before the new bytes, so start
// the search just before the previous end, rounded up to the next code-unit.
int firstAligned = previousLen - (previousLen % nullLen);
if (firstAligned < 0) firstAligned = 0;
int limit = accumulated.Count - nullLen;
for (int i = firstAligned; i <= limit; i += nullLen)
{
bool match = true;
for (int j = 0; j < nullLen; j++)
{
if (accumulated[i + j] != nullTerminator[j])
{
match = false;
break;
}
}
if (match)
{
accumulated.RemoveRange(i, accumulated.Count - i);
remaining = 0;
break;
}
}
if (remaining > 0)
remaining -= chunk.Length;
}
return encoding.GetString(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(accumulated));
}
/// Writes a null-terminated string to the target address.
public virtual bool WriteString(IntPtr address, string value, Encoding encoding, bool relative = false)
{
if (value.Length == 0 || value[^1] != '\0')
value += '\0';
byte[] bytes = encoding.GetBytes(value);
int written = WriteBytes(address, bytes, relative);
return written == bytes.Length;
}
// ── Addressing ─────────────────────────────────────────────────────────
/// Converts a relative offset to an absolute address relative to .
public IntPtr GetAbsolute(IntPtr relative)
{
return ImageBase + (nint)relative;
}
/// Converts an absolute address to a relative offset from .
/// This is the inverse of : GetAbsolute(GetRelative(a)) == a.
public IntPtr GetRelative(IntPtr absolute)
{
return (IntPtr)((nint)absolute - (nint)ImageBase);
}
// ── Memory region query ────────────────────────────────────────────────
///
/// Queries the memory region that contains in the target
/// process using VirtualQueryEx.
///
/// An immutable snapshot of the region.
/// The query fails.
public MemoryRegion QueryRegion(IntPtr address)
{
nuint bufferSize = (nuint)Marshal.SizeOf();
nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize);
if (result == 0)
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException($"VirtualQueryEx failed for address 0x{address:X}: error {error}.");
}
return new MemoryRegion(info);
}
///
/// Enumerates the memory regions of the target process from the lowest address upward.
/// The walk is lazy; callers can stop early without walking the entire address space.
///
public IEnumerable EnumerateRegions()
{
IntPtr address = IntPtr.Zero;
nuint bufferSize = (nuint)Marshal.SizeOf();
while (true)
{
nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize);
if (result == 0)
yield break;
yield return new MemoryRegion(info);
IntPtr next = info.BaseAddress + (nint)info.RegionSize;
if (next.ToInt64() <= address.ToInt64())
yield break;
address = next;
}
}
///
/// Changes the page protection on a region of memory and returns a disposable scope
/// that restores the original protection on dispose, including when an exception escapes
/// the guarded body.
///
public ProtectionScope ChangeProtection(IntPtr address, nint size, MemoryProtectionType protection)
{
return new ProtectionScope(this, address, size, protection);
}
// ── Lifecycle ──────────────────────────────────────────────────────────
///
public virtual void Dispose()
{
DetourManager.RemoveAll();
PatchManager.RestoreAll();
Handle?.Dispose();
}
// ── Private helpers ────────────────────────────────────────────────────
private static T MarshalByteArrayToStructure(byte[] bytes) where T : struct
{
GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
return Marshal.PtrToStructure(pin.AddrOfPinnedObject());
}
finally
{
pin.Free();
}
}
private static byte[] StructureToByteArray(T value, int size) where T : struct
{
byte[] bytes = new byte[size];
GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false);
}
finally
{
pin.Free();
}
return bytes;
}
private static void StructureToByteArray(T value, Span destination, int size) where T : struct
{
byte[] bytes = StructureToByteArray(value, size);
bytes.CopyTo(destination);
}
}