Files
UA-.NETStandard/fuzzing/Opc.Ua.Encoders.Fuzz.Tools/Encoders.Testcases.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# 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>
2026-10-08 08:48:13 +02:00

512 lines
22 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 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.IO;
using System.Text;
namespace Opc.Ua.Fuzzing
{
public static partial class Testcases
{
public enum TestCaseEncoders
{
Binary = 0,
Json = 1,
Xml = 2
}
/// <summary>
/// Run the encoder test cases
/// </summary>
/// <param name="workPath"></param>
/// <param name="telemetry">Unused; retained for the common fuzz-tool signature.</param>
public static void Run(string workPath, ITelemetryContext telemetry)
{
_ = telemetry;
string pathSuffix = GetTestcaseEncoderSuffix(TestCaseEncoders.Binary);
string pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar;
Directory.CreateDirectory(pathTarget);
foreach (MessageEncoder messageEncoder in GetMessageEncoders())
{
byte[] message;
using (var encoder = new BinaryEncoder(FuzzableCode.MessageContext))
{
messageEncoder(encoder);
message = encoder.CloseAndReturnBuffer()!;
}
// Test the fuzz targets with the message.
FuzzableCode.LibfuzzBinaryDecoder(message);
FuzzableCode.LibfuzzBinaryEncoder(message);
FuzzableCode.LibfuzzBinaryEncoderIndempotent(message);
FuzzableCode.LibfuzzBinaryDecoderSegmented(message);
FuzzableCode.LibfuzzBinaryEncoderSegmented(message);
FuzzableCode.LibfuzzBinaryEncoderIndempotentSegmented(message);
using (var stream = new MemoryStream(message!))
{
FuzzableCode.AflfuzzBinaryDecoder(stream);
}
using (var stream = new MemoryStream(message))
{
FuzzableCode.AflfuzzBinaryEncoder(stream);
}
using (var stream = new MemoryStream(message))
{
IEncodeable decoded = FuzzableCode.FuzzBinaryDecoderCore(stream, true)!;
foreach (JsonEncoderOptions options in FuzzableCode.JsonEncodingModes)
{
FuzzableCode.FuzzJsonRoundTripCore(decoded!, options);
}
}
string fileName = Path.Combine(
pathTarget,
$"{messageEncoder.Method.Name}.bin".ToLowerInvariant());
File.WriteAllBytes(fileName, message);
}
// Create the Testcases for the json decoder.
pathSuffix = GetTestcaseEncoderSuffix(TestCaseEncoders.Json);
pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar;
Directory.CreateDirectory(pathTarget);
foreach (MessageEncoder messageEncoder in GetMessageEncoders())
{
byte[] message;
using (var memoryStream = new MemoryStream(0x1000))
using (var encoder = new JsonEncoder(memoryStream, FuzzableCode.MessageContext))
{
messageEncoder(encoder);
encoder.Close();
message = memoryStream.ToArray();
}
// Test the fuzz targets with the message.
FuzzableCode.LibfuzzJsonDecoder(message);
FuzzableCode.LibfuzzJsonEncoder(message);
string json = Encoding.UTF8.GetString(message);
FuzzableCode.AflfuzzJsonDecoder(json);
FuzzableCode.AflfuzzJsonEncoder(json);
IEncodeable decoded = FuzzableCode.FuzzJsonDecoderCore(json, true)!;
foreach (JsonEncoderOptions options in FuzzableCode.JsonEncodingModes)
{
FuzzableCode.FuzzJsonRoundTripCore(decoded!, options);
}
string fileName = Path.Combine(
pathTarget,
$"{messageEncoder.Method.Name}.json".ToLowerInvariant());
File.WriteAllBytes(fileName, message);
}
// Create the Testcases for the xml decoder.
pathSuffix = GetTestcaseEncoderSuffix(TestCaseEncoders.Xml);
pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar;
Directory.CreateDirectory(pathTarget);
foreach (MessageEncoder messageEncoder in GetMessageEncoders())
{
string xml;
using (var encoder = new XmlEncoder(FuzzableCode.MessageContext))
{
encoder.SetMappingTables(
FuzzableCode.MessageContext.NamespaceUris,
FuzzableCode.MessageContext.ServerUris);
messageEncoder(encoder);
xml = encoder.CloseAndReturnText()!;
}
// Test the fuzz targets with the message.
byte[] message = Encoding.UTF8.GetBytes(xml!);
using (var stream = new MemoryStream(message))
{
FuzzableCode.AflfuzzXmlDecoder(stream);
}
using (var stream = new MemoryStream(message))
{
FuzzableCode.AflfuzzXmlEncoder(stream);
}
using (var stream = new MemoryStream(message))
{
IEncodeable decoded = FuzzableCode.FuzzXmlDecoderCore(stream, true)!;
foreach (JsonEncoderOptions options in FuzzableCode.JsonEncodingModes)
{
FuzzableCode.FuzzJsonRoundTripCore(decoded!, options);
}
}
FuzzableCode.LibfuzzXmlDecoder(message);
FuzzableCode.LibfuzzXmlEncoder(message);
FuzzableCode.LibfuzzXmlEncoderIndempotent(message);
string fileName = Path.Combine(
pathTarget,
$"{messageEncoder.Method.Name}.xml".ToLowerInvariant());
File.WriteAllBytes(fileName, Encoding.UTF8.GetBytes(xml));
}
WriteBuiltInTypeTestcases(workPath);
WriteParserTestcases(workPath);
}
private static string GetTestcaseEncoderSuffix(TestCaseEncoders encoder)
{
return "." + encoder;
}
private delegate void BuiltInEncoder(IEncoder encoder);
internal static IEnumerable<MessageEncoder> GetMessageEncoders()
{
yield return ReadRequestRich;
yield return ReadResponseRich;
yield return PublishResponse;
yield return BrowseRequest;
yield return WriteRequestRich;
yield return DataTypeNodeMessage;
yield return VariableNodeMessage;
}
private static void ReadRequestRich(IEncoder encoder)
{
ReadRequest(encoder);
}
private static void ReadResponseRich(IEncoder encoder)
{
ReadResponse(encoder);
}
private static void BrowseRequest(IEncoder encoder)
{
var browseRequest = new BrowseRequest
{
RequestHeader = CreateRequestHeader(),
RequestedMaxReferencesPerNode = 10,
NodesToBrowse =
[
new BrowseDescription
{
NodeId = ObjectIds.ObjectsFolder,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable),
ResultMask = (uint)BrowseResultMask.All
}
]
};
encoder.EncodeMessage(browseRequest);
}
private static void WriteRequestRich(IEncoder encoder)
{
WriteRequest(encoder);
}
private static RequestHeader CreateRequestHeader()
{
return new RequestHeader
{
Timestamp = SeedTimestamp,
RequestHandle = 42,
AdditionalHeader = new ExtensionObject(new ReadValueId
{
NodeId = new NodeId(2253),
AttributeId = Attributes.Value
})
};
}
// The two Node seeds below are permanent regression coverage for
// https://github.com/OPCFoundation/UA-.NETStandard/issues/3546 — they
// populate the fields (References, RolePermissions, DataTypeDefinition,
// multi-dim Value Variant) that the original failure-pattern exercises.
// Keeping them as seeds means FuzzGoodTestcases (the assertive theory
// in FuzzTargetTestsBase) covers the IsEqual round-trip end-to-end on
// every run, not only when the libfuzz pipeline happens to mutate
// into these shapes.
private static void DataTypeNodeMessage(IEncoder encoder)
{
var dataTypeNode = new DataTypeNode
{
NodeId = new NodeId(1234, 2),
NodeClass = NodeClass.DataType,
BrowseName = new QualifiedName("Issue3546DataType", 2),
DisplayName = new LocalizedText("en", "Issue 3546 DataType"),
Description = new LocalizedText("en", "Round-trip regression seed"),
IsAbstract = false,
DataTypeDefinition = new ExtensionObject(
new StructureDefinition
{
DefaultEncodingId = new NodeId(5678, 2),
BaseDataType = DataTypeIds.Structure,
StructureType = StructureType.Structure,
Fields =
[
new StructureField
{
Name = "Field0",
DataType = DataTypeIds.UInt32,
ValueRank = ValueRanks.Scalar,
IsOptional = false
},
new StructureField
{
Name = "Field1",
DataType = DataTypeIds.String,
ValueRank = ValueRanks.Scalar,
IsOptional = false
}
]
}),
References =
[
new ReferenceNode
{
ReferenceTypeId = ReferenceTypeIds.HasSubtype,
IsInverse = true,
TargetId = (ExpandedNodeId)DataTypeIds.Structure
},
new ReferenceNode
{
ReferenceTypeId = ReferenceTypeIds.HasEncoding,
IsInverse = false,
TargetId = new ExpandedNodeId(new NodeId(5678, 2))
}
],
RolePermissions =
[
new RolePermissionType
{
RoleId = ObjectIds.WellKnownRole_AuthenticatedUser,
Permissions = (uint)PermissionType.Read
}
],
UserRolePermissions =
[
new RolePermissionType
{
RoleId = ObjectIds.WellKnownRole_AuthenticatedUser,
Permissions = (uint)PermissionType.Read
}
]
};
encoder.EncodeMessage(dataTypeNode);
}
private static void VariableNodeMessage(IEncoder encoder)
{
var variableNode = new VariableNode
{
NodeId = new NodeId(4321, 2),
NodeClass = NodeClass.Variable,
BrowseName = new QualifiedName("Issue3546Variable", 2),
DisplayName = new LocalizedText("en", "Issue 3546 Variable"),
Description = new LocalizedText("en", "Round-trip regression seed"),
Value = Variant.From(new ExtensionObject(
new Argument
{
Name = "Sample",
DataType = DataTypeIds.UInt32,
ValueRank = ValueRanks.Scalar,
Description = new LocalizedText("en", "fuzz #3546 sample")
})),
DataType = DataTypeIds.Argument,
ValueRank = ValueRanks.Scalar,
ArrayDimensions = new ArrayOf<uint>(s_arrayDimensions),
AccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite,
UserAccessLevel = AccessLevels.CurrentRead,
MinimumSamplingInterval = 0,
Historizing = false,
AccessLevelEx = 0,
References =
[
new ReferenceNode
{
ReferenceTypeId = ReferenceTypeIds.HasTypeDefinition,
IsInverse = false,
TargetId = (ExpandedNodeId)VariableTypeIds.BaseDataVariableType
},
new ReferenceNode
{
ReferenceTypeId = ReferenceTypeIds.Organizes,
IsInverse = true,
TargetId = (ExpandedNodeId)ObjectIds.ObjectsFolder
}
],
RolePermissions =
[
new RolePermissionType
{
RoleId = ObjectIds.WellKnownRole_Anonymous,
Permissions = (uint)PermissionType.Read
}
],
UserRolePermissions =
[
new RolePermissionType
{
RoleId = ObjectIds.WellKnownRole_Anonymous,
Permissions = (uint)PermissionType.Read
}
]
};
encoder.EncodeMessage(variableNode);
}
private static readonly uint[] s_arrayDimensions = [1u, 2u];
private static void WriteBuiltInTypeTestcases(string workPath)
{
string builtInPath = workPath + ".BuiltInTypes" + Path.DirectorySeparatorChar;
Directory.CreateDirectory(builtInPath);
foreach ((string name, BuiltInEncoder builtInEncoder) in GetBuiltInEncoders())
{
string binaryPath = Path.Combine(builtInPath, "Binary");
Directory.CreateDirectory(binaryPath);
using (var encoder = new BinaryEncoder(FuzzableCode.MessageContext))
{
builtInEncoder(encoder);
File.WriteAllBytes(
Path.Combine(binaryPath, name + ".bin"),
encoder.CloseAndReturnBuffer() ?? []);
}
string jsonPath = Path.Combine(builtInPath, "Json");
Directory.CreateDirectory(jsonPath);
using (var memoryStream = new MemoryStream(0x1000))
using (var encoder = new JsonEncoder(memoryStream, FuzzableCode.MessageContext))
{
builtInEncoder(encoder);
encoder.Close();
File.WriteAllBytes(Path.Combine(jsonPath, name + ".json"), memoryStream.ToArray());
}
string xmlPath = Path.Combine(builtInPath, "Xml");
Directory.CreateDirectory(xmlPath);
File.WriteAllText(
Path.Combine(xmlPath, name + ".xml"),
GetXmlBuiltInSeed(name),
Encoding.UTF8);
}
}
private static IEnumerable<(string Name, BuiltInEncoder Encoder)> GetBuiltInEncoders()
{
yield return ("nodeid", encoder => encoder.WriteNodeId("Value", new NodeId(1000, 2)));
yield return ("expandednodeid", encoder => encoder.WriteExpandedNodeId(
"Value",
new ExpandedNodeId(new NodeId(1000, 2), "urn:example:namespace", 1)));
yield return ("variant", encoder => encoder.WriteVariant(
"Value",
Variant.From(new LocalizedText("en-US", "Hello World"))));
yield return ("extensionobject", encoder => encoder.WriteExtensionObject(
"Value",
new ExtensionObject(new ReadValueId
{
NodeId = new NodeId(2253),
AttributeId = Attributes.Value
})));
yield return ("datavalue", encoder => encoder.WriteDataValue(
"Value",
new DataValue(
Variant.From(new QualifiedName("Temperature", 2)),
StatusCodes.Good,
SeedTimestamp)));
yield return ("diagnosticinfo", encoder => encoder.WriteDiagnosticInfo(
"Value",
new DiagnosticInfo
{
AdditionalInfo = "seed diagnostic",
InnerStatusCode = StatusCodes.BadNodeIdUnknown,
InnerDiagnosticInfo = new DiagnosticInfo
{
AdditionalInfo = "inner diagnostic",
InnerStatusCode = StatusCodes.BadDecodingError
}
}));
yield return ("qualifiedname", encoder => encoder.WriteQualifiedName(
"Value",
new QualifiedName("Temperature", 2)));
yield return ("localizedtext", encoder => encoder.WriteLocalizedText(
"Value",
new LocalizedText("en-US", "Hello World")));
}
private static string GetXmlBuiltInSeed(string name)
{
const string ns = " xmlns=\"http://opcfoundation.org/UA/2008/02/Types.xsd\"";
return name switch
{
"nodeid" => $"<Value{ns}><Identifier>ns=2;i=1000</Identifier></Value>",
"expandednodeid" => $"<Value{ns}><Identifier>svr=1;nsu=urn:example:namespace;i=1000" +
"</Identifier></Value>",
"variant" => $"<Value{ns}><Value><String>Hello World</String></Value></Value>",
"extensionobject" => $"<Value{ns}><TypeId><Identifier>i=628</Identifier></TypeId></Value>",
"datavalue" => $"<Value{ns}><Value><Value><String>Hello World</String></Value></Value></Value>",
"diagnosticinfo" => $"<Value{ns}><AdditionalInfo>seed diagnostic</AdditionalInfo></Value>",
"qualifiedname" => $"<Value{ns}><NamespaceIndex>2</NamespaceIndex><Name>Temperature</Name></Value>",
"localizedtext" => $"<Value{ns}><Locale>en-US</Locale><Text>Hello World</Text></Value>",
_ => $"<Value{ns} />"
};
}
private static void WriteParserTestcases(string workPath)
{
string parserPath = workPath + ".Parsers" + Path.DirectorySeparatorChar;
Directory.CreateDirectory(parserPath);
foreach ((string name, string text) in GetParserSeeds())
{
File.WriteAllText(Path.Combine(parserPath, name + ".txt"), text, Encoding.UTF8);
}
}
private static IEnumerable<(string Name, string Text)> GetParserSeeds()
{
yield return ("nodeid_numeric", "i=85");
yield return ("nodeid_string", "ns=2;s=Demo.Node");
yield return ("nodeid_guid", "ns=2;g=00000000-0000-0000-0000-000000000001");
yield return ("expandednodeid_uri", "svr=1;nsu=http://opcfoundation.org/UA/;i=85");
yield return ("expandednodeid_string", "nsu=urn:example:namespace;s=Demo");
yield return ("relativepath_child", "/2:Block/2:Parameter");
yield return ("relativepath_property", ".2:EngineeringUnits");
yield return ("qualifiedname", "2:Temperature");
yield return ("numericrange_single", "0");
yield return ("numericrange_matrix", "0:10,1:2");
yield return ("uuid", "00000000-0000-0000-0000-000000000001");
}
}
}