Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,8 @@ _ReSharper.Caches/
# unicorn.dll
# libunicorn.so
# libunicorn.dylib

# Agent skills local tracker
CLAUDE.md
docs/agents/
.scratch/
37 changes: 37 additions & 0 deletions UnicornNet.Tests/ControlEngineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using Xunit;

namespace UnicornNet.Tests;

public sealed class ControlEngineTests
{
[Fact]
public void ControlCommand_CarriesArgumentsWithPackedCommandValue()
{
ReadOnlySpan<nint> arguments = [(nint)0x1000, (nint)0x2000];

var command = Unicorn.ControlCommand.Create(
Unicorn.ControlType.TranslationBlockRemove,
arguments,
Unicorn.ControlIo.Write);

Assert.Equal(Unicorn.ControlType.TranslationBlockRemove, command.Type);
Assert.Equal(Unicorn.ControlIo.Write, command.Access);
Assert.Equal(2, command.ArgumentCount);
Assert.Equal(arguments.ToArray(), command.Arguments.ToArray());
}

[Fact]
public void ControlEngine_ForwardsCommandAndArgumentsToNativeProxy()
{
var native = new FakeNativeProxy();
var engine = new ControlEngine(native, () => new IntPtr(0x1234), () => { });
var command = Unicorn.ControlCommand.Read(Unicorn.ControlType.PageSize, [(nint)0x4000]);

engine.Control(command);

Assert.True(native.LastControl.HasValue);
Assert.Equal(command.Value, native.LastControl.Value.Control);
Assert.Equal(command.Arguments.ToArray(), native.LastControl.Value.Arguments);
}
}
54 changes: 54 additions & 0 deletions UnicornNet.Tests/FakeHookManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;

namespace UnicornNet.Tests;

internal sealed class FakeHookManager : IHookManager
{
private readonly Dictionary<Unicorn.HookHandle, (Unicorn.HookType Type, Delegate Callback, object? State)> _hooks = new();
private nuint _nextHandle;

public List<Unicorn.HookHandle> RemovedHooks { get; } = [];

public Unicorn.HookHandle AddHook(Unicorn.HookType type, Delegate callback, Unicorn.HookRange? range = null, object? state = null)
{
ArgumentNullException.ThrowIfNull(callback);

var handle = new Unicorn.HookHandle(++_nextHandle);
_hooks.Add(handle, (type, callback, state));
return handle;
}

public void RemoveHook(Unicorn.HookHandle handle)
{
if (_hooks.Remove(handle))
{
RemovedHooks.Add(handle);
}
}

public void Dispose()
{
_hooks.Clear();
}

public bool TrySimulateHook(Unicorn.HookHandle handle, ulong address, int size)
{
if (!_hooks.TryGetValue(handle, out var registration))
{
return false;
}

switch (registration.Type)
{
case Unicorn.HookType.Code when registration.Callback is Unicorn.CodeHook codeHook:
codeHook(null!, address, size, registration.State);
return true;
case Unicorn.HookType.Block when registration.Callback is Unicorn.BlockHook blockHook:
blockHook(null!, address, size, registration.State);
return true;
default:
return false;
}
}
}
17 changes: 16 additions & 1 deletion UnicornNet.Tests/FakeNativeProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ internal class FakeNativeProxy : IUnicornNativeProxy

public List<(ulong Address, ulong Size, uint Permissions, IntPtr Pointer)> MemMapPtrRequests { get; } = [];

public (ulong Address, ulong Size, uint Permissions)? LastMemMap { get; private set; }

public (ulong Address, ulong Size)? LastMemUnmap { get; private set; }

public (ulong Address, ulong Size, uint Permissions)? LastMemProtect { get; private set; }

public (ulong Address, byte[] Data)? LastMemWrite { get; private set; }

public (ulong Address, int Length)? LastMemRead { get; private set; }

public (int RegisterId, byte[] Value)? LastRegisterWrite { get; private set; }

public Dictionary<int, byte[]> RegisterValues { get; } = new();
Expand All @@ -42,6 +52,7 @@ public virtual int Close(IntPtr engine)

