using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using WhiteMagic; using WhiteMagic.Native; namespace WhiteMagicTest; /// /// Tests for abstract contract and /// round-trip (Read<T>/Write<T>, arrays) using the current process as target. /// public class MemoryBaseTests { private static ExternalReader OpenSelf() { return new ExternalReader( Process.GetCurrentProcess(), ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation); } [Fact] public void ImageBase_is_nonzero_for_self() { using var reader = OpenSelf(); Assert.NotEqual(IntPtr.Zero, reader.ImageBase); } [Fact] public void Read_int_writes_and_reads_back() { using var reader = OpenSelf(); int slot = 0; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.Write(addr, 0x1BADB002)); Assert.Equal(0x1BADB002, reader.Read(addr)); } finally { pin.Free(); } } [Fact] public void Read_byte_writes_and_reads_back() { using var reader = OpenSelf(); byte slot = 0; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.Write(addr, (byte)0xAB)); Assert.Equal(0xAB, reader.Read(addr)); } finally { pin.Free(); } } [Fact] public void Read_long_writes_and_reads_back() { using var reader = OpenSelf(); long slot = 0; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.Write(addr, unchecked((long)0xDEADBEEF_CAFEBABE))); Assert.Equal(unchecked((long)0xDEADBEEF_CAFEBABE), reader.Read(addr)); } finally { pin.Free(); } } [Fact] public void Read_blittable_struct_writes_and_reads_back() { using var reader = OpenSelf(); var slot = new TestStruct { X = 42, Y = 99 }; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.Write(addr, new TestStruct { X = 100, Y = 200 })); var result = reader.Read(addr); Assert.Equal(100, result.X); Assert.Equal(200, result.Y); } finally { pin.Free(); } } [Fact] public void Read_bytes_writes_and_reads_back() { using var reader = OpenSelf(); byte[] buffer = new byte[16]; GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); byte[] expected = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; int written = reader.WriteBytes(addr, expected); Assert.Equal(expected.Length, written); byte[] actual = reader.ReadBytes(addr, expected.Length); Assert.Equal(expected, actual); } finally { pin.Free(); } } [Fact] public void Read_int_array_writes_and_reads_back() { using var reader = OpenSelf(); int[] buffer = new int[4]; GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); int[] expected = [10, 20, 30, 40]; Assert.True(reader.Write(addr, expected)); int[] actual = reader.Read(addr, 4); Assert.Equal(expected, actual); } finally { pin.Free(); } } [Fact] public void Read_struct_array_writes_and_reads_back() { using var reader = OpenSelf(); var buffer = new TestStruct[4]; GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); var expected = new[] { new TestStruct { X = 1, Y = 2 }, new TestStruct { X = 3, Y = 4 }, new TestStruct { X = 5, Y = 6 }, new TestStruct { X = 7, Y = 8 }, }; Assert.True(reader.Write(addr, expected)); var actual = reader.Read(addr, 4); Assert.Equal(expected, actual); } finally { pin.Free(); } } [Fact] public void Write_returns_false_for_invalid_address() { using var reader = OpenSelf(); Assert.False(reader.Write(IntPtr.Zero, 42)); } [Fact] public void Dispose_closes_handle() { var reader = OpenSelf(); Assert.False(reader.Handle.IsClosed); reader.Dispose(); Assert.True(reader.Handle.IsClosed); } [Fact] public void Double_dispose_does_not_throw() { var reader = OpenSelf(); reader.Dispose(); reader.Dispose(); } // ── Marshal-path round-trip ─────────────────────────────────────────────── // // The marshal path was previously sized using MarshalCache.Size (managed // layout width). When a struct carries an inline marshal-expanded field // (ByValTStr, ByValArray, etc.) this width is smaller than the actual // read/write width — writing overflows the pinned buffer and reading // under-fetches the remote bytes, producing silent heap corruption. // // The marshal path must use MarshalCache.MarshalSize (= Marshal.SizeOf) // so the pinned buffer is large enough for PtrToStructure / StructureToPtr. [Fact] public void Read_struct_via_marshal_path_round_trips_inline_string() { using var reader = OpenSelf(); // A marshal-path struct carries a reference, so it cannot be pinned; the // target must be an unmanaged buffer of the FULL marshal width. Pre-patch, // Write sized its scratch buffer with MarshalCache.Size (managed pointer // width, 8) and StructureToPtr overran it, while Read under-fetched the // remote bytes — the string came back wrong. Post-patch both use // MarshalSize (Marshal.SizeOf). int size = Marshal.SizeOf(); IntPtr addr = Marshal.AllocHGlobal(size); try { InlineStr original = new InlineStr { Name = "Hello, World!" }; Assert.True(reader.Write(addr, original)); InlineStr read = reader.Read(addr); Assert.Equal("Hello, World!", read.Name); } finally { Marshal.FreeHGlobal(addr); } } // ── Graceful failure on invalid addresses ─────────────────────────────── [Fact] public void Read_int_on_invalid_address_returns_default() { using var reader = OpenSelf(); Assert.Equal(0, reader.Read(IntPtr.Zero)); } [Fact] public void Read_struct_on_invalid_address_returns_default() { using var reader = OpenSelf(); var result = reader.Read(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(IntPtr.Zero, 10)); } [Fact] public void Read_bytes_on_invalid_address_returns_empty() { using var reader = OpenSelf(); Assert.Empty(reader.ReadBytes(IntPtr.Zero, 10)); } } /// /// A simple blittable struct for use in tests. /// [StructLayout(LayoutKind.Sequential)] public struct TestStruct : IEquatable { public int X; public int Y; public bool Equals(TestStruct other) => X == other.X && Y == other.Y; public override bool Equals(object? obj) => obj is TestStruct other && Equals(other); public override int GetHashCode() => HashCode.Combine(X, Y); public override string ToString() => $"({X}, {Y})"; } /// /// A struct whose managed layout is just a reference pointer (8 bytes) but whose /// unmanaged marshal layout carries an inline character buffer. Exercised by /// /// to catch regressions where the marshal path uses the managed width instead /// of the marshal width. /// [StructLayout(LayoutKind.Sequential)] public struct InlineStr { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string Name; }