using WhiteMagic.Native; 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 { /// 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; } // ── 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.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.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 { if (isRelative) address = GetAbsolute(address); int elementSize = MarshalCache.Size; int totalSize = elementSize * count; byte[] raw = ReadBytes(address, 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.Size; int totalSize = elementSize * values.Length; 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. /// The text encoding. /// The maximum number of bytes to read. /// If , is relative /// to . 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(); 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); address += take; remaining -= take; } 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); } /// 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); } // ── Lifecycle ────────────────────────────────────────────────────────── /// public virtual void Dispose() { 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); } private static int IndexOfPattern(byte[] data, byte[] pattern) { int lastStart = data.Length - pattern.Length; for (int i = 0; i <= lastStart; i++) { 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; } }