using System.Buffers.Binary; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures; /// One EtherNet/IP encapsulation frame from a golden capture. /// true for a client→server (>) frame. /// The full frame: 24-byte encapsulation header plus data. internal sealed record CapturedFrame(bool FromClient, byte[] Bytes); /// /// Loads the golden libplctag ⇄ ab_server captures in Captures/*.hex. See /// Captures/README.md for how each was taken. /// internal static class CaptureFile { /// The directory the .hex fixtures are copied to. public static string Dir => Path.Combine(AppContext.BaseDirectory, "Captures"); /// Loads every frame of a capture, in wire order; # lines are skipped. /// The capture's file name, e.g. logix-read-dint.hex. /// The frames. public static IReadOnlyList 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(); /// The CIP payload of a SendRRData / SendUnitData frame (the last CPF item's data, /// minus the 2-byte sequence count for connected data). /// The frame. /// The CIP payload, or empty for any other encapsulation command. 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; } }