Skip to content

Fix Variant encoding and decoding edge cases - #1830

Merged
kevinherron merged 3 commits into
mainfrom
fix/variant-encoding-fixes
Jul 29, 2026
Merged

Fix Variant encoding and decoding edge cases#1830
kevinherron merged 3 commits into
mainfrom
fix/variant-encoding-fixes

Conversation

@kevinherron

@kevinherron kevinherron commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #1829. One commit per fix, consolidating the fixes that were floating on individual fix/* branches:

  • Return -1 from getBuiltinTypeId for non-builtin classes — the BiMap lookup NPE'd on unboxing for a non-builtin class, making the -1 guards in Variant.getDataTypeId and encodeVariant dead code. encodeVariant now throws Bad_EncodingError instead of writing a corrupt encoding mask, and OpcUaJsonEncoder gets the same guard.
  • Fix OptionSet arrays and matrices in VariantsencodeVariant picked an OptionSet's builtin type by casting the value to OptionSetUInteger, which only holds for scalars; a Variant carrying an array or Matrix of option sets failed with ClassCastException (or AssertionError with assertions enabled). The type is now derived from the element class, as the JSON and XML encoders already do.
  • Consume ArrayDimensions when a Variant array is nulldecodeVariant returned early on a -1 array length without reading the ArrayDimensions field when the dimensions bit was set, desynchronizing every subsequent read in the message.

Testing

spotless:check and the stack-core + encoding-json test suites pass, including new regression tests for each fix.

🤖 Generated with Claude Code

kevinherron and others added 3 commits July 28, 2026 20:12
getBuiltinTypeId read an Integer out of a BiMap and returned it as an
int, so a class that backs no builtin type produced a
NullPointerException on unboxing rather than a value. Two callers
already branch on a -1 sentinel that could therefore never be reached:
Variant.getDataTypeId and OpcUaBinaryEncoder.encodeVariant.

Return -1 as those callers expect, and document it.

That makes encodeVariant's dead branch live. It logged a warning and
then carried on to write `typeId | 0x80` as the encoding mask, which for
-1 is 0xFF: a corrupt message the peer rejects as a framing error, far
from where the mistake was made. Throw Bad_EncodingError instead,
matching how the rest of the encoder reports an unencodable value.

OpcUaJsonEncoder gets the same guard. It used the sentinel as a "UaType"
value without checking it, so the contract change would have turned an
exception into silently malformed JSON.

Reproduced by: new Variant(new Object()) — the Variant constructor
performs no verification, so a non-builtin can reach either encoder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
encodeVariant picked an OptionSet's builtin type by casting value to
OptionSetUInteger and inspecting the instance. That only holds for a
scalar. When a Variant carries an array or a Matrix of option sets,
valueClass comes from the element type but value is still the container,
so the cast fails: ClassCastException, or an AssertionError from the
preceding assert when assertions are enabled.

Derive the type from the element class instead, walking the
OptionSetUI8/16/32/64 hierarchy. That is what OpcUaJsonEncoder and
OpcUaXmlEncoder already do; the binary encoder was the outlier. It also
resolves empty arrays, which have no element to inspect.

Reachable from any Variant holding an option set array, e.g. a Read of
an AccessLevelType-typed array Variable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
decodeVariant returned early for a -1 array length without reading the
ArrayDimensions field when the dimensions bit was set, leaving those
bytes in the buffer and desynchronizing every subsequent read in the
enclosing message.

Reproduced by: encoding mask with array and dimensions bits set,
length -1, followed by a dimensions field — any field decoded after
the Variant reads misaligned bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kevinherron kevinherron added this to the 1.1.6 milestone Jul 29, 2026
@kevinherron
kevinherron changed the base branch from binary-serialization-performance to main July 29, 2026 11:02
@kevinherron
kevinherron merged commit c67c00e into main Jul 29, 2026
1 check passed
@kevinherron kevinherron modified the milestones: 1.1.6, 1.1.7 Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant