mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-24 21:51:49 +01:00
feat(network): Add SafePacket, pointer validation, and network tests
Add debug/testing utilities for network packet inspection and first-ever test coverage for the network monitoring area, as discussed in #2592. - SafePacket: managed wrapper with bounds checking and lifetime safety - NetworkPointerValidator: debug utility for pointer address validation - Make NetworkMonitorWidget.IsFiltered internal static for testability - Add 49 unit tests covering SafePacket, pointer validation, and opcode filter parsing (ranges, exclusions, complex filters)
This commit is contained in:
parent
bef50438f5
commit
347fb4de5d
6 changed files with 830 additions and 49 deletions
90
Dalamud.Test/Game/Network/NetworkPointerValidatorTests.cs
Normal file
90
Dalamud.Test/Game/Network/NetworkPointerValidatorTests.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
|
||||
using Dalamud.Game.Network;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Game.Network;
|
||||
|
||||
public class NetworkPointerValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullPointer_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NetworkPointerValidator.IsValidPacketPointer(nint.Zero, 32));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x1)]
|
||||
[InlineData(0xFF)]
|
||||
[InlineData(0xFFFF)]
|
||||
public void BelowMinAddress_ReturnsFalse(long address)
|
||||
{
|
||||
Assert.False(NetworkPointerValidator.IsValidPacketPointer((nint)address, 32));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x800000000000)]
|
||||
[InlineData(0xFFFFFFFFFFFF)]
|
||||
public void AboveMaxAddress_ReturnsFalse(long address)
|
||||
{
|
||||
Assert.False(NetworkPointerValidator.IsValidPacketPointer((nint)address, 32));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(-100)]
|
||||
public void NonPositiveSize_ReturnsFalse(int size)
|
||||
{
|
||||
Assert.False(NetworkPointerValidator.IsValidPacketPointer((nint)0x10000, size));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x10000, 1)]
|
||||
[InlineData(0x100000, 1024)]
|
||||
[InlineData(0x7FFFFFFFFFFF, 1)]
|
||||
public void ValidPointerAndSize_ReturnsTrue(long address, int size)
|
||||
{
|
||||
Assert.True(NetworkPointerValidator.IsValidPacketPointer((nint)address, size));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SafeRead_InvalidPointer_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
NetworkPointerValidator.SafeRead<int>(nint.Zero, 0, 32));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SafeRead_NegativeOffset_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
NetworkPointerValidator.SafeRead<int>((nint)0x10000, -1, 32));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SafeRead_OffsetExceedsPacket_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
NetworkPointerValidator.SafeRead<int>((nint)0x10000, 30, 32));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySafeRead_InvalidPointer_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NetworkPointerValidator.TrySafeRead<int>(nint.Zero, 0, 32, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySafeRead_NegativeOffset_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NetworkPointerValidator.TrySafeRead<int>((nint)0x10000, -1, 32, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySafeRead_OffsetExceedsPacket_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NetworkPointerValidator.TrySafeRead<int>((nint)0x10000, 30, 32, out _));
|
||||
}
|
||||
}
|
||||
165
Dalamud.Test/Game/Network/OpCodeFilterTests.cs
Normal file
165
Dalamud.Test/Game/Network/OpCodeFilterTests.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
using Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Game.Network;
|
||||
|
||||
public class OpCodeFilterTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmptyFilter_MatchesAll()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(string.Empty, 100));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(string.Empty, 0));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(string.Empty, ushort.MaxValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhitespaceOnlyFilter_MatchesAll()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(" ", 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleExactMatch()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("100", 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleExact_NoMatch()
|
||||
{
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("100", 200));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommaSeparatedList_MatchesIncluded()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("100,200,300", 200));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommaSeparatedList_RejectsExcluded()
|
||||
{
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("100,200,300", 150));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_MatchesWithinBounds()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("100-200", 150));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_MatchesLowerBound()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("100-200", 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_MatchesUpperBound()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("100-200", 200));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_RejectsOutside()
|
||||
{
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("100-200", 50));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("100-200", 250));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenEndedRangeStart_MatchesUpTo()
|
||||
{
|
||||
// "-400" means everything up to 400
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("-400", 0));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("-400", 200));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("-400", 400));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("-400", 401));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenEndedRangeEnd_MatchesFrom()
|
||||
{
|
||||
// "700-" means everything from 700 onward
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("700-", 700));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("700-", 1000));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("700-", ushort.MaxValue));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("700-", 699));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exclusion_ExcludesSingleOpCode()
|
||||
{
|
||||
// Only exclusion, no inclusion -> matches everything except excluded
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("!50", 50));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("!50", 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exclusion_ExcludesRange()
|
||||
{
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("!50-100", 75));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("!50-100", 150));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedInclusionAndExclusion()
|
||||
{
|
||||
// Include 0-400, but exclude 50-100
|
||||
var filter = "-400,!50-100";
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 10));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 75));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 200));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 500));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComplexFilter()
|
||||
{
|
||||
// Example from UI tooltip: -400,!50-100,650,700-980,!941
|
||||
var filter = "-400,!50-100,650,700-980,!941";
|
||||
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 10)); // in -400 range
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 75)); // excluded by !50-100
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 300)); // in -400 range
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 500)); // not in any include
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 650)); // exact match
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 700)); // in 700-980
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 800)); // in 700-980
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 941)); // excluded by !941
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 980)); // in 700-980
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 990)); // not in any include
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpacesAreStripped()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(" 100 , 200 ", 100));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered(" 100 , 200 ", 200));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered(" 100 , 200 ", 150));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidFilter_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("abc", 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExclusionOnly_MatchesEverythingElse()
|
||||
{
|
||||
// No include entries -> treat as "match all except excluded"
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("!50,!100", 200));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("!50,!100", 50));
|
||||
Assert.False(NetworkMonitorWidget.IsFiltered("!50,!100", 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundaryValues()
|
||||
{
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("0", 0));
|
||||
Assert.True(NetworkMonitorWidget.IsFiltered("65535", ushort.MaxValue));
|
||||
}
|
||||
}
|
||||
234
Dalamud.Test/Game/Network/SafePacketTests.cs
Normal file
234
Dalamud.Test/Game/Network/SafePacketTests.cs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
using System;
|
||||
|
||||
using Dalamud.Game.Network;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Game.Network;
|
||||
|
||||
public class SafePacketTests
|
||||
{
|
||||
[Fact]
|
||||
public void ConstructFromByteArray_CopiesData()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Equal(4, packet.Size);
|
||||
Assert.Equal(data, packet.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructFromByteArray_IsolatesFromSource()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
data[0] = 0xFF;
|
||||
Assert.Equal(0x01, packet.ToArray()[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructFromNull_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new SafePacket(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructFromEmptyArray_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new SafePacket(Array.Empty<byte>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpCode_ReadsFirstTwoBytes()
|
||||
{
|
||||
var data = new byte[] { 0x34, 0x12, 0x00, 0x00 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Equal(0x1234, packet.OpCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpCode_SingleByte_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => packet.OpCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_Int32AtOffset()
|
||||
{
|
||||
var data = new byte[] { 0x00, 0x00, 0x78, 0x56, 0x34, 0x12 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Equal(0x12345678, packet.Read<int>(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_NegativeOffset_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Read<int>(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_ExceedsBounds_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Read<int>(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_OffsetAtBoundary_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Read<int>(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRead_Success()
|
||||
{
|
||||
var data = new byte[] { 0x34, 0x12, 0x00, 0x00 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.True(packet.TryRead<ushort>(0, out var value));
|
||||
Assert.Equal(0x1234, value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRead_OutOfBounds_ReturnsFalse()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.False(packet.TryRead<int>(0, out var value));
|
||||
Assert.Equal(default, value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRead_NegativeOffset_ReturnsFalse()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.False(packet.TryRead<int>(-1, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsSpan_ReturnsFullData()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Equal(data, packet.AsSpan().ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsSpan_WithRange_ReturnsSubset()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
var span = packet.AsSpan(1, 3);
|
||||
Assert.Equal(new byte[] { 0x02, 0x03, 0x04 }, span.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsSpan_InvalidRange_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => packet.AsSpan(1, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsSpan_NegativeOffset_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => packet.AsSpan(-1, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToArray_ReturnsIndependentCopy()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03 };
|
||||
using var packet = new SafePacket(data);
|
||||
|
||||
var copy = packet.ToArray();
|
||||
copy[0] = 0xFF;
|
||||
Assert.Equal(0x01, packet.ToArray()[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_ClearsData()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
var packet = new SafePacket(data);
|
||||
|
||||
packet.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(() => packet.AsSpan());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_DoubleDispose_NoThrow()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02 };
|
||||
var packet = new SafePacket(data);
|
||||
|
||||
packet.Dispose();
|
||||
packet.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_AfterDispose_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
var packet = new SafePacket(data);
|
||||
packet.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(() => packet.Read<int>(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpCode_AfterDispose_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02 };
|
||||
var packet = new SafePacket(data);
|
||||
packet.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(() => packet.OpCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRead_AfterDispose_ReturnsFalse()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
|
||||
var packet = new SafePacket(data);
|
||||
packet.Dispose();
|
||||
|
||||
Assert.False(packet.TryRead<int>(0, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToArray_AfterDispose_Throws()
|
||||
{
|
||||
var data = new byte[] { 0x01, 0x02 };
|
||||
var packet = new SafePacket(data);
|
||||
packet.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(() => packet.ToArray());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue