7892eb13a8
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
50 lines
2.5 KiB
C#
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>></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;
|
|
}
|
|
}
|