using System.Collections.Concurrent; using System.Diagnostics; namespace WhiteMagic.Discovery; /// /// Caches pattern scan results to avoid repeated scans of the same memory range. /// public sealed class PatternScannerCache { private readonly ConcurrentDictionary _cache = new(); private readonly MemoryBase _memory; /// /// Creates a new cache for the given memory accessor. /// /// The memory accessor to scan. public PatternScannerCache(MemoryBase memory) { ArgumentNullException.ThrowIfNull(memory); _memory = memory; } /// /// Finds a pattern, returning a cached result if available. /// /// The byte pattern to search for. /// /// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard". /// If , all bytes are treated as 'x' (exact match). /// /// The starting address of the scan range. /// The ending address (exclusive) of the scan range. /// /// The address of the first match from cache or memory, or if not found. /// 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; } /// /// Finds a pattern within a module, returning a cached result if available. /// /// The byte pattern to search for. /// /// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard". /// If , all bytes are treated as 'x' (exact match). /// /// The module to scan. /// /// The address of the first match from cache or memory, or if not found. /// 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); } /// /// Finds a pattern across multiple modules, returning a cached result if available. /// /// The byte pattern to search for. /// /// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard". /// If , all bytes are treated as 'x' (exact match). /// /// The modules to scan, in order. /// /// The address of the first match from cache or memory, or if not found. /// public IntPtr FindInModulesCached( byte[] pattern, string? mask, IEnumerable 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; } /// /// Clears all cached scan results. /// public void Clear() { _cache.Clear(); } /// /// Cache key combining pattern, mask, and address range. /// private sealed record CacheKey( byte[] Pattern, string? Mask, IntPtr Start, IntPtr End, IReadOnlyList? Modules = null) : IEquatable { // 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(); } } }