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:
kbe
2026-07-21 19:24:18 +02:00
parent 6eb78e7974
commit 8374650aac
4 changed files with 182 additions and 63 deletions
+27 -13
View File
@@ -7,6 +7,8 @@ namespace WhiteMagicTest;
/// <summary>
/// Tests for relative/absolute addressing in <see cref="MemoryBase"/>.
/// GetAbsolute(relative) = ImageBase + relative.
/// GetRelative(absolute) = absolute - ImageBase (inverse of GetAbsolute).
/// </summary>
public class AddressingTests
{
@@ -27,31 +29,45 @@ public class AddressingTests
}
[Fact]
public void GetRelative_computes_offset_from_image_base()
public void GetRelative_returns_absolute_minus_image_base()
{
using var reader = OpenSelf();
IntPtr imageBase = reader.ImageBase;
IntPtr absolute = imageBase + 0x2000;
IntPtr relative = reader.GetRelative(absolute);
// GetRelative returns ImageBase - absolute (GreyMagic convention)
Assert.Equal((IntPtr)((int)imageBase - (int)absolute), relative);
Assert.Equal((IntPtr)((nint)absolute - (nint)imageBase), relative);
}
[Fact]
public void GetAbsolute_after_GetRelative_at_image_base_returns_to_base()
public void GetAbsolute_and_GetRelative_are_inverses()
{
using var reader = OpenSelf();
IntPtr atBase = reader.ImageBase;
IntPtr relative = reader.GetRelative(atBase);
IntPtr back = reader.GetAbsolute(relative);
Assert.Equal(atBase, back);
IntPtr offset = (IntPtr)0x3000;
// Round-trip: offset -> absolute -> back to offset
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]
public void Read_with_isRelative_true_uses_image_base()
{
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);
Assert.Equal(0x4D, firstByte);
}
@@ -65,10 +81,9 @@ public class AddressingTests
try
{
IntPtr absolute = pin.AddrOfPinnedObject();
nint relative = (nint)absolute - (nint)reader.ImageBase;
IntPtr relativePtr = (IntPtr)relative;
IntPtr relative = reader.GetRelative(absolute);
Assert.True(reader.Write(relativePtr, 42, isRelative: true));
Assert.True(reader.Write(relative, 42, isRelative: true));
Assert.Equal(42, reader.Read<int>(absolute));
}
finally
@@ -81,7 +96,6 @@ public class AddressingTests
public void ReadBytes_with_isRelative_true_resolves_correctly()
{
using var reader = OpenSelf();
// DOS header 'MZ' at the image base
byte[] data = reader.ReadBytes(IntPtr.Zero, 2, isRelative: true);
Assert.Equal(0x4D, data[0]);
Assert.Equal(0x5A, data[1]);
+34 -8
View File
@@ -1,8 +1,8 @@
using WhiteMagic.Native;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using WhiteMagic;
using WhiteMagic.Native;
namespace WhiteMagicTest;
@@ -31,7 +31,6 @@ public class MemoryBaseTests
{
using var reader = OpenSelf();
// Pin a local int to use as our "remote" address
int slot = 0;
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
try
@@ -91,10 +90,7 @@ public class MemoryBaseTests
try
{
IntPtr addr = pin.AddrOfPinnedObject();
// Write a new value
Assert.True(reader.Write(addr, new TestStruct { X = 100, Y = 200 }));
var result = reader.Read<TestStruct>(addr);
Assert.Equal(100, result.X);
Assert.Equal(200, result.Y);
@@ -140,7 +136,6 @@ public class MemoryBaseTests
int[] expected = [10, 20, 30, 40];
Assert.True(reader.Write(addr, expected));
int[] actual = reader.Read<int>(addr, 4);
Assert.Equal(expected, actual);
}
@@ -168,7 +163,6 @@ public class MemoryBaseTests
};
Assert.True(reader.Write(addr, expected));
var actual = reader.Read<TestStruct>(addr, 4);
Assert.Equal(expected, actual);
}
@@ -199,7 +193,39 @@ public class MemoryBaseTests
{
var reader = OpenSelf();
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));
}
}