public virtual int MemMap(IntPtr engine, ulong address, ulong size, uint permissions)
{
LastMemMap = (address, size, permissions);
return 0;
}

Expand All @@ -53,21 +64,25 @@ public virtual int MemMapPtr(IntPtr engine, ulong address, ulong size, uint perm

public virtual int MemUnmap(IntPtr engine, ulong address, ulong size)
{
LastMemUnmap = (address, size);
return 0;
}

public virtual int MemProtect(IntPtr engine, ulong address, ulong size, uint permissions)
{
LastMemProtect = (address, size, permissions);
return 0;
}

public virtual int MemWrite(IntPtr engine, ulong address, ReadOnlySpan<byte> data)
{
LastMemWrite = (address, data.ToArray());
return 0;
}

public virtual int MemRead(IntPtr engine, ulong address, Span<byte> buffer)
{
LastMemRead = (address, buffer.Length);
buffer.Clear();
return 0;
}
Expand Down Expand Up @@ -181,4 +196,4 @@ private int RegisterHook(out nuint hookId)
ActiveHooks.Add(hookId);
return 0;
}
}
}
39 changes: 39 additions & 0 deletions UnicornNet.Tests/HookManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Linq;
using Xunit;

namespace UnicornNet.Tests;

public sealed class HookManagerTests
{
[Fact]
public void FakeHookManager_RegistersAndRemovesHooks()
{
var manager = new FakeHookManager();
var invocationCount = 0;

var handle = manager.AddHook(Unicorn.HookType.Code, new Unicorn.CodeHook((_, _, _, _) => invocationCount++));
var invokedBeforeRemoval = manager.TrySimulateHook(handle, 0x1000, 4);

manager.RemoveHook(handle);
var invokedAfterRemoval = manager.TrySimulateHook(handle, 0x1000, 4);

Assert.True(invokedBeforeRemoval);
Assert.False(invokedAfterRemoval);
Assert.Equal(1, invocationCount);
Assert.Contains(handle, manager.RemovedHooks);
}

[Fact]
public void PublicHookRegistrationMethods_AreNotGeneric()
{
var genericHookMethods = typeof(Unicorn).GetMethods()
.Where(method => method.Name.StartsWith("Add", StringComparison.Ordinal)
&& method.Name.EndsWith("Hook", StringComparison.Ordinal)
&& method.IsGenericMethod)
.Select(method => method.Name)
.ToArray();

Assert.Empty(genericHookMethods);
}
}
120 changes: 120 additions & 0 deletions UnicornNet.Tests/MemoryManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System;
using Xunit;

namespace UnicornNet.Tests;

public sealed class MemoryManagerTests
{
private const ulong BaseAddress = 0x10000;
private const ulong RegionSize = 0x1000;

[Fact]
public void MemoryRegion_UsesMemoryManagerForReadWriteProtectAndDispose()
{
var memory = new FakeMemoryManager();
var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var data = new byte[] { 1, 2, 3 };
Span<byte> buffer = stackalloc byte[3];

region.Write(data, 4);
region.Read(buffer, 4);
region.Protect(Unicorn.MemoryPermissions.Read);
region.Dispose();

Assert.Equal((BaseAddress + 4, data), memory.LastWrite);
Assert.Equal((BaseAddress + 4, 3), memory.LastRead);
Assert.Equal((BaseAddress, RegionSize, Unicorn.MemoryPermissions.Read), memory.LastProtect);
Assert.Equal((BaseAddress, RegionSize), memory.LastUnmap);
}

[Fact]
public void MemoryRegion_RejectsWritesOutsideRegionBounds()
{
var memory = new FakeMemoryManager();
var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var data = new byte[2];

Assert.Throws<ArgumentOutOfRangeException>(() => region.Write(data, RegionSize));
}

[Fact]
public void MemoryRegion_RejectsReadsOutsideRegionBounds()
{
var memory = new FakeMemoryManager();
var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var buffer = new byte[2];

Assert.Throws<ArgumentOutOfRangeException>(() => region.Read(buffer, RegionSize));
}

[Fact]
public void MemoryManager_MapCreatesRegionAndForwardsToNativeProxy()
{
var native = new FakeNativeProxy();
var manager = new MemoryManager(native, () => new IntPtr(0x1234), () => { });

var region = manager.Map(BaseAddress, RegionSize, Unicorn.MemoryPermissions.Read);

Assert.Equal(BaseAddress, region.Address);
Assert.Equal(RegionSize, region.Size);
Assert.Equal(Unicorn.MemoryPermissions.Read, region.Permissions);
Assert.Equal((BaseAddress, RegionSize, (uint)Unicorn.MemoryPermissions.Read), native.LastMemMap);
}

[Fact]
public void MemoryManager_ReadAndWriteForwardToNativeProxy()
{
var native = new FakeNativeProxy();
var manager = new MemoryManager(native, () => new IntPtr(0x1234), () => { });
var data = new byte[] { 0xAA, 0xBB };
Span<byte> buffer = stackalloc byte[2];

manager.Write(BaseAddress, data);
manager.Read(BaseAddress + 8, buffer);

Assert.True(native.LastMemWrite.HasValue);
Assert.Equal(BaseAddress, native.LastMemWrite.Value.Address);
Assert.Equal(data, native.LastMemWrite.Value.Data);
Assert.Equal((BaseAddress + 8, 2), native.LastMemRead);
}
}

