Files
whitemagic/WhiteMagic/MemoryBase.cs
T
2026-07-21 22:30:10 +02:00

302 lines
11 KiB
C#

using WhiteMagic.Native;
using System.Runtime.InteropServices;
using System.Text;
namespace WhiteMagic;
/// <summary>
/// Abstract base for all memory-access readers and writers. Provides typed
/// <see cref="Read{T}"/>/<see cref="Write{T}"/>, array IO, string IO, and
/// relative/absolute addressing. Subclasses implement the concrete
/// <see cref="ReadBytes"/> and <see cref="WriteBytes"/> methods.
/// </summary>
public abstract class MemoryBase : IDisposable
{
/// <summary>The base address of the target process's main module.</summary>
public abstract IntPtr ImageBase { get; }
/// <summary>The native handle to the target process.</summary>
public abstract SafeMemoryHandle Handle { get; }
// ── Raw byte IO ────────────────────────────────────────────────────────
/// <summary>Reads a sequence of bytes from the target address.</summary>
public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false);
/// <summary>Writes a sequence of bytes to the target address.</summary>
/// <returns>The number of bytes written.</returns>
public abstract int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false);
// ── Typed IO ───────────────────────────────────────────────────────────
/// <summary>Reads a value of type <typeparamref name="T"/> from the target address.</summary>
/// <returns>The value, or <c>default(T)</c> when the read fails or returns fewer bytes than
/// <see cref="MarshalCache{T}.Size"/>.</returns>
public T Read<T>(IntPtr address, bool isRelative = false) where T : struct
{
if (isRelative)
address = GetAbsolute(address);
int size = MarshalCache<T>.Size;
byte[] raw = ReadBytes(address, size);
if (raw.Length < size)
return default;
if (MarshalCache<T>.TypeRequiresMarshal)
return MarshalByteArrayToStructure<T>(raw);
return MemoryMarshal.Read<T>(raw.AsSpan());
}
/// <summary>Writes a value of type <typeparamref name="T"/> to the target address.</summary>
/// <returns><see langword="true"/> if all bytes were written.</returns>
public bool Write<T>(IntPtr address, T value, bool isRelative = false) where T : struct
{
if (isRelative)
address = GetAbsolute(address);
int size = MarshalCache<T>.Size;
byte[] raw;
if (MarshalCache<T>.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;
}
/// <summary>Reads an array of values of type <typeparamref name="T"/> from the target address.</summary>
/// <returns>An array of at most <paramref name="count"/> elements. May be shorter when the read
/// returns fewer bytes than expected.</returns>
public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
if (isRelative)
address = GetAbsolute(address);
int elementSize = MarshalCache<T>.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<T>.TypeRequiresMarshal)
{
GCHandle pin = GCHandle.Alloc(raw, GCHandleType.Pinned);
try
{
IntPtr basePtr = pin.AddrOfPinnedObject();
for (int i = 0; i < actualCount; i++)
result[i] = Marshal.PtrToStructure<T>(basePtr + (i * elementSize));
}
finally
{
pin.Free();
}
}
else
{
ReadOnlySpan<byte> span = raw;
for (int i = 0; i < actualCount; i++)
result[i] = MemoryMarshal.Read<T>(span.Slice(i * elementSize, elementSize));
}
return result;
}
/// <summary>Writes an array of values of type <typeparamref name="T"/> to the target address.</summary>
/// <returns><see langword="true"/> if all bytes were written.</returns>
public bool Write<T>(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<T>.Size;
long total = (long)elementSize * values.Length;
ArgumentOutOfRangeException.ThrowIfGreaterThan(total, int.MaxValue, nameof(values));
int totalSize = (int)total;
byte[] raw = new byte[totalSize];
Span<byte> span = raw;
for (int i = 0; i < values.Length; i++)
{
Span<byte> slice = span.Slice(i * elementSize, elementSize);
if (MarshalCache<T>.TypeRequiresMarshal)
StructureToByteArray(values[i], slice, elementSize);
else
MemoryMarshal.Write(slice, in values[i]);
}
int written = WriteBytes(address, raw, false);
return written == totalSize;
}
// ── String IO ──────────────────────────────────────────────────────────
/// <summary>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).</summary>
/// <param name="address">The address to read from.</param>
/// <param name="encoding">The text encoding.</param>
/// <param name="maxLength">The maximum number of bytes to read.</param>
/// <param name="relative">If <see langword="true"/>, <paramref name="address"/> is relative
/// to <see cref="ImageBase"/>.</param>
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");
const int chunkSize = 64;
int remaining = maxLength;
var accumulated = new System.Collections.Generic.List<byte[]>();
while (remaining > 0)
{
int take = Math.Min(chunkSize, remaining);
byte[] chunk = ReadBytes(address, take);
if (chunk.Length == 0)
break;
int nullPos = IndexOfPattern(chunk, nullTerminator);
if (nullPos >= 0)
{
if (nullPos > 0)
accumulated.Add(chunk[..nullPos]);
break;
}
accumulated.Add(chunk);
// Advance by the bytes actually read, not the amount requested: a partial
// read (chunk.Length < take) must not skip the unread tail of the window.
address += chunk.Length;
remaining -= chunk.Length;
}
int totalLength = 0;
foreach (byte[] part in accumulated)
totalLength += part.Length;
byte[] combined = new byte[totalLength];
int offset = 0;
foreach (byte[] part in accumulated)
{
part.CopyTo(combined, offset);
offset += part.Length;
}
return encoding.GetString(combined);
}
/// <summary>Writes a null-terminated string to the target address.</summary>
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 ─────────────────────────────────────────────────────────
/// <summary>Converts a relative offset to an absolute address relative to <see cref="ImageBase"/>.</summary>
public IntPtr GetAbsolute(IntPtr relative)
{
return ImageBase + (nint)relative;
}
/// <summary>Converts an absolute address to a relative offset from <see cref="ImageBase"/>.
/// This is the inverse of <see cref="GetAbsolute"/>: <c>GetAbsolute(GetRelative(a)) == a</c>.</summary>
public IntPtr GetRelative(IntPtr absolute)
{
return (IntPtr)((nint)absolute - (nint)ImageBase);
}
// ── Lifecycle ──────────────────────────────────────────────────────────
/// <inheritdoc />
public virtual void Dispose()
{
Handle?.Dispose();
}
// ── Private helpers ────────────────────────────────────────────────────
private static T MarshalByteArrayToStructure<T>(byte[] bytes) where T : struct
{
GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
return Marshal.PtrToStructure<T>(pin.AddrOfPinnedObject());
}
finally
{
pin.Free();
}
}
private static byte[] StructureToByteArray<T>(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>(T value, Span<byte> destination, int size) where T : struct
{
byte[] bytes = StructureToByteArray(value, size);
bytes.CopyTo(destination);
}
private static int IndexOfPattern(byte[] data, byte[] pattern)
{
int lastStart = data.Length - pattern.Length;
int stride = Math.Max(1, pattern.Length);
for (int i = 0; i <= lastStart; i += stride)
{
bool match = true;
for (int j = 0; j < pattern.Length; j++)
{
if (data[i + j] != pattern[j])
{
match = false;
break;
}
}
if (match)
return i;
}
return -1;
}
}