Implement core diagnostic memory layer, execution helpers, and high-level facade slices

Implemented:
- Core: UTF-16 ReadString boundary/alignment fix, target bitness and process id on MemoryBase
- function interception: PatchManager, DetourManager, InstructionAnalyzer, MainThreadDispatcher
- Execution: BackgroundTaskExecutor, InProcessInvoker
- High-level: Magic facade, RemotePointer, async wrappers
- Discovery/external code loading/Window groundwork (PEB/TEB, pattern scanning, raw allocations, DLL external code loading, window/input)

Tests: 180 passing, 4 integration/interactive tests skipped.
This commit is contained in:
kbe
2026-07-21 23:43:14 +02:00
parent a0ca7050a2
commit 3f0bea6bd4
44 changed files with 5595 additions and 84 deletions
+157
View File
@@ -0,0 +1,157 @@
using System.Collections.Concurrent;
using System.Diagnostics;
namespace WhiteMagic.Discovery;
/// <summary>
/// Caches pattern scan results to avoid repeated scans of the same memory range.
/// </summary>
public sealed class PatternScannerCache
{
private readonly ConcurrentDictionary<CacheKey, IntPtr> _cache = new();
private readonly MemoryBase _memory;
/// <summary>
/// Creates a new cache for the given memory accessor.
/// </summary>
/// <param name="memory">The memory accessor to scan.</param>
public PatternScannerCache(MemoryBase memory)
{
ArgumentNullException.ThrowIfNull(memory);
_memory = memory;
}
/// <summary>
/// Finds a pattern, returning a cached result if available.
/// </summary>
/// <param name="pattern">The byte pattern to search for.</param>
/// <param name="mask">
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
/// </param>
/// <param name="start">The starting address of the scan range.</param>
/// <param name="end">The ending address (exclusive) of the scan range.</param>
/// <returns>
/// The address of the first match from cache or memory, or <see cref="IntPtr.Zero"/> if not found.
/// </returns>
public IntPtr FindCached(
byte[] pattern,
string? mask,
IntPtr start,
IntPtr end)
{
var key = new CacheKey(pattern, mask, start, end);
// Try to get from cache first
if (_cache.TryGetValue(key, out IntPtr cached))
return cached;
// Not in cache, perform the scan
IntPtr found = PatternScanner.Find(_memory, pattern, mask, start, end);
// Cache the result (even if Zero)
_cache[key] = found;
return found;
}
/// <summary>
/// Finds a pattern within a module, returning a cached result if available.
/// </summary>
/// <param name="pattern">The byte pattern to search for.</param>
/// <param name="mask">
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
/// </param>
/// <param name="module">The module to scan.</param>
/// <returns>
/// The address of the first match from cache or memory, or <see cref="IntPtr.Zero"/> if not found.
/// </returns>
public IntPtr FindInModuleCached(
byte[] pattern,
string? mask,
ProcessModule module)
{
ArgumentNullException.ThrowIfNull(module);
IntPtr start = module.BaseAddress;
IntPtr end = start + module.ModuleMemorySize;
return FindCached(pattern, mask, start, end);
}
/// <summary>
/// Finds a pattern across multiple modules, returning a cached result if available.
/// </summary>
/// <param name="pattern">The byte pattern to search for.</param>
/// <param name="mask">
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
/// </param>
/// <param name="modules">The modules to scan, in order.</param>
/// <returns>
/// The address of the first match from cache or memory, or <see cref="IntPtr.Zero"/> if not found.
/// </returns>
public IntPtr FindInModulesCached(
byte[] pattern,
string? mask,
IEnumerable<ProcessModule> modules)
{
// For multiple modules, we use a combined key (all modules hashed together)
// This is less granular but still useful for repeated queries
var moduleList = modules.ToList();
var key = new CacheKey(pattern, mask, IntPtr.Zero, IntPtr.Zero, Modules: moduleList);
if (_cache.TryGetValue(key, out IntPtr cached))
return cached;
IntPtr found = PatternScanner.FindInModules(_memory, pattern, mask, moduleList);
_cache[key] = found;
return found;
}
/// <summary>
/// Clears all cached scan results.
/// </summary>
public void Clear()
{
_cache.Clear();
}
/// <summary>
/// Cache key combining pattern, mask, and address range.
/// </summary>
private sealed record CacheKey(
byte[] Pattern,
string? Mask,
IntPtr Start,
IntPtr End,
IReadOnlyList<ProcessModule>? Modules = null) : IEquatable<CacheKey>
{
// Override GetHashCode to hash the contents, not references
public override int GetHashCode()
{
var hash = new HashCode();
// Hash pattern bytes
foreach (byte b in Pattern)
hash.Add(b);
// Hash mask
hash.Add(Mask?.GetHashCode() ?? 0);
// Hash address range
hash.Add(Start.GetHashCode());
hash.Add(End.GetHashCode());
// Hash modules if present (by base address)
if (Modules is not null)
{
foreach (var m in Modules)
hash.Add(m.BaseAddress.GetHashCode());
}
return hash.ToHashCode();
}
}
}