Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Captures/CaptureFile.cs
T
Joseph Doherty 7892eb13a8 fix(abcip): harden Ab.Transport CIP codec and EncapsulationClient (#564, #565)
CIP codec (#564): PatchU16 bounds guard; CipReply content equality; CipPath
port/length/ASCII/odd/oversize validation with ArgumentException for malformed
tag names and routes; CipPath.MessageRouter/ConnectionManager are now
ReadOnlySpan<byte>; refused Forward Open surfaces ForwardOpenRefusedException
with structured general/extended status; test CaptureFile uses BinaryPrimitives.

EncapsulationClient (#565): frame write bounded by the exchange timeout, which
faults (never cancels) the connection; finite positive timeouts enforced at
every entry point; gate wait bounded by the timeout; deterministic reorder of
the 300 ms test; new dispose-during-drain, gate-wait and RegisterSession
zero-context tests.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-07 22:54:32 -04:00

50 lines
2.5 KiB
C#

using System.Buffers.Binary;
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures;
/// <summary>One EtherNet/IP encapsulation frame from a golden capture.</summary>
/// <param name="FromClient"><c>true</c> for a client→server (<c>&gt;</c>) frame.</param>
/// <param name="Bytes">The full frame: 24-byte encapsulation header plus data.</param>
internal sealed record CapturedFrame(bool FromClient, byte[] Bytes);
/// <summary>
/// Loads the golden libplctag ⇄ ab_server captures in <c>Captures/*.hex</c>. See
/// <c>Captures/README.md</c> for how each was taken.
/// </summary>
internal static class CaptureFile
{
/// <summary>The directory the <c>.hex</c> fixtures are copied to.</summary>
public static string Dir => Path.Combine(AppContext.BaseDirectory, "Captures");
/// <summary>Loads every frame of a capture, in wire order; <c>#</c> lines are skipped.</summary>
/// <param name="name">The capture's file name, e.g. <c>logix-read-dint.hex</c>.</param>
/// <returns>The frames.</returns>
public static IReadOnlyList<CapturedFrame> Load(string name) =>
File.ReadAllLines(Path.Combine(Dir, name))
.Where(l => l.Length > 2 && l[0] is '>' or '<')
.Select(l => new CapturedFrame(l[0] == '>', Convert.FromHexString(l[2..].Trim())))
.ToList();
/// <summary>The CIP payload of a SendRRData / SendUnitData frame (the last CPF item's data,
/// minus the 2-byte sequence count for connected data).</summary>
/// <param name="f">The frame.</param>
/// <returns>The CIP payload, or empty for any other encapsulation command.</returns>
public static byte[] CipPayload(CapturedFrame f)
{
var b = f.Bytes;
var cmd = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(0));
if (cmd is not (0x6F or 0x70)) return []; // only SendRRData / SendUnitData carry CPF items
var pos = 24 + 4 + 2; // header + interface handle + timeout
var itemCount = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(pos)); pos += 2;
byte[] last = [];
for (var i = 0; i < itemCount; i++)
{
var type = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(pos));
var len = BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(pos + 2));
last = b.AsSpan(pos + 4, len).ToArray();
if (type == 0x00B1) last = last[2..]; // strip connected sequence count
pos += 4 + len;
}
return last;
}
}