using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using WhiteMagic; using WhiteMagic.Native; namespace WhiteMagicTest; /// /// Regression tests for the edge-case defects found in the second review pass: /// char sizing, reference-containing structs, unchecked counts, the ReadString /// partial-chunk skip, and ExternalReader construction against awkward targets. /// Each test fails against the pre-fix code. /// public class MemoryHardeningTests { private static ExternalReader OpenSelf() { return new ExternalReader( Process.GetCurrentProcess(), ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation); } // ── char sizing (MarshalCache / MemoryBase.Read) ────────────────── [Fact] public void MarshalCache_char_size_is_two_bytes() { Assert.Equal(2, MarshalCache.Size); } [Fact] public void Read_char_writes_and_reads_back() { using var reader = OpenSelf(); char slot = '\0'; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.Write(addr, 'Z')); Assert.Equal('Z', reader.Read(addr)); } finally { pin.Free(); } } [Fact] public void Read_char_array_writes_and_reads_back() { using var reader = OpenSelf(); char[] slot = new char[4]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); char[] expected = ['w', 'o', 'w', '!']; Assert.True(reader.Write(addr, expected)); Assert.Equal(expected, reader.Read(addr, 4)); } finally { pin.Free(); } } // ── reference-containing structs route to the marshal path ────────────── [Fact] public void MarshalCache_flags_struct_with_reference_field_as_marshal_required() { // Has a string field but no [MarshalAs]; the blittable path (MemoryMarshal.Read) // throws for a reference-containing T, so the cache must route it to the marshal // path. A struct with a managed reference cannot be pinned, so the routing flag — // not a live round-trip — is the regression guard here. Assert.True(MarshalCache.TypeRequiresMarshal); } // ── unchecked count guards ────────────────────────────────────────────── [Fact] public void Read_array_with_negative_count_throws_argument_out_of_range() { using var reader = OpenSelf(); int dummy = 0; GCHandle pin = GCHandle.Alloc(dummy, GCHandleType.Pinned); try { Assert.Throws(() => reader.Read(pin.AddrOfPinnedObject(), -1)); } finally { pin.Free(); } } [Fact] public void Read_array_with_zero_count_returns_empty() { using var reader = OpenSelf(); int dummy = 0; GCHandle pin = GCHandle.Alloc(dummy, GCHandleType.Pinned); try { Assert.Empty(reader.Read(pin.AddrOfPinnedObject(), 0)); } finally { pin.Free(); } } // ── ReadString partial-chunk advance ──────────────────────────────────── [Fact] public void ReadString_advances_by_actual_bytes_when_reads_are_partial() { // The reader serves at most 3 bytes per call. The string is longer than one // chunk with the terminator well past it. If ReadString advanced by the // requested count instead of the bytes actually returned, it would skip // data and truncate the result. byte[] data = Encoding.ASCII.GetBytes("ABCDEFGHIJ\0"); var reader = new PartialReader(data, maxChunk: 3); string result = reader.ReadString(IntPtr.Zero, Encoding.ASCII, maxLength: 64); Assert.Equal("ABCDEFGHIJ", result); } [Fact] public void ReadString_stops_at_null_across_partial_chunks() { byte[] data = Encoding.ASCII.GetBytes("hi\0garbage"); var reader = new PartialReader(data, maxChunk: 1); string result = reader.ReadString(IntPtr.Zero, Encoding.ASCII, maxLength: 64); Assert.Equal("hi", result); } // ── ExternalReader construction ───────────────────────────────────────── [Fact] public void ExternalReader_opens_self_with_default_access() { // The default access set must be small enough to open a normal process. using var reader = new ExternalReader(Process.GetCurrentProcess()); Assert.False(reader.Handle.IsInvalid); } /// /// A that serves bytes from an in-memory buffer and /// caps every read to maxChunk bytes, to exercise partial-read handling. /// The address is treated as a zero-based index into the buffer. /// private sealed class PartialReader(byte[] data, int maxChunk) : MemoryBase { public override IntPtr ImageBase => IntPtr.Zero; public override SafeMemoryHandle Handle => null!; public override bool Is64Bit => Environment.Is64BitProcess; public override int ProcessId => Environment.ProcessId; public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false) { int start = (int)address; if (start < 0 || start >= data.Length || count <= 0) return []; int n = Math.Min(Math.Min(count, maxChunk), data.Length - start); return data[start..(start + n)]; } public override int WriteBytes(IntPtr address, ReadOnlySpan bytes, bool isRelative = false) => throw new NotSupportedException(); public override void Dispose() { } } } /// /// A struct that carries a managed reference. /// reports , so it cannot travel the blittable read path. /// [StructLayout(LayoutKind.Sequential)] public struct StructWithReference { public int Id; public string Name; }