using System.Buffers.Binary; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes; /// /// Scriptable : settable , records every request /// and the token it was sent with, and answers via . The default responder /// answers any service with an empty success reply and splits a Multiple Service Packet into /// per-service replies via . Reused by Tasks 12 and 13. /// internal sealed class FakeCipChannel : ICipChannel { private readonly SemaphoreSlim _entered = new(0); public FakeCipChannel(int maxRequestBytes = 504) { MaxRequestBytes = maxRequestBytes; Respond = DefaultRespond; } private int _maxRequestBytes; public int MaxRequestBytes { get => MaxRequestBytesThrows is { } ex ? throw ex : _maxRequestBytes; set => _maxRequestBytes = value; } /// When set, reading throws it (simulates a broken channel). public Exception? MaxRequestBytesThrows { get; set; } /// Every request, in send order. public List Sent { get; } = []; /// The token each request was sent with, parallel to . public List Tokens { get; } = []; /// Answers one request with raw CIP reply bytes. public Func> Respond { get; set; } /// Answers one embedded (non-MSP) service; used by the default . public Func RespondPerService { get; set; } = OkReply; /// When set, every send blocks on it (after being recorded) until the test completes it. public TaskCompletionSource? Gate { get; set; } public int SentCount { get { lock (Sent) return Sent.Count; } } public async Task> SendAsync(ReadOnlyMemory cipRequest, CancellationToken cancellationToken) { var req = cipRequest.ToArray(); lock (Sent) { Sent.Add(req); Tokens.Add(cancellationToken); } _entered.Release(); if (Gate is { } g) await g.Task.WaitAsync(cancellationToken); return await Respond(req); } /// Completes once the channel has received requests in total. public async Task WaitForSendsAsync(int count) { while (SentCount < count) { if (!await _entered.WaitAsync(TimeSpan.FromSeconds(10))) throw new TimeoutException($"Expected {count} sends, saw {SentCount}."); } } /// A success reply for with optional data. public static byte[] OkReply(byte[] request) => Reply(request[0], 0x00, []); public static byte[] Reply(byte requestService, byte generalStatus, byte[] data, params ushort[] extended) { var r = new byte[4 + 2 * extended.Length + data.Length]; r[0] = (byte)(requestService | 0x80); r[2] = generalStatus; r[3] = (byte)extended.Length; for (var i = 0; i < extended.Length; i++) BinaryPrimitives.WriteUInt16LittleEndian(r.AsSpan(4 + 2 * i), extended[i]); data.CopyTo(r, 4 + 2 * extended.Length); return r; } /// Splits an MSP request into its embedded requests. public static List SplitMsp(byte[] msp) { const int countAt = 6; // 0A 02 20 02 24 01 var data = msp.AsSpan(countAt); int n = BinaryPrimitives.ReadUInt16LittleEndian(data); var offsets = new int[n]; for (var i = 0; i < n; i++) offsets[i] = BinaryPrimitives.ReadUInt16LittleEndian(data[(2 + 2 * i)..]); var result = new List(n); for (var i = 0; i < n; i++) { var end = i + 1 < n ? offsets[i + 1] : data.Length; result.Add(data[offsets[i]..end].ToArray()); } return result; } /// An MSP reply (service 0x8A) carrying the given embedded replies. public static byte[] MspReply(IReadOnlyList replies, byte generalStatus = 0x00) { var body = new List(); var header = new byte[2 + 2 * replies.Count]; BinaryPrimitives.WriteUInt16LittleEndian(header, (ushort)replies.Count); var offset = header.Length; for (var i = 0; i < replies.Count; i++) { BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(2 + 2 * i), (ushort)offset); offset += replies[i].Length; } body.AddRange(header); foreach (var r in replies) body.AddRange(r); return Reply(0x0A, generalStatus, [.. body]); } /// The default responder: per-service replies, split and re-packed for an MSP. public Task DefaultRespond(byte[] request) { if (request[0] != 0x0A) return Task.FromResult(RespondPerService(request)); var replies = SplitMsp(request).Select(RespondPerService).ToList(); var anyFailed = replies.Any(r => r[2] != 0); return Task.FromResult(MspReply(replies, anyFailed ? (byte)0x1E : (byte)0x00)); } }