review(Driver.Modbus.Contracts): first review; guard equipment-tag bitIndex/stringLength

First review at 7286d320. -001 (Medium): reject bitIndex>15 for BitInRegister equipment tags
(was silent always-false/no-op via the (1<<16) mask). -002 (Medium): reject stringLength<1
for String (was misleading BadCommunicationError). -003/-004 cap-doc + [Range(0,2000)] on
MaxCoilsPerRead. Consuming Driver.Modbus.Tests green.
This commit is contained in:
Joseph Doherty
2026-06-19 12:22:53 -04:00
parent 45711e437d
commit a7ce742763
3 changed files with 114 additions and 2 deletions
@@ -37,7 +37,9 @@ public sealed class ModbusDriverOptions
/// Omron CJ/CS cap at <c>125</c>. Set to the lowest cap across the devices this driver
/// instance talks to; the driver auto-chunks larger reads into consecutive requests.
/// Default <c>125</c> — the spec maximum, safe against any conforming server. Setting
/// to <c>0</c> disables the cap (discouraged — the spec upper bound still applies).
/// to <c>0</c> resets to the spec maximum (<c>125</c>) without requiring the operator
/// to hard-code the limit. Values above <c>125</c> are spec-illegal for conforming
/// servers; only use them when the target device's documentation explicitly allows it.
/// </summary>
public ushort MaxRegistersPerRead { get; init; } = 125;
@@ -55,8 +57,11 @@ public sealed class ModbusDriverOptions
/// spec allows up to <c>2000</c> bits per request — separate from
/// <see cref="MaxRegistersPerRead"/> because the underlying packing is different
/// (1 bit per coil vs 16 bits per register). Default <c>2000</c>; setting to <c>0</c>
/// disables the cap. The driver auto-chunks coil-array reads above the cap.
/// resets to the spec maximum (<c>2000</c>). Values above <c>2000</c> are spec-illegal
/// and will cause most PLCs to reject the request with exception 03. The driver
/// auto-chunks coil-array reads above the cap.
/// </summary>
[Range(0, 2000)]
public ushort MaxCoilsPerRead { get; init; } = 2000;
/// <summary>
@@ -34,6 +34,16 @@ public static class ModbusEquipmentTagParser
var byteOrder = ReadEnum(root, "byteOrder", ModbusByteOrder.BigEndian);
var bitIndex = (byte)ReadInt(root, "bitIndex");
var stringLength = (ushort)ReadInt(root, "stringLength");
// Guard: String tags require StringLength >= 1. RegisterCount = (StringLength+1)/2,
// so StringLength=0 → 0 registers → spec-illegal FC03/FC04 with quantity=0 → PLC
// returns exception 03. Reject here (analogous to Driver.Modbus-009 / ValidateStringLength
// in the pre-declared tag path) so the driver surfaces BadNodeIdUnknown rather than a
// misleading BadCommunicationError. (Driver.Modbus.Contracts-002)
if (dataType == ModbusDataType.String && stringLength < 1) return false;
// Guard: BitInRegister tags require BitIndex 015. Shift by ≥ 16 on an int overflows
// a ushort mask silently — reads always return false, writes have no effect.
// (Driver.Modbus.Contracts-001)
if (dataType == ModbusDataType.BitInRegister && bitIndex > 15) return false;
// isArray / arrayLength — optional keys authored by the typed Modbus tag editor.
// Canonical rule: a tag is an array iff isArray:true AND arrayLength >= 1.
// isArray:false (with any arrayLength) is scalar — the foundation materialises a