fix(driver-modbus-addressing): resolve Low code-review findings (Driver.Modbus.Addressing-006,007,009)

- Driver.Modbus.Addressing-006: broaden the catch in TryParseFamilyNative
  so a future helper throwing a non-Argument/Overflow type still satisfies
  the try-parse contract.
- Driver.Modbus.Addressing-007: document that the address grammar does
  not carry ModbusStringByteOrder (the structured-tag path does);
  add a 'Grammar scope' bullet to docs/v2/dl205.md.
- Driver.Modbus.Addressing-009: reword the ModbusModiconAddress comments
  so they don't imply a leading-digit invariant the parser doesn't
  enforce.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-23 08:18:15 -04:00
parent 1f29b215c8
commit 9263519852
6 changed files with 178 additions and 15 deletions

View File

@@ -29,6 +29,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
/// <item><c>C100</c> — Coils[99] (mnemonic).</item>
/// </list>
/// </para>
/// <para>
/// <b>Grammar scope — out of band (Driver.Modbus.Addressing-007):</b> per-string byte
/// order (<see cref="ModbusStringByteOrder"/>) is NOT expressible through this grammar.
/// The DL205 low-byte-first string-packing knob is configurable only via the structured
/// tag form (the driver's <c>ModbusTagDefinition.StringByteOrder</c> field). The 3rd
/// grammar field is the multi-register word/byte order (ABCD/CDAB/BADC/DCBA), not the
/// per-string byte order — adding a 5th token would conflict with the array-count slot.
/// See <see cref="ModbusStringByteOrder"/> and <c>docs/v2/dl205.md</c> §Strings.
/// </para>
/// </remarks>
public static class ModbusAddressParser
{
@@ -341,8 +350,15 @@ public static class ModbusAddressParser
return false;
}
}
catch (Exception ex) when (ex is ArgumentException or OverflowException)
catch (Exception ex)
{
// Driver.Modbus.Addressing-006: a try-parse method must never throw, so any helper
// exception is converted to a structured error. The current helpers throw only
// ArgumentException (incl. ArgumentOutOfRangeException) and OverflowException, but
// catching narrowly would silently break the TryParse contract if a helper ever
// switches to e.g. FormatException from a ushort.Parse swap. Config-bind hot-path
// callers depend on TryParse returning a structured (false, error) rather than
// throwing an unhandled exception that escapes their TryParse wrapper.
error = $"Family-native parse for {family} failed on '{text}': {ex.Message}";
return false;
}

View File

@@ -88,6 +88,25 @@ public enum ModbusByteOrder
/// each register. Word ordering across multiple registers is always ascending address for
/// strings — only the byte order inside each register flips.
/// </summary>
/// <remarks>
/// <para>
/// <b>Grammar scope (Driver.Modbus.Addressing-007):</b> this enum is intentionally NOT
/// expressible through the <see cref="ModbusAddressParser"/> grammar string. The grammar
/// has no token form for it, and <see cref="ParsedModbusAddress"/> has no field for it —
/// a DL205 string tag parsed from the grammar always carries the driver's default order.
/// </para>
/// <para>
/// The string byte order is configurable only via the structured tag definition (the
/// driver's <c>ModbusTagDefinition.StringByteOrder</c> field). Adding a grammar token
/// was explicitly considered and rejected: the 3rd-field slot is the multi-register
/// word/byte order (ABCD/CDAB/BADC/DCBA) and the 4th-field slot is the array count, so
/// a fifth <c>:&lt;order&gt;</c> suffix would conflict with the count-shape disambiguation.
/// Sites that need per-tag low-byte-first strings must use the structured form. The
/// default high-byte-first matches the Modbus spec and Ignition / Kepware default
/// behaviour.
/// </para>
/// <para>See <c>docs/v2/dl205.md</c> §Strings for the DL205-specific rationale.</para>
/// </remarks>
public enum ModbusStringByteOrder
{
HighByteFirst,

View File

@@ -52,10 +52,14 @@ public static class ModbusModiconAddress
return false;
}
// Range check up-front — keeps the rest of the parser straight-line. 5-digit Modicon
// is always exactly 5 chars (40001..49999, with the lead digit selecting region), and
// 6-digit is exactly 6 (400001..465536-shaped). Anything else is unambiguously
// malformed so we reject before doing the per-character work.
// Range check up-front — keeps the rest of the parser straight-line. Modicon addresses
// are exactly 5 or 6 characters: a leading region digit (0/1/3/4 — coils, discrete
// inputs, input registers, holding registers respectively) followed by 4 (5-digit form)
// or 5 (6-digit form) trailing digits encoding the 1-based register number. The
// 5-digit form covers 1..9999 per region (e.g. coils 00001..09999, holding registers
// 40001..49999); the 6-digit form covers the full 1..65536 wire range (e.g. coils
// 000001..065536, holding 400001..465536). Anything else is unambiguously malformed so
// we reject before doing the per-character work.
var s = address.Trim();
if (s.Length is not (5 or 6))
{
@@ -100,9 +104,10 @@ public static class ModbusModiconAddress
return false;
}
// 5-digit form caps at 9999 by construction (4 trailing digits); reject if the parsed
// value exceeds the wire-protocol maximum of 65536 (i.e. PDU offset 65535). 6-digit
// form can address the full 65535-offset range.
// Wire-protocol maximum is register number 65536 (PDU offset 65535). The 5-digit form's
// 4 trailing digits can only encode up to 9999, so this check is reached only by the
// 6-digit form in practice — but it is applied to both for safety / simplicity rather
// than relying on the digit-count invariant.
if (registerNumber > 65536)
{
error = $"Modicon register number {registerNumber} exceeds the wire maximum (65536 / PDU offset 65535)";