internal sealed class FakeMemoryManager : IMemoryManager
{
public (ulong Address, ulong Size, Unicorn.MemoryPermissions Permissions)? LastMap { get; private set; }

public (ulong Address, ulong Size)? LastUnmap { get; private set; }

public (ulong Address, ulong Size, Unicorn.MemoryPermissions Permissions)? LastProtect { get; private set; }

public (ulong Address, byte[] Data)? LastWrite { get; private set; }

public (ulong Address, int Length)? LastRead { get; private set; }

public MemoryRegion Map(ulong address, ulong size, Unicorn.MemoryPermissions permissions)
{
LastMap = (address, size, permissions);
return new MemoryRegion(this, address, size, permissions);
}

public void Unmap(ulong address, ulong size)
{
LastUnmap = (address, size);
}

public void Protect(ulong address, ulong size, Unicorn.MemoryPermissions permissions)
{
LastProtect = (address, size, permissions);
}

public void Write(ulong address, ReadOnlySpan<byte> data)
{
LastWrite = (address, data.ToArray());
}

public void Read(ulong address, Span<byte> buffer)
{
LastRead = (address, buffer.Length);
buffer.Clear();
}
}
11 changes: 7 additions & 4 deletions UnicornNet.Tests/NewFeaturesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public sealed class NewFeaturesTests
[Fact]
public void MemoryRegion_TracksAddressAndSize()
{
var region = new MemoryRegion(null!, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var memory = new FakeMemoryManager();
var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var expectedEndAddress = BaseAddress + RegionSize;

Assert.Equal(BaseAddress, region.Address);
Expand All @@ -25,7 +26,8 @@ public void MemoryRegion_TracksAddressAndSize()
[Fact]
public void MemoryRegion_ContainsCheck_ReturnsCorrectResult()
{
var region = new MemoryRegion(null!, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var memory = new FakeMemoryManager();
var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var middleAddress = BaseAddress + 0x500;
var lastAddress = BaseAddress + RegionSize - 1;
var beforeStart = BaseAddress - 1;
Expand All @@ -41,7 +43,8 @@ public void MemoryRegion_ContainsCheck_ReturnsCorrectResult()
[Fact]
public void MemoryRegion_ContainsRange_ReturnsCorrectResult()
{
var region = new MemoryRegion(null!, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
var memory = new FakeMemoryManager();
var region = new MemoryRegion(memory, BaseAddress, RegionSize, Unicorn.MemoryPermissions.All);
const ulong smallSize = 0x100;
const ulong exceedsRegionByOne = RegionSize + 1;
const ulong largeSize = 0x200;
Expand Down Expand Up @@ -302,4 +305,4 @@ public override int MemMapPtr(IntPtr engine, ulong address, ulong size, uint per
return (int)Unicorn.ErrorCode.Map;
}
}
}
}
Loading