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;
}
}
}