fix(abcip): #611 review round 1 — native keeps BOOL type distinction, WriteWidthRefusal tests, doc caveats

- L2: WriteWidthRefusal leaves a BOOL over a wider value to a runtime that sees the CIP
  type (native): mistyped integer member -> BadTypeMismatch (as its read), 0xD3 BOOL-array
  DWORD -> BadNotSupported; libplctag keeps BadNotSupported for both (size only).
- L4: direct theory over WriteWidthRefusal (1/2/4/8 widths incl LINT/ULINT/LREAL, String/Dt,
  unknown size) + array/bit-selector exemptions with a non-vacuous control.
- L1: CLAUDE.md member/BIT-member writes are ab_server-proven only.
- L3: patch 0003 blind spots documented (Docker README, AbServer-Test-Fixture.md); nits.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 04:31:45 -04:00
parent 5018627b35
commit f85fc8fbeb
7 changed files with 146 additions and 9 deletions
@@ -1197,7 +1197,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// BOOL-array DWORD wrote all 32 bits. Discovered UDT members are where a mis-authored type
// is most likely (browse-commit fills dataType from the template, an operator may retype).
size = runtime.GetSize();
refusal = WriteWidthRefusal(def, parsedPath?.BitIndex, size);
refusal = WriteWidthRefusal(def, parsedPath?.BitIndex, size, runtimeTypesBool: runtime is NativeAbCipTagRuntime);
if (refusal is null)
{
runtime.EncodeValue(def.DataType, parsedPath?.BitIndex, w.Value);
@@ -1710,15 +1710,24 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// would clobber the neighbouring bits, the same refusal the native runtime raises; any other width
/// mismatch is <c>BadTypeMismatch</c>. Arrays, bit selectors (<c>Flags.3</c>, served by the
/// read-modify-write path) and an unknown size (<c>&lt;= 0</c>) are not judged here.
/// <para>
/// A BOOL over a wider value is left to the runtime when it can see the CIP type
/// (<paramref name="runtimeTypesBool"/> — the native transport): it tells a packed BOOL-array DWORD
/// (<c>0xD3</c>, <c>BadNotSupported</c>) from a mistyped integer member (<c>BadTypeMismatch</c>, the
/// same verdict as its read), and refuses both before sending. libplctag exposes only the size, so the
/// two are indistinguishable there and both get the safe <c>BadNotSupported</c>.
/// </para>
/// </summary>
/// <param name="def">The tag being written.</param>
/// <param name="bitIndex">The parsed bit selector, if any.</param>
/// <param name="size">The runtime's read-buffer size in bytes.</param>
/// <param name="runtimeTypesBool">The runtime checks a BOOL write against the controller's CIP type itself.</param>
/// <returns>The refusal status, or <c>null</c>.</returns>
internal static uint? WriteWidthRefusal(AbCipTagDefinition def, int? bitIndex, int size)
internal static uint? WriteWidthRefusal(AbCipTagDefinition def, int? bitIndex, int size, bool runtimeTypesBool = false)
{
if (IsArrayTag(def) || bitIndex is not null || size <= 0) return null;
if (ExpectedScalarWriteWidth(def.DataType) is not { } width || size == width) return null;
if (def.DataType == AbCipDataType.Bool && runtimeTypesBool) return null;
return def.DataType == AbCipDataType.Bool
? AbCipStatusMapper.BadNotSupported
: AbCipStatusMapper.BadTypeMismatch;