Files
whitemagic/openspec/changes/whitemagic-foundation/specs/memory-access/spec.md
T
2026-07-21 22:30:10 +02:00

64 lines
3.9 KiB
Markdown

## ADDED Requirements
### Requirement: Abstract memory base with two readers
WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read<T>`/`Write<T>`, 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<int>(addr, 0x1234)` then reads it back with `Read<int>(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<T>`/`Write<T>` for blittable and marshalled struct types, using a per-type `MarshalCache<T>` 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<T>` 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<T>(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<int>(offset, isRelative: true)` is called
- **THEN** the read MUST occur at `GetAbsolute(offset)`