fix(ablegacy): refuse libplctag writes to translated status bits (#659 fix round 1)

The read translation R6:0.DN -> R6:0.CON/13 made those names parse in libplctag, which
would have turned a write - previously impossible, tag creation failed - into a masked
write to the control word. LibplctagLegacyTagRuntime.WriteRefusal (via
IAbLegacyTagFactory.WriteRefusal, same plumbing as native) now refuses writes to every
rewritten status bit (R EN/EU/DN/EM/ER/UL/IN/FD, C UA) with BadNotSupported before a
runtime is created; the runtime also refuses as defence in depth. Reads stay
translated. T/C status bits libplctag parses itself keep their pre-existing behaviour.
13 new tests fail with the predicate disabled.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 05:38:24 -04:00
parent 0abce36ca3
commit 7dbb060f92
5 changed files with 184 additions and 14 deletions
+10 -5
View File
@@ -194,8 +194,9 @@ Native routing: SLC500/MicroLogix/PLC-5 are direct; `logixpccc` uses Unconnected
follow-up). PLC-5 writes are refused the same way. libplctag performs a masked bit write here; a native
read-modify-write of the control word would race the PLC updating TT/DN. Reads of status bits are unaffected.
- ⚠️ **On libplctag (the default) a status-bit write is NOT refused** — it goes to the controller as a write to the
timer/counter/control element (libplctag's masked bit-tag write; an `R` status bit is sent as `R<f>:<e>.CON/<bit>`
since #659; not exercised on a wire — `ab_server` has no T/C/R files). Nothing in authoring prevents it: the address is free text,
timer/counter element (libplctag's masked bit-tag write; not exercised on a wire — `ab_server` has no T/C/R
files). This covers the status bits libplctag parses itself (`T` `EN/TT/DN`, `C` `CU/CD/DN/OV/UN`); the ones it
has no mnemonic for — every `R` status bit and `C…UA` — **are** refused on libplctag too (`BadNotSupported`, #659). Nothing in authoring prevents it: the address is free text,
any `dataType` may be chosen (a status bit is naturally authored `Bit`), and `writable` defaults to `true`.
Author status-bit tags with `"writable": false` unless writing them is genuinely intended.
- **PLC-5 native is N-file reads only**; every other PLC-5 operation is refused with `BadNotSupported`.
@@ -242,9 +243,13 @@ status file when a digit follows it), and its sub-element table has only `con`/`
bit libplctag lacks (`R` `EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) → `R6:0.CON/<bit>` (control word + bit, bit numbers
from the native table). The authored grammar is unchanged; `S2:0`/`O0:0`/`I1:0` stay rejected by the parser (one
spelling per address). Re-run on the same idle press 2026-10-09: every address above reads **identical** on both
transports (`S:4` is a free-running clock and differs read to read on each). ⚠️ The translation also means a
libplctag **write** to an `R` status bit now reaches the controller as libplctag's masked bit write (it used to fail
at tag creation) — the same behaviour T/C status bits always had on libplctag; see the status-bit write note above.
transports (`S:4` is a free-running clock and differs read to read on each). **The translation is read-side only —
it does not widen the write surface:** before it, a libplctag write to an `R` status bit (or `C…UA`) could never reach
the PLC (tag creation failed), and `R6:0.CON/13` would otherwise turn it into a masked write to the control word. So
on libplctag a write to any status bit the translation rewrites (`R` `EN/EU/DN/EM/ER/UL/IN/FD`, `C` `UA`) is refused
**`BadNotSupported` before a runtime is created** — nothing sent, health unchanged
(`LibplctagLegacyTagRuntime.WriteRefusal`, asked through `IAbLegacyTagFactory.WriteRefusal`, the same plumbing as the
native refusal). T/C status bits libplctag parses itself keep their pre-existing write behaviour (note above).
Not tested on hardware: L and A files (no press has them), function files, PLC-5, SLC, `logixpccc`.
Simulator evidence: