review(Driver.Modbus): validate FC03 RMW response + correct write error mapping
Re-review at 7286d320. Modbus-013 (Low): bit RMW now routes the FC03 read through the
validated ReadRegisterBlockAsync (was raw-indexing readResp -> IndexOutOfRange on a truncated
PDU). Modbus-014 (Low): WriteAsync maps InvalidDataException to BadCommunicationError (was
BadInternalError), matching ReadAsync. + TDD.
This commit is contained in:
@@ -153,4 +153,64 @@ public sealed class ModbusBitRmwTests
|
||||
|
||||
fake.HoldingRegisters[30].ShouldBe((ushort)((1 << 5) | (1 << 10)));
|
||||
}
|
||||
|
||||
// ---- Driver.Modbus-013: RMW read-response not validated before indexing ----
|
||||
|
||||
/// <summary>
|
||||
/// Transport that returns a too-short PDU for FC03 reads — simulates a buggy device
|
||||
/// returning a malformed response during a BitInRegister read-modify-write. Pre-fix,
|
||||
/// WriteBitInRegisterAsync indexed readResp[2]/[3] without checking resp.Length, causing
|
||||
/// IndexOutOfRangeException. Post-fix it throws InvalidDataException which WriteAsync's
|
||||
/// catch-all maps to BadInternalError.
|
||||
/// </summary>
|
||||
private sealed class TruncatedRmwTransport : IModbusTransport
|
||||
{
|
||||
/// <summary>Connects asynchronously (no-op for fake).</summary>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Returns a truncated FC03 response to trigger the validation path.</summary>
|
||||
/// <param name="unitId">The Modbus unit ID (unused).</param>
|
||||
/// <param name="pdu">The PDU to process.</param>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
{
|
||||
if (pdu[0] == 0x03)
|
||||
// Return only [fc][byte-count=2] — missing the actual register data bytes.
|
||||
return Task.FromResult(new byte[] { 0x03, 0x02 });
|
||||
// FC06 write echoes the PDU
|
||||
return Task.FromResult(pdu);
|
||||
}
|
||||
|
||||
/// <summary>Disposes asynchronously (no-op for fake).</summary>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Modbus-013/014: a truncated FC03 read response during a BitInRegister RMW must
|
||||
/// surface as BadCommunicationError, not BadInternalError (a structural/driver-code
|
||||
/// problem) — and not as an IndexOutOfRangeException that escapes WriteAsync uncaught.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BitInRegister_write_with_truncated_FC03_response_returns_BadCommunicationError()
|
||||
{
|
||||
var truncated = new TruncatedRmwTransport();
|
||||
var opts = new ModbusDriverOptions
|
||||
{
|
||||
Host = "fake",
|
||||
Tags = [new ModbusTagDefinition("Bit5", ModbusRegion.HoldingRegisters, 10, ModbusDataType.BitInRegister, BitIndex: 5)],
|
||||
Probe = new ModbusProbeOptions { Enabled = false },
|
||||
};
|
||||
var drv = new ModbusDriver(opts, "modbus-rmw-trunc", _ => truncated);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
// WriteAsync must complete (not throw) and return BadCommunicationError — the same
|
||||
// status the ReadAsync path returns for a truncated response. BadInternalError (0x80020000)
|
||||
// is wrong because the failure is a communication/protocol error, not a driver bug.
|
||||
const uint BadCommunicationError = 0x80050000u;
|
||||
var results = await drv.WriteAsync([new WriteRequest("Bit5", true)], CancellationToken.None);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(BadCommunicationError,
|
||||
"A truncated FC03 RMW response is a communication error, not an internal driver error");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user