From 347fb4de5d4350cdbb638496958b8418183b046e Mon Sep 17 00:00:00 2001 From: RoseOfficial Date: Fri, 20 Feb 2026 21:10:27 -0500 Subject: [PATCH] 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) --- .../Network/NetworkPointerValidatorTests.cs | 90 +++++++ .../Game/Network/OpCodeFilterTests.cs | 165 ++++++++++++ Dalamud.Test/Game/Network/SafePacketTests.cs | 234 ++++++++++++++++++ .../Game/Network/NetworkPointerValidator.cs | 98 ++++++++ Dalamud/Game/Network/SafePacket.cs | 188 ++++++++++++++ .../Data/Widgets/NetworkMonitorWidget.cs | 104 ++++---- 6 files changed, 830 insertions(+), 49 deletions(-) create mode 100644 Dalamud.Test/Game/Network/NetworkPointerValidatorTests.cs create mode 100644 Dalamud.Test/Game/Network/OpCodeFilterTests.cs create mode 100644 Dalamud.Test/Game/Network/SafePacketTests.cs create mode 100644 Dalamud/Game/Network/NetworkPointerValidator.cs create mode 100644 Dalamud/Game/Network/SafePacket.cs diff --git a/Dalamud.Test/Game/Network/NetworkPointerValidatorTests.cs b/Dalamud.Test/Game/Network/NetworkPointerValidatorTests.cs new file mode 100644 index 000000000..e5fdc9ff9 --- /dev/null +++ b/Dalamud.Test/Game/Network/NetworkPointerValidatorTests.cs @@ -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(() => + NetworkPointerValidator.SafeRead(nint.Zero, 0, 32)); + } + + [Fact] + public void SafeRead_NegativeOffset_Throws() + { + Assert.Throws(() => + NetworkPointerValidator.SafeRead((nint)0x10000, -1, 32)); + } + + [Fact] + public void SafeRead_OffsetExceedsPacket_Throws() + { + Assert.Throws(() => + NetworkPointerValidator.SafeRead((nint)0x10000, 30, 32)); + } + + [Fact] + public void TrySafeRead_InvalidPointer_ReturnsFalse() + { + Assert.False(NetworkPointerValidator.TrySafeRead(nint.Zero, 0, 32, out _)); + } + + [Fact] + public void TrySafeRead_NegativeOffset_ReturnsFalse() + { + Assert.False(NetworkPointerValidator.TrySafeRead((nint)0x10000, -1, 32, out _)); + } + + [Fact] + public void TrySafeRead_OffsetExceedsPacket_ReturnsFalse() + { + Assert.False(NetworkPointerValidator.TrySafeRead((nint)0x10000, 30, 32, out _)); + } +} diff --git a/Dalamud.Test/Game/Network/OpCodeFilterTests.cs b/Dalamud.Test/Game/Network/OpCodeFilterTests.cs new file mode 100644 index 000000000..4ebe97fde --- /dev/null +++ b/Dalamud.Test/Game/Network/OpCodeFilterTests.cs @@ -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)); + } +} diff --git a/Dalamud.Test/Game/Network/SafePacketTests.cs b/Dalamud.Test/Game/Network/SafePacketTests.cs new file mode 100644 index 000000000..061c0ce75 --- /dev/null +++ b/Dalamud.Test/Game/Network/SafePacketTests.cs @@ -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(() => new SafePacket(null!)); + } + + [Fact] + public void ConstructFromEmptyArray_Throws() + { + Assert.Throws(() => new SafePacket(Array.Empty())); + } + + [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(() => 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(2)); + } + + [Fact] + public void Read_NegativeOffset_Throws() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using var packet = new SafePacket(data); + + Assert.Throws(() => packet.Read(-1)); + } + + [Fact] + public void Read_ExceedsBounds_Throws() + { + var data = new byte[] { 0x01, 0x02 }; + using var packet = new SafePacket(data); + + Assert.Throws(() => packet.Read(0)); + } + + [Fact] + public void Read_OffsetAtBoundary_Throws() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + using var packet = new SafePacket(data); + + Assert.Throws(() => packet.Read(1)); + } + + [Fact] + public void TryRead_Success() + { + var data = new byte[] { 0x34, 0x12, 0x00, 0x00 }; + using var packet = new SafePacket(data); + + Assert.True(packet.TryRead(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(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(-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(() => packet.AsSpan(1, 5)); + } + + [Fact] + public void AsSpan_NegativeOffset_Throws() + { + var data = new byte[] { 0x01, 0x02 }; + using var packet = new SafePacket(data); + + Assert.Throws(() => 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(() => 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(() => packet.Read(0)); + } + + [Fact] + public void OpCode_AfterDispose_Throws() + { + var data = new byte[] { 0x01, 0x02 }; + var packet = new SafePacket(data); + packet.Dispose(); + + Assert.Throws(() => 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(0, out _)); + } + + [Fact] + public void ToArray_AfterDispose_Throws() + { + var data = new byte[] { 0x01, 0x02 }; + var packet = new SafePacket(data); + packet.Dispose(); + + Assert.Throws(() => packet.ToArray()); + } +} diff --git a/Dalamud/Game/Network/NetworkPointerValidator.cs b/Dalamud/Game/Network/NetworkPointerValidator.cs new file mode 100644 index 000000000..35c902fd9 --- /dev/null +++ b/Dalamud/Game/Network/NetworkPointerValidator.cs @@ -0,0 +1,98 @@ +using System.Runtime.InteropServices; + +namespace Dalamud.Game.Network; + +/// +/// Provides validation utilities for network packet pointers. +/// +/// +/// This is a debug/development utility for validating pointer safety +/// during packet inspection and analysis workflows. +/// +internal static class NetworkPointerValidator +{ + /// + /// Minimum address threshold below which pointers are considered invalid. + /// Addresses below this are typically reserved by the OS. + /// + private const long MinValidAddress = 0x10000; + + /// + /// Maximum valid user-mode address for 64-bit Windows. + /// Addresses above this are kernel-mode and inaccessible from user-mode. + /// + private const long MaxValidAddress = 0x7FFFFFFFFFFF; + + /// + /// Validates a network packet pointer before use. + /// + /// The pointer to validate. + /// The minimum expected size of the data. + /// True if the pointer appears valid; false otherwise. + public static bool IsValidPacketPointer(nint ptr, int minSize) + { + if (ptr == nint.Zero) + return false; + + if (ptr < MinValidAddress) + return false; + + if (ptr > MaxValidAddress) + return false; + + if (minSize <= 0) + return false; + + return true; + } + + /// + /// Safely reads a value from a packet pointer with bounds checking. + /// + /// The unmanaged type to read. + /// The base pointer to read from. + /// The byte offset from the base pointer. + /// The total size of the packet for bounds checking. + /// The value read from memory. + /// Thrown when the read would exceed packet boundaries. + /// Thrown when the pointer is invalid. + public static unsafe T SafeRead(nint ptr, int offset, int packetSize) where T : unmanaged + { + if (!IsValidPacketPointer(ptr, packetSize)) + throw new ArgumentException("Invalid packet pointer.", nameof(ptr)); + + var size = sizeof(T); + if (offset < 0 || offset > packetSize || size > packetSize - offset) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + $"Cannot read {size} bytes at offset {offset} from packet of size {packetSize}."); + } + + return *(T*)(ptr + offset); + } + + /// + /// Attempts to safely read a value from a packet pointer with bounds checking. + /// + /// The unmanaged type to read. + /// The base pointer to read from. + /// The byte offset from the base pointer. + /// The total size of the packet for bounds checking. + /// The value read from memory, or default if the read failed. + /// True if the read succeeded; false otherwise. + public static unsafe bool TrySafeRead(nint ptr, int offset, int packetSize, out T value) where T : unmanaged + { + value = default; + + if (!IsValidPacketPointer(ptr, packetSize)) + return false; + + var size = sizeof(T); + if (offset < 0 || offset > packetSize || size > packetSize - offset) + return false; + + value = *(T*)(ptr + offset); + return true; + } +} diff --git a/Dalamud/Game/Network/SafePacket.cs b/Dalamud/Game/Network/SafePacket.cs new file mode 100644 index 000000000..915e27c1a --- /dev/null +++ b/Dalamud/Game/Network/SafePacket.cs @@ -0,0 +1,188 @@ +using System.Runtime.InteropServices; + +namespace Dalamud.Game.Network; + +/// +/// A safe wrapper around network packet data with lifetime and bounds guarantees. +/// +/// +/// +/// This class copies packet data to managed memory, ensuring: +/// +/// +/// Lifetime safety - data persists as long as this object. +/// Bounds checking - all reads are validated against packet size. +/// Thread safety - the copied data cannot be modified externally. +/// +/// +/// Intended for debug tooling and packet inspection utilities. +/// +/// +internal sealed class SafePacket : IDisposable +{ + private readonly byte[] data; + private bool disposed; + + /// + /// Initializes a new instance of the class by copying data from an unmanaged pointer. + /// + /// The source pointer to copy from. + /// The number of bytes to copy. + /// Thrown when the pointer is invalid. + /// Thrown when size is not positive. + internal SafePacket(nint ptr, int size) + { + if (!NetworkPointerValidator.IsValidPacketPointer(ptr, size)) + throw new ArgumentException("Invalid packet pointer.", nameof(ptr)); + + if (size <= 0) + throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive."); + + this.data = new byte[size]; + Marshal.Copy(ptr, this.data, 0, size); + this.Size = size; + } + + /// + /// Initializes a new instance of the class from existing data. + /// + /// The source data to copy. + /// Thrown when data is null. + /// Thrown when data is empty. + internal SafePacket(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + if (data.Length == 0) + throw new ArgumentException("Data cannot be empty.", nameof(data)); + + this.data = new byte[data.Length]; + data.CopyTo(this.data, 0); + this.Size = data.Length; + } + + /// + /// Gets the total size of the packet data in bytes. + /// + public int Size { get; } + + /// + /// Gets the packet opcode (first two bytes interpreted as ushort). + /// + /// Thrown when the packet has been disposed. + /// Thrown when packet is too small to contain an opcode. + public ushort OpCode + { + get + { + ObjectDisposedException.ThrowIf(this.disposed, this); + + if (this.Size < sizeof(ushort)) + throw new InvalidOperationException("Packet too small to contain an opcode."); + + return BitConverter.ToUInt16(this.data, 0); + } + } + + /// + /// Safely reads a value of type T at the specified byte offset. + /// + /// The unmanaged type to read. + /// The byte offset from the start of the packet. + /// The value read from the packet data. + /// Thrown when the packet has been disposed. + /// Thrown when the read would exceed packet boundaries. + public unsafe T Read(int offset) where T : unmanaged + { + ObjectDisposedException.ThrowIf(this.disposed, this); + + var size = sizeof(T); + if (offset < 0 || offset > this.Size || size > this.Size - offset) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + $"Cannot read {size} bytes at offset {offset} from packet of size {this.Size}."); + } + + return MemoryMarshal.Read(this.data.AsSpan(offset)); + } + + /// + /// Attempts to safely read a value of type T at the specified byte offset. + /// + /// The unmanaged type to read. + /// The byte offset from the start of the packet. + /// When this method returns, contains the value read, or default if the read failed. + /// True if the read succeeded; false otherwise. + public unsafe bool TryRead(int offset, out T value) where T : unmanaged + { + value = default; + + if (this.disposed) + return false; + + var size = sizeof(T); + if (offset < 0 || offset > this.Size || size > this.Size - offset) + return false; + + value = MemoryMarshal.Read(this.data.AsSpan(offset)); + return true; + } + + /// + /// Gets a read-only span of the entire packet data. + /// + /// A read-only span covering all packet data. + /// Thrown when the packet has been disposed. + public ReadOnlySpan AsSpan() + { + ObjectDisposedException.ThrowIf(this.disposed, this); + return this.data; + } + + /// + /// Gets a read-only span of a portion of the packet data. + /// + /// The starting offset. + /// The number of bytes to include. + /// A read-only span covering the specified portion of packet data. + /// Thrown when the packet has been disposed. + /// Thrown when the range exceeds packet boundaries. + public ReadOnlySpan AsSpan(int offset, int length) + { + ObjectDisposedException.ThrowIf(this.disposed, this); + + if (offset < 0 || length < 0 || offset > this.Size || length > this.Size - offset) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + $"Range [{offset}, {offset + length}) exceeds packet size {this.Size}."); + } + + return this.data.AsSpan(offset, length); + } + + /// + /// Creates a copy of the packet data as a new byte array. + /// + /// A new byte array containing a copy of the packet data. + /// Thrown when the packet has been disposed. + public byte[] ToArray() + { + ObjectDisposedException.ThrowIf(this.disposed, this); + + var copy = new byte[this.Size]; + this.data.CopyTo(copy, 0); + return copy; + } + + /// + public void Dispose() + { + if (!this.disposed) + { + Array.Clear(this.data); + this.disposed = true; + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 73916761b..5e7384bf3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -139,7 +139,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget foreach (var packet in this.packets) { - if (!this.filterRecording && !this.IsFiltered(packet.OpCode)) + if (!this.filterRecording && !IsFiltered(this.filterString, packet.OpCode)) continue; ImGui.TableNextColumn(); @@ -200,55 +200,15 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget } } - private static string GetTargetName(uint targetId) + /// + /// Determines whether a given opcode passes the filter criteria. + /// + /// The comma-separated filter string (e.g. "-400,!50-100,650,700-980,!941"). + /// The opcode to test. + /// True if the opcode passes the filter (should be shown/recorded); false otherwise. + internal static bool IsFiltered(string filterString, ushort opcode) { - if (targetId == PlayerState.Instance()->EntityId) - return "Local Player"; - - var cachedName = NameCache.Instance()->GetNameByEntityId(targetId); - if (cachedName.HasValue) - return cachedName.ToString(); - - var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(targetId); - if (obj != null) - return obj->NameString; - - return string.Empty; - } - - private void OnReceivePacketDetour(PacketDispatcher* thisPtr, uint targetId, nint packet) - { - var opCode = *(ushort*)(packet + 2); - var targetName = GetTargetName(targetId); - this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName)); - this.hookZoneDown.OriginalDisposeSafe(thisPtr, targetId, packet); - } - - private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5) - { - var opCode = *(ushort*)packet; - this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); - return this.hookZoneUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); - } - - private void RecordPacket(NetworkPacketData packet) - { - if (this.filterRecording && !this.IsFiltered(packet.OpCode)) - return; - - this.packets.Enqueue(packet); - - while (this.packets.Count > this.trackedPackets) - { - this.packets.TryDequeue(out _); - } - - this.autoScrollPending = true; - } - - private bool IsFiltered(ushort opcode) - { - var filterString = this.filterString.Replace(" ", string.Empty); + filterString = filterString.Replace(" ", string.Empty); if (filterString.Length == 0) return true; @@ -304,6 +264,52 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget } } + private static string GetTargetName(uint targetId) + { + if (targetId == PlayerState.Instance()->EntityId) + return "Local Player"; + + var cachedName = NameCache.Instance()->GetNameByEntityId(targetId); + if (cachedName.HasValue) + return cachedName.ToString(); + + var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(targetId); + if (obj != null) + return obj->NameString; + + return string.Empty; + } + + private void OnReceivePacketDetour(PacketDispatcher* thisPtr, uint targetId, nint packet) + { + var opCode = *(ushort*)(packet + 2); + var targetName = GetTargetName(targetId); + this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName)); + this.hookZoneDown.OriginalDisposeSafe(thisPtr, targetId, packet); + } + + private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5) + { + var opCode = *(ushort*)packet; + this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); + return this.hookZoneUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); + } + + private void RecordPacket(NetworkPacketData packet) + { + if (this.filterRecording && !IsFiltered(this.filterString, packet.OpCode)) + return; + + this.packets.Enqueue(packet); + + while (this.packets.Count > this.trackedPackets) + { + this.packets.TryDequeue(out _); + } + + this.autoScrollPending = true; + } + #pragma warning disable SA1313 private readonly record struct NetworkPacketData(ulong Index, DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetEntityId, string TargetName); #pragma warning restore SA1313