using System.Buffers.Binary;
using System.Net.Sockets;
using System.Text;
using Big.Application;
namespace Big.Infrastructure;
///
/// Scans a document with clamd over its INSTREAM protocol (S-29, ADR-0036): zINSTREAM\0, the
/// document as one big-endian length-prefixed chunk, a zero-length terminator, then one reply —
/// stream: OK or stream: <signature> FOUND. Anything else (an ERROR reply, a refused
/// connection, a timeout) is , so the caller fails closed.
///
public sealed class ClamdDocumentScanner(ClamAvOptions options) : IDocumentScanner
{
public async Task ScanAsync(byte[] content, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(content);
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(options.Timeout);
string reply;
try
{
using var client = new TcpClient();
await client.ConnectAsync(options.Host, options.Port, timeout.Token);
var stream = client.GetStream();
var length = new byte[4];
BinaryPrimitives.WriteInt32BigEndian(length, content.Length);
await stream.WriteAsync("zINSTREAM\0"u8.ToArray(), timeout.Token);
await stream.WriteAsync(length, timeout.Token);
await stream.WriteAsync(content, timeout.Token);
await stream.WriteAsync(new byte[4], timeout.Token);
using var reader = new StreamReader(stream, Encoding.ASCII);
reply = (await reader.ReadToEndAsync(timeout.Token)).TrimEnd('\0', '\n');
}
catch (Exception e) when (e is SocketException or IOException
|| (e is OperationCanceledException && !ct.IsCancellationRequested))
{
return ScanVerdict.Unavailable;
}
if (reply == "stream: OK") return ScanVerdict.Clean;
return reply.EndsWith(" FOUND", StringComparison.Ordinal) ? ScanVerdict.Infected : ScanVerdict.Unavailable;
}
}