/* ======================================================================== * 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; using System.Xml; 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; } /// /// True if the structure field is optional (encoding mask bit). /// Carried in the structure field trailer. /// public bool IsOptional { get; init; } /// /// True if the structure field allows subtypes of its data type /// (encoded as ExtensionObject). Carried in the structure field /// trailer. /// public bool AllowSubTypes { get; init; } /// /// The declared array dimensions ("0,0,0"), or null. /// Carried in the structure field trailer. /// public string? ArrayDimensions { get; init; } /// /// 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; /// /// Namespace URI qualifying . Empty when the /// declaring type's own namespace qualifies it. A child re-declared /// from a base type in another namespace keeps the namespace of its /// original declaration here, so a consumer that instantiates the /// derived type on its own still emits 0:EngineeringUnits /// rather than re-qualifying it into the declaring model. /// public string BrowseNameNamespace { 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; } /// /// Name of the reference type binding the child to its parent. Empty /// when the kind default applies (HasProperty for a property, /// HasComponent otherwise). /// public string ReferenceTypeName { get; set; } = string.Empty; /// /// Namespace URI of . /// public string ReferenceTypeNamespace { get; set; } = string.Empty; /// /// ModelDesign access level value (variables only). /// public byte AccessLevel { get; set; } /// /// Whether was explicitly specified. /// public bool AccessLevelSpecified { get; set; } /// /// Exact OPC UA AccessLevel bitmask imported from NodeSet2. /// public uint? RawAccessLevel { get; set; } /// /// Exact OPC UA UserAccessLevel bitmask imported from NodeSet2. /// public uint? RawUserAccessLevel { get; set; } /// /// Minimum sampling interval (variables only). /// public int MinimumSamplingInterval { get; set; } /// /// Whether was explicitly specified. /// public bool MinimumSamplingIntervalSpecified { get; set; } /// /// Whether historical data is collected (variables only). /// public bool Historizing { get; set; } /// /// Whether was explicitly specified. /// public bool HistorizingSpecified { get; set; } /// /// Serialized ModelDesign default value XML (variables only). /// public string? DefaultValueXml { get; set; } /// /// The namespace table the namespace indexes in /// refer to in the producing model /// (index 0 is the OPC UA namespace): the producer's design /// namespaces, or the table of the NodeSet or dependency the value /// was decoded from. Null when the payload does not carry it (older /// producers); the consumer then resolves the indexes as before. /// Carried in the value namespace trailer. /// public IReadOnlyList? DefaultValueNamespaceUris { get; set; } /// /// Input arguments (methods only). /// public IReadOnlyList InputArguments { get; set; } = []; /// /// Output arguments (methods only). /// public IReadOnlyList OutputArguments { get; set; } = []; /// /// Qualified method state identity used by the producing assembly. /// Empty for payloads without method identity and non-method children. /// public string MethodStateName { get; set; } = string.Empty; /// /// Namespace URI for . /// public string MethodStateNamespace { get; set; } = string.Empty; /// /// Qualified method declaration identity used on the wire. /// Empty for payloads without method identity and non-method children. /// public string MethodDeclarationName { get; set; } = string.Empty; /// /// Namespace URI for . /// public string MethodDeclarationNamespace { get; set; } = string.Empty; /// /// Numeric NodeId identifier for the effective method declaration. /// public uint MethodDeclarationNumericId { get; set; } /// /// String NodeId identifier for the effective method declaration. /// public string? MethodDeclarationStringId { get; set; } } /// /// 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; } /// /// Optional Guid NodeId in the "D" format (null when not assigned). /// public string? GuidId { get; set; } /// /// Optional opaque NodeId as base64 (null when not assigned). /// public string? OpaqueId { 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 restriction of a VariableType (null for every other kind). /// A consumer needs it to decide whether a variable typed by this /// VariableType requires a template parameter on its generated state /// class; without it the consumer cannot tell State<T> from the /// non-generic state and dereferences a null DataTypeNode. /// public string? DataTypeName { get; set; } /// /// Namespace URI of . /// public string? DataTypeNamespace { get; set; } /// /// ValueRank of a VariableType, or null when the VariableType does not /// specify one (ValueRankSpecified = false upstream). /// public int? ValueRank { get; set; } /// /// DataType fields (empty for non-DataType kinds). /// public IReadOnlyList Fields { get; set; } = []; /// /// Declared instance children (empty for DataType / no-child types). /// public IReadOnlyList Children { get; set; } = []; } /// /// 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; private const byte kFluentAccessorsEmitted = 0x01; private const byte kFluentAccessorsKnown = 0x40; private const byte kMethodIdentityTrailer = 0x80; private const byte kMethodIdentityTrailerVersion = 1; private const byte kNodeIsAbstract = 0x01; private const byte kNodeIsEnumeration = 0x02; // The declaration trailer carries the DataType restriction of each // VariableType and the BrowseName namespace and ReferenceType of each // child, with a flag byte per node and per child. private const byte kDeclarationTrailer = 0x04; private const byte kDeclarationTrailerVersion = 1; private const byte kDeclarationDataType = 0x01; private const byte kDeclarationBrowseNameNamespace = 0x01; private const byte kDeclarationReferenceType = 0x02; private const byte kExtendedIdentifierTrailer = 0x20; private const byte kExtendedIdentifierTrailerVersion = 1; private const byte kStructureFieldTrailer = 0x10; private const byte kStructureFieldTrailerVersion = 1; private const byte kValueNamespaceTrailer = 0x08; private const byte kValueNamespaceTrailerVersion = 1; private const byte kFieldIsOptional = 0x01; private const byte kFieldAllowSubTypes = 0x02; private const byte kAccessLevelSpecified = 0x01; private const byte kRawAccessLevel = 0x02; private const byte kRawUserAccessLevel = 0x04; private const byte kMinimumSamplingIntervalSpecified = 0x08; private const byte kHistorizingSpecified = 0x10; /// /// Namespace URI that qualifies the standard reference types. /// public const string OpcUaNamespaceUri = "http://opcfoundation.org/UA/"; /// /// Reference type a child of the given instance kind is bound with when /// the declaring model does not name one. Mirrors the defaulting the /// validator applies to a locally declared instance, so a child /// materialised from a dependency payload is bound the same way as the /// identical child declared in the consuming model. /// /// /// Instance kind: 1=Object 2=Variable 3=Property 4=Method. /// public static string GetDefaultReferenceTypeName(byte instanceKind) { return instanceKind == 3 ? "HasProperty" : "HasComponent"; } /// /// The model URI this dependency payload describes. /// public string ModelUri { get; set; } = string.Empty; /// /// Whether the producing assembly emitted the per-ObjectType typed /// fluent accessor extension classes. null means the payload /// does not declare whether accessors exist. /// public bool? FluentAccessorsEmitted { get; set; } /// /// 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 |= kNodeIsAbstract; } if (node.IsEnumeration) { flags |= kNodeIsEnumeration; } 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); byte variableFlags = 0; if (child.AccessLevelSpecified) { variableFlags |= kAccessLevelSpecified; } if (child.RawAccessLevel.HasValue) { variableFlags |= kRawAccessLevel; } if (child.RawUserAccessLevel.HasValue) { variableFlags |= kRawUserAccessLevel; } if (child.MinimumSamplingIntervalSpecified) { variableFlags |= kMinimumSamplingIntervalSpecified; } if (child.HistorizingSpecified) { variableFlags |= kHistorizingSpecified; } writer.Write(variableFlags); writer.Write(child.AccessLevel); writer.Write(child.RawAccessLevel.GetValueOrDefault()); writer.Write(child.RawUserAccessLevel.GetValueOrDefault()); writer.Write(child.MinimumSamplingInterval); writer.Write(child.Historizing); WriteNullableString(writer, child.DefaultValueXml); 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); } } } bool hasMethodIdentityTrailer = HasMethodIdentityTrailer(); bool hasStructureFieldTrailer = HasStructureFieldTrailer(); bool hasValueNamespaceTrailer = HasValueNamespaceTrailer(); bool hasDeclarationTrailer = HasDeclarationTrailer(); // The structure field, value namespace and declaration trailers // are written after the extended identifier trailer, which is // always written with them: a reader that predates them only reads // the capabilities as "trailers present" (honouring the // FluentAccessorsKnown bit) when the method identity or the // extended identifier bit is set, and it stops reading after the // trailers it knows - so it never sees the new ones. bool hasExtendedIdentifierTrailer = hasStructureFieldTrailer || hasValueNamespaceTrailer || hasDeclarationTrailer || HasExtendedIdentifierTrailer(); if (FluentAccessorsEmitted.HasValue || hasMethodIdentityTrailer || hasExtendedIdentifierTrailer) { byte capabilities = 0; if (FluentAccessorsEmitted.HasValue) { capabilities |= kFluentAccessorsKnown; if (FluentAccessorsEmitted.Value) { capabilities |= kFluentAccessorsEmitted; } } if (hasMethodIdentityTrailer) { capabilities |= kMethodIdentityTrailer; } if (hasExtendedIdentifierTrailer) { capabilities |= kExtendedIdentifierTrailer; } if (hasStructureFieldTrailer) { capabilities |= kStructureFieldTrailer; } if (hasValueNamespaceTrailer) { capabilities |= kValueNamespaceTrailer; } if (hasDeclarationTrailer) { capabilities |= kDeclarationTrailer; } writer.Write(capabilities); if (hasMethodIdentityTrailer) { WriteMethodIdentityTrailer(writer); } if (hasExtendedIdentifierTrailer) { WriteExtendedIdentifierTrailer(writer); } if (hasStructureFieldTrailer) { WriteStructureFieldTrailer(writer); } if (hasValueNamespaceTrailer) { WriteValueNamespaceTrailer(writer); } if (hasDeclarationTrailer) { WriteDeclarationTrailer(writer); } } } private void ReadUncompressed(Stream source) { using var reader = new BinaryReader(source, Encoding.UTF8, leaveOpen: true); ModelUri = ReadString(reader); int typeCount = reader.ReadInt32(); if (typeCount is < 0 or > 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 & kNodeIsAbstract) != 0; node.IsEnumeration = (flags & kNodeIsEnumeration) != 0; int fieldCount = reader.ReadInt32(); if (fieldCount is < 0 or > 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 is < 0 or > 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() }; byte variableFlags = reader.ReadByte(); c.AccessLevelSpecified = (variableFlags & kAccessLevelSpecified) != 0; c.AccessLevel = reader.ReadByte(); uint rawAccessLevel = reader.ReadUInt32(); if ((variableFlags & kRawAccessLevel) != 0) { c.RawAccessLevel = rawAccessLevel; } uint rawUserAccessLevel = reader.ReadUInt32(); if ((variableFlags & kRawUserAccessLevel) != 0) { c.RawUserAccessLevel = rawUserAccessLevel; } c.MinimumSamplingInterval = reader.ReadInt32(); c.MinimumSamplingIntervalSpecified = (variableFlags & kMinimumSamplingIntervalSpecified) != 0; c.Historizing = reader.ReadBoolean(); c.HistorizingSpecified = (variableFlags & kHistorizingSpecified) != 0; c.DefaultValueXml = ReadNullableString(reader); if (c.DefaultValueXml != null) { ValidateDefaultValueXml(c.DefaultValueXml); } int inCount = reader.ReadInt32(); if (inCount is < 0 or > 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 is < 0 or > 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); } if (reader.BaseStream.Position < reader.BaseStream.Length) { byte capabilities = reader.ReadByte(); bool hasMethodIdentityTrailer = (capabilities & kMethodIdentityTrailer) != 0; bool hasExtendedIdentifierTrailer = (capabilities & kExtendedIdentifierTrailer) != 0; if (hasMethodIdentityTrailer || hasExtendedIdentifierTrailer) { FluentAccessorsEmitted = (capabilities & kFluentAccessorsKnown) != 0 ? (capabilities & kFluentAccessorsEmitted) != 0 : null; } else { FluentAccessorsEmitted = (capabilities & kFluentAccessorsEmitted) != 0; } // Trailers carry no length prefix, so a trailer of a version // this reader does not know cannot be skipped: its body is left // unread and whatever follows it cannot be located. Stop there // rather than parse the next trailer out of the middle of the // unknown one - which either assigned garbage identifiers or // threw and dropped the whole payload. bool trailersReadable = true; if (hasMethodIdentityTrailer && reader.BaseStream.Position < reader.BaseStream.Length) { trailersReadable = ReadMethodIdentityTrailer(reader); } if (trailersReadable && hasExtendedIdentifierTrailer && reader.BaseStream.Position < reader.BaseStream.Length) { trailersReadable = ReadExtendedIdentifierTrailer(reader); } // The later trailers are length prefixed: an unknown version // is skipped and the ones after it stay readable. if (trailersReadable && (capabilities & kStructureFieldTrailer) != 0 && reader.BaseStream.Position < reader.BaseStream.Length) { ReadStructureFieldTrailer(reader); } if (trailersReadable && (capabilities & kValueNamespaceTrailer) != 0 && reader.BaseStream.Position < reader.BaseStream.Length) { ReadValueNamespaceTrailer(reader); } if (trailersReadable && (capabilities & kDeclarationTrailer) != 0 && reader.BaseStream.Position < reader.BaseStream.Length) { ReadDeclarationTrailer(reader); } } } private bool HasDeclarationTrailer() { foreach (DependencyNode node in Nodes) { if (node.DataTypeName != null) { return true; } foreach (DependencyChild child in node.Children) { if (!string.IsNullOrEmpty(child.BrowseNameNamespace) || !string.IsNullOrEmpty(child.ReferenceTypeName)) { return true; } } } return false; } /// /// Writes the declaration trailer: per node the DataType and ValueRank /// of a VariableType, and per child of that node the namespace of its /// BrowseName and the ReferenceType that binds it, each behind a flag /// byte. The body is length prefixed so that a reader can skip a /// later version. /// private void WriteDeclarationTrailer(BinaryWriter writer) { using var body = new MemoryStream(); using (var bodyWriter = new BinaryWriter(body, Encoding.UTF8, leaveOpen: true)) { bodyWriter.Write(Nodes.Count); foreach (DependencyNode node in Nodes) { bool hasDataType = node.DataTypeName != null; bodyWriter.Write(hasDataType ? kDeclarationDataType : (byte)0); if (hasDataType) { WriteString(bodyWriter, node.DataTypeName ?? string.Empty); WriteString(bodyWriter, node.DataTypeNamespace ?? string.Empty); bodyWriter.Write(node.ValueRank.HasValue); bodyWriter.Write(node.ValueRank ?? 0); } bodyWriter.Write(node.Children.Count); foreach (DependencyChild child in node.Children) { bool hasBrowseNameNamespace = !string.IsNullOrEmpty(child.BrowseNameNamespace); bool hasReferenceType = !string.IsNullOrEmpty(child.ReferenceTypeName); byte childFlags = 0; if (hasBrowseNameNamespace) { childFlags |= kDeclarationBrowseNameNamespace; } if (hasReferenceType) { childFlags |= kDeclarationReferenceType; } bodyWriter.Write(childFlags); if (hasBrowseNameNamespace) { WriteString(bodyWriter, child.BrowseNameNamespace); } if (hasReferenceType) { WriteString(bodyWriter, child.ReferenceTypeName); WriteString(bodyWriter, child.ReferenceTypeNamespace); } } } } writer.Write(kDeclarationTrailerVersion); writer.Write((int)body.Length); writer.Write(body.GetBuffer(), 0, (int)body.Length); } private void ReadDeclarationTrailer(BinaryReader reader) { byte trailerVersion = reader.ReadByte(); int length = reader.ReadInt32(); if (length < 0 || length > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException( "ModelDependencyV1: invalid declaration trailer length " + length); } if (trailerVersion != kDeclarationTrailerVersion) { // Length prefixed: a later version is skipped as a whole. reader.BaseStream.Seek(length, SeekOrigin.Current); return; } int nodeCount = reader.ReadInt32(); if (nodeCount != Nodes.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid declaration node count " + nodeCount); } for (int i = 0; i < nodeCount; i++) { DependencyNode node = Nodes[i]; byte nodeFlags = reader.ReadByte(); if ((nodeFlags & kDeclarationDataType) != 0) { node.DataTypeName = ReadString(reader); node.DataTypeNamespace = ReadString(reader); bool valueRankSpecified = reader.ReadBoolean(); int valueRank = reader.ReadInt32(); node.ValueRank = valueRankSpecified ? valueRank : null; } int childCount = reader.ReadInt32(); if (childCount != node.Children.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid declaration child count " + childCount); } for (int j = 0; j < childCount; j++) { DependencyChild child = node.Children[j]; byte childFlags = reader.ReadByte(); if ((childFlags & kDeclarationBrowseNameNamespace) != 0) { child.BrowseNameNamespace = ReadString(reader); } if ((childFlags & kDeclarationReferenceType) != 0) { child.ReferenceTypeName = ReadString(reader); child.ReferenceTypeNamespace = ReadString(reader); } } } } private bool HasValueNamespaceTrailer() { foreach (DependencyNode node in Nodes) { foreach (DependencyChild child in node.Children) { if (child.DefaultValueNamespaceUris != null) { return true; } } } return false; } /// /// Writes the value namespace trailer: the distinct namespace tables /// the default values of the children refer to, then per node and /// child (in order) the index of the child's table, -1 for none. The /// body is length prefixed so that a reader can skip a later version. /// private void WriteValueNamespaceTrailer(BinaryWriter writer) { var tables = new List>(); var tableIndexes = new Dictionary(StringComparer.Ordinal); using var body = new MemoryStream(); using (var bodyWriter = new BinaryWriter(body, Encoding.UTF8, leaveOpen: true)) { var childTables = new List(); foreach (DependencyNode node in Nodes) { foreach (DependencyChild child in node.Children) { IReadOnlyList? uris = child.DefaultValueNamespaceUris; if (uris == null) { childTables.Add(-1); continue; } string key = GetTableKey(uris); if (!tableIndexes.TryGetValue(key, out int index)) { index = tables.Count; tables.Add(uris); tableIndexes.Add(key, index); } childTables.Add(index); } } bodyWriter.Write(tables.Count); foreach (IReadOnlyList table in tables) { bodyWriter.Write(table.Count); foreach (string uri in table) { WriteString(bodyWriter, uri); } } int next = 0; bodyWriter.Write(Nodes.Count); foreach (DependencyNode node in Nodes) { bodyWriter.Write(node.Children.Count); for (int j = 0; j < node.Children.Count; j++) { bodyWriter.Write(childTables[next++]); } } } writer.Write(kValueNamespaceTrailerVersion); writer.Write((int)body.Length); writer.Write(body.GetBuffer(), 0, (int)body.Length); } /// /// An unambiguous key for a namespace table (length prefixed URIs). /// private static string GetTableKey(IReadOnlyList uris) { var key = new StringBuilder(); foreach (string uri in uris) { string value = uri ?? string.Empty; key.Append(value.Length.ToString(System.Globalization.CultureInfo.InvariantCulture)) .Append(':') .Append(value); } return key.ToString(); } private void ReadValueNamespaceTrailer(BinaryReader reader) { byte trailerVersion = reader.ReadByte(); int length = reader.ReadInt32(); if (length < 0 || length > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException( "ModelDependencyV1: invalid value namespace trailer length " + length); } if (trailerVersion != kValueNamespaceTrailerVersion) { // Length prefixed: a later version is skipped as a whole. reader.BaseStream.Seek(length, SeekOrigin.Current); return; } int tableCount = reader.ReadInt32(); if (tableCount is < 0 or > 100_000) { throw new InvalidDataException( "ModelDependencyV1: invalid value namespace table count " + tableCount); } string[][] tables = new string[tableCount][]; for (int i = 0; i < tableCount; i++) { int uriCount = reader.ReadInt32(); if (uriCount is < 1 or > ushort.MaxValue + 1) { throw new InvalidDataException( "ModelDependencyV1: invalid value namespace table size " + uriCount); } string[] uris = new string[uriCount]; for (int k = 0; k < uriCount; k++) { uris[k] = ReadString(reader); } tables[i] = uris; } int nodeCount = reader.ReadInt32(); if (nodeCount != Nodes.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid value namespace node count " + nodeCount); } for (int i = 0; i < nodeCount; i++) { DependencyNode node = Nodes[i]; int childCount = reader.ReadInt32(); if (childCount != node.Children.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid value namespace child count " + childCount); } for (int j = 0; j < childCount; j++) { int index = reader.ReadInt32(); if (index < -1 || index >= tableCount) { throw new InvalidDataException( "ModelDependencyV1: invalid value namespace table index " + index); } node.Children[j].DefaultValueNamespaceUris = index < 0 ? null : tables[index]; } } } private bool HasStructureFieldTrailer() { foreach (DependencyNode node in Nodes) { foreach (DependencyDataField field in node.Fields) { if (field.IsOptional || field.AllowSubTypes || !string.IsNullOrEmpty(field.ArrayDimensions)) { return true; } } } return false; } /// /// Writes the structure field trailer: the IsOptional, AllowSubTypes /// and ArrayDimensions of every field, in node and field order. The /// body is length prefixed so that a reader can skip a later version. /// private void WriteStructureFieldTrailer(BinaryWriter writer) { using var body = new MemoryStream(); using (var bodyWriter = new BinaryWriter(body, Encoding.UTF8, leaveOpen: true)) { bodyWriter.Write(Nodes.Count); foreach (DependencyNode node in Nodes) { bodyWriter.Write(node.Fields.Count); foreach (DependencyDataField field in node.Fields) { byte fieldFlags = 0; if (field.IsOptional) { fieldFlags |= kFieldIsOptional; } if (field.AllowSubTypes) { fieldFlags |= kFieldAllowSubTypes; } bodyWriter.Write(fieldFlags); WriteNullableString( bodyWriter, string.IsNullOrEmpty(field.ArrayDimensions) ? null : field.ArrayDimensions); } } } writer.Write(kStructureFieldTrailerVersion); writer.Write((int)body.Length); writer.Write(body.GetBuffer(), 0, (int)body.Length); } private void ReadStructureFieldTrailer(BinaryReader reader) { byte trailerVersion = reader.ReadByte(); int length = reader.ReadInt32(); if (length < 0 || length > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException( "ModelDependencyV1: invalid structure field trailer length " + length); } if (trailerVersion != kStructureFieldTrailerVersion) { // Length prefixed: a later version is skipped as a whole. reader.BaseStream.Seek(length, SeekOrigin.Current); return; } int nodeCount = reader.ReadInt32(); if (nodeCount != Nodes.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid structure field node count " + nodeCount); } for (int i = 0; i < nodeCount; i++) { DependencyNode node = Nodes[i]; int fieldCount = reader.ReadInt32(); if (fieldCount != node.Fields.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid structure field count " + fieldCount); } if (fieldCount == 0) { continue; } var fields = new DependencyDataField[fieldCount]; for (int j = 0; j < fieldCount; j++) { byte fieldFlags = reader.ReadByte(); fields[j] = node.Fields[j] with { IsOptional = (fieldFlags & kFieldIsOptional) != 0, AllowSubTypes = (fieldFlags & kFieldAllowSubTypes) != 0, ArrayDimensions = ReadNullableString(reader) }; } node.Fields = fields; } } private bool HasExtendedIdentifierTrailer() { foreach (DependencyNode node in Nodes) { // Null, not empty, means "not assigned" for both: an empty // GuidId has to be written or it reads back as null. if (node.GuidId != null || node.OpaqueId != null) { return true; } } return false; } private void WriteExtendedIdentifierTrailer(BinaryWriter writer) { writer.Write(kExtendedIdentifierTrailerVersion); writer.Write(Nodes.Count); foreach (DependencyNode node in Nodes) { WriteNullableString(writer, node.GuidId); WriteNullableString(writer, node.OpaqueId); } } /// /// Reads the extended identifier trailer. Returns false when the /// trailer has a version this reader does not know - its body is /// then left unread and no later trailer can be located. /// private bool ReadExtendedIdentifierTrailer(BinaryReader reader) { byte trailerVersion = reader.ReadByte(); if (trailerVersion != kExtendedIdentifierTrailerVersion) { return false; } int nodeCount = reader.ReadInt32(); if (nodeCount != Nodes.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid extended identifier node count " + nodeCount); } for (int i = 0; i < nodeCount; i++) { Nodes[i].GuidId = ReadNullableString(reader); Nodes[i].OpaqueId = ReadNullableString(reader); } return true; } private bool HasMethodIdentityTrailer() { foreach (DependencyNode node in Nodes) { foreach (DependencyChild child in node.Children) { // Every field the trailer carries, or a child whose only // method identity is a namespace does not round-trip. if (!string.IsNullOrEmpty(child.MethodStateName) || !string.IsNullOrEmpty(child.MethodStateNamespace) || !string.IsNullOrEmpty(child.MethodDeclarationName) || !string.IsNullOrEmpty(child.MethodDeclarationNamespace) || child.MethodDeclarationNumericId != 0 || !string.IsNullOrEmpty(child.MethodDeclarationStringId)) { return true; } } } return false; } private void WriteMethodIdentityTrailer(BinaryWriter writer) { writer.Write(kMethodIdentityTrailerVersion); writer.Write(Nodes.Count); foreach (DependencyNode node in Nodes) { writer.Write(node.Children.Count); foreach (DependencyChild child in node.Children) { WriteString(writer, child.MethodStateName); WriteString(writer, child.MethodStateNamespace); WriteString(writer, child.MethodDeclarationName); WriteString(writer, child.MethodDeclarationNamespace); writer.Write(child.MethodDeclarationNumericId); WriteNullableString(writer, child.MethodDeclarationStringId); } } } /// /// Reads the method identity trailer. Returns false when the trailer /// has a version this reader does not know - its body is then left /// unread and no later trailer can be located. /// private bool ReadMethodIdentityTrailer(BinaryReader reader) { byte trailerVersion = reader.ReadByte(); if (trailerVersion != kMethodIdentityTrailerVersion) { return false; } int nodeCount = reader.ReadInt32(); if (nodeCount != Nodes.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid method identity node count " + nodeCount); } for (int i = 0; i < nodeCount; i++) { DependencyNode node = Nodes[i]; int childCount = reader.ReadInt32(); if (childCount != node.Children.Count) { throw new InvalidDataException( "ModelDependencyV1: invalid method identity child count " + childCount); } for (int j = 0; j < childCount; j++) { DependencyChild child = node.Children[j]; child.MethodStateName = ReadString(reader); child.MethodStateNamespace = ReadString(reader); child.MethodDeclarationName = ReadString(reader); child.MethodDeclarationNamespace = ReadString(reader); child.MethodDeclarationNumericId = reader.ReadUInt32(); child.MethodDeclarationStringId = ReadNullableString(reader); } } return true; } private static void WriteString(BinaryWriter writer, string value) { writer.Write(value ?? string.Empty); } private static void ValidateDefaultValueXml(string value) { try { using var textReader = new StringReader(value); using XmlReader reader = XmlReader.Create( textReader, CoreUtils.DefaultXmlReaderSettings()); var document = new XmlDocument(); document.Load(reader); if (document.DocumentElement == null) { throw new XmlException("Default value XML has no document element."); } } catch (XmlException ex) { throw new InvalidDataException( "ModelDependencyV1: invalid default value XML.", ex); } } 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(); } } }