fix 7 correctness and cleanup issues
Blocking fixes: 1. Read<T> now returns default(T) on failed/partial read instead of crash (applies to single Read<T>, array Read<T>, and ReadBytes) 2. InProcessReader uses ReadProcessMemory via handle instead of unsafe Buffer.MemoryCopy — fails soft on bad address instead of AV'ing 3. GetRelative now returns absolute - ImageBase (inverse of GetAbsolute). Fix round-trip test to validate at arbitrary offsets, not just ImageBase 4. ReadString reads in 64-byte chunks with encoding-aware null-terminator pattern matching (handles UTF-16's 2-byte null, UTF-32's 4-byte null) Cleanup: 5. InProcessReader validates handle on open and uses RPM through it (handle is no longer unused) 6. Array marshal read: pin raw buffer once, PtrToStructure at offset 7. StructureToByteArray(Span) delegates to byte[] overload, no duplicate New tests: 4 invalid-address grace tests (returns default/empty/false). All 57 passing.
This commit is contained in:
+96
-33
@@ -30,6 +30,8 @@ public abstract class MemoryBase : IDisposable
|
||||
// ── Typed IO ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reads a value of type <typeparamref name="T"/> from the target address.</summary>
|
||||
/// <returns>The value, or <c>default(T)</c> when the read fails or returns fewer bytes than
|
||||
/// <see cref="MarshalCache{T}.Size"/>.</returns>
|
||||
public T Read<T>(IntPtr address, bool isRelative = false) where T : struct
|
||||
{
|
||||
if (isRelative)
|
||||
@@ -38,10 +40,11 @@ public abstract class MemoryBase : IDisposable
|
||||
int size = MarshalCache<T>.Size;
|
||||
byte[] raw = ReadBytes(address, size);
|
||||
|
||||
if (raw.Length < size)
|
||||
return default;
|
||||
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
return MarshalByteArrayToStructure<T>(raw);
|
||||
}
|
||||
|
||||
return MemoryMarshal.Read<T>(raw.AsSpan());
|
||||
}
|
||||
@@ -57,9 +60,7 @@ public abstract class MemoryBase : IDisposable
|
||||
|
||||
byte[] raw;
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
raw = StructureToByteArray(value, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
raw = new byte[size];
|
||||
@@ -71,6 +72,8 @@ public abstract class MemoryBase : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Reads an array of values of type <typeparamref name="T"/> from the target address.</summary>
|
||||
/// <returns>An array of at most <paramref name="count"/> elements. May be shorter when the read
|
||||
/// returns fewer bytes than expected.</returns>
|
||||
public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct
|
||||
{
|
||||
if (isRelative)
|
||||
@@ -79,24 +82,32 @@ public abstract class MemoryBase : IDisposable
|
||||
int elementSize = MarshalCache<T>.Size;
|
||||
int totalSize = elementSize * count;
|
||||
byte[] raw = ReadBytes(address, totalSize);
|
||||
int actualCount = Math.Min(count, raw.Length / elementSize);
|
||||
|
||||
var result = new T[count];
|
||||
var result = new T[actualCount];
|
||||
|
||||
if (actualCount == 0)
|
||||
return result;
|
||||
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
GCHandle pin = GCHandle.Alloc(raw, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
var elementBytes = new ReadOnlySpan<byte>(raw, i * elementSize, elementSize);
|
||||
result[i] = MarshalByteArrayToStructure<T>(elementBytes.ToArray());
|
||||
IntPtr basePtr = pin.AddrOfPinnedObject();
|
||||
for (int i = 0; i < actualCount; i++)
|
||||
result[i] = Marshal.PtrToStructure<T>(basePtr + (i * elementSize));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadOnlySpan<byte> span = raw;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
for (int i = 0; i < actualCount; i++)
|
||||
result[i] = MemoryMarshal.Read<T>(span.Slice(i * elementSize, elementSize));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -121,13 +132,9 @@ public abstract class MemoryBase : IDisposable
|
||||
{
|
||||
Span<byte> slice = span.Slice(i * elementSize, elementSize);
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
StructureToByteArray(values[i], slice, elementSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
MemoryMarshal.Write(slice, in values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int written = WriteBytes(address, raw, false);
|
||||
@@ -136,17 +143,61 @@ public abstract class MemoryBase : IDisposable
|
||||
|
||||
// ── String IO ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reads a null-terminated string from the target address.</summary>
|
||||
/// <summary>Reads a null-terminated string from the target address by scanning in small
|
||||
/// chunks. Stops at the null terminator, the maximum length, or the first page boundary
|
||||
/// that fails to read (avoids an atomic failure when a 512-byte window crosses an unmapped
|
||||
/// region).</summary>
|
||||
/// <param name="address">The address to read from.</param>
|
||||
/// <param name="encoding">The text encoding.</param>
|
||||
/// <param name="maxLength">The maximum number of bytes to read.</param>
|
||||
/// <param name="relative">If <see langword="true"/>, <paramref name="address"/> is relative
|
||||
/// to <see cref="ImageBase"/>.</param>
|
||||
public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false)
|
||||
{
|
||||
byte[] buffer = ReadBytes(address, maxLength, relative);
|
||||
string decoded = encoding.GetString(buffer);
|
||||
int nullIndex = decoded.IndexOf('\0');
|
||||
if (nullIndex >= 0)
|
||||
if (relative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
// The encoded null terminator. For ASCII/UTF-8 this is a single 0x00 byte;
|
||||
// for UTF-16 it is two zero bytes (0x00 0x00); for UTF-32 it is four.
|
||||
byte[] nullTerminator = encoding.GetBytes("\0");
|
||||
|
||||
const int chunkSize = 64;
|
||||
int remaining = maxLength;
|
||||
var accumulated = new System.Collections.Generic.List<byte[]>();
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
return decoded[..nullIndex];
|
||||
int take = Math.Min(chunkSize, remaining);
|
||||
byte[] chunk = ReadBytes(address, take);
|
||||
if (chunk.Length == 0)
|
||||
break;
|
||||
|
||||
int nullPos = IndexOfPattern(chunk, nullTerminator);
|
||||
if (nullPos >= 0)
|
||||
{
|
||||
if (nullPos > 0)
|
||||
accumulated.Add(chunk[..nullPos]);
|
||||
break;
|
||||
}
|
||||
|
||||
accumulated.Add(chunk);
|
||||
address += take;
|
||||
remaining -= take;
|
||||
}
|
||||
return decoded;
|
||||
|
||||
int totalLength = 0;
|
||||
foreach (byte[] part in accumulated)
|
||||
totalLength += part.Length;
|
||||
|
||||
byte[] combined = new byte[totalLength];
|
||||
int offset = 0;
|
||||
foreach (byte[] part in accumulated)
|
||||
{
|
||||
part.CopyTo(combined, offset);
|
||||
offset += part.Length;
|
||||
}
|
||||
|
||||
return encoding.GetString(combined);
|
||||
}
|
||||
|
||||
/// <summary>Writes a null-terminated string to the target address.</summary>
|
||||
@@ -168,10 +219,11 @@ public abstract class MemoryBase : IDisposable
|
||||
return ImageBase + (nint)relative;
|
||||
}
|
||||
|
||||
/// <summary>Converts an absolute address to a relative offset from <see cref="ImageBase"/>.</summary>
|
||||
/// <summary>Converts an absolute address to a relative offset from <see cref="ImageBase"/>.
|
||||
/// This is the inverse of <see cref="GetAbsolute"/>: <c>GetAbsolute(GetRelative(a)) == a</c>.</summary>
|
||||
public IntPtr GetRelative(IntPtr absolute)
|
||||
{
|
||||
return (IntPtr)((nint)ImageBase - (nint)absolute);
|
||||
return (IntPtr)((nint)absolute - (nint)ImageBase);
|
||||
}
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────
|
||||
@@ -214,16 +266,27 @@ public abstract class MemoryBase : IDisposable
|
||||
|
||||
private static void StructureToByteArray<T>(T value, Span<byte> destination, int size) where T : struct
|
||||
{
|
||||
byte[] temp = destination.ToArray();
|
||||
GCHandle pin = GCHandle.Alloc(temp, GCHandleType.Pinned);
|
||||
try
|
||||
byte[] bytes = StructureToByteArray(value, size);
|
||||
bytes.CopyTo(destination);
|
||||
}
|
||||
|
||||
private static int IndexOfPattern(byte[] data, byte[] pattern)
|
||||
{
|
||||
int lastStart = data.Length - pattern.Length;
|
||||
for (int i = 0; i <= lastStart; i++)
|
||||
{
|
||||
Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false);
|
||||
temp.CopyTo(destination);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
bool match = true;
|
||||
for (int j = 0; j < pattern.Length; j++)
|
||||
{
|
||||
if (data[i + j] != pattern[j])
|
||||
{
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user