From b2a5090533ef4fa0661b7b543df06864a599488e Mon Sep 17 00:00:00 2001 From: Kevin Bataille Date: Tue, 21 Jul 2026 17:06:34 +0200 Subject: [PATCH] task 2.5-2.6: string Read/Write with encoding tests Add 8 tests for string IO: ASCII/UTF8/Unicode round-trip, null-terminator stop, max-length truncation, auto-append of null terminator, empty string. Fix ReadString null-terminator detection for multi-byte encodings (UTF-16): decode string first, then find \0 in characters not bytes. All passing (total: 38). --- WhiteMagic/MemoryBase.cs | 7 +- WhiteMagicTest/StringReadWriteTests.cs | 183 +++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 WhiteMagicTest/StringReadWriteTests.cs diff --git a/WhiteMagic/MemoryBase.cs b/WhiteMagic/MemoryBase.cs index 2f7ef33..638d1cf 100644 --- a/WhiteMagic/MemoryBase.cs +++ b/WhiteMagic/MemoryBase.cs @@ -141,12 +141,13 @@ public abstract class MemoryBase : IDisposable public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false) { byte[] buffer = ReadBytes(address, maxLength, relative); - int nullIndex = Array.IndexOf(buffer, 0); + string decoded = encoding.GetString(buffer); + int nullIndex = decoded.IndexOf('\0'); if (nullIndex >= 0) { - return encoding.GetString(buffer, 0, nullIndex); + return decoded[..nullIndex]; } - return encoding.GetString(buffer); + return decoded; } /// Writes a null-terminated string to the target address. diff --git a/WhiteMagicTest/StringReadWriteTests.cs b/WhiteMagicTest/StringReadWriteTests.cs new file mode 100644 index 0000000..6e0cf37 --- /dev/null +++ b/WhiteMagicTest/StringReadWriteTests.cs @@ -0,0 +1,183 @@ +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(); + } + } + + [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(); + } + } +}