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:
RoseOfficial 2026-02-20 21:10:27 -05:00
parent bef50438f5
commit 347fb4de5d
6 changed files with 830 additions and 49 deletions

View 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 _));
}
}

View 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));
}
}

View 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());
}
}