using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using WhiteMagic; using WhiteMagic.Native; namespace WhiteMagicTest; /// /// Tests for and /// with encoding, null-terminator stop, and max-length behavior. /// public class StringReadWriteTests { private static ExternalReader OpenSelf() { return new ExternalReader( Process.GetCurrentProcess(), ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation); } [Fact] public void WriteString_ascii_then_ReadString_round_trips() { using var reader = OpenSelf(); byte[] slot = new byte[64]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.WriteString(addr, "hello", Encoding.ASCII)); string result = reader.ReadString(addr, Encoding.ASCII); Assert.Equal("hello", result); } finally { pin.Free(); } } [Fact] public void WriteString_utf8_then_ReadString_round_trips() { using var reader = OpenSelf(); byte[] slot = new byte[64]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.WriteString(addr, "héllo wörld", Encoding.UTF8)); string result = reader.ReadString(addr, Encoding.UTF8); Assert.Equal("héllo wörld", result); } finally { pin.Free(); } } [Fact] public void WriteString_unicode_then_ReadString_round_trips() { using var reader = OpenSelf(); byte[] slot = new byte[128]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); Assert.True(reader.WriteString(addr, "Hello\u00A9\u00AE\u20AC", Encoding.Unicode)); string result = reader.ReadString(addr, Encoding.Unicode); Assert.Equal("Hello\u00A9\u00AE\u20AC", result); } finally { pin.Free(); } } [Fact] public void ReadString_stops_at_null_terminator() { using var reader = OpenSelf(); byte[] slot = Encoding.ASCII.GetBytes("hello\0world"); GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 64); Assert.Equal("hello", result); } finally { pin.Free(); } } [Fact] public void ReadString_respects_max_length() { using var reader = OpenSelf(); byte[] slot = Encoding.ASCII.GetBytes("hello world this is a test"); GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 5); Assert.Equal("hello", result); } finally { pin.Free(); } } [Fact] public void WriteString_appends_null_terminator_automatically() { using var reader = OpenSelf(); byte[] slot = new byte[32]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); // Write without terminator Assert.True(reader.WriteString(addr, "test", Encoding.ASCII)); // The written bytes should end with \0 byte[] read = reader.ReadBytes(addr, 8); Assert.Equal((byte)'t', read[0]); Assert.Equal((byte)'e', read[1]); Assert.Equal((byte)'s', read[2]); Assert.Equal((byte)'t', read[3]); Assert.Equal(0, read[4]); // null terminator } finally { pin.Free(); } } [Fact] public void ReadString_empty_buffer_returns_empty_string() { using var reader = OpenSelf(); byte[] slot = new byte[1] { 0 }; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 1); Assert.Equal("", result); } finally { pin.Free(); } } /// /// UTF-16 null terminator split across the 64-byte chunk boundary must still be found. /// The first chunk ends at byte 63, so the null bytes at 64/65 are in the second chunk. /// [Fact] public void ReadString_utf16_null_across_chunk_boundary_is_found() { using var reader = OpenSelf(); byte[] slot = new byte[256]; // 32 'A' UTF-16 chars = 64 bytes, no embedded null. byte[] text = Encoding.Unicode.GetBytes(new string('A', 32)); Assert.Equal(64, text.Length); text.CopyTo(slot, 0); // Null terminator at bytes 64/65. slot[64] = 0x00; slot[65] = 0x00; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); string result = reader.ReadString(addr, Encoding.Unicode, maxLength: 256); Assert.Equal(new string('A', 32), result); } finally { pin.Free(); } } /// /// A byte sequence that looks like a null at a misaligned offset must not stop the scan. /// "A" + U+4200 produces bytes 41 00 00 42 00 00; bytes 1-2 are an aligned-position null /// only if scanned byte-by-byte. The aligned UTF-16 scan must see the real terminator. /// [Fact] public void ReadString_utf16_does_not_stop_at_misaligned_null() { using var reader = OpenSelf(); byte[] slot = Encoding.Unicode.GetBytes("A\u4200\0"); GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); string result = reader.ReadString(addr, Encoding.Unicode, maxLength: 64); Assert.Equal("A\u4200", result); } finally { pin.Free(); } } [Fact] public void WriteString_empty_string_writes_only_null() { using var reader = OpenSelf(); byte[] slot = new byte[8]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); // Write a marker first reader.WriteBytes(addr, [0xAB, 0xCD, 0xEF, 0x00]); // Now overwrite with empty string Assert.True(reader.WriteString(addr, "", Encoding.ASCII)); byte[] read = reader.ReadBytes(addr, 4); Assert.Equal(0, read[0]); // null } finally { pin.Free(); } } }