/* ======================================================================== * 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.IO; using System.IO.Compression; using System.Text; #nullable enable namespace Opc.Ua.SourceGeneration.Dependency { /// /// Kind of node carried in a . /// public enum DependencyNodeKind : byte { /// /// Unknown — invalid in a well-formed dependency payload. /// Unknown = 0, /// /// ObjectType. /// ObjectType = 1, /// /// VariableType. /// VariableType = 2, /// /// ReferenceType. /// ReferenceType = 3, /// /// DataType. /// DataType = 4, /// /// Method (when carried as standalone declaration). /// Method = 5, } /// /// Reduced view of a DataTypeDesign field carried in a dependency payload. /// public readonly record struct DependencyDataField { /// /// Field name. /// public string Name { get; } /// /// DataType name (qualified). /// public string DataTypeName { get; } /// /// DataType namespace URI. /// public string DataTypeNamespace { get; } /// Value rank (). public int ValueRank { get; } /// /// Constructor. /// public DependencyDataField(string name, string dataTypeName, string dataTypeNamespace, int valueRank) { Name = name ?? string.Empty; DataTypeName = dataTypeName ?? string.Empty; DataTypeNamespace = dataTypeNamespace ?? string.Empty; ValueRank = valueRank; } } /// /// A method input or output argument carried in a dependency payload. /// public readonly record struct DependencyMethodArg { /// /// Argument name. /// public string Name { get; } /// /// DataType name (qualified). /// public string DataTypeName { get; } /// /// DataType namespace URI. /// public string DataTypeNamespace { get; } /// /// Value rank. /// public int ValueRank { get; } /// /// Constructor. /// public DependencyMethodArg(string name, string dataTypeName, string dataTypeNamespace, int valueRank) { Name = name ?? string.Empty; DataTypeName = dataTypeName ?? string.Empty; DataTypeNamespace = dataTypeNamespace ?? string.Empty; ValueRank = valueRank; } } /// /// Reduced view of an InstanceDesign child carried in a /// dependency payload type. Carries enough metadata for downstream /// generators to set OveriddenNode + TypeDefinitionNode /// / DataTypeNode on consumer's re-declared inherited /// members. /// public sealed class DependencyChild { /// /// Browse name of the child. /// public string BrowseName { get; set; } = string.Empty; /// /// Symbolic name (often equal to BrowseName). /// public string SymbolicName { get; set; } = string.Empty; /// /// TypeDefinition name (for all kinds). Empty when not declared. /// public string TypeDefinitionName { get; set; } = string.Empty; /// /// TypeDefinition namespace URI. /// public string TypeDefinitionNamespace { get; set; } = string.Empty; /// /// DataType name (variables only). Empty when not applicable. /// public string DataTypeName { get; set; } = string.Empty; /// /// DataType namespace URI (variables only). /// public string DataTypeNamespace { get; set; } = string.Empty; /// Value rank (variables only; Scalar = 0 per Opc.Ua.ValueRanks). public int ValueRank { get; set; } /// /// Modelling rule (0=None, 1=Mandatory, 2=Optional, 3=OptionalPlaceholder, 4=MandatoryPlaceholder, 5=ExposesItsArray). /// public byte ModellingRule { get; set; } /// /// Instance kind: 1=Object 2=Variable 3=Property 4=Method. /// public byte InstanceKind { get; set; } /// /// Input arguments (methods only). /// public IReadOnlyList InputArguments { get; set; } = Array.Empty(); /// /// Output arguments (methods only). /// public IReadOnlyList OutputArguments { get; set; } = Array.Empty(); } /// /// Reduced view of a node entry carried in a dependency payload. One entry per /// node the producing assembly emits as a public type definition /// (objects/instances are not carried; consumers re-derive /// inheritance via ). /// public sealed class DependencyNode { /// /// Symbolic name. /// public string SymbolicName { get; set; } = string.Empty; /// /// Symbolic namespace URI. /// public string SymbolicNamespace { get; set; } = string.Empty; /// /// Emitted C# class name (post-Type-suffix-stripping). /// public string ClassName { get; set; } = string.Empty; /// /// Node kind. /// public DependencyNodeKind Kind { get; set; } /// /// Base type name (null for root types). /// public string? BaseTypeName { get; set; } /// /// Base type namespace URI (null for root types). /// public string? BaseTypeNamespace { get; set; } /// /// Numeric NodeId (0 when not assigned). /// public uint NumericId { get; set; } /// /// Optional string NodeId. /// public string? StringId { get; set; } /// /// True when the type is abstract. /// public bool IsAbstract { get; set; } /// /// True when the entry represents an enumeration DataType. /// public bool IsEnumeration { get; set; } /// /// DataType fields (empty for non-DataType kinds). /// public IReadOnlyList Fields { get; set; } = Array.Empty(); /// /// Declared instance children (empty for DataType / no-child types). /// public IReadOnlyList Children { get; set; } = Array.Empty(); } /// /// In-memory representation of a ModelDependencyV1 payload. /// public sealed class ModelDependencyV1 { /// /// The magic byte sequence. /// public static readonly byte[] Magic = [0xAA, 0xC7]; /// /// The version byte for V1. /// public const byte Version = 1; /// /// Compression scheme: 1 = Deflate. /// public const byte CompressionDeflate = 1; /// /// The model URI this dependency payload describes. /// public string ModelUri { get; set; } = string.Empty; /// /// Nodes in the dependency payload. /// public List Nodes { get; } = []; /// /// Serialises the dependency payload to a base64-encoded /// Deflate-compressed payload suitable for embedding in /// ModelDependencyAttribute. /// public string ToBase64Payload() { using var raw = new MemoryStream(); WriteUncompressed(raw); raw.Position = 0; using var output = new MemoryStream(); // Header is uncompressed; payload below is Deflate-compressed. output.WriteByte(Magic[0]); output.WriteByte(Magic[1]); output.WriteByte(Version); output.WriteByte(CompressionDeflate); using (var deflate = new DeflateStream(output, CompressionLevel.Optimal, leaveOpen: true)) { raw.CopyTo(deflate); } return Convert.ToBase64String(output.ToArray()); } /// /// Reads a dependency payload from a base64-encoded payload. /// /// Null when the payload version is unrecognised or the /// magic does not match. public static ModelDependencyV1? FromBase64Payload(string base64) { if (string.IsNullOrEmpty(base64)) { return null; } byte[] bytes; try { bytes = Convert.FromBase64String(base64); } catch (FormatException) { return null; } if (bytes.Length < 4) { return null; } if (bytes[0] != Magic[0] || bytes[1] != Magic[1]) { return null; } if (bytes[2] != Version) { return null; } if (bytes[3] != CompressionDeflate) { return null; } using var input = new MemoryStream(bytes, index: 4, count: bytes.Length - 4, writable: false); using var deflate = new DeflateStream(input, CompressionMode.Decompress, leaveOpen: false); using var inflated = new MemoryStream(); deflate.CopyTo(inflated); inflated.Position = 0; var result = new ModelDependencyV1(); result.ReadUncompressed(inflated); return result; } private void WriteUncompressed(Stream destination) { using var writer = new BinaryWriter(destination, Encoding.UTF8, leaveOpen: true); WriteString(writer, ModelUri); writer.Write(Nodes.Count); foreach (DependencyNode node in Nodes) { WriteString(writer, node.SymbolicName); WriteString(writer, node.SymbolicNamespace); WriteString(writer, node.ClassName); writer.Write((byte)node.Kind); WriteNullableString(writer, node.BaseTypeName); WriteNullableString(writer, node.BaseTypeNamespace); writer.Write(node.NumericId); WriteNullableString(writer, node.StringId); byte flags = 0; if (node.IsAbstract) { flags |= 0x01; } if (node.IsEnumeration) { flags |= 0x02; } writer.Write(flags); writer.Write(node.Fields.Count); foreach (DependencyDataField field in node.Fields) { WriteString(writer, field.Name); WriteString(writer, field.DataTypeName); WriteString(writer, field.DataTypeNamespace); writer.Write(field.ValueRank); } writer.Write(node.Children.Count); foreach (DependencyChild child in node.Children) { WriteString(writer, child.BrowseName); WriteString(writer, child.SymbolicName); WriteString(writer, child.TypeDefinitionName); WriteString(writer, child.TypeDefinitionNamespace); WriteString(writer, child.DataTypeName); WriteString(writer, child.DataTypeNamespace); writer.Write(child.ValueRank); writer.Write(child.ModellingRule); writer.Write(child.InstanceKind); writer.Write(child.InputArguments.Count); foreach (DependencyMethodArg a in child.InputArguments) { WriteString(writer, a.Name); WriteString(writer, a.DataTypeName); WriteString(writer, a.DataTypeNamespace); writer.Write(a.ValueRank); } writer.Write(child.OutputArguments.Count); foreach (DependencyMethodArg a in child.OutputArguments) { WriteString(writer, a.Name); WriteString(writer, a.DataTypeName); WriteString(writer, a.DataTypeNamespace); writer.Write(a.ValueRank); } } } } private void ReadUncompressed(Stream source) { using var reader = new BinaryReader(source, Encoding.UTF8, leaveOpen: true); ModelUri = ReadString(reader); int typeCount = reader.ReadInt32(); if (typeCount < 0 || typeCount > 1_000_000) { throw new InvalidDataException( "ModelDependencyV1: invalid type count " + typeCount); } Nodes.Capacity = typeCount; for (int i = 0; i < typeCount; i++) { var node = new DependencyNode { SymbolicName = ReadString(reader), SymbolicNamespace = ReadString(reader), ClassName = ReadString(reader), Kind = (DependencyNodeKind)reader.ReadByte(), BaseTypeName = ReadNullableString(reader), BaseTypeNamespace = ReadNullableString(reader), NumericId = reader.ReadUInt32(), StringId = ReadNullableString(reader), }; byte flags = reader.ReadByte(); node.IsAbstract = (flags & 0x01) != 0; node.IsEnumeration = (flags & 0x02) != 0; int fieldCount = reader.ReadInt32(); if (fieldCount < 0 || fieldCount > 100_000) { throw new InvalidDataException( "ModelDependencyV1: invalid field count " + fieldCount); } if (fieldCount > 0) { var fields = new DependencyDataField[fieldCount]; for (int j = 0; j < fieldCount; j++) { fields[j] = new DependencyDataField( ReadString(reader), ReadString(reader), ReadString(reader), reader.ReadInt32()); } node.Fields = fields; } int childCount = reader.ReadInt32(); if (childCount < 0 || childCount > 100_000) { throw new InvalidDataException( "ModelDependencyV1: invalid child count " + childCount); } if (childCount > 0) { var children = new DependencyChild[childCount]; for (int j = 0; j < childCount; j++) { var c = new DependencyChild { BrowseName = ReadString(reader), SymbolicName = ReadString(reader), TypeDefinitionName = ReadString(reader), TypeDefinitionNamespace = ReadString(reader), DataTypeName = ReadString(reader), DataTypeNamespace = ReadString(reader), ValueRank = reader.ReadInt32(), ModellingRule = reader.ReadByte(), InstanceKind = reader.ReadByte() }; int inCount = reader.ReadInt32(); if (inCount < 0 || inCount > 100) { throw new InvalidDataException( "ModelDependencyV1: invalid input arg count " + inCount); } if (inCount > 0) { var args = new DependencyMethodArg[inCount]; for (int k = 0; k < inCount; k++) { args[k] = new DependencyMethodArg( ReadString(reader), ReadString(reader), ReadString(reader), reader.ReadInt32()); } c.InputArguments = args; } int outCount = reader.ReadInt32(); if (outCount < 0 || outCount > 100) { throw new InvalidDataException( "ModelDependencyV1: invalid output arg count " + outCount); } if (outCount > 0) { var args = new DependencyMethodArg[outCount]; for (int k = 0; k < outCount; k++) { args[k] = new DependencyMethodArg( ReadString(reader), ReadString(reader), ReadString(reader), reader.ReadInt32()); } c.OutputArguments = args; } children[j] = c; } node.Children = children; } Nodes.Add(node); } } private static void WriteString(BinaryWriter writer, string value) { writer.Write(value ?? string.Empty); } private static void WriteNullableString(BinaryWriter writer, string? value) { if (value == null) { writer.Write((byte)0); } else { writer.Write((byte)1); writer.Write(value); } } private static string ReadString(BinaryReader reader) { return reader.ReadString(); } private static string? ReadNullableString(BinaryReader reader) { byte marker = reader.ReadByte(); return marker == 0 ? null : reader.ReadString(); } } }