fix 7 correctness and cleanup issues
Blocking fixes: 1. Read<T> now returns default(T) on failed/partial read instead of crash (applies to single Read<T>, array Read<T>, and ReadBytes) 2. InProcessReader uses ReadProcessMemory via handle instead of unsafe Buffer.MemoryCopy — fails soft on bad address instead of AV'ing 3. GetRelative now returns absolute - ImageBase (inverse of GetAbsolute). Fix round-trip test to validate at arbitrary offsets, not just ImageBase 4. ReadString reads in 64-byte chunks with encoding-aware null-terminator pattern matching (handles UTF-16's 2-byte null, UTF-32's 4-byte null) Cleanup: 5. InProcessReader validates handle on open and uses RPM through it (handle is no longer unused) 6. Array marshal read: pin raw buffer once, PtrToStructure at offset 7. StructureToByteArray(Span) delegates to byte[] overload, no duplicate New tests: 4 invalid-address grace tests (returns default/empty/false). All 57 passing.
This commit is contained in:
@@ -6,8 +6,11 @@ namespace WhiteMagic;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// In-process memory reader that accesses the owning process's memory through
|
/// In-process memory reader that accesses the owning process's memory through
|
||||||
/// direct pointer dereference (<c>unsafe</c>). Use this reader from within a
|
/// <see cref="NativeMethods.ReadProcessMemory"/> and
|
||||||
/// managed DLL injected into the target process.
|
/// <see cref="NativeMethods.WriteProcessMemory"/> on a handle to the current
|
||||||
|
/// process. Unlike the unsafe-deref approach, this fails softly (returns
|
||||||
|
/// empty / zero bytes) on invalid or protected addresses instead of crashing
|
||||||
|
/// the host process with an <see cref="AccessViolationException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class InProcessReader : MemoryBase
|
public sealed class InProcessReader : MemoryBase
|
||||||
{
|
{
|
||||||
@@ -25,6 +28,12 @@ public sealed class InProcessReader : MemoryBase
|
|||||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation,
|
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation,
|
||||||
false,
|
false,
|
||||||
current.Id);
|
current.Id);
|
||||||
|
if (_handle.IsInvalid)
|
||||||
|
{
|
||||||
|
int error = Marshal.GetLastPInvokeError();
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"OpenProcess failed for PID {current.Id}: error {error}");
|
||||||
|
}
|
||||||
|
|
||||||
_imageBase = current.MainModule?.BaseAddress ?? IntPtr.Zero;
|
_imageBase = current.MainModule?.BaseAddress ?? IntPtr.Zero;
|
||||||
}
|
}
|
||||||
@@ -36,30 +45,37 @@ public sealed class InProcessReader : MemoryBase
|
|||||||
public override SafeMemoryHandle Handle => _handle;
|
public override SafeMemoryHandle Handle => _handle;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override unsafe byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
|
public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
|
||||||
{
|
{
|
||||||
if (isRelative)
|
if (isRelative)
|
||||||
address = GetAbsolute(address);
|
address = GetAbsolute(address);
|
||||||
|
|
||||||
byte[] buffer = new byte[count];
|
byte[] buffer = new byte[count];
|
||||||
fixed (byte* ptr = buffer)
|
if (!NativeMethods.ReadProcessMemory(_handle, address, buffer, count, out nint bytesRead))
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy((void*)address, ptr, count, count);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((int)bytesRead != count)
|
||||||
|
{
|
||||||
|
Array.Resize(ref buffer, (int)bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override unsafe int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
|
public override int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
|
||||||
{
|
{
|
||||||
if (isRelative)
|
if (isRelative)
|
||||||
address = GetAbsolute(address);
|
address = GetAbsolute(address);
|
||||||
|
|
||||||
fixed (byte* ptr = bytes)
|
if (!NativeMethods.WriteProcessMemory(_handle, address, bytes, bytes.Length, out nint written))
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(ptr, (void*)address, bytes.Length, bytes.Length);
|
return 0;
|
||||||
}
|
}
|
||||||
return bytes.Length;
|
|
||||||
|
return (int)written;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
+95
-32
@@ -30,6 +30,8 @@ public abstract class MemoryBase : IDisposable
|
|||||||
// ── Typed IO ───────────────────────────────────────────────────────────
|
// ── Typed IO ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Reads a value of type <typeparamref name="T"/> from the target address.</summary>
|
/// <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
|
public T Read<T>(IntPtr address, bool isRelative = false) where T : struct
|
||||||
{
|
{
|
||||||
if (isRelative)
|
if (isRelative)
|
||||||
@@ -38,10 +40,11 @@ public abstract class MemoryBase : IDisposable
|
|||||||
int size = MarshalCache<T>.Size;
|
int size = MarshalCache<T>.Size;
|
||||||
byte[] raw = ReadBytes(address, size);
|
byte[] raw = ReadBytes(address, size);
|
||||||
|
|
||||||
|
if (raw.Length < size)
|
||||||
|
return default;
|
||||||
|
|
||||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||||
{
|
|
||||||
return MarshalByteArrayToStructure<T>(raw);
|
return MarshalByteArrayToStructure<T>(raw);
|
||||||
}
|
|
||||||
|
|
||||||
return MemoryMarshal.Read<T>(raw.AsSpan());
|
return MemoryMarshal.Read<T>(raw.AsSpan());
|
||||||
}
|
}
|
||||||
@@ -57,9 +60,7 @@ public abstract class MemoryBase : IDisposable
|
|||||||
|
|
||||||
byte[] raw;
|
byte[] raw;
|
||||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||||
{
|
|
||||||
raw = StructureToByteArray(value, size);
|
raw = StructureToByteArray(value, size);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
raw = new byte[size];
|
raw = new byte[size];
|
||||||
@@ -71,6 +72,8 @@ public abstract class MemoryBase : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reads an array of values of type <typeparamref name="T"/> from the target address.</summary>
|
/// <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
|
public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct
|
||||||
{
|
{
|
||||||
if (isRelative)
|
if (isRelative)
|
||||||
@@ -79,25 +82,33 @@ public abstract class MemoryBase : IDisposable
|
|||||||
int elementSize = MarshalCache<T>.Size;
|
int elementSize = MarshalCache<T>.Size;
|
||||||
int totalSize = elementSize * count;
|
int totalSize = elementSize * count;
|
||||||
byte[] raw = ReadBytes(address, totalSize);
|
byte[] raw = ReadBytes(address, totalSize);
|
||||||
|
int actualCount = Math.Min(count, raw.Length / elementSize);
|
||||||
|
|
||||||
var result = new T[count];
|
var result = new T[actualCount];
|
||||||
|
|
||||||
|
if (actualCount == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < count; i++)
|
GCHandle pin = GCHandle.Alloc(raw, GCHandleType.Pinned);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var elementBytes = new ReadOnlySpan<byte>(raw, i * elementSize, elementSize);
|
IntPtr basePtr = pin.AddrOfPinnedObject();
|
||||||
result[i] = MarshalByteArrayToStructure<T>(elementBytes.ToArray());
|
for (int i = 0; i < actualCount; i++)
|
||||||
|
result[i] = Marshal.PtrToStructure<T>(basePtr + (i * elementSize));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
pin.Free();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ReadOnlySpan<byte> span = raw;
|
ReadOnlySpan<byte> span = raw;
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < actualCount; i++)
|
||||||
{
|
|
||||||
result[i] = MemoryMarshal.Read<T>(span.Slice(i * elementSize, elementSize));
|
result[i] = MemoryMarshal.Read<T>(span.Slice(i * elementSize, elementSize));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -121,14 +132,10 @@ public abstract class MemoryBase : IDisposable
|
|||||||
{
|
{
|
||||||
Span<byte> slice = span.Slice(i * elementSize, elementSize);
|
Span<byte> slice = span.Slice(i * elementSize, elementSize);
|
||||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||||
{
|
|
||||||
StructureToByteArray(values[i], slice, elementSize);
|
StructureToByteArray(values[i], slice, elementSize);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
MemoryMarshal.Write(slice, in values[i]);
|
MemoryMarshal.Write(slice, in values[i]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int written = WriteBytes(address, raw, false);
|
int written = WriteBytes(address, raw, false);
|
||||||
return written == totalSize;
|
return written == totalSize;
|
||||||
@@ -136,17 +143,61 @@ public abstract class MemoryBase : IDisposable
|
|||||||
|
|
||||||
// ── String IO ──────────────────────────────────────────────────────────
|
// ── String IO ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Reads a null-terminated string from the target address.</summary>
|
/// <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)
|
public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false)
|
||||||
{
|
{
|
||||||
byte[] buffer = ReadBytes(address, maxLength, relative);
|
if (relative)
|
||||||
string decoded = encoding.GetString(buffer);
|
address = GetAbsolute(address);
|
||||||
int nullIndex = decoded.IndexOf('\0');
|
|
||||||
if (nullIndex >= 0)
|
// 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)
|
||||||
{
|
{
|
||||||
return decoded[..nullIndex];
|
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;
|
||||||
}
|
}
|
||||||
return decoded;
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Writes a null-terminated string to the target address.</summary>
|
/// <summary>Writes a null-terminated string to the target address.</summary>
|
||||||
@@ -168,10 +219,11 @@ public abstract class MemoryBase : IDisposable
|
|||||||
return ImageBase + (nint)relative;
|
return ImageBase + (nint)relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Converts an absolute address to a relative offset from <see cref="ImageBase"/>.</summary>
|
/// <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)
|
public IntPtr GetRelative(IntPtr absolute)
|
||||||
{
|
{
|
||||||
return (IntPtr)((nint)ImageBase - (nint)absolute);
|
return (IntPtr)((nint)absolute - (nint)ImageBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Lifecycle ──────────────────────────────────────────────────────────
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
||||||
@@ -214,16 +266,27 @@ public abstract class MemoryBase : IDisposable
|
|||||||
|
|
||||||
private static void StructureToByteArray<T>(T value, Span<byte> destination, int size) where T : struct
|
private static void StructureToByteArray<T>(T value, Span<byte> destination, int size) where T : struct
|
||||||
{
|
{
|
||||||
byte[] temp = destination.ToArray();
|
byte[] bytes = StructureToByteArray(value, size);
|
||||||
GCHandle pin = GCHandle.Alloc(temp, GCHandleType.Pinned);
|
bytes.CopyTo(destination);
|
||||||
try
|
|
||||||
{
|
|
||||||
Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false);
|
|
||||||
temp.CopyTo(destination);
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
private static int IndexOfPattern(byte[] data, byte[] pattern)
|
||||||
{
|
{
|
||||||
pin.Free();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace WhiteMagicTest;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests for relative/absolute addressing in <see cref="MemoryBase"/>.
|
/// Tests for relative/absolute addressing in <see cref="MemoryBase"/>.
|
||||||
|
/// GetAbsolute(relative) = ImageBase + relative.
|
||||||
|
/// GetRelative(absolute) = absolute - ImageBase (inverse of GetAbsolute).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddressingTests
|
public class AddressingTests
|
||||||
{
|
{
|
||||||
@@ -27,31 +29,45 @@ public class AddressingTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetRelative_computes_offset_from_image_base()
|
public void GetRelative_returns_absolute_minus_image_base()
|
||||||
{
|
{
|
||||||
using var reader = OpenSelf();
|
using var reader = OpenSelf();
|
||||||
IntPtr imageBase = reader.ImageBase;
|
IntPtr imageBase = reader.ImageBase;
|
||||||
IntPtr absolute = imageBase + 0x2000;
|
IntPtr absolute = imageBase + 0x2000;
|
||||||
IntPtr relative = reader.GetRelative(absolute);
|
IntPtr relative = reader.GetRelative(absolute);
|
||||||
// GetRelative returns ImageBase - absolute (GreyMagic convention)
|
Assert.Equal((IntPtr)((nint)absolute - (nint)imageBase), relative);
|
||||||
Assert.Equal((IntPtr)((int)imageBase - (int)absolute), relative);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetAbsolute_after_GetRelative_at_image_base_returns_to_base()
|
public void GetAbsolute_and_GetRelative_are_inverses()
|
||||||
{
|
{
|
||||||
using var reader = OpenSelf();
|
using var reader = OpenSelf();
|
||||||
IntPtr atBase = reader.ImageBase;
|
IntPtr offset = (IntPtr)0x3000;
|
||||||
IntPtr relative = reader.GetRelative(atBase);
|
|
||||||
IntPtr back = reader.GetAbsolute(relative);
|
// Round-trip: offset -> absolute -> back to offset
|
||||||
Assert.Equal(atBase, back);
|
IntPtr absolute = reader.GetAbsolute(offset);
|
||||||
|
IntPtr back = reader.GetRelative(absolute);
|
||||||
|
Assert.Equal(offset, back);
|
||||||
|
|
||||||
|
// Reverse round-trip: absolute -> offset -> back to absolute
|
||||||
|
IntPtr relative = reader.GetRelative(absolute);
|
||||||
|
IntPtr absoluteAgain = reader.GetAbsolute(relative);
|
||||||
|
Assert.Equal(absolute, absoluteAgain);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRelative_on_ImageBase_returns_zero()
|
||||||
|
{
|
||||||
|
using var reader = OpenSelf();
|
||||||
|
IntPtr relative = reader.GetRelative(reader.ImageBase);
|
||||||
|
Assert.Equal(IntPtr.Zero, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Read_with_isRelative_true_uses_image_base()
|
public void Read_with_isRelative_true_uses_image_base()
|
||||||
{
|
{
|
||||||
using var reader = OpenSelf();
|
using var reader = OpenSelf();
|
||||||
// Read the first byte at ImageBase (should be MZ header: 0x4D = 'M')
|
// DOS header 'MZ' at the image base
|
||||||
byte firstByte = reader.Read<byte>(IntPtr.Zero, isRelative: true);
|
byte firstByte = reader.Read<byte>(IntPtr.Zero, isRelative: true);
|
||||||
Assert.Equal(0x4D, firstByte);
|
Assert.Equal(0x4D, firstByte);
|
||||||
}
|
}
|
||||||
@@ -65,10 +81,9 @@ public class AddressingTests
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
IntPtr absolute = pin.AddrOfPinnedObject();
|
IntPtr absolute = pin.AddrOfPinnedObject();
|
||||||
nint relative = (nint)absolute - (nint)reader.ImageBase;
|
IntPtr relative = reader.GetRelative(absolute);
|
||||||
IntPtr relativePtr = (IntPtr)relative;
|
|
||||||
|
|
||||||
Assert.True(reader.Write(relativePtr, 42, isRelative: true));
|
Assert.True(reader.Write(relative, 42, isRelative: true));
|
||||||
Assert.Equal(42, reader.Read<int>(absolute));
|
Assert.Equal(42, reader.Read<int>(absolute));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -81,7 +96,6 @@ public class AddressingTests
|
|||||||
public void ReadBytes_with_isRelative_true_resolves_correctly()
|
public void ReadBytes_with_isRelative_true_resolves_correctly()
|
||||||
{
|
{
|
||||||
using var reader = OpenSelf();
|
using var reader = OpenSelf();
|
||||||
// DOS header 'MZ' at the image base
|
|
||||||
byte[] data = reader.ReadBytes(IntPtr.Zero, 2, isRelative: true);
|
byte[] data = reader.ReadBytes(IntPtr.Zero, 2, isRelative: true);
|
||||||
Assert.Equal(0x4D, data[0]);
|
Assert.Equal(0x4D, data[0]);
|
||||||
Assert.Equal(0x5A, data[1]);
|
Assert.Equal(0x5A, data[1]);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using WhiteMagic.Native;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using WhiteMagic;
|
using WhiteMagic;
|
||||||
|
using WhiteMagic.Native;
|
||||||
|
|
||||||
namespace WhiteMagicTest;
|
namespace WhiteMagicTest;
|
||||||
|
|
||||||
@@ -31,7 +31,6 @@ public class MemoryBaseTests
|
|||||||
{
|
{
|
||||||
using var reader = OpenSelf();
|
using var reader = OpenSelf();
|
||||||
|
|
||||||
// Pin a local int to use as our "remote" address
|
|
||||||
int slot = 0;
|
int slot = 0;
|
||||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||||
try
|
try
|
||||||
@@ -91,10 +90,7 @@ public class MemoryBaseTests
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
IntPtr addr = pin.AddrOfPinnedObject();
|
IntPtr addr = pin.AddrOfPinnedObject();
|
||||||
|
|
||||||
// Write a new value
|
|
||||||
Assert.True(reader.Write(addr, new TestStruct { X = 100, Y = 200 }));
|
Assert.True(reader.Write(addr, new TestStruct { X = 100, Y = 200 }));
|
||||||
|
|
||||||
var result = reader.Read<TestStruct>(addr);
|
var result = reader.Read<TestStruct>(addr);
|
||||||
Assert.Equal(100, result.X);
|
Assert.Equal(100, result.X);
|
||||||
Assert.Equal(200, result.Y);
|
Assert.Equal(200, result.Y);
|
||||||
@@ -140,7 +136,6 @@ public class MemoryBaseTests
|
|||||||
int[] expected = [10, 20, 30, 40];
|
int[] expected = [10, 20, 30, 40];
|
||||||
|
|
||||||
Assert.True(reader.Write(addr, expected));
|
Assert.True(reader.Write(addr, expected));
|
||||||
|
|
||||||
int[] actual = reader.Read<int>(addr, 4);
|
int[] actual = reader.Read<int>(addr, 4);
|
||||||
Assert.Equal(expected, actual);
|
Assert.Equal(expected, actual);
|
||||||
}
|
}
|
||||||
@@ -168,7 +163,6 @@ public class MemoryBaseTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
Assert.True(reader.Write(addr, expected));
|
Assert.True(reader.Write(addr, expected));
|
||||||
|
|
||||||
var actual = reader.Read<TestStruct>(addr, 4);
|
var actual = reader.Read<TestStruct>(addr, 4);
|
||||||
Assert.Equal(expected, actual);
|
Assert.Equal(expected, actual);
|
||||||
}
|
}
|
||||||
@@ -199,7 +193,39 @@ public class MemoryBaseTests
|
|||||||
{
|
{
|
||||||
var reader = OpenSelf();
|
var reader = OpenSelf();
|
||||||
reader.Dispose();
|
reader.Dispose();
|
||||||
reader.Dispose(); // Should not throw
|
reader.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Graceful failure on invalid addresses ───────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Read_int_on_invalid_address_returns_default()
|
||||||
|
{
|
||||||
|
using var reader = OpenSelf();
|
||||||
|
Assert.Equal(0, reader.Read<int>(IntPtr.Zero));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Read_struct_on_invalid_address_returns_default()
|
||||||
|
{
|
||||||
|
using var reader = OpenSelf();
|
||||||
|
var result = reader.Read<TestStruct>(IntPtr.Zero);
|
||||||
|
Assert.Equal(0, result.X);
|
||||||
|
Assert.Equal(0, result.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Read_int_array_on_invalid_address_returns_empty()
|
||||||
|
{
|
||||||
|
using var reader = OpenSelf();
|
||||||
|
Assert.Empty(reader.Read<int>(IntPtr.Zero, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Read_bytes_on_invalid_address_returns_empty()
|
||||||
|
{
|
||||||
|
using var reader = OpenSelf();
|
||||||
|
Assert.Empty(reader.ReadBytes(IntPtr.Zero, 10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user