using System.ComponentModel;
using System.Diagnostics;
namespace WhiteMagic.Discovery;
///
/// Scans process memory for a byte pattern with an optional wildcard mask.
///
public static class PatternScanner
{
///
/// Scans a memory range for the first occurrence of a pattern with an optional wildcard mask.
///
/// The memory accessor.
/// 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, or if not found.
///
/// is empty, or length does not match
/// length, or contains invalid characters.
///
/// Memory read fails with an unexpected error.
public static IntPtr Find(
MemoryBase memory,
byte[] pattern,
string? mask,
IntPtr start,
IntPtr end)
{
ArgumentNullException.ThrowIfNull(memory);
ArgumentNullException.ThrowIfNull(pattern);
if (pattern.Length == 0)
throw new ArgumentException("Pattern cannot be empty.", nameof(pattern));
// Validate and normalize mask
if (mask is not null)
{
if (mask.Length != pattern.Length)
throw new ArgumentException(
$"Mask length ({mask.Length}) must match pattern length ({pattern.Length}).",
nameof(mask));
foreach (char c in mask)
{
if (c != 'x' && c != '?')
throw new ArgumentException(
$"Mask may contain only 'x' (match) or '?' (wildcard); found '{c}'.",
nameof(mask));
}
}
// Null mask means treat all bytes as 'x' (exact match)
mask ??= new string('x', pattern.Length);
// Scan range in reasonable chunks (64 KB to avoid massive single reads)
const int chunkSize = 64 * 1024;
int patternLen = pattern.Length;
long rangeSize = (long)end - (long)start;
if (rangeSize <= 0)
return IntPtr.Zero;
// For small ranges, read all at once
if (rangeSize <= chunkSize)
{
byte[] buffer = memory.ReadBytes(start, (int)rangeSize);
return FindInBuffer(buffer, pattern, mask, start);
}
// For larger ranges, scan in chunks
long remaining = rangeSize;
IntPtr current = start;
while (remaining > 0)
{
int toRead = (int)Math.Min(chunkSize, remaining);
byte[] chunk = memory.ReadBytes(current, toRead);
// Empty read means we hit an unmapped region or read failure
if (chunk.Length == 0)
{
// Skip past this unreadable region
current += toRead;
remaining -= toRead;
continue;
}
// Search in this chunk
IntPtr found = FindInBuffer(chunk, pattern, mask, current);
if (found != IntPtr.Zero)
return found;
// Move to next chunk, leaving room for pattern that might straddle boundary
// We advance by (chunkSize - patternLen + 1) to ensure we don't miss matches
int advance = toRead - patternLen + 1;
if (advance <= 0)
advance = toRead;
current += advance;
remaining -= advance;
}
return IntPtr.Zero;
}
///
/// Scans a module's memory region (from its base address through its size) for a pattern.
///
/// The memory accessor.
/// 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, or if not found.
public static IntPtr FindInModule(
MemoryBase memory,
byte[] pattern,
string? mask,
ProcessModule module)
{
ArgumentNullException.ThrowIfNull(module);
IntPtr start = module.BaseAddress;
IntPtr end = start + module.ModuleMemorySize;
return Find(memory, pattern, mask, start, end);
}
///
/// Scans multiple modules for a pattern, returning the first match found.
///
/// The memory accessor.
/// 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, or if not found.
public static IntPtr FindInModules(
MemoryBase memory,
byte[] pattern,
string? mask,
IEnumerable modules)
{
ArgumentNullException.ThrowIfNull(modules);
foreach (var module in modules)
{
IntPtr found = FindInModule(memory, pattern, mask, module);
if (found != IntPtr.Zero)
return found;
}
return IntPtr.Zero;
}
///
/// Searches a buffer for the first pattern match given a mask.
///
private static IntPtr FindInBuffer(
byte[] buffer,
byte[] pattern,
string mask,
IntPtr bufferBase)
{
if (buffer.Length < pattern.Length)
return IntPtr.Zero;
int patternLen = pattern.Length;
int maxOffset = buffer.Length - patternLen;
for (int offset = 0; offset <= maxOffset; offset++)
{
bool match = true;
for (int i = 0; i < patternLen; i++)
{
// Only compare if mask says 'x' (exact match required)
if (mask[i] == 'x' && buffer[offset + i] != pattern[i])
{
match = false;
break;
}
}
if (match)
return bufferBase + offset;
}
return IntPtr.Zero;
}
}