mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 17:16:16 +08:00
9b034f8291
# Description Nullable reference types are now enabled once, in `common.props`, and every project in the repository builds warning-clean with them. **Configuration** - `<Nullable>enable</Nullable>` lives in `common.props`. All per-project `<Nullable>` settings are removed: 178 project files (enable, annotations, and the `disable` in the three Aot test projects). - 419 per-file `#nullable enable`/`restore` directives and the 4 `#nullable disable` directives are removed. - Kept on purpose: - The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`, `PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the project setting in generated code, so the directive is required there. - `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against the 1.5.378 packages and does not import the repository props. - `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now describe the central setting. **Migration** (the 43 unset and 13 annotations-only projects, roughly 10k diagnostics) - **Production code: annotated at the declarations.** Covers `tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the LDS sample. - Members, parameters and returns are nullable where null actually flows. - Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`, `IsMethodTypeDesign`). - `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take `params object?[]`. `Template.AddReplacement` takes `string?` (rendering already skips null replacements). - `IRegisteredServerStore.RegisterAsync` returns `Task<RegistrationEntry?>`: it returns null when a registration is removed. - Dereferences backed by an invariant (for example, the validated model) use `!`, so runtime behaviour is unchanged. - **Behavioural changes in production code** (small, deliberate): - `NodeSetToModelDesign` throws `InvalidOperationException` when the file is not a NodeSet. Before, it threw a NullReferenceException on the next line. - `ModelDesignValidator` starts its lookup tables empty instead of null. A redundant `if (m_nodes == null) return;` guard is gone. - **Test code: minimal annotations.** - `?` on values that can be null, `!` where the test asserts non-null. - `= null!` for fixture members assigned in SetUp. - `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the `IEncodeable` constraint. - A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`) folded into the adjacent `IsNull`/`Count` checks. **Notes for reviewers** - The diff is large (~1,500 files) but almost entirely mechanical: `?`, `!` and directive removal. The non-annotation changes are listed above. - Known follow-up, not done here: `IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay non-nullable even though the validator returns null before it loads a dictionary. Making them nullable would ripple through every generator call site. ## Related Issues - Fixes #4439 ## 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. - [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. The "no new warnings" box covers net10.0 and net48: full non-incremental builds of `UA.slnx`, `tools/SourceGeneration.slnx`, `tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0 warnings and 0 errors under the repository's warnings-as-errors settings. net9.0 and net8.0 also had no nullable diagnostics, but they were not rebuilt after the final fixes. Tests have not been run locally yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
765 lines
42 KiB
C#
765 lines
42 KiB
C#
/* ========================================================================
|
|
* 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.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using System.Xml.Linq;
|
|
using NUnit.Framework;
|
|
|
|
namespace Opc.Ua.Fuzzing
|
|
{
|
|
/// <summary>
|
|
/// Replays captured April 2025 fork messages, never regenerated semantic substitutes.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Fuzzing")]
|
|
public sealed class EncoderForkCorpusTests
|
|
{
|
|
[TestCaseSource(nameof(OriginalCases))]
|
|
public async Task OriginalMessageBytesMatchCapturedForkProvenanceAsync(string wire, string messageName)
|
|
{
|
|
byte[] input = await ReadOriginalAsync(wire, messageName).ConfigureAwait(false);
|
|
|
|
Assert.That(input.Length, Is.GreaterThan(64), "Original messages cross segmented buffer boundaries.");
|
|
}
|
|
|
|
[TestCaseSource(nameof(AcceptedOriginalCases))]
|
|
public async Task OriginalBinaryAndXmlMessagesDecodeAsPopulatedTypesAsync(string wire, string messageName)
|
|
{
|
|
byte[] input = await ReadOriginalAsync(wire, messageName).ConfigureAwait(false);
|
|
|
|
IEncodeable decoded = DecodeOriginal(input, wire);
|
|
|
|
AssertOriginalMessage(decoded, wire, messageName);
|
|
}
|
|
|
|
[TestCaseSource(nameof(CallbackCases))]
|
|
public async Task OriginalMessagesReplayExactCallbacksAfterPositiveTypedDecodeAsync(
|
|
string wire,
|
|
string messageName,
|
|
string callbackName)
|
|
{
|
|
byte[] input = await ReadOriginalAsync(wire, messageName).ConfigureAwait(false);
|
|
IEncodeable decoded = DecodeOriginal(input, wire);
|
|
AssertOriginalMessage(decoded, wire, messageName);
|
|
if (wire == "Binary" &&
|
|
(callbackName.StartsWith("Aflfuzz", StringComparison.Ordinal) ||
|
|
callbackName.EndsWith("Segmented", StringComparison.Ordinal)))
|
|
{
|
|
using MemoryStream segmented = FuzzableCode.PrepareArraySegmentStream(input);
|
|
IEncodeable segmentedDecode = FuzzableCode.FuzzBinaryDecoderCore(segmented, throwAll: true)!;
|
|
AssertOriginalMessage(segmentedDecode!, wire, messageName);
|
|
Assert.That(Utils.IsEqual(decoded, segmentedDecode), Is.True);
|
|
}
|
|
Delegate callback = FuzzMethods.FindFuzzMethod(TestContext.Error, callbackName)!;
|
|
Assert.That(callback, Is.Not.Null);
|
|
Assert.That(callback!.Method.Name, Is.EqualTo(callbackName));
|
|
string[] namespaces = FuzzableCode.MessageContext.NamespaceUris.ToArray();
|
|
string[] servers = FuzzableCode.MessageContext.ServerUris.ToArray();
|
|
|
|
FuzzMethods.Replay(callback, input);
|
|
|
|
Assert.That(FuzzableCode.MessageContext.NamespaceUris.ToArray(), Is.EqualTo(namespaces));
|
|
Assert.That(FuzzableCode.MessageContext.ServerUris.ToArray(), Is.EqualTo(servers));
|
|
AssertOriginalHash(input, wire, messageName);
|
|
int conversion = callbackName.IndexOf("JsonEncoder", StringComparison.Ordinal);
|
|
if (conversion >= 0)
|
|
{
|
|
string mode = callbackName[(conversion + "JsonEncoder".Length)..];
|
|
AssertJsonGenerations(decoded, wire, messageName, mode.Length == 0 ? "Verbose" : mode);
|
|
}
|
|
else
|
|
{
|
|
AssertWireGenerations(decoded, wire, messageName);
|
|
}
|
|
}
|
|
|
|
[TestCase(nameof(ReadResponse), "ListOfByte")]
|
|
[TestCase(nameof(PublishResponse), "ListOfUInt32")]
|
|
public async Task OriginalMalformedXmlMatrixIsRejectedAsInTheForkAsync(
|
|
string messageName,
|
|
string elementName)
|
|
{
|
|
byte[] input = await ReadOriginalAsync("Xml", messageName).ConfigureAwait(false);
|
|
using var stream = new MemoryStream(input, writable: false);
|
|
XDocument document = XDocument.Load(stream);
|
|
XNamespace ns = Namespaces.OpcUaXsd;
|
|
XElement matrix = document.Descendants(ns + "Matrix").First();
|
|
|
|
Assert.That(matrix.Elements().Select(element => element.Name.LocalName),
|
|
Is.EqualTo(s_legacyMatrixFields));
|
|
Assert.That(matrix.Element(ns + "Elements")!.Elements().Single().Name, Is.EqualTo(ns + elementName));
|
|
Assert.That(() => DecodeOriginal(input, "Xml"),
|
|
Throws.TypeOf<ServiceResultException>()
|
|
.With.Property(nameof(ServiceResultException.StatusCode)).EqualTo(StatusCodes.BadDecodingError));
|
|
}
|
|
|
|
[TestCaseSource(nameof(RejectedXmlCallbackCases))]
|
|
public async Task OriginalMalformedXmlIsReplayedWithoutClaimingSuccessfulDecodingAsync(
|
|
string messageName,
|
|
string callbackName)
|
|
{
|
|
byte[] input = await ReadOriginalAsync("Xml", messageName).ConfigureAwait(false);
|
|
Assert.That(() => DecodeOriginal(input, "Xml"),
|
|
Throws.TypeOf<ServiceResultException>()
|
|
.With.Property(nameof(ServiceResultException.StatusCode)).EqualTo(StatusCodes.BadDecodingError));
|
|
Delegate callback = FuzzMethods.FindFuzzMethod(TestContext.Error, callbackName)!;
|
|
Assert.That(callback, Is.Not.Null);
|
|
|
|
FuzzMethods.Replay(callback!, input);
|
|
|
|
AssertOriginalHash(input, "Xml", messageName);
|
|
}
|
|
|
|
[TestCase("Binary", 11, 8)]
|
|
[TestCase("Xml", 8, 8)]
|
|
[TestCase("Json", 7, 7)]
|
|
public void OriginalReplayCatalogPinsEveryApplicableModeAndRoute(string wire, int spanCount, int aflCount)
|
|
{
|
|
string[] callbacks = FuzzMethods.Delegates
|
|
.SelectMany(FuzzMethods.FindFuzzMethods)
|
|
.Select(callback => callback.Method.Name)
|
|
.Where(name => name.StartsWith("Libfuzz" + wire, StringComparison.Ordinal) ||
|
|
name.StartsWith("Aflfuzz" + wire, StringComparison.Ordinal))
|
|
.ToArray();
|
|
|
|
Assert.That(callbacks, Is.EquivalentTo(CallbackNames(wire)));
|
|
Assert.That(callbacks.Count(name => name.StartsWith("Libfuzz", StringComparison.Ordinal)),
|
|
Is.EqualTo(spanCount));
|
|
Assert.That(callbacks.Count(name => name.StartsWith("Aflfuzz", StringComparison.Ordinal)),
|
|
Is.EqualTo(aflCount));
|
|
Assert.That(FuzzableCode.JsonEncodingModes.ToArray()!.Select(mode => mode.Name),
|
|
Is.EqualTo(s_jsonModes));
|
|
}
|
|
|
|
[TestCaseSource(nameof(LegacyJsonCases))]
|
|
[Category("UnsupportedLegacyEnvelope")]
|
|
public async Task OriginalJsonMessagesHaveUnsupportedLegacyEnvelopeAsync(string wire, string messageName)
|
|
{
|
|
byte[] input = await ReadOriginalAsync(wire, messageName).ConfigureAwait(false);
|
|
string json = Encoding.UTF8.GetString(input);
|
|
using JsonDocument document = JsonDocument.Parse(input);
|
|
JsonElement root = document.RootElement;
|
|
int typeId = messageName switch
|
|
{
|
|
nameof(ReadRequest) => 629,
|
|
nameof(ReadResponse) => 632,
|
|
nameof(PublishResponse) => 827,
|
|
nameof(WriteRequest) => 671,
|
|
_ => throw new ArgumentException("Unknown original message.", nameof(messageName))
|
|
};
|
|
|
|
Assert.That(root.EnumerateObject().Select(property => property.Name), Is.EqualTo(s_legacyEnvelope));
|
|
Assert.That(root.GetProperty("TypeId").ValueKind, Is.EqualTo(JsonValueKind.Object));
|
|
Assert.That(root.GetProperty("TypeId").GetProperty("Id").GetInt32(), Is.EqualTo(typeId));
|
|
Assert.That(root.GetProperty("Body").ValueKind, Is.EqualTo(JsonValueKind.Object));
|
|
Assert.That(root.TryGetProperty("UaTypeId", out _), Is.False);
|
|
Assert.That(root.TryGetProperty("UaBody", out _), Is.False);
|
|
AssertLegacyBody(root.GetProperty("Body"), messageName);
|
|
Assert.That(
|
|
() => FuzzableCode.FuzzJsonDecoderCore(json, throwAll: true),
|
|
Throws.TypeOf<ServiceResultException>()
|
|
.With.Property(nameof(ServiceResultException.StatusCode)).EqualTo(StatusCodes.BadDecodingError)
|
|
.And.Message.StartsWith("Parsing encountered invalid information. ")
|
|
.And.Property(nameof(Exception.InnerException)).Null);
|
|
TestContext.Out.WriteLine(
|
|
$"GAP: {messageName}.json, unsupported TypeId/Body legacy envelope; " +
|
|
"strict decoder throws ServiceResultException, BadDecodingError (0x80070000). " +
|
|
"All 14 applicable JSON callbacks remain without positive original-byte coverage.\n" +
|
|
"Input: " + json);
|
|
}
|
|
|
|
private static IEnumerable<TestCaseData> OriginalCases()
|
|
{
|
|
return s_originals.Select(original => new TestCaseData(original.Wire, original.Message));
|
|
}
|
|
|
|
private static IEnumerable<TestCaseData> AcceptedOriginalCases()
|
|
{
|
|
return s_originals.Where(original => IsAcceptedOriginal(original.Wire, original.Message))
|
|
.Select(original => new TestCaseData(original.Wire, original.Message));
|
|
}
|
|
|
|
private static IEnumerable<TestCaseData> LegacyJsonCases()
|
|
{
|
|
return s_originals.Where(original => original.Wire == "Json")
|
|
.Select(original => new TestCaseData(original.Wire, original.Message));
|
|
}
|
|
|
|
private static IEnumerable<TestCaseData> CallbackCases()
|
|
{
|
|
foreach (var original in s_originals.Where(original => IsAcceptedOriginal(original.Wire, original.Message)))
|
|
{
|
|
foreach (string callback in CallbackNames(original.Wire))
|
|
{
|
|
yield return new TestCaseData(original.Wire, original.Message, callback);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<TestCaseData> RejectedXmlCallbackCases()
|
|
{
|
|
foreach (string message in new[] { nameof(ReadResponse), nameof(PublishResponse) })
|
|
{
|
|
foreach (string callback in CallbackNames("Xml"))
|
|
{
|
|
yield return new TestCaseData(message, callback);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsAcceptedOriginal(string wire, string message)
|
|
{
|
|
// The pinned fork itself rejects both response XML files' nested ListOf matrix encoding.
|
|
return wire == "Binary" || (wire == "Xml" && message is nameof(ReadRequest) or nameof(WriteRequest));
|
|
}
|
|
|
|
private static IEnumerable<string> CallbackNames(string wire)
|
|
{
|
|
foreach (string route in new[] { "Libfuzz", "Aflfuzz" })
|
|
{
|
|
yield return route + wire + "Decoder";
|
|
yield return route + wire + "Encoder";
|
|
yield return route + wire + "EncoderIndempotent";
|
|
foreach (string mode in s_jsonModes)
|
|
{
|
|
if (wire != "Json" || mode != "Verbose")
|
|
{
|
|
yield return route + wire + (wire == "Json" ? "Encoder" : "JsonEncoder") +
|
|
(mode == "Verbose" ? string.Empty : mode);
|
|
}
|
|
}
|
|
}
|
|
if (wire == "Binary")
|
|
{
|
|
yield return nameof(FuzzableCode.LibfuzzBinaryDecoderSegmented);
|
|
yield return nameof(FuzzableCode.LibfuzzBinaryEncoderSegmented);
|
|
yield return nameof(FuzzableCode.LibfuzzBinaryEncoderIndempotentSegmented);
|
|
}
|
|
}
|
|
|
|
private static async Task<byte[]> ReadOriginalAsync(string wire, string messageName)
|
|
{
|
|
string extension = wire == "Binary" ? "bin" : wire.ToLowerInvariant();
|
|
string path = Path.Combine(
|
|
AppContext.BaseDirectory,
|
|
"Assets",
|
|
"ForkMessages",
|
|
wire,
|
|
messageName.ToLowerInvariant() + "." + extension);
|
|
using var source = File.OpenRead(path);
|
|
using var buffer = new MemoryStream();
|
|
await source.CopyToAsync(buffer).ConfigureAwait(false);
|
|
byte[] input = buffer.ToArray();
|
|
|
|
AssertOriginalHash(input, wire, messageName);
|
|
return input;
|
|
}
|
|
|
|
private static void AssertOriginalHash(byte[] input, string wire, string messageName)
|
|
{
|
|
var original = s_originals.Single(item => item.Wire == wire && item.Message == messageName);
|
|
Assert.That(input.Length, Is.EqualTo(original.Length));
|
|
#if NET6_0_OR_GREATER
|
|
byte[] digest = SHA256.HashData(input);
|
|
#else
|
|
using var sha256 = SHA256.Create();
|
|
byte[] digest = sha256.ComputeHash(input);
|
|
#endif
|
|
Assert.That(
|
|
CoreUtils.ToHexString(digest),
|
|
Is.EqualTo(original.Sha256),
|
|
"The copied fork bytes must not be normalized, translated or regenerated.");
|
|
}
|
|
|
|
private static IEncodeable DecodeOriginal(byte[] input, string wire)
|
|
{
|
|
using var stream = new MemoryStream(input, writable: false);
|
|
return (wire switch
|
|
{
|
|
"Binary" => FuzzableCode.FuzzBinaryDecoderCore(stream, throwAll: true),
|
|
"Xml" => FuzzableCode.FuzzXmlDecoderCore(stream, throwAll: true),
|
|
_ => throw new ArgumentException("Only Binary/XML originals use this typed replay.", nameof(wire))
|
|
})!;
|
|
}
|
|
|
|
private static void AssertOriginalMessage(IEncodeable decoded, string wire, string messageName)
|
|
{
|
|
Type expectedType = messageName switch
|
|
{
|
|
nameof(ReadRequest) => typeof(ReadRequest),
|
|
nameof(ReadResponse) => typeof(ReadResponse),
|
|
nameof(PublishResponse) => typeof(PublishResponse),
|
|
nameof(WriteRequest) => typeof(WriteRequest),
|
|
_ => throw new ArgumentException("Unknown original message.", nameof(messageName))
|
|
};
|
|
var original = s_originals.Single(item => item.Wire == wire && item.Message == messageName);
|
|
DateTimeUtc timestamp = DateTimeUtc.Parse(original.Timestamp, CultureInfo.InvariantCulture);
|
|
Assert.That(decoded, Is.TypeOf(expectedType), "A tolerated rejection is not a compatible source decode.");
|
|
|
|
switch (decoded)
|
|
{
|
|
case ReadRequest read:
|
|
Assert.That(read.RequestHeader.Timestamp, Is.EqualTo(timestamp));
|
|
AssertReadRequest(read, wire);
|
|
break;
|
|
case ReadResponse read:
|
|
Assert.That(read.ResponseHeader.Timestamp, Is.EqualTo(timestamp));
|
|
AssertReadResponse(read, wire);
|
|
break;
|
|
case PublishResponse publish:
|
|
Assert.That(publish.ResponseHeader.Timestamp, Is.EqualTo(timestamp));
|
|
AssertPublishResponse(publish, wire);
|
|
break;
|
|
case WriteRequest write:
|
|
Assert.That(write.RequestHeader.Timestamp, Is.EqualTo(timestamp));
|
|
AssertWriteRequest(write, wire);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void AssertReadRequest(ReadRequest request, string wire)
|
|
{
|
|
AssertRequestHeader(request.RequestHeader);
|
|
AssertAdditionalHeader(request.RequestHeader.AdditionalHeader);
|
|
Assert.That(request.MaxAge, Is.EqualTo(1000));
|
|
Assert.That(request.TimestampsToReturn, Is.EqualTo(TimestampsToReturn.Source));
|
|
Assert.That(request.NodesToRead.ToArray()!.Select(node => node.NodeId), Is.EqualTo(new[]
|
|
{
|
|
new NodeId(123),
|
|
new NodeId(4444, 2),
|
|
new NodeId("RevisionCounter", 3),
|
|
new NodeId(new Guid(wire == "Binary"
|
|
? "043ca476-ffe5-4357-bbf8-85e3ee064689"
|
|
: "ecad8d48-3bf7-46b9-9d12-bb52deb71f93")),
|
|
new NodeId(4444, 2),
|
|
new NodeId(ByteString.From([66, 22, 55, 44, 11]))
|
|
}));
|
|
Assert.That(request.NodesToRead.ToArray()!.Select(node => node.AttributeId), Is.EqualTo(s_readAttributes));
|
|
Assert.That(request.NodesToRead[2].IndexRange, Is.EqualTo("1:2"));
|
|
Assert.That(request.NodesToRead[2].DataEncoding.IsNull, Is.True);
|
|
}
|
|
|
|
private static void AssertReadResponse(ReadResponse response, string wire)
|
|
{
|
|
Assert.That(response.ResponseHeader.RequestHandle, Is.EqualTo(42));
|
|
Assert.That(response.ResponseHeader.ServiceResult, Is.EqualTo((StatusCode)StatusCodes.Good));
|
|
Assert.That(response.ResponseHeader.StringTable.ToArray(), Is.EqualTo(s_readStrings));
|
|
AssertAdditionalHeader(response.ResponseHeader.AdditionalHeader);
|
|
AssertDiagnostics(response.ResponseHeader.ServiceDiagnostics!,
|
|
s_serviceDiagnostics, "NodeId not found");
|
|
Assert.That(response.Results.Count, Is.EqualTo(11));
|
|
Assert.That(response.Results[0].WrappedValue.TryGetValue(out string greeting), Is.True);
|
|
Assert.That(greeting, Is.EqualTo("Hello World"));
|
|
DateTimeUtc captured = CapturedTime(wire == "Binary"
|
|
? "2025-04-01T16:03:22.0278537Z"
|
|
: "2025-04-01T16:03:31.1133521Z");
|
|
AssertTimestamps(response.Results[0], captured + TimeSpan.FromMinutes(1), captured, 10, 100);
|
|
Assert.That(response.Results[1].WrappedValue.TryGetValue(out uint number), Is.True);
|
|
Assert.That(number, Is.EqualTo(12345678));
|
|
Assert.That(response.Results[1].StatusCode, Is.EqualTo((StatusCode)StatusCodes.BadDataLost));
|
|
Assert.That(response.Results[1].ServerTimestamp, Is.EqualTo(new DateTimeUtc(2025, 3, 31, 22)));
|
|
Assert.That(response.Results[2].WrappedValue.TryGetValue(out ByteString bytes), Is.True);
|
|
Assert.That(bytes, Is.EqualTo(ByteString.From([0, 1, 2, 3, 4, 5, 6])));
|
|
Assert.That(response.Results[2].ServerTimestamp, Is.EqualTo(DateTimeUtc.MaxValue));
|
|
Assert.That(response.Results[3].WrappedValue.TryGetValue(out byte small), Is.True);
|
|
Assert.That(small, Is.EqualTo(42));
|
|
Assert.That(response.Results[4].WrappedValue.TryGetValue(out ulong unsigned), Is.True);
|
|
Assert.That(unsigned, Is.EqualTo(0xbadbeefUL));
|
|
Assert.That(response.Results[5].WrappedValue.TryGetValue(out MatrixOf<byte> matrix), Is.True);
|
|
Assert.That(matrix.Dimensions, Is.EqualTo(s_dimensions));
|
|
Assert.That(matrix.Span.ToArray(), Is.EqualTo(s_byteMatrix));
|
|
Assert.That(response.Results[6].WrappedValue.TryGetValue(out double real), Is.True);
|
|
Assert.That(real, Is.EqualTo(2025.111));
|
|
Assert.That(response.Results[7].WrappedValue.TryGetValue(out LocalizedText text), Is.True);
|
|
Assert.That(text, Is.EqualTo(new LocalizedText("en-us", "The text")));
|
|
Assert.That(response.Results[8].WrappedValue.TryGetValue(out QualifiedName name), Is.True);
|
|
Assert.That(name, Is.EqualTo(new QualifiedName("The text", 2)));
|
|
for (int i = 6; i <= 8; i++)
|
|
{
|
|
Assert.That(response.Results[i].StatusCode, Is.EqualTo((StatusCode)StatusCodes.BadTooManyOperations));
|
|
Assert.That(response.Results[i].SourceTimestamp, Is.EqualTo(captured));
|
|
}
|
|
Assert.That(response.Results[9].WrappedValue.TryGetStructure<ThreeDVector>(out ThreeDVector? vector), Is.True);
|
|
AssertVector(vector!);
|
|
Assert.That(response.Results[10].WrappedValue.TryGetValue(out ArrayOf<ExtensionObject> vectors), Is.True);
|
|
Assert.That(vectors.Count, Is.EqualTo(2));
|
|
foreach (ExtensionObject item in vectors)
|
|
{
|
|
Assert.That(item.TryGetValue(out ThreeDVector? element), Is.True);
|
|
AssertVector(element!);
|
|
}
|
|
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
|
|
AssertNestedDiagnostic(response.DiagnosticInfos[0]);
|
|
}
|
|
|
|
private static void AssertPublishResponse(PublishResponse response, string wire)
|
|
{
|
|
Assert.That(response.ResponseHeader.RequestHandle, Is.EqualTo(42));
|
|
Assert.That(response.ResponseHeader.ServiceResult, Is.EqualTo((StatusCode)StatusCodes.Good));
|
|
Assert.That(response.ResponseHeader.StringTable.ToArray(), Is.EqualTo(s_publishStrings));
|
|
Assert.That(response.SubscriptionId, Is.EqualTo(1234));
|
|
Assert.That(response.MoreNotifications, Is.True);
|
|
Assert.That(response.AvailableSequenceNumbers.ToArray(), Is.EqualTo(s_sequenceNumbers));
|
|
Assert.That(response.NotificationMessage.SequenceNumber, Is.EqualTo(123456));
|
|
DateTimeUtc captured = CapturedTime(wire == "Binary"
|
|
? "2025-04-01T16:03:27.6517200Z"
|
|
: "2025-04-01T16:03:31.8846442Z");
|
|
Assert.That(response.NotificationMessage.PublishTime, Is.EqualTo(captured));
|
|
Assert.That(response.NotificationMessage.NotificationData.Count, Is.EqualTo(1));
|
|
Assert.That(response.NotificationMessage.NotificationData[0]
|
|
.TryGetValue(out DataChangeNotification? notification), Is.True);
|
|
Assert.That(notification!.MonitoredItems.ToArray()!.Select(item => item.ClientHandle),
|
|
Is.EqualTo(s_clientHandles));
|
|
Assert.That(notification.MonitoredItems[0].Value.WrappedValue.TryGetValue(out string greeting), Is.True);
|
|
Assert.That(greeting, Is.EqualTo("Hello World"));
|
|
AssertTimestamps(notification.MonitoredItems[0].Value,
|
|
captured + TimeSpan.FromMinutes(1), captured, 10, 100);
|
|
Assert.That(notification.MonitoredItems[1].Value.WrappedValue.TryGetValue(out MatrixOf<uint> matrix),
|
|
Is.True);
|
|
Assert.That(matrix.Dimensions, Is.EqualTo(s_dimensions));
|
|
Assert.That(matrix.Span.ToArray(), Is.EqualTo(s_uintMatrix));
|
|
AssertTimestamps(notification.MonitoredItems[1].Value,
|
|
captured + TimeSpan.FromSeconds(1), captured, 10, 100);
|
|
for (int i = 2; i <= 3; i++)
|
|
{
|
|
Assert.That(notification.MonitoredItems[i].Value.WrappedValue.TryGetValue(out NodeId nodeId), Is.True);
|
|
Assert.That(nodeId, Is.EqualTo(new NodeId(1000)));
|
|
}
|
|
AssertTimestamps(notification.MonitoredItems[2].Value,
|
|
captured + TimeSpan.FromDays(1), captured, 0, 0);
|
|
Assert.That(notification.MonitoredItems[4].Value.WrappedValue.TryGetValue(out bool boolean), Is.True);
|
|
Assert.That(boolean, Is.True);
|
|
Assert.That(notification.MonitoredItems[5].Value.WrappedValue.TryGetValue(out byte small), Is.True);
|
|
Assert.That(small, Is.EqualTo(123));
|
|
Assert.That(notification.MonitoredItems[6].Value.WrappedValue.TryGetValue(out float single), Is.True);
|
|
Assert.That(single, Is.EqualTo(123.123f));
|
|
Assert.That(notification.MonitoredItems[7].Value.WrappedValue.TryGetValue(out double real), Is.True);
|
|
Assert.That(real, Is.EqualTo(12301232.123));
|
|
Assert.That(notification.MonitoredItems[8].Value.WrappedValue.TryGetValue(out long signed), Is.True);
|
|
Assert.That(signed, Is.EqualTo(-123012321234123L));
|
|
Assert.That(notification.MonitoredItems[9].Value.WrappedValue.TryGetValue(out ulong unsigned), Is.True);
|
|
Assert.That(unsigned, Is.EqualTo(123012321234123UL));
|
|
Assert.That(notification.MonitoredItems[10].Value.WrappedValue.TryGetValue(out ArrayOf<uint> array),
|
|
Is.True);
|
|
Assert.That(array.ToArray(), Is.EqualTo(s_uintArray));
|
|
Assert.That(notification.MonitoredItems.ToArray()!.Select(item => item.Value.StatusCode),
|
|
Is.All.EqualTo((StatusCode)StatusCodes.Good));
|
|
Assert.That(notification.DiagnosticInfos.Count, Is.Zero);
|
|
Assert.That(response.Results.Count, Is.Zero);
|
|
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
|
|
AssertNestedDiagnostic(response.DiagnosticInfos[0]);
|
|
}
|
|
|
|
private static void AssertWriteRequest(WriteRequest request, string wire)
|
|
{
|
|
AssertRequestHeader(request.RequestHeader);
|
|
Assert.That(request.RequestHeader.AdditionalHeader.IsNull, Is.True);
|
|
Assert.That(request.NodesToWrite.ToArray()!.Select(node => node.NodeId), Is.EqualTo(new[]
|
|
{
|
|
new NodeId(123), new NodeId(124), new NodeId(125), new NodeId(126), new NodeId(127), new NodeId(128),
|
|
new NodeId("s=\"FastCounter\"", 2),
|
|
new NodeId(ByteString.From([0xaa, 0xbb, 0xcc, 0xdd, 0xee]), 3),
|
|
new NodeId(new Guid(wire == "Binary"
|
|
? "f3d6a9a8-cb8c-448d-ad1d-1d80237a9736"
|
|
: "11b4c177-c253-437e-a2bd-20fa3e20edad")),
|
|
new NodeId(132, 3)
|
|
}));
|
|
Assert.That(request.NodesToWrite.ToArray()!.Select(node => node.AttributeId), Is.EqualTo(s_writeAttributes));
|
|
Assert.That(request.NodesToWrite[0].IndexRange, Is.EqualTo("1:2"));
|
|
Assert.That(request.NodesToWrite[0].Value.WrappedValue.TryGetValue(out string greeting), Is.True);
|
|
Assert.That(greeting, Is.EqualTo("Hello World"));
|
|
AssertTimestamps(request.NodesToWrite[0].Value,
|
|
CapturedTime(wire == "Binary" ? "2025-04-01T16:04:28.0594019Z" : "2025-04-01T16:04:32.6097615Z"),
|
|
CapturedTime(wire == "Binary" ? "2025-04-01T16:03:28.0594012Z" : "2025-04-01T16:03:32.6097611Z"),
|
|
10, 100);
|
|
Assert.That(request.NodesToWrite[1].Value.WrappedValue.TryGetValue(out int number), Is.True);
|
|
Assert.That(number, Is.EqualTo(12345));
|
|
Assert.That(request.NodesToWrite[2].Value.WrappedValue.TryGetValue(out float single), Is.True);
|
|
Assert.That(single, Is.EqualTo(123.45f));
|
|
Assert.That(request.NodesToWrite[3].Value.WrappedValue.TryGetValue(out double real), Is.True);
|
|
Assert.That(real, Is.EqualTo(123.45));
|
|
Assert.That(request.NodesToWrite[4].Value.WrappedValue.TryGetValue(out bool boolean), Is.True);
|
|
Assert.That(boolean, Is.True);
|
|
Assert.That(request.NodesToWrite[5].Value.WrappedValue.TryGetValue(out ByteString bytes), Is.True);
|
|
Assert.That(bytes, Is.EqualTo(ByteString.From([1, 2, 3, 4, 5])));
|
|
Assert.That(request.NodesToWrite[6].Value.WrappedValue.TryGetValue(out ArrayOf<int> integers), Is.True);
|
|
Assert.That(integers.ToArray(), Is.EqualTo(s_integers));
|
|
Assert.That(request.NodesToWrite[7].Value.WrappedValue.TryGetValue(out ArrayOf<float> singles), Is.True);
|
|
Assert.That(singles.ToArray(), Is.EqualTo(s_singles));
|
|
Assert.That(request.NodesToWrite[8].Value.WrappedValue.TryGetValue(out ArrayOf<double> doubles), Is.True);
|
|
Assert.That(doubles.ToArray(), Is.EqualTo(s_doubles));
|
|
Assert.That(request.NodesToWrite[9].Value.WrappedValue.TryGetValue(out ArrayOf<string> strings), Is.True);
|
|
Assert.That(strings.ToArray(), Is.EqualTo(s_strings));
|
|
Assert.That(request.NodesToWrite.ToArray()!.Select(node => node.Value.StatusCode),
|
|
Is.All.EqualTo((StatusCode)StatusCodes.Good));
|
|
}
|
|
|
|
private static void AssertRequestHeader(RequestHeader header)
|
|
{
|
|
Assert.That(header.RequestHandle, Is.EqualTo(422));
|
|
Assert.That(header.TimeoutHint, Is.EqualTo(10000));
|
|
Assert.That(header.ReturnDiagnostics, Is.EqualTo(1023));
|
|
}
|
|
|
|
private static void AssertAdditionalHeader(ExtensionObject header)
|
|
{
|
|
Assert.That(header.TryGetValue(out AdditionalParametersType? parameters), Is.True);
|
|
Assert.That(parameters!.Parameters.Count, Is.EqualTo(1));
|
|
Assert.That(parameters.Parameters[0].Key, Is.EqualTo(new QualifiedName("traceparent")));
|
|
Assert.That(parameters.Parameters[0].Value.TryGetValue(out string trace), Is.True);
|
|
Assert.That(trace, Is.EqualTo(kTraceParent));
|
|
}
|
|
|
|
private static void AssertVector(ThreeDVector vector)
|
|
{
|
|
Assert.That(vector.X, Is.EqualTo(1));
|
|
Assert.That(vector.Y, Is.EqualTo(-1));
|
|
Assert.That(vector.Z, Is.Zero);
|
|
}
|
|
|
|
private static void AssertNestedDiagnostic(DiagnosticInfo diagnostic)
|
|
{
|
|
AssertDiagnostics(diagnostic, s_nestedDiagnostics, "Hello World");
|
|
}
|
|
|
|
private static void AssertDiagnostics(DiagnosticInfo diagnostic, StatusCode[] statuses, string firstMessage)
|
|
{
|
|
for (int i = 0; i < statuses.Length; i++)
|
|
{
|
|
Assert.That(diagnostic, Is.Not.Null);
|
|
Assert.That(diagnostic!.SymbolicId, Is.EqualTo(-1));
|
|
Assert.That(diagnostic.NamespaceUri, Is.EqualTo(-1));
|
|
Assert.That(diagnostic.Locale, Is.EqualTo(-1));
|
|
Assert.That(diagnostic.LocalizedText, Is.EqualTo(-1));
|
|
Assert.That(diagnostic.AdditionalInfo, Is.EqualTo(i == 0 ? firstMessage : "Hello World"));
|
|
Assert.That(diagnostic.InnerStatusCode, Is.EqualTo(statuses[i]));
|
|
diagnostic = diagnostic.InnerDiagnosticInfo!;
|
|
}
|
|
Assert.That(diagnostic, Is.Null);
|
|
}
|
|
|
|
private static void AssertTimestamps(
|
|
in DataValue value,
|
|
DateTimeUtc source,
|
|
DateTimeUtc server,
|
|
ushort sourcePicoseconds,
|
|
ushort serverPicoseconds)
|
|
{
|
|
Assert.That(value.SourceTimestamp, Is.EqualTo(source));
|
|
Assert.That(value.ServerTimestamp, Is.EqualTo(server));
|
|
Assert.That(value.SourcePicoseconds, Is.EqualTo(sourcePicoseconds));
|
|
Assert.That(value.ServerPicoseconds, Is.EqualTo(serverPicoseconds));
|
|
}
|
|
|
|
private static DateTimeUtc CapturedTime(string timestamp)
|
|
{
|
|
return DateTimeUtc.Parse(timestamp, CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static void AssertWireGenerations(IEncodeable source, string wire, string messageName)
|
|
{
|
|
// Canonical OUTPUT is compared with later generations, never substituted for the original input.
|
|
byte[] encoded = EncodeDecodedMessage(source, wire);
|
|
IEncodeable second = DecodeOriginal(encoded, wire);
|
|
AssertOriginalMessage(second, wire, messageName);
|
|
Assert.That(Utils.IsEqual(source, second), Is.True, "First-generation encoding must preserve the source.");
|
|
byte[] canonical = EncodeDecodedMessage(second, wire);
|
|
Assert.That(canonical, Is.EqualTo(encoded));
|
|
IEncodeable third = DecodeOriginal(canonical, wire);
|
|
AssertOriginalMessage(third, wire, messageName);
|
|
Assert.That(Utils.IsEqual(second, third), Is.True);
|
|
}
|
|
|
|
private static byte[] EncodeDecodedMessage(IEncodeable message, string wire)
|
|
{
|
|
if (wire == "Binary")
|
|
{
|
|
return BinaryEncoder.EncodeMessage(message, FuzzableCode.MessageContext);
|
|
}
|
|
using var encoder = new XmlEncoder(FuzzableCode.MessageContext);
|
|
encoder.EncodeMessage(message, message.TypeId);
|
|
return Encoding.UTF8.GetBytes(encoder.CloseAndReturnText()!);
|
|
}
|
|
|
|
private static void AssertJsonGenerations(IEncodeable source, string wire, string messageName, string mode)
|
|
{
|
|
JsonEncoderOptions options = FuzzableCode.JsonEncodingModes.ToArray()!.Single(option => option.Name == mode);
|
|
FuzzableCode.FuzzJsonRoundTripCore(source, options);
|
|
string encoded = FuzzableCode.EncodeJsonMessage(source, options);
|
|
IEncodeable second = FuzzableCode.DecodeJsonWithMetadata(
|
|
encoded, source, options, FuzzableCode.MessageContext);
|
|
AssertOriginalMessage(second, wire, messageName);
|
|
Assert.That(Utils.IsEqual(source, second), Is.True, mode);
|
|
string canonical = FuzzableCode.EncodeJsonMessage(second, options);
|
|
Assert.That(canonical, Is.EqualTo(encoded), mode);
|
|
IEncodeable third = FuzzableCode.DecodeJsonWithMetadata(
|
|
canonical, source, options, FuzzableCode.MessageContext);
|
|
AssertOriginalMessage(third, wire, messageName);
|
|
Assert.That(Utils.IsEqual(second, third), Is.True, mode);
|
|
|
|
// Challenge the existing oracles on derived output, not on translated or mutated corpus input.
|
|
Assert.That(
|
|
() => FuzzableCode.FuzzJsonEncoderIndempotentCore(encoded + " ", source, options),
|
|
Throws.TypeOf<EncodingFidelityException>()
|
|
.With.Message.EqualTo($"Idempotent JSON encoding failed. Type={messageName}."));
|
|
switch (third)
|
|
{
|
|
case ReadRequest read:
|
|
read.RequestHeader.RequestHandle++;
|
|
break;
|
|
case ReadResponse read:
|
|
read.ResponseHeader.RequestHandle++;
|
|
break;
|
|
case PublishResponse publish:
|
|
publish.ResponseHeader.RequestHandle++;
|
|
break;
|
|
case WriteRequest write:
|
|
write.RequestHeader.RequestHandle++;
|
|
break;
|
|
}
|
|
Assert.That(Utils.IsEqual(source, third), Is.False);
|
|
Assert.That(
|
|
() => FuzzableCode.FuzzJsonEncoderIndempotentCore(encoded, third, options),
|
|
Throws.TypeOf<EncodingFidelityException>()
|
|
.With.Message.EqualTo($"JSON semantic round-trip failed. Type={messageName}, Mode={mode}."));
|
|
AssertOriginalMessage(source, wire, messageName);
|
|
}
|
|
|
|
private static void AssertLegacyBody(JsonElement body, string messageName)
|
|
{
|
|
bool request = messageName is nameof(ReadRequest) or nameof(WriteRequest);
|
|
JsonElement header = body.GetProperty(request ? "RequestHeader" : "ResponseHeader");
|
|
Assert.That(header.GetProperty("RequestHandle").GetUInt32(), Is.EqualTo(request ? 422 : 42));
|
|
Assert.That(header.GetProperty("Timestamp").GetString(),
|
|
Is.EqualTo(s_originals.Single(item => item.Wire == "Json" && item.Message == messageName).Timestamp));
|
|
JsonElement variant;
|
|
switch (messageName)
|
|
{
|
|
case nameof(ReadRequest):
|
|
Assert.That(body.GetProperty("NodesToRead").GetArrayLength(), Is.EqualTo(6));
|
|
Assert.That(body.GetProperty("NodesToRead")[0].GetProperty("NodeId").GetProperty("Id").GetInt32(),
|
|
Is.EqualTo(123));
|
|
variant = header.GetProperty("AdditionalHeader").GetProperty("Body")
|
|
.GetProperty("Parameters")[0].GetProperty("Value");
|
|
break;
|
|
case nameof(ReadResponse):
|
|
Assert.That(body.GetProperty("Results").GetArrayLength(), Is.EqualTo(11));
|
|
variant = body.GetProperty("Results")[0].GetProperty("Value");
|
|
break;
|
|
case nameof(PublishResponse):
|
|
JsonElement notification = body.GetProperty("NotificationMessage");
|
|
Assert.That(notification.GetProperty("SequenceNumber").GetUInt32(), Is.EqualTo(123456));
|
|
JsonElement items = notification.GetProperty("NotificationData")[0]
|
|
.GetProperty("Body").GetProperty("MonitoredItems");
|
|
Assert.That(items.GetArrayLength(), Is.EqualTo(11));
|
|
variant = items[0].GetProperty("Value").GetProperty("Value");
|
|
break;
|
|
case nameof(WriteRequest):
|
|
Assert.That(body.GetProperty("NodesToWrite").GetArrayLength(), Is.EqualTo(10));
|
|
Assert.That(body.GetProperty("NodesToWrite")[0].GetProperty("NodeId").GetProperty("Id").GetInt32(),
|
|
Is.EqualTo(123));
|
|
variant = body.GetProperty("NodesToWrite")[0].GetProperty("Value").GetProperty("Value");
|
|
break;
|
|
default:
|
|
throw new ArgumentException("Unknown original message.", nameof(messageName));
|
|
}
|
|
Assert.That(variant.GetProperty("Type").GetInt32(), Is.EqualTo(12));
|
|
Assert.That(variant.GetProperty("Body").GetString(),
|
|
Is.EqualTo(messageName == nameof(ReadRequest) ? kTraceParent : "Hello World"));
|
|
Assert.That(variant.TryGetProperty("UaType", out _), Is.False);
|
|
}
|
|
|
|
private const string kTraceParent = "00-480e22a2781fe54d992d878662248d94-b4b37b64bb3f6141-00";
|
|
private static readonly string[] s_legacyMatrixFields = ["Elements", "Dimensions"];
|
|
private static readonly string[] s_legacyEnvelope = ["TypeId", "Body"];
|
|
private static readonly int[] s_dimensions = [2, 2, 2];
|
|
private static readonly uint[] s_readAttributes = [25, 5, 13, 4, 17, 24];
|
|
private static readonly uint[] s_writeAttributes = [13, 15, 13, 13, 13, 13, 13, 13, 13, 13];
|
|
private static readonly string[] s_readStrings = ["Hello", "World", "Goodbye"];
|
|
private static readonly string[] s_publishStrings = ["No error occurred"];
|
|
private static readonly byte[] s_byteMatrix = [1, 2, 3, 4, 11, 22, 33, 44];
|
|
private static readonly uint[] s_uintMatrix = [1, 2, 3, 4, 11, 22, 33, 44];
|
|
private static readonly uint[] s_sequenceNumbers = [1, 2, 3, 4];
|
|
private static readonly uint[] s_clientHandles = [122, 123, 124, 125, 125, 126, 127, 128, 129, 130, 131];
|
|
private static readonly uint[] s_uintArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
|
|
private static readonly int[] s_integers = [1, 2, 3, 4, 5];
|
|
private static readonly float[] s_singles = [1.1f, 2.2f, 3.3f];
|
|
private static readonly double[] s_doubles = [1.1, 2.2, 3.3];
|
|
private static readonly string[] s_strings = ["one", "two", "three"];
|
|
private static readonly StatusCode[] s_serviceDiagnostics =
|
|
[
|
|
StatusCodes.BadAggregateConfigurationRejected, StatusCodes.BadIndexRangeInvalid,
|
|
StatusCodes.BadSecureChannelIdInvalid, StatusCodes.BadAlreadyExists
|
|
];
|
|
private static readonly StatusCode[] s_nestedDiagnostics =
|
|
[StatusCodes.BadCertificateHostNameInvalid, StatusCodes.BadNodeIdUnknown];
|
|
private static readonly string[] s_jsonModes =
|
|
["Verbose", "Compact", "RawData", "LegacyReversible", "LegacyNonReversible"];
|
|
|
|
// SHA-256 and lengths were captured from the fork's Testcases.{Binary,Json,Xml} files.
|
|
// Dates and GUIDs deliberately differ between encodings; no generator is consulted.
|
|
private static readonly (string Wire, string Message, int Length, string Sha256, string Timestamp)[]
|
|
s_originals =
|
|
[
|
|
("Binary", nameof(ReadRequest), 286,
|
|
"E3784C1601B5D64BB409677D0B9F505F85BB44F581C05C87AE412BE28461E5BC", "2025-04-01T16:03:21.1443128Z"),
|
|
("Binary", nameof(ReadResponse), 650,
|
|
"9A47DB38B190C0ABBB9D8A4EB6060C170BABE77728A51B6CF80FDBA8DC220BDB", "2025-04-01T16:03:27.2566961Z"),
|
|
("Binary", nameof(PublishResponse), 466,
|
|
"D928EBAA3D115A1A972D5975EBCD7C081A432E5ED32C9AA82DC42EEFEA913F04", "2025-04-01T16:03:27.6557070Z"),
|
|
("Binary", nameof(WriteRequest), 509,
|
|
"A8187E9FDEEB0E17AFFBFF3456E911BC39A19971B163A735D197330CAA0D3584", "2025-04-01T16:03:28.0588252Z"),
|
|
("Json", nameof(ReadRequest), 776,
|
|
"E2FAEC5C2DC442E4866C7112E828017DD3790ED25C8D8854531F7A717F132EDF", "2025-04-01T16:03:28.5765948Z"),
|
|
("Json", nameof(ReadResponse), 2367,
|
|
"CC74A0EA870ACB2DC899DECBA868E0920CC18E02552B7FB7B4F2066A87E66441", "2025-04-01T16:03:28.9988675Z"),
|
|
("Json", nameof(PublishResponse), 2042,
|
|
"12D98201BDEA7BD466F14907C9EEAD8773ABA3753D1C439CE6BC7B611CE51A21", "2025-04-01T16:03:29.528844Z"),
|
|
("Json", nameof(WriteRequest), 2219,
|
|
"2B9D9F9E2A9394C5A08BB2F97567C612E781FCCE37336A43E6C95D825465C31E", "2025-04-01T16:03:29.8977872Z"),
|
|
("Xml", nameof(ReadRequest), 2073,
|
|
"314464A7F63DB0DC931F387EA0038A53FAE0942DCD4C495CC22FD6A84F1671F7", "2025-04-01T16:03:30.2701254Z"),
|
|
("Xml", nameof(ReadResponse), 9855,
|
|
"FA57DE52547618BED1F4E663EF6DE6D81948E33A3FB291CDFFDA255379E0A79F", "2025-04-01T16:03:31.1134096Z"),
|
|
("Xml", nameof(PublishResponse), 12947,
|
|
"BF30ED99E2E2FBAEEC9F4C86357F94F45576DF4C7BF287AF6E566520E6129190", "2025-04-01T16:03:31.884721Z"),
|
|
("Xml", nameof(WriteRequest), 7073,
|
|
"D81D9AD7B68E3CAE5648D81E0E6E20213358EB3198B1D61A0A080A19E460C185", "2025-04-01T16:03:32.6097561Z")
|
|
];
|
|
}
|
|
}
|