Skip to content

Add Avro DataEncoding to the stack (1/3, supersedes part of #7) - #10

Merged
marcschier merged 3 commits into
mainfrom
marcschier/encoding-avro
Jul 12, 2026
Merged

marcschier merged 3 commits into
mainfrom
marcschier/encoding-avro

Conversation

@marcschier

Copy link
Copy Markdown
Owner

Part 1 of 3 splitting #7 (Experimental Avro/Protobuf/Arrow encoders) into one PR per encoding, porting each codec into the production stack so the *.Experimental packages are no longer needed.

This PR — Avro

Ports the reference Avro encoder/decoder + schema-exchange into the stack, next to the existing codecs:

  • Core: Stack/Opc.Ua.Types/Encoders/Avro/ (AvroEncoder/Decoder + AvroBinaryReader/Writer), plus shared SchemaId and Encoders/SchemaExchange/Avro* — alongside Binary/Json/Xml.
  • PubSub: Libraries/Opc.Ua.PubSub/Encoding/Avro/ (NetworkMessage adapter) + shared Encoding/SchemaExchange/* and the renamed internal helper PubSubMessageEncoding — alongside Json/Uadp.

Notes

  • Encoders implement the standard IEncoder/IDecoder and already use namespace Opc.Ua / Opc.Ua.PubSub.Encoding, so this is a relocation, not a rewrite.
  • They need net5+ BCL APIs, so they are compile-excluded on the legacy TFMs (net472/net48/netstandard2.0/2.1) via IsTargetFrameworkCompatible('net8.0') conditions; net8/9/10 include them.
  • AvroDecoder.DecodeMessage<T> uses an idiomatic UnconditionalSuppressMessage for the AOT trim warning (all IEncodeable messages have a public parameterless ctor).
  • Arrow-specific members of the shared schema-exchange files are added by the stacked Arrow PR.

Validation

  • net10: Opc.Ua.Core.Encoders.Tests Avro/SchemaId 8/8, Opc.Ua.PubSub.Tests Avro 1/1 pass.
  • Opc.Ua.Types and Opc.Ua.PubSub build clean (0/0) on netstandard2.0 / netstandard2.1 (exclusion verified) and net10.

Split plan

Ports the reference Avro encoder/decoder, SchemaId and Avro schema-exchange types into Stack/Opc.Ua.Types/Encoders (where Binary/Json/Xml live), and the Avro PubSub NetworkMessage adapter plus shared schema-exchange/cache infra into Libraries/Opc.Ua.PubSub/Encoding (where Json/Uadp live). This removes the need for the Opc.Ua.Core.Experimental / Opc.Ua.PubSub.Experimental packages for Avro.

- The encoders implement the standard IEncoder/IDecoder and already use namespace Opc.Ua / Opc.Ua.PubSub.Encoding, so this is a relocation, not a rewrite.

- They require net5+ BCL APIs, so they are compile-excluded on the legacy target frameworks (net472/net48/netstandard2.0/netstandard2.1) via IsTargetFrameworkCompatible(net8.0) conditions; modern net8/9/10 builds include them.

- AvroDecoder.DecodeMessage<T> uses an idiomatic UnconditionalSuppressMessage for the AOT trimming warning (all IEncodeable message types have a public parameterless constructor).

- Renamed the internal shared helper ExperimentalMessageEncoding -> PubSubMessageEncoding.

- Arrow-specific members in the shared schema-exchange files are added by the stacked Arrow PR.

- Tests: AvroRoundTripTests + SchemaIdTests -> Opc.Ua.Core.Encoders.Tests; AvroNetworkMessageTests -> Opc.Ua.PubSub.Tests. net10: core 8/8 + pubsub 1/1 pass; Opc.Ua.Types/PubSub build clean on netstandard2.0/2.1.
…imits)

Addresses two review findings on the ported Avro codec:

1. (High) AvroDecoder had no recursion-depth guard. ReadDiagnosticInfo (InnerDiagnosticInfo),
   ReadDataValue<->ReadVariant, ReadExtensionObject/ReadEncodeable body decode are mutually/self
   recursive on attacker-controlled PubSub input, so a small deeply-nested frame could exhaust the
   stack (uncatchable StackOverflowException -> process crash). Added CheckAndIncrementNestingLevel()
   enforcing Context.MaxEncodingNestingLevels around ReadVariantBody, ReadDataValue,
   ReadDiagnosticInfo, ReadExtensionObject and both ReadEncodeable<T> overloads, mirroring
   BinaryDecoder/XmlDecoder/JsonDecoder.

2. (High) Decoded length/count fields sized allocations with no limit. AvroBinaryReader.ReadBytes/
   ReadString now take a maxLength and throw BadEncodingLimitsExceeded when exceeded; AvroDecoder
   passes Context.MaxByteStringLength/MaxStringLength. AvroNetworkMessageDecoder validates the
   DataSetMessage count and field count against Context.MaxArrayLength before pre-sizing the lists.

Because the guards raise ServiceResultException(BadEncodingLimitsExceeded), the PubSub
AvroNetworkMessageDecoder catch filter now also catches ServiceResultException so limit violations
mark the message invalid (ReceivedInvalidNetworkMessages) instead of escaping TryDecode.

Tests: added deep-nesting, oversized-string and oversized-bytestring decode regressions
(assert BadEncodingLimitsExceeded). Core Avro/SchemaId: 11 passed; PubSub Avro: 1 passed.
avro3 (Low): AvroSchemaAnnouncement.Encode/Decode and AvroSchemaRequest.Encode/Decode construct an
AvroBinaryWriter/AvroBinaryReader (each rents an 8 KB ArrayPool buffer) but only called Flush(), never
Release(), so the rented buffers were dropped instead of returned to the pool on this hot handshake
path. Wrapped usage in try/finally and call Release(). No behavior change; Core Avro/SchemaId: 11 passed.
@marcschier
marcschier force-pushed the marcschier/encoding-avro branch from 01278aa to 7796c37 Compare July 12, 2026 12:45
@marcschier
marcschier merged commit 7624515 into main Jul 12, 2026
@marcschier
marcschier deleted the marcschier/encoding-avro branch July 12, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant