## ADDED Requirements ### Requirement: Abstract memory base with two readers WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read`/`Write`, array read/write, and string read/write, with two concrete implementations: `ExternalReader` (out-of-process via ReadProcessMemory/WriteProcessMemory) and `InProcessReader` (in-process, reading the current process through ReadProcessMemory/WriteProcessMemory on a self-handle). > **Deviation from design D1.** D1 originally specified `InProcessReader` as `unsafe` direct pointer dereference (the "fast/crash-free" path). Implementation revised it to `ReadProcessMemory`/`WriteProcessMemory` on a handle to the current process, because .NET (Core) cannot catch `AccessViolationException` (`HandleProcessCorruptedStateExceptions` is removed), so a raw deref of a bad address terminates the host process with no soft-failure path. RPM on a self-handle fails soft (returns empty) like `ExternalReader`. The in-process performance win therefore moves to the delegate-call and detour paths (`InProcessInvoker`, `DetourManager`), not the reader. #### Scenario: in-process read fails soft on an invalid address - **WHEN** an `InProcessReader` reads an unmapped or protected address - **THEN** it MUST return empty/`default` rather than crash the host process #### Scenario: external read round-trip - **WHEN** an `ExternalReader` opens a target process and writes a value with `Write(addr, 0x1234)` then reads it back with `Read(addr)` - **THEN** the returned value MUST equal `0x1234` #### Scenario: in-process read of own memory - **WHEN** an `InProcessReader` reads a known address in its own process - **THEN** the value MUST match a direct managed read of the same address #### Scenario: shared API surface - **WHEN** code is written against the `MemoryBase` abstract type - **THEN** it MUST operate unchanged against both `ExternalReader` and `InProcessReader` ### Requirement: Typed read/write via marshal cache `MemoryBase` SHALL support generic `Read`/`Write` for blittable and marshalled struct types, using a per-type `MarshalCache` that caches size, type code, and marshalling requirements to avoid per-call reflection. #### Scenario: blittable struct round-trip - **WHEN** a blittable `[StructLayout(LayoutKind.Sequential)]` struct is written and read back - **THEN** all fields MUST be preserved exactly #### Scenario: marshal cache computed once - **WHEN** `Read` is invoked repeatedly for the same type `T` - **THEN** `Marshal.SizeOf` and type inspection for `T` MUST be computed at most once and reused #### Scenario: array read - **WHEN** `Read(addr, count)` is called - **THEN** it MUST return an array of exactly `count` elements read contiguously from `addr` ### Requirement: String read and write with encoding `MemoryBase` SHALL read and write strings with a caller-specified `Encoding` and a maximum length, terminating reads at a null terminator or the maximum length. #### Scenario: ASCII write then read - **WHEN** `WriteString(addr, "hello", Encoding.ASCII)` is called then `ReadString(addr, Encoding.ASCII)` - **THEN** the result MUST equal `"hello"` #### Scenario: read stops at null terminator - **WHEN** a null-terminated string shorter than `maxLength` is read - **THEN** the returned string MUST exclude the terminator and everything after it ### Requirement: Relative and absolute addressing `MemoryBase` SHALL convert between addresses relative to the module image base and absolute addresses via `GetAbsolute` and `GetRelative`, and accept an `isRelative` flag on read/write operations. #### Scenario: relative resolves against image base - **WHEN** `GetAbsolute(relative)` is called with the process image base known - **THEN** the result MUST equal `imageBase + relative` #### Scenario: read with isRelative - **WHEN** `Read(offset, isRelative: true)` is called - **THEN** the read MUST occur at `GetAbsolute(offset)`