Files
lmxopcua/code-reviews/Driver.AbCip.Contracts/findings.md
T
Joseph Doherty ab57e53b92 fix(code-review): resolve Batch 2 open findings (AbCip, AbLegacy, Galaxy, FOCAS)
- Driver.AbCip.Contracts-001: parse 'writable' from TagConfig JSON (default true) instead of hardcoding
- Driver.AbCip.Contracts-002/-003: Dt type comment; drop dead [Display]/[Range] annotations
- Driver.AbCip.Contracts-004: dedicated AbCipEquipmentTagParser test class (+15)
- Driver.AbCip-017: document Tick severity Low-fallback on Bad severity read
- Driver.AbLegacy.Contracts-002/-003/-004: isArray-scalar remarks (+tests), MaxTagBytes/ForFamily docs
- Driver.Galaxy.Browser-003 + Driver.Galaxy.Contracts-003: extract ResolveApiKey -> GalaxySecretRef (dedup)
- Driver.Galaxy-019: cache buffered-interval only on Ok + ILogger warnings + ClassifyIntervalReply (+tests)
- Driver.FOCAS.Contracts-002: thread WriteIdempotent through DiscoverAsync (+test)
2026-06-20 22:43:36 -04:00

247 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Code Review — Driver.AbCip.Contracts
<!-- Template for a per-module findings file. Copy to code-reviews/<Module>/findings.md.
See ../../REVIEW-PROCESS.md for the full process. The base README.md is generated
from the per-module `findings.md` files by regen-readme.py — do not edit README.md by hand. -->
| Field | Value |
|---|---|
| Module | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts` |
| Reviewer | Claude Code |
| Review date | 2026-06-19 |
| Commit reviewed | `a19b0f86` |
| Status | Reviewed |
| Open findings | 0 |
## Checklist coverage
A comprehensive review completes every category, recording "No issues found" where
a category produced nothing rather than leaving it blank.
| # | Category | Result |
|---|---|---|
| 1 | Correctness & logic bugs | Driver.AbCip.Contracts-001, Driver.AbCip.Contracts-002 |
| 2 | OtOpcUa conventions | No issues found |
| 3 | Concurrency & thread safety | No issues found |
| 4 | Error handling & resilience | No issues found |
| 5 | Security | No issues found |
| 6 | Performance & resource management | No issues found |
| 7 | Design-document adherence | No issues found |
| 8 | Code organization & conventions | Driver.AbCip.Contracts-003 |
| 9 | Testing coverage | Driver.AbCip.Contracts-004 |
| 10 | Documentation & comments | Driver.AbCip.Contracts-005, Driver.AbCip.Contracts-006 |
## Findings
<!-- One ### entry per finding. IDs are <Module>-NNN, sequential within the module,
never reused. Findings are never deleted — close them by changing Status and
completing Resolution. -->
### Driver.AbCip.Contracts-001
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Correctness & logic bugs |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs:42` |
| Status | Resolved |
**Description:** `AbCipEquipmentTagParser.TryParse` hard-codes `Writable: true` on every
equipment-tag definition it produces, regardless of any `writable` field in the TagConfig JSON.
The consequence is that an operator who intends a read-only equipment tag — relying on the PLC's
ExternalAccess attribute to block writes — will still receive a writable OPC UA node in the
address space: the server advertises the node as writable, and the `OnWriteValue` path in the
driver dispatches writes to the PLC (which the PLC may then reject). The node is never declared
`BadNotWritable` proactively. This is inconsistent with the pre-declared tag path, where
`AbCipTagDefinition.Writable` is explicitly authored per tag and controls the OPC UA
`AccessLevel` bit at materialization.
Additionally, `TryParse` does not guard against `AbCipDataType.Structure` in the `"dataType"`
JSON field. An equipment tag authored with `"dataType":"Structure"` succeeds and returns a
Structure-typed definition with `Members: null`. The driver treats this as a black-box
Structure (readable via dotted-path child addressing), but the address space emits a placeholder
`String` variable for it rather than a UDT folder. No error or warning is surfaced. The AdminUI
tag editor does not expose UDT member declarations, so a Structure equipment tag is essentially
a misconfiguration.
**Recommendation:** (1) Read a `"writable"` boolean field from the TagConfig JSON (default `true`
when absent) and thread it into `AbCipTagDefinition.Writable`. This matches the contract on the
record's `Writable` parameter. (2) Either return `false` from `TryParse` when `dataType` parses
to `AbCipDataType.Structure` (equipment-tag flow cannot declare members), or add an explicit
comment documenting the black-box dotted-path behaviour so the next reader understands the intent.
**Resolution:** Resolved 2026-06-20 — (1) `TryParse` now reads the optional `"writable"` boolean
field from the TagConfig JSON and threads it into `AbCipTagDefinition.Writable`, defaulting to
`true` when the field is absent. The `<remarks>` on `TryParse` was updated to document this
behaviour. (2) For the Structure concern, zero-behaviour-change option taken: an inline comment
was added at the `dataType` parse site in `TryParse` documenting that a `Structure` dataType on an
equipment tag is treated as a black-box dotted-path read (libplctag resolves the full path; the
equipment-tag flow does not enumerate UDT members). New tests in `AbCipEquipmentTagParserTests`
cover `writable:false`, `writable` absent, and the `Structure` path. Suite green (322 tests).
---
### Driver.AbCip.Contracts-002
| Field | Value |
|---|---|
| Severity | Low |
| Category | Correctness & logic bugs |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDataType.cs:28` |
| Status | Resolved |
**Description:** The inline comment on `AbCipDataType.Dt` reads:
```
Dt, // Date/Time — Logix DT == DINT representing seconds-since-epoch per Rockwell conventions
```
This description is inaccurate in two ways. First, `CipSymbolObjectDecoder` (in the driver
module) maps both CIP type code `0xCD` (Logix `DATE` — a 4-byte unsigned integer representing
days since 1 January 1984) and `0xCF` (Logix `DATE_AND_TIME` / `DT` — an 8-byte unsigned
integer representing microseconds since 1 January 1970) to `AbCipDataType.Dt`. The comment
fits only the `DT` / `DATE_AND_TIME` form, not `DATE`. Second, even for `DT` proper, the unit
is microseconds, not seconds.
In practice the driver reads `Dt` via `GetInt32(offset)` with a 4-byte stride: this is correct
for `DATE` but truncates `DATE_AND_TIME` to its low 4 bytes, losing the upper 32 bits. That
underlying decode issue lives in `LibplctagTagRuntime` (the Driver.AbCip module), but the
inaccurate comment here perpetuates the misconception for anyone authoring or reading configs
that reference the `Dt` member.
**Recommendation:** Update the inline comment to describe both mapped CIP types and the
4-byte stride constraint, e.g.:
```csharp
Dt, // Logix DATE (0xCD — 4-byte unsigned days since 1984-01-01) or DATE_AND_TIME / DT
// (0xCF — 8-byte unsigned microseconds since 1970-01-01). The driver reads 4 bytes
// via GetInt32; DATE decodes correctly, DATE_AND_TIME is truncated to the low 4 bytes
// (a known limitation tracked in LibplctagTagRuntime).
```
No behaviour change; documentation only.
**Resolution:** Resolved 2026-06-20 — replaced the inaccurate single-line comment on `Dt` with a
3-line comment describing both mapped CIP types (`0xCD` DATE / `0xCF` DT), the 4-byte read stride,
and the truncation note for DATE_AND_TIME. Build green (0 errors, 0 warnings).
---
### Driver.AbCip.Contracts-003
| Field | Value |
|---|---|
| Severity | Low |
| Category | Code organization & conventions |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs:84-85` |
| Status | Resolved |
**Description:** `AbCipDriverOptions.ProbeTimeoutSeconds` carries `[Display]` and `[Range(1, 60)]`
attributes from `System.ComponentModel.DataAnnotations`. No other driver contracts project
(Modbus, S7, AbLegacy, Galaxy, TwinCAT, FOCAS, OpcUaClient) annotates `ProbeTimeoutSeconds`
with data-annotation attributes. More importantly, `AbCipDriverOptions` is never bound directly
by an ASP.NET model binder: the AdminUI driver page (`AbCipDriverPage.razor`) deserializes it
via `JsonSerializer.Deserialize<AbCipDriverOptions>` and then transfers values into a separate
`FormModel`. Neither the `[Display]` grouping hint nor the `[Range]` validation is evaluated at
runtime — they are dead metadata. The `using System.ComponentModel.DataAnnotations;` directive
exists in this project solely to support these two unused attributes.
**Recommendation:** Remove `[Display]` and `[Range(1, 60)]` from `ProbeTimeoutSeconds` and
remove the `using System.ComponentModel.DataAnnotations;` directive. This brings the project in
line with every other driver contracts project and removes a superfluous framework dependency.
If the intent is to document the valid range, an `<remarks>` tag (e.g. "Valid range: 160
seconds; the AdminUI clamps to 60s server-side.") achieves the same goal without the attribute
dependency.
**Resolution:** Resolved 2026-06-20 — removed `[Display]` and `[Range(1, 60)]` from
`ProbeTimeoutSeconds` and removed the `using System.ComponentModel.DataAnnotations;` directive
(confirmed it was used only by those two attributes). Added a `<remarks>` element reading
"Valid range: 160 seconds; the AdminUI clamps to 60s server-side." to preserve the intent
in documentation. Build green (0 errors, 0 warnings).
---
### Driver.AbCip.Contracts-004
| Field | Value |
|---|---|
| Severity | Low |
| Category | Testing coverage |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs` (entire file) |
| Status | Resolved |
**Description:** The contracts module has no dedicated test project, and `AbCipEquipmentTagParser.TryParse`
is the module's only non-trivial logic. Its `ReadArrayShape` helper has four distinct outcome
branches; the method also has multiple early-return paths and one JSON exception path. None of
these are covered by targeted unit tests. Coverage gaps include:
- `isArray: true` + `arrayLength: 0` — canonical rule says this is a scalar; not tested.
- `isArray: true` + `arrayLength` absent — same canonical rule; not tested.
- `"dataType": "Structure"` input — no test (see Driver.AbCip.Contracts-001).
- The `Writable: true` hardcode is not asserted anywhere.
- A `tagPath` that is present as a JSON string but contains only whitespace — returns `false`; not tested.
- Malformed JSON input (the `JsonException` catch) — not tested.
**Recommendation:** Add a dedicated test class for `AbCipEquipmentTagParser` in the existing
driver test project (`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/`). No new project
is needed. Cover: valid scalar round-trip, 1-element array, N-element array, each degenerate
array-shape combination, non-JSON and non-object input, missing/blank `tagPath`, the Structure
DataType path, and the `Writable` default.
**Resolution:** Resolved 2026-06-20 — added `AbCipEquipmentTagParserTests.cs` to
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/` (15 tests). Covers: valid scalar
round-trip; 1-element array (`isArray:true, arrayLength:1`); N-element array; `isArray:true +
arrayLength:0` → scalar; `isArray:true + arrayLength absent` → scalar; non-JSON input → false;
non-object JSON array → false; non-object JSON string → false; missing `tagPath` → false; blank
`tagPath` → false; `tagPath` as number → false; `writable:false` honoured; `writable` absent
defaults to `true`; `writable:true` explicit; `dataType:"Structure"` accepted with `Members:null`
(documents current black-box behaviour). Suite green (322 tests).
---
### Driver.AbCip.Contracts-005
| Field | Value |
|---|---|
| Severity | Low |
| Category | Documentation & comments |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs:141-145` |
| Status | Resolved |
**Description:** The `<param name="IsArray">` doc for `AbCipTagDefinition` did not state that
the field is "Ignored for `AbCipDataType.Structure`", while the immediately preceding
`<param name="ElementCount">` (line 140) explicitly carries that note. The asymmetry left readers
unable to determine from the XML doc whether `IsArray = true` on a Structure-typed definition
is meaningful or silently ignored.
**Recommendation:** Add "Ignored for `<see cref="AbCipDataType.Structure"/>`." to the
`IsArray` param doc, matching `ElementCount`.
**Resolution:** Resolved 2026-06-19 — added "Ignored for `<see cref="AbCipDataType.Structure"/>`."
to the `IsArray` `<param>` doc on `AbCipTagDefinition`. Build verified green (0 errors, 0 warnings).
---
### Driver.AbCip.Contracts-006
| Field | Value |
|---|---|
| Severity | Low |
| Category | Documentation & comments |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs:11-14` |
| Status | Resolved |
**Description:** The `TryParse` method summary and parameter docs were silent about the fact
that `Writable` is always set to `true` in the produced `AbCipTagDefinition`. This is a
non-obvious behavioral contract: callers who inspect the returned definition expecting `Writable`
to reflect an authored intent from the JSON will find it unconditionally enabled. The omission
made the hardcode invisible to readers of the public-facing doc comment.
**Recommendation:** Add a `<remarks>` element to `TryParse` stating that `Writable` is always
`true` in the produced definition and that the PLC's ExternalAccess attribute is the effective
write gate.
**Resolution:** Resolved 2026-06-19 — added a `<remarks>` element to `TryParse` documenting
that `Writable` is always `true` and that PLC ExternalAccess is the effective write gate.
Build verified green (0 errors, 0 warnings).