Repository navigation
Fix Core/Types audit findings and conform them to the OPC UA spec - #4573
Conversation
- V-1: reserve the NumericIds pinned in the design before allocating identifiers, so an auto-assigned id never duplicates a pinned one. - V-2: order the namespace list with a stable topological sort of the imports instead of pairwise insert-before moves; the dependency load order no longer depends on the order of the dependency files. - V-3: only create the InputArguments/OutputArguments properties of a child method when it has arguments of that direction (regression of #4124, both in ValidateInstance and LinkDependencyInstance). Methods without arguments no longer get empty Argument[] property nodes. - V-4: the trailing "_<digits>" name convention only supplies an enum value when no Identifier/BitMask is authored. - V-5: dependency instances keep an authored ValueRank and matrices keep their rank (mirrors ValidateInstance). - V-6: a method with a TypeDefinition keeps its own authored description. - V-7: IsExplicitlyDefined matches the explicit child or its descendants, not siblings sharing a name prefix. - V-8: the child reference type check was inverted and never ran; unknown reference types are now reported. - V-9: ImportField keeps one dimension per rank for matrix fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r resolution - S-1: always key a NodeSet by the model URI it declares; a differing ModelUri item metadata is reported as a warning instead of silently dropping the model. - S-4: newest-NodeSet selection uses a total order (versioned > unparseable > date-only > none), so the winner no longer depends on input order. - S-5: seed the dependency walk with the root so mutually referencing NodeSets do not list the root twice. - S-8: '+' build metadata no longer marks a version pre-release; the fourth (revision) component is compared. - S-15: default Name/Prefix derived from the model URI are repaired only when they are not valid identifiers (leading digit, '.', invalid chars, keyword, empty path), identically on every OS; valid names are unchanged. - S-2/S-3: identifier sidecar paths are lexically normalized (./, ../, mixed separators) and the suffix fallback matches only on a directory boundary. - S-6: CSVs claimed by a NodeSet IdentifierFile are no longer offered to the ModelDesign pass as identifier files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- S-9: trailers have no length prefix, so an unknown trailer version cannot be skipped. The reader now stops after it instead of parsing the next trailer out of its middle (garbage ids or a dropped payload). The wire format is unchanged; old payloads and old readers are unaffected. - S-10: an empty GuidId and a namespace-only method identity are written and round-trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names (S-7) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- S-11: non-Good StatusCode defaults emit new global::Opc.Ua.StatusCode(0x...u). - S-12: NodeSet values keep their NodeSet-local namespace indexes together with that table (DecodedValueNamespaceUris). NodeId/QualifiedName defaults resolve the namespace by URI at run time (context.NamespaceUris), and ExpandedNodeId defaults are emitted in nsu= form. Without a namespace table in scope (structure fields) the literal id is emitted and compiles. - S-13: double/float literals use the shortest of G15..G17 (G6..G9) that round-trips - identical on .NET Framework and .NET, and double.MaxValue no longer overflows. - S-14: remove the dead BaseDataVariableState<...> special case in IsOverriddenWithSameClass (generated behaviour unchanged). - S-16: ArrayDimensions is not emitted for Scalar/ScalarOrArray/ ScalarOrOneDimension/Any ValueRanks (Part 3 5.6.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stack package carried no build\<PackageId>.props, so StackSourceGeneratorMode never reached the compiler for package consumers (the generator silently emitted nothing) and StackSourceGeneratorExclude was not compiler-visible anywhere. Add the props (Mode + Exclude), pack it like the model generator's props and import it from Opc.Ua.Core / Opc.Ua.Core.Types instead of the hand-written CompilerVisibleProperty. (R-10) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep incremental pipeline values equatable: [NodeManager] discovery and [DataType] compilations carry EquatableArray lists and a LocationInfo value snapshot instead of arrays/lists/Location, and the Exclude option is an EquatableArray, so unrelated edits and analyzer-config re-evaluation leave model and [DataType] generation cached. (R-1, R-7, R-8) - Let cancellation propagate instead of reporting it as MODELGEN003. (R-2) - [DataType] on a struct, a generic type, a private/protected nested type or a type nested in a non-partial type reports MODELGEN037; nested [DataType] types are now generated inside partial declarations of their containing types with nesting-qualified activators. Nested/generic [NodeManager] classes report MODELGEN036. (R-3, R-4) - An ignored ModelDesign input is no longer generated (it stays a dependency, like an ignored NodeSet2); document that URI/Name/Prefix/ Version metadata applies to NodeSet2/WoT inputs only. (R-5) - Only xml AdditionalFiles with a ModelDesign/UANodeSet root enter the model pipeline. (R-6) - WoT virtual-path collisions and [DataType] hint names are compared case-insensitively. (R-9, R-11) Generator diagnostics for [NodeManager]/[DataType] now carry a file location (path + span) rather than a syntax-tree location. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tring resources E-7: EndpointsGenerator dereferenced a null Request (|| instead of &&). E-11: ResourceGenerator emitted "public const string X = ;" for AsUtf16 string resources; emit the escaped literal directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-9: check the cancellation token between models and between generators in GenerateCode/GenerateStack (not inside the emitters' node loops), and let OperationCanceledException escape the Roslyn host instead of turning it into an exception diagnostic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…holders E-2: generated ObjectType proxy accessors (and the Bad_MethodInvalid method fallback) asked the server for the SymbolicName instead of the BrowseName, so they never resolved when the two differ. Placeholder children (<Name>, SymbolicName Name_Placeholder) got single-child accessors that could never resolve anything; they are no longer emitted, neither on the client proxies nor as fluent builder type accessors. RoboticsClient relied on the broken GetMotorIdentifier_PlaceholderAsync / GetGearIdentifier_PlaceholderAsync accessors and on a DriveIdentifier placeholder browse, so MotorIds/GearIds/Drives were always empty against a real server. Motors and gears are now the PowerTrain's HasComponent children of (a subtype of) MotorType/GearType, drives the motor's forward IsDrivenBy targets. The facade test harness no longer fakes children named after the placeholders and uses an exact IsTypeOf answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ObjectTypes E-4: no proxy is emitted for an excluded ObjectType (Purpose=Testing or a configured exclusion such as the host's default "Draft"), yet child accessors returned it and derived proxies inherited from it (CS0246). Child accessors of excluded types are skipped, proxies derive from the nearest emitted ancestor, and the fluent per-type accessors skip Object children of excluded types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anceBrowseName constant
E-1: a DefaultInstanceBrowseName ("Device Set" -> constant DeviceSet)
registered first made a later child with the real symbolic name DeviceSet
fail generation with BadTypeMismatch, while the opposite order succeeded.
Keys derived from DefaultInstanceBrowseName are now tracked and replaced
by a real symbolic name; two real symbolic names still clash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-6: StringLiteralEscaper did not escape NEL/LINE SEPARATOR/PARAGRAPH SEPARATOR, which terminate a C# regular string literal (CS1010). The ad-hoc escapers in the fluent builder, model dependency, constants and server capabilities generators only handled backslash and quote, and the Namespaces constants (URI and XmlNamespace) and the proxy child-accessor namespace URI were emitted raw. All of them now go through StringLiteralEscaper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…non-nullable E-3 (false positive): GetDotNetTypeName only adds a nullable annotation for string, DiagnosticInfo (a class) and structure classes; primitives, DateTimeUtc and enumerations stay non-nullable, so the proxy never emits Variant.From(int?). Add a guard test for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e emitted, and completely E-5: the instance-wrapper collision check ran for every model, so a model without a node manager failed over names no emitted code uses. It now only runs with GenerateManagerWrappers, and additionally reports the collisions that break compilation of the emitted wrappers: nested wrapper types against sibling accessors, other nested types, the reserved members (Builder, Node, Publish) and the enclosing class (CS0542), and top-level accessors against the members the typed manager builder forwards (Context, NodeManager, AddObject, ...). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-8: with a [NodeManager] binding the manager, factory and fluent builder
files were named after the class alone, so two bound managers sharing a
class name in different namespaces overwrote each other in the output
file system and only the last one was generated. The stem is now
{Namespace}.{Class}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ady have accessors E-10: a [NodeManager] bound to a model a reference supplies went through the fluent-accessors-only validation, and when a referenced assembly already provided the accessors the whole pass was skipped - no manager, only a fluent-accessors-only diagnostic. For such binding-driven runs the manager and its typed wrappers are now emitted without the accessors, silently; explicit FluentAccessorsOnly runs keep their diagnostics. E-12: the ModelFluentAccessorProvider attribute recorded the manager namespace of a binding instead of the model prefix, which is what the provider detection compares, so downstream accessors-only builds emitted a second ambiguous set (CS0121). It now records the model prefix, and is only emitted when the accessors are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-9 follow-up: StackGeneration turned OperationCanceledException into an exception diagnostic, like ModelCompilation did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CallAsync fallback The protected CallAsync(context, objectId, ...) fallback called Call(context, default, ...) so a sync OnCallMethod2 handler invoked through the async call path received NodeId.Null as objectId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…method arguments - NS-1: NodeSet categories were written into the generated Categories string array without escaping; a quote or backslash broke compilation. Use AsStringLiteral like every other text sink. - NS-2: structure-typed method arguments with ValueRank ScalarOrArray, ScalarOrOneDimension or Any are typed as Variant, but Call/CallAsync used the IEncodeable-constrained TryGetValue/FromStructure overloads (CS0315). Route them through the plain Variant path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es and type method declarations The ObjectType, VariableType, ReferenceType, DataType and child Method factories wrapped the AccessRestrictions/RolePermissions assignments in if (forInstance), but type factories are only invoked with forInstance: false, so the attributes of type nodes (and of method declarations on types) were never set. The generator already documents emitting them on all nodes; enforcement on type hierarchy nodes is bypassed at runtime via NodeState.IsPartOfTypeHierarchy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erty name
The {ClassName}Value class read and wrote m_value.{FieldName} with the
authored structure field name, while DataTypeGenerator renames fields that
collide with the enclosing type name, reserved members (TypeId, Encode,
...) or inherited properties. Use Parameter.GetPropertyName() for the
structure accessor and keep the authored name for the child variable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ke node attributes Factories assigned node attributes through the `state` local, which is typed as the generated class, so a child named DataType, ValueRank, AccessLevel, EventNotifier, TypeDefinitionId, NumericId, ... bound the assignment to the child property (CS0029). Assign attributes through a `baseState` local typed as the runtime base (BaseVariableState, BaseObjectState, MethodState, BaseVariableTypeState) and RolePermissions through `nodeState`, and declare such child properties `public new` when they hide a runtime base member (was CS0108). Tests that matched the emitted `state.X =` text are updated to the new `baseState.X =` / `nodeState.RolePermissions =` shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetDefaultTypeDefinitionId/GetDefaultDataTypeId and the DataTypeDefinition
factory call emitted `{Prefix}.ObjectTypes/VariableTypes/DataTypes/
DataTypeDefinitions` without global::. Inside `namespace Acme.Opc.Ua.Pumps`
the reference `Opc.Ua.DataTypes.Double` binds `Opc` to `Acme.Opc` (CS0234,
or a silent wrong binding when that class exists).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e real root A HierarchyReference with TargetPath "" names the root of the hierarchy (the type or top-level instance), but GetReferences emitted it against the source node's immediate parent. For a grandchild such as MachineType/Components/Motor the reference pointed at Components while the root's own factory emitted the inverse end from Motor. Walk up to the hierarchy root instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l reset of generated data types - D-1: CollectStructureDefinitionFields returned before walking the base chain when a type declared no own fields, so e.g. AnonymousIdentityToken published an empty StructureDefinition and a type deriving from a field-less type lost all fields above it. - D-2: DerivedClassWithOptionalFields.IsEqual now compares EncodingMask; with a base without optional fields equality was asymmetric. - D-3: GetHashCode hashes an optional field only when its mask bit is set and a union member only while it is the switch field, matching IsEqual. - D-4: Reuse() now resets through a virtual ResetForReuse() that calls Initialize() (model defaults, new T() structure fields, EncodingMask, SwitchField) and chains to the pooled base; inherited fields of abstract or library ancestors are reset through their properties. - D-6: OptionSet EnumDefinition bit index is computed through ulong so a UInt64 OptionSet using bit 63 no longer throws OverflowException. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… matrices and clone consistent - D-7: the emitted StructureDefinition now describes what Encode() writes: nullable properties are no longer declared optional (no encoding mask is written), inline (sealed / StructureHandling.Inline) structure fields name the concrete structure instead of the abstract Structure, and a derived type whose base activator exposes a definition prepends the base fields, names the base data type and sets FirstExplicitFieldIndex. - D-8: DefaultValueHandling.Exclude omits a value only when a missing field decodes back to it: the literal initializer (or the CLR default without one); a property with a non-literal initializer is always encoded. - D-9: MatrixOf<T> properties emitted non-existent Write/Read<T>Matrix calls. Structure matrices use Write/ReadEncodeableMatrix, other element types travel in a Variant like the model driven generator does; element types without a Variant matrix accessor are reported as unsupported. - D-10: Clone() deep copies ArrayOf/MatrixOf of structures, Variant, ExtensionObject and DataValue. - D-11: [Flags] enum definitions use the bit position as EnumField value and skip the zero and combined members; ulong values above long.MaxValue no longer collapse to 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and empty unions correctly in the schemas - D-12 (bsd): a subtype of the OptionSet structure is emitted as a StructuredType with BaseType ua:OptionSet (Value and ValidBits) instead of a 32 bit EnumeratedType; LengthInBits of an integer OptionSet walks the base chain to the built-in integer; the synthetic "None" is only added when no zero member exists and no longer throws without fields. - D-13 (bsd): fields the generated Encode writes as a Variant (ranks other than Scalar/Array, matrices of built-in types, enums, subtyped structures) are described as ua:Variant; a concrete structure matrix as its Int32 dimensions followed by the counted elements. - D-14 (xsd): a structure with optional fields starts with the EncodingMask element the XmlEncoder writes; a derived type introducing optional fields (mask written before the base fields) is flattened; the union choice is minOccurs="0" for SwitchField 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…simple data types - D-15: a companion model's event records now expose Decoder.GetStandardFields(NamespaceTable), which puts the browse names the model declares into the model's namespace index of the session; the generated registration uses it, so select clauses match (OPC 10000-4 7.7.4). Standard (ns=0) fields stay in namespace 0 and StandardFields keeps its positions for existing callers. - D-16: a property whose data type is a companion model simple subtype of a built-in type (e.g. Percent : Double) is typed as that built-in type instead of a class that is never generated; the name based switch of standard data types only applies to namespace 0, so a model data type named e.g. Duration is no longer mistaken for the standard one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… namespaces - D-8: treat a string.Empty initializer as the literal "" so such properties keep being omitted when empty. - D-15: emit the model namespace URI as a literal in GetStandardFields so event records do not depend on the model's Namespaces constants class. - Tests: static readonly expectation arrays (CA1861). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#4569 hashes every empty MatrixOf alike and treats them as equal whatever their dimensions; the T2-2 fix compared the shape first, so [0,5] and [5,0] were unequal while hashing alike. Check emptiness before the shape, which matches #4569 and OPC 10000-4 7.7.3 (empty arrays of the same DataType are equal). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@copilot resolve the merge conflicts in this pull request |
…e-bugs-0d1069 # Conflicts: # tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignValidator.cs # tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Merged |
|
|
|
@copilot fix ci breaks |
Code coverage✅ Coverage gate passed.
Uncovered changed lines
Coverage is above the recorded baseline - consider ratcheting Thresholds live in |
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Fixed the CI breaks in d087bff. Six causes:
Locally on net10.0, Core.Tests, PubSub.Tests, the AOT VariantStorage tests and the filter/certificate/redundancy Server tests pass, and Core.Tests builds for net48. The macOS AOT and Windows jobs weren't run locally. |
…CTT regression from #4573) (#4593) ## Description Fixes a CTT regression introduced by #4573, found in a full CTT run of master after the merge. **Attribute Write Index `Err-001.js` went Pass → Error.** The test writes a range that starts beyond the end of each array (`1073741824:1073741824+len`), with data whose size differs from the range by one element. It expects `Bad_IndexRangeNoData` or `Bad_WriteNotSupported`. #4573 checked the size of the written data before it checked whether the range lies within the target, so the server returned `Bad_IndexRangeDataMismatch`. Part 4 §7.27 says no data exists within such a range, so `Bad_IndexRangeNoData` is the correct result. `NumericRange.UpdateRange` now checks the lower bounds against the target first, for arrays, Strings, ByteStrings and matrices. A size mismatch within the target still returns `Bad_IndexRangeDataMismatch` (Part 4 §5.11.4.4), and the existing mismatch tests are unchanged. Also in this PR: `plans/ctt-issues.md` C1 now lists Aggregate – NumberOfTransitions `003-02.js` and `004-02.js`. Since #4545, NumberOfTransitions honours TreatUncertainAsBad, and the harness always sends `UseServerCapabilitiesDefaults = TRUE` (Mantis 11420). ## CTT run (scripts 1.05.513, all 23 parts) The run compared master with #4573 merged (`20c2d77d5`) against the last recorded full run (2026-09-24, `22d07a31e`) at test-case level. It also compared against master just before the merge (`87c35b84b`). | Change vs recorded run | Cause | | --- | --- | | Attribute Write Index `Err-001.js` Pass → Error | This PR fixes it; with the fix: Pass, 0 errors in the group | | NumberOfTransitions: 11 cases Error → Pass; `003-02`/`004-02` Pass → Error | #4545 (intentional), CTT defect C1 | | Subscription Basic `Err-002.js` | Local project still claimed 100 subscriptions while #4545 raised the server to 200; passes with the checked-in template value | | A & C Refresh2/Shelving/Confirm: 9 cases improved | — | Every other part is identical at test-case level, apart from the usual "Timestamp shows a delay" warnings. All remaining errors are documented CTT defects. ## Tests - 3 new `NumericRangeTests` (array, String, matrix: range beyond the target with a wrong-size slice → `Bad_IndexRangeNoData`) - `Opc.Ua.Types.Tests` (net10.0): 11006 passed, 0 failed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Description
Makes every push to `master` and `release/*` report both required
checks, so a release branch can be cut from `master`.
Two things stopped `master` commits from getting a green `images
summary`, which the `Release` ruleset requires, including when a branch
is created:
1. **Cancellation.** `CI` (`buildandtest.yml`) and `Images CI`
(`docker-image.yml`) used one concurrency group per ref with
`cancel-in-progress: true`. Each merge to `master` cancels the
still-running checks of the commit before it. A queued run in the same
group is replaced as well ("Canceling since a higher priority waiting
request … exists"). `Images CI` takes about 100 minutes, and `master`
currently gets a merge about every 90 minutes:
| Commit | `build-and-test summary` | `images summary` |
| --- | --- | --- |
| `87c35b84b` (#4572) | success | failure: `pumpserver` / `refserver`
cancelled after ~100 min by the #4573 push |
| `20c2d77d5` (#4573) | success | failure: `refserver` / `mcpserver` /
`pumpserver` cancelled by the #4577 push |
The ruleset has no bypass actors, so `git push origin
20c2d77:refs/heads/release/2.0` is rejected with `Required status
check "images summary" is failing`.
2. **Push path filter.** `Images CI`'s push trigger had `paths-ignore:
docs/**, tests/**`. A documentation- or test-only push to `master` or
`release/*` never started the workflow, so it never reported `images
summary` and could never be a cut point.
## Changes
- **Concurrency (both workflows):** pushes to protected lines (`master`,
`main`, `master378`, `release/*`) get one concurrency group per commit
(`github.sha`) and never cancel, so every commit runs its checks to
completion. Pull requests and other branches are unchanged: they group
by PR number or ref and cancel superseded runs.
- The per-commit group is needed as well as `cancel-in-progress: false`.
With a shared group GitHub still replaces a *queued* run.
- **`Images CI` push trigger:** the `paths-ignore` is gone. The
`discover` job now decides relevance for pushes the same way it already
did for pull requests.
- It diffs `github.event.before..HEAD` and skips the image builds for
docs/tests-only changes, while `images summary` still reports.
- A new branch (all-zero `before`) or a `before` commit the clone
doesn't have builds everything, and a failed diff still fails the gate.
- `main` and `master378` are not added to the push trigger: `main`
doesn't exist, and `master378` runs its own copy of this workflow.
## Verification
- The `discover` script was extracted and run locally for push events:
| Push | `relevant_changes` |
| --- | --- |
| docs-only | `false` (images skipped, summary reports) |
| workflow change | `true` |
| new branch | `true` |
| unknown `before` | `true` |
- The CI-related `Opc.Ua.Tools.Tests` (`~Ci`, 53 tests, including the
fail-closed diff pin) pass on net10.0.
## Cost
Overlapping merges to `master` now run CI and image builds in parallel
instead of cancelling each other, which uses more runner minutes on busy
days. Image pushes from overlapping `master` runs can finish out of
order, so `latest-master` reflects whichever finishes last. Version tags
are per build and unaffected.
## Related Issues
- Blocks cutting `release/2.0` for `2.0.0-preview.6` (see #4574).
## Checklist
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Description The first PubSub exchange in [docs/PubSub.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/PubSub.md#run-a-first-publisher-and-subscriber) delivers no DataSets on master or in the 2.0.0 source. The subscriber only logs `DataSetReader Reader 1 faulted on MessageReceiveTimeout`, while the publisher logs `UADP security wrap failed; dropping message` with `ArgumentException: AES-CTR nonce must be exactly 12 bytes` for every NetworkMessage. Cause: the reference PubSub sample's demo keys use a 12-byte KeyNonce. Since #4573, the AES-CTR counter block follows OPC 10000-14 Table 157 (`KeyNonce[4] || MessageNonce[8] || BlockCounter[4]`). With the sample's keys, the publisher builds a 20-byte nonce, and `AesCtrTransform` rejects it. The 2.0.0 release (release/2.0 at bc5da10) contains #4573 and the 12-byte sample, so it is affected too. Fix: the sample sizes its KeyNonce with `AesCtrNonceLayout.KeyNonceLength`. No SDK change. Regression test: `PubSubAotTests.SamplePublisherDeliversSecuredDataSetToSampleSubscriberAsync` starts the sample publisher and subscriber with the sample keys over unicast loopback and waits for a DataSet. The existing sample-based AOT tests only built or started the applications, so nothing caught the regression. ### Validation | Command | Result | | --- | --- | | `tests\Opc.Ua.Aot.Tests\bin\Debug\net10.0\Opc.Ua.Aot.Tests.exe --treenode-filter "/*/*/PubSubAotTests/SamplePublisherDeliversSecuredDataSetToSampleSubscriberAsync"` (JIT), before the fix | Failed: `TimeoutException`, no DataSet within 10 s | | same, after the fix | Passed, 3 of 3 runs | | `tests\Opc.Ua.Aot.Tests\bin\Debug\net10.0\Opc.Ua.Aot.Tests.exe --treenode-filter "/*/*/PubSubAotTests/*"` | 8 of 8 passed | | The two `dotnet run` commands of the docs/PubSub.md first exchange, run verbatim | 15 DataSets in 15 s; the publisher logs no failures | The release/2.0 branch has the same sample and would need a backport, which is not part of this PR. ## Related Issues - No tracking issue. Found while re-verifying the documented first PubSub exchange after the 2.0.0 release. ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Bug audit of
Opc.Ua.Core,Opc.Ua.Core.Types,Opc.Ua.TypesandOpc.Ua.Core.Schema, with the findings fixed and the fixes grounded against the OPC UA specification (reference.opcfoundation.org). 133 commits in four groups:Fix <ID>: ...): 116 findings from a read-only audit, each with a regression test. IDs follow the audit scopes: T1-T6Opc.Ua.Types, C1-C4Opc.Ua.Core, S1Opc.Ua.Core.Types+Opc.Ua.Core.Schema.Fix review <ID>: ...): issues a code review of group 1 found, plus lower-severity leftovers.Spec: #<n> ...): the 18 places where an earlier fix only partly matched the spec text.Where
master(#4516, #4522, #4540, #4564) or #4569 already fixed the same issue, their version was kept and this PR only adds what they didn't cover.Behaviour and user-facing changes
Wire format and interoperability (breaking)
KeyNonce[4] | MessageNonce[8] | BlockCounter[4], and the block counter starts at 1.Random[4] | UInt32 SequenceNumber, with the sequence number starting at 1.ServerNonce | HASH(ServerCertificate) | ClientNonce.SecurityPolicyInfo.GetUserTokenSignatureData(..., MessageSecurityMode).LastSequenceNumberis correct across the 32-bit sequence wrap (Part 6 6.8.1).Sessions (server)
Bad_NonceInvalidon every channel (Part 4 5.7.2.3).EccEncryptedSecretsigning certificate is validated with the server's peer certificate validator (Part 6 6.8.3). Byte equality with the client certificate is only the fallback when no validator is available. New opt-in interfaceICertificateValidatorProvider.GatewayServerUriandDiscoveryProfileUriare no longer swapped.Write, Read and Call (NodeState / node managers)
ValueForVariableType), returningBad_UserAccessDenied.OnReadUserWriteMaskhandler fails only that item.OnWriteValueAsynchandlers.Bad_IndexRangeDataMismatch(new status code inOpc.Ua.Types).Bad_IndexRangeNoData.Bad_WriteNotSupported(Part 4 7.27, 5.11.4.4)."0:40000,0:40000"no longer allocates about 1.6e9 elements).Bad_TypeMismatch.DistributedValueParticipationmerges index-range writes and applies the index range on reads.ClearChangeMaskstakes the bits atomically, so concurrent changes are no longer lost.AccessLevelEx: preserved by Clone/Export.Alarms & Conditions (Part 9)
Bad_EventIdUnknown.Bad_ConditionBranchAlreadyAcked/...AlreadyConfirmed.OneShotShelved.SelectedResponseis a UInt32.ContentFilterElement.Validate: an unknown FilterOperator is reported per element asBad_FilterOperatorInvalid, instead of failing the whole service call.ContentFilter evaluation (Part 4 7.7.3, on top of #4564)
Variant.ConvertTokeeps its wider set.Likewith a NULL operand is NULL.Encoders and built-in types
XmlDecoder.TreatWhitespaceOnlyStringsAsEmpty.<Identifier>values is ignored, and trailing control characters are stripped froms=ids.XmlElementignores DTDs without expanding entities.nsu=NodeId and an unknownsvu=ExpandedNodeId decode to a namespace-0 String NodeId (5.4.2.10 / 5.4.2.11).svr=is rejected in a NodeId field.BadDecodingError.MaxByteStringLengthno longer applies to strings.i=/s=/g=/b=.ExpandedNodeId.Parserejects an emptynsu=/svu=.,withBad_IndexRangeInvalid.#.StatusCode.WithSubCode/WithCodeBitskeep the severity bits and look up the symbolic id for the new code.TryCastToreturns false on a type mismatch. SoDataValue.GetValue<T>(default)returns the default, andGetValueOrDefault/CastTothrowBadTypeMismatchas documented.System.DateTimecasts are supported.Argument[]) no longer throw.ConvertTokeeps one-element and empty arrays as arrays and checks the conversion is legal for empty arrays.CastFrom(object[]),CastFrom(Enum[])and the obsoleteVariant(object)constructors keep the array shape (previously a one-element array collapsed to a scalar and an empty one to Null).TypeInfo.Unknown/Variant.Null.TypeInforeportValueRanks.Any.MatrixOf.Dimensionsreturns a copy."mul"and no usable translations, or null client locale ids, no longer throw.NodeSet, address space and utilities
svr=65535.StructureDefinition.BaseDataTypeis filled from the supertype on import.NodeTable.Import: replaces duplicates, keeping the references neighbouring nodes hold.TypeTable.Adddetaches a re-parented type from its old supertype, andNode.Clonegets its own ReferenceTable.StringTable/NamespaceTable: capped at UInt16 indexes (BadEncodingLimitsExceeded) for Append and Update, instead of silently wrapping.UnsecureRandom.Sharedis seeded randomly.CancelAsyncandstring.GetHashCode(StringComparison)polyfills match BCL behaviour.CoreUtils.IsEqualcompares strings ordinally.Schema generation and ComplexTypeSystem
JsonEncoderoutput.EncodingMask(xs:unsignedLong) only for StructureWithOptionalFields.Types.xsdnamespace.{Value, ValidBits}. NewUaTypeDescription.IsStructureOptionSetproperty and constructor overload.Certificates and trust lists (Part 4 6.1.3, Part 12)
RejectUnknownRevocationStatusis honoured for inline-trusted issuers unless the entry setsSuppressRevocationStatusUnknown. This changes the strict case ofExplicitIssuerChainIsHonoredWithoutStorePathAsync.Bad_CertificateInvalid.TreatAsInvalid:Bad_CertificateUntrusted, which can't be suppressed or auto-accepted.EccEncryptedSecret: enforces the result of validating the sender certificate.TrustListTransaction:CertificateChangesobserver no longer stops the others.InMemory:stores share their contents per path, andInMemoryStoreProviderisIDisposable.WoT (
Opc.Ua.Types/Wot)WotDiagnosticCode.ProjectionSecurityConflict = 6063.JsonException.nsu=ids are percent-encoded (Part 6 5.1.12).componentOflinking and the namespace table are now linear.New or changed public API
ICertificateValidatorProvider(server)SecurityPolicyInfo.GetUserTokenSignatureData(..., MessageSecurityMode)overloadXmlDecoder.TreatWhitespaceOnlyStringsAsEmptyUaTypeDescription.IsStructureOptionSetplus a constructor overloadWotDiagnosticCode.ProjectionSecurityConflictStatusCodes.BadIndexRangeDataMismatchin theOpc.Ua.TypeslistInMemoryStoreProvider : IDisposableNot changed / open
UaTypeDescription, and BSD is deprecated).MaxArrayLengthalready bounds them).ConvertToStringwrites a QualifiedName asns:Name, while Table 121 drops the namespace.Verification
Spec: #…commits): 0 build warnings and 16 test suites green on net10.0. The suites: Types, Core, Core.Encoders, Core.Security, Core.Schema, Client, Client.ComplexTypes, Server, Sessions, Gds, History, Features, Redundancy.Server, PubSub, WotCon and WotCon.Bindings. Core hit its known intermittent end-of-run hang once, and reruns were clean.Spec: #…fixes were verified per group on their own suites. Those include PubSub.Udp, Redundancy.PubSub, PubSub.Server, Configuration and SourceGeneration.Core.🤖 Generated with Claude Code