using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace NcProgramManager.Tests.Integration.Stubs { /// /// Minimal in-process LSV2 server. /// Frame format: [0x02][SIZE_HI][SIZE_LO][CMD_HI][CMD_LO][data...][CRC_HI][CRC_LO] /// SIZE = len(CMD) + len(data) = 2 + dataLen /// public sealed class Lsv2ServerStub : IDisposable { private const ushort T_OK = 0x0000; private const ushort T_SE = 0x0001; private const ushort T_LS_SELECT = 0x00A0; private const ushort T_LS_LOG_IN = 0x00A1; private const ushort T_LS_LOG_OUT = 0x00A2; private const ushort T_R_FL = 0x0023; private const ushort T_S_FL = 0x0024; private const ushort T_FD = 0x0025; private readonly TcpListener _listener; private readonly Thread _acceptThread; private volatile bool _running; public int Port { get; } /// In-memory file store: full path → content. public Dictionary Files { get; } = new Dictionary(StringComparer.Ordinal); public Lsv2ServerStub() { _listener = new TcpListener(IPAddress.Loopback, 0); _listener.Start(); Port = ((IPEndPoint)_listener.LocalEndpoint).Port; _running = true; _acceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "Lsv2Stub-Accept" }; _acceptThread.Start(); } public void Dispose() { _running = false; try { _listener.Stop(); } catch { } } private void AcceptLoop() { while (_running) { TcpClient client; try { client = _listener.AcceptTcpClient(); } catch { break; } var t = new Thread(() => HandleClient(client)) { IsBackground = true, Name = "Lsv2Stub-Client" }; t.Start(); } } private void HandleClient(TcpClient client) { using (client) using (var stream = client.GetStream()) { while (true) { ushort cmd; byte[] data = ReadFrame(stream, out cmd); if (data == null) break; switch (cmd) { case T_LS_SELECT: case T_LS_LOG_IN: SendFrame(stream, T_OK, new byte[0]); break; case T_LS_LOG_OUT: SendFrame(stream, T_OK, new byte[0]); return; case T_R_FL: { // data = path\0 string path = ExtractString(data, 0); string content; if (!Files.TryGetValue(path, out content)) { SendFrame(stream, T_SE, new byte[0]); break; } byte[] contentBytes = Encoding.ASCII.GetBytes(content); SendFrame(stream, T_FD, contentBytes); SendFrame(stream, T_OK, new byte[0]); break; } case T_S_FL: { // data = path\0 + content string path = ExtractString(data, 0); int pathLen = Encoding.ASCII.GetByteCount(path) + 1; // +1 for \0 int contentLen = data.Length - pathLen; string content = contentLen > 0 ? Encoding.ASCII.GetString(data, pathLen, contentLen) : ""; Files[path] = content; SendFrame(stream, T_OK, new byte[0]); break; } default: SendFrame(stream, T_SE, new byte[0]); break; } } } } private static byte[] ReadFrame(Stream stream, out ushort cmd) { cmd = 0xFFFF; int stx = stream.ReadByte(); if (stx != 0x02) return null; int sizeHi = stream.ReadByte(); int sizeLo = stream.ReadByte(); if (sizeHi < 0 || sizeLo < 0) return null; int size = (sizeHi << 8) | sizeLo; if (size < 2) return null; int cmdHi = stream.ReadByte(); int cmdLo = stream.ReadByte(); if (cmdHi < 0 || cmdLo < 0) return null; cmd = (ushort)((cmdHi << 8) | cmdLo); int dataLen = size - 2; byte[] data = new byte[dataLen]; int read = 0; while (read < dataLen) { int n = stream.Read(data, read, dataLen - read); if (n == 0) return null; read += n; } // consume CRC stream.ReadByte(); stream.ReadByte(); return data; } private static void SendFrame(Stream stream, ushort cmdCode, byte[] data) { int dataLen = data.Length; ushort size = (ushort)(2 + dataLen); byte sizeHi = (byte)(size >> 8); byte sizeLo = (byte)(size & 0xFF); byte cmdHi = (byte)(cmdCode >> 8); byte cmdLo = (byte)(cmdCode & 0xFF); byte[] crcInput = new byte[4 + dataLen]; crcInput[0] = sizeHi; crcInput[1] = sizeLo; crcInput[2] = cmdHi; crcInput[3] = cmdLo; Buffer.BlockCopy(data, 0, crcInput, 4, dataLen); ushort crc = Crc16(crcInput); byte[] frame = new byte[1 + 4 + dataLen + 2]; int i = 0; frame[i++] = 0x02; frame[i++] = sizeHi; frame[i++] = sizeLo; frame[i++] = cmdHi; frame[i++] = cmdLo; Buffer.BlockCopy(data, 0, frame, i, dataLen); i += dataLen; frame[i++] = (byte)(crc >> 8); frame[i] = (byte)(crc & 0xFF); stream.Write(frame, 0, frame.Length); stream.Flush(); } private static string ExtractString(byte[] data, int offset) { int end = offset; while (end < data.Length && data[end] != 0) end++; return Encoding.ASCII.GetString(data, offset, end - offset); } // CRC16 CCITT — same poly/init as Lsv2Client private static ushort Crc16(byte[] data) { ushort crc = 0xFFFF; foreach (byte b in data) { crc ^= (ushort)(b << 8); for (int i = 0; i < 8; i++) crc = (crc & 0x8000) != 0 ? (ushort)((crc << 1) ^ 0x1021) : (ushort)(crc << 1); } return crc; } } }