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).
This commit is contained in:
kbe
2026-07-21 17:06:34 +02:00
parent 6b34e0fab8
commit b2a5090533
2 changed files with 187 additions and 3 deletions
+4 -3
View File
@@ -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<byte>(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;
}
/// <summary>Writes a null-terminated string to the target address.</summary>
+183
View File
@@ -0,0 +1,183 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using WhiteMagic;
using WhiteMagic.Native;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="MemoryBase.ReadString"/> and <see cref="MemoryBase.WriteString"/>
/// with encoding, null-terminator stop, and max-length behavior.
/// </summary>
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();
}
}
}