/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.IO; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using NUnit.Framework; using Opc.Ua.PubSub.Encoding; using PubSubJsonDecoder = Opc.Ua.PubSub.Encoding.Json.JsonDecoder; namespace Opc.Ua.Fuzzing { /// /// Soft rejections are observed at the production core and both retained adapters. /// No test catches unexpected decoder or oracle failures. /// [TestFixture] [Category("Fuzzing")] public sealed class PubSubDecoderRejectionTests { [TestCase("", 0)] [TestCase("{", 0)] [TestCase("null", 0)] [TestCase("42", 0)] [TestCase("[42,true]", 1)] [TestCase("{\"MessageId\":\"missing-type\",\"PublisherId\":\"300\"}", 0)] [TestCase("{\"MessageType\":42,\"Messages\":[]}", 0)] [TestCase("{\"MessageType\":\"unknown\"}", 1)] public async Task MalformedJsonAndInvalidRootsAreSoftRejectedAsync(string text, int received) { await AssertJsonRejectedAsync(Encoding.UTF8.GetBytes(text), received).ConfigureAwait(false); } [TestCase("PublisherId", "301")] [TestCase("DataSetClassId", "aabbccdd-1122-3344-5566-778899aabbcd")] public async Task NestedIdentityConflictRejectsTheEntireStoredJsonMessageAsync( string propertyName, string conflictingValue) { byte[] seed = PubSubSeedAssertions.LoadSeed("Json", "metadata-keyframe-verbose.json"); JsonObject envelope = JsonNode.Parse(seed)!.AsObject(); JsonObject dataSet = envelope["Messages"]!.AsArray()[0]!.AsObject(); dataSet[propertyName] = conflictingValue; byte[] conflicting = JsonSerializer.SerializeToUtf8Bytes(envelope); await AssertJsonRejectedAsync(conflicting, received: 1).ConfigureAwait(false); // Removing only the conflicting nested identity restores all four valid fields. Assert.That(dataSet.Remove(propertyName), Is.True); PubSubNetworkMessageContext context = FuzzableCode.NewContext(); PubSubNetworkMessage recovered = FuzzableCode.DecodePubSubJson( JsonSerializer.SerializeToUtf8Bytes(envelope), context); PubSubSeedAssertions.AssertJsonSeed(recovered, "retained-Verbose-Variant", PubSubFieldEncoding.Variant); PubSubSeedAssertions.AssertDiagnostics(context, received: 1, dataSets: 1); } [TestCase("metadata-keyframe-variant.uadp", PubSubFieldEncoding.Variant)] [TestCase("metadata-keyframe-datavalue.uadp", PubSubFieldEncoding.DataValue)] [TestCase("metadata-keyframe-raw.uadp", PubSubFieldEncoding.RawData)] public void EveryTruncatedSeedHeaderAndFinalFieldAreRejected(string name, PubSubFieldEncoding encoding) { byte[] seed = PubSubSeedAssertions.LoadSeed("Uadp", name); // The fixed seed has a 36-byte network prefix and a 20-byte DataSetMessage header. const int headerSize = 56; Assert.That(seed.Length, Is.GreaterThan(headerSize)); for (int length = 0; length < headerSize; length++) { byte[] truncated = seed.AsSpan(0, length).ToArray(); PubSubNetworkMessageContext context = FuzzableCode.NewContext(); Assert.That(FuzzableCode.DecodeUadp(truncated, context), Is.Null, $"Header prefix length: {length}"); PubSubSeedAssertions.AssertDiagnostics(context, invalid: length == 0 ? 0 : 1); PubSubSeedAssertions.ReplayUadpAdapters(truncated); } PubSubNetworkMessageContext tailContext = FuzzableCode.NewContext(); byte[] missingFinalByte = seed[..^1]; Assert.That(FuzzableCode.DecodeUadp(missingFinalByte, tailContext), Is.Null); PubSubSeedAssertions.AssertDiagnostics(tailContext, invalid: 1); PubSubSeedAssertions.ReplayUadpAdapters(missingFinalByte); PubSubNetworkMessageContext completeContext = FuzzableCode.NewContext(); PubSubSeedAssertions.AssertUadpSeed(FuzzableCode.DecodeUadp(seed, completeContext), encoding); PubSubSeedAssertions.AssertDiagnostics(completeContext, received: 1, dataSets: 1); } [Test] public void StreamAdapterReadFailuresAreNotSwallowedOrConvertedIntoSoftRejections() { Action[] callbacks = [ FuzzableCode.AflfuzzPubSubJsonDecode, FuzzableCode.AflfuzzUadpNetworkMessageDecode, FuzzableCode.AflfuzzUadpChunkReassembly ]; foreach (Action callback in callbacks) { var failure = new InvalidOperationException("Injected input-reader failure."); using var stream = new FailingReadStream(failure); Assert.That( () => callback(stream), Throws.TypeOf().And.SameAs(failure), callback.Method.Name); Assert.That(stream.CanRead, Is.True, "A failed adapter must not dispose the caller's stream."); } } private static async Task AssertJsonRejectedAsync(byte[] frame, int received) { PubSubNetworkMessageContext coreContext = FuzzableCode.NewContext(); PubSubNetworkMessageContext publicContext = FuzzableCode.NewContext(); PubSubNetworkMessage coreResult = FuzzableCode.DecodePubSubJson(frame, coreContext); PubSubNetworkMessage publicResult = (await new PubSubJsonDecoder() .TryDecodeAsync(frame, publicContext).ConfigureAwait(false))!; Assert.That(coreResult, Is.Null); Assert.That(publicResult, Is.Null); PubSubSeedAssertions.AssertDiagnostics(coreContext, received: received, invalid: 1); PubSubSeedAssertions.AssertDiagnostics(publicContext, received: received, invalid: 1); PubSubSeedAssertions.ReplayJsonAdapters(frame); } private sealed class FailingReadStream(InvalidOperationException failure) : MemoryStream { public override int Read(byte[] buffer, int offset, int count) { throw failure; } } } }