/* ======================================================================== * 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.Collections.Generic; using System.Linq; using System.Xml; namespace Opc.Ua.Client.ComplexTypes { /// /// Extensions to convert binary schema type definitions to DataTypeDefinitions. /// public static class DataTypeDefinitionExtension { /// /// Convert a binary schema type definition to a /// StructureDefinition. /// /// /// Support for: /// - Structures, structures with optional fields and unions. /// - Nested types and typed arrays with length field. /// The converter has the following known restrictions: /// - Support only for V1.03 structured types which can be mapped to the V1.04 /// structured type definition. /// The following dictionary tags cause bail out for a structure: /// - use of a terminator of length in bytes /// - an array length field is not a direct predecessor of the array /// - The switch value of a union is not the first field. /// - The selector bits of optional fields are not stored in a 32 bit variable /// and do not add up to 32 bits. /// /// public static StructureDefinition ToStructureDefinition( this Schema.Binary.StructuredType structuredType, ExpandedNodeId defaultEncodingId, Dictionary typeDictionary, NamespaceTable namespaceTable, NodeId dataTypeNodeId) { var structureDefinition = new StructureDefinition { BaseDataType = default, DefaultEncodingId = ExpandedNodeId.ToNodeId(defaultEncodingId, namespaceTable), Fields = [], StructureType = StructureType.Structure }; bool isSupportedType = true; bool hasBitField = false; bool isUnionType = false; foreach (Schema.Binary.FieldType field in structuredType.Field) { // check for yet unsupported properties if (field.IsLengthInBytes || field.Terminator != null) { isSupportedType = false; } if (field.SwitchValue != 0) { isUnionType = true; } if (field.TypeName.Namespace is Namespaces.OpcBinarySchema or Namespaces.OpcUa && field.TypeName.Name == "Bit") { hasBitField = true; continue; } if (field.Length != 0) { isSupportedType = false; } } // test forbidden combinations if (!isSupportedType) { throw new DataTypeNotSupportedException( "The structure definition uses a Terminator or LengthInBytes, which are not supported."); } if (isUnionType && hasBitField) { throw new DataTypeNotSupportedException( "The structure definition combines a Union and a bit field, both of which are not supported in a single structure."); } if (isUnionType) { structureDefinition.StructureType = StructureType.Union; } if (hasBitField) { structureDefinition.StructureType = StructureType.StructureWithOptionalFields; } byte switchFieldBitPosition = 0; int dataTypeFieldPosition = 0; var switchFieldBits = new Dictionary(); // convert fields foreach (Schema.Binary.FieldType field in structuredType.Field) { // consume optional bits if (field.TypeName.IsXmlBitType()) { int count = structureDefinition.Fields.Count; if (count == 0 && switchFieldBitPosition < 32) { structureDefinition.StructureType = StructureType.StructureWithOptionalFields; byte fieldLength = (byte)(field.Length == 0 ? 1u : field.Length); switchFieldBits[field.Name] = switchFieldBitPosition; switchFieldBitPosition += fieldLength; } else { throw new DataTypeNotSupportedException( "Options for bit selectors must be 32 bit in size, use the Int32 datatype and must be the first element in the structure."); } continue; } if (switchFieldBitPosition is not 0 and not 32) { throw new DataTypeNotSupportedException( "Bitwise option selectors must have 32 bits."); } NodeId fieldDataTypeNodeId; if (field.TypeName == structuredType.QName) { // recursive type fieldDataTypeNodeId = dataTypeNodeId; } else { fieldDataTypeNodeId = field.TypeName.ToNodeId(typeDictionary); } var dataTypeField = new StructureField { Name = field.Name, Description = null, DataType = fieldDataTypeNodeId, IsOptional = false, MaxStringLength = 0, ArrayDimensions = null, ValueRank = -1 }; if (field.LengthField != null) { // handle array length StructureField lastField = structureDefinition.Fields[^1]; if (lastField.Name != field.LengthField) { throw new DataTypeNotSupportedException( "The length field must precede the type field of an array."); } lastField.Name = field.Name; lastField.DataType = fieldDataTypeNodeId; lastField.ValueRank = 1; } else { if (isUnionType) { // ignore the switchfield if (field.SwitchField == null) { if (structureDefinition.Fields.Count != 0) { throw new DataTypeNotSupportedException( "The switch field of a union must be the first field in the complex type."); } continue; } if (structureDefinition.Fields.Count != dataTypeFieldPosition) { throw new DataTypeNotSupportedException( "The count of the switch field of the union member is not matching the field position."); } dataTypeFieldPosition++; } else if (field.SwitchField != null) { dataTypeField.IsOptional = true; if (!switchFieldBits.TryGetValue(field.SwitchField, out byte value)) { throw new DataTypeNotSupportedException( $"The switch field for {field.SwitchField} does not exist."); } } structureDefinition.Fields.Add(dataTypeField); } } return structureDefinition; } /// /// Convert a binary schema enumerated type to an enum data type definition /// Available before OPC UA V1.04. /// public static EnumDefinition ToEnumDefinition( this Schema.Binary.EnumeratedType enumeratedType, string enumTypeName) { var enumDefinition = new EnumDefinition(); if (enumeratedType.EnumeratedValue != null) { foreach (Schema.Binary.EnumeratedValue enumValue in enumeratedType.EnumeratedValue) { string fieldName = enumValue.Name; if (string.IsNullOrEmpty(fieldName)) { if (string.IsNullOrEmpty(enumTypeName)) { // Here we give up because the overall type is broken return null; } fieldName = $"{enumTypeName}_{enumValue.Value}"; } var enumTypeField = new EnumField { Name = fieldName, Value = enumValue.Value, Description = enumValue.Documentation?.Text?.FirstOrDefault(), DisplayName = enumValue.Name }; enumDefinition.Fields.Add(enumTypeField); } } return enumDefinition; } /// /// Convert a list of EnumValues to an enum data type definition /// Available before OPC UA V1.04. /// public static EnumDefinition ToEnumDefinition( this ExtensionObject[] enumValueTypes, string enumTypeName) { var enumDefinition = new EnumDefinition(); foreach (ExtensionObject extensionObject in enumValueTypes) { if (extensionObject.Body is not EnumValueType enumValue) { // All we can do here is skip this value. Since there is no // fallback it is better to include all other type fields if // they are in the extension object array. continue; } string name = enumValue.DisplayName.Text; if (string.IsNullOrEmpty(name)) { if (string.IsNullOrEmpty(enumTypeName)) { // Here we give up because the overall type is broken return null; } name = $"{enumTypeName}_{enumValue.Value}"; } var enumTypeField = new EnumField { Name = name, Value = enumValue.Value, DisplayName = name }; enumDefinition.Fields.Add(enumTypeField); } return enumDefinition; } /// /// Convert a list of EnumValues to an enum data type definition /// Available before OPC UA V1.04. /// public static EnumDefinition ToEnumDefinition( this LocalizedText[] enumFieldNames, string enumTypeName) { var enumDefinition = new EnumDefinition(); for (int ii = 0; ii < enumFieldNames.Length; ii++) { LocalizedText enumFieldName = enumFieldNames[ii]; string name = enumFieldName.Text; if (string.IsNullOrEmpty(name)) { if (string.IsNullOrEmpty(enumTypeName)) { // Here we give up because the overall type is broken return null; } name = $"{enumTypeName}_{ii}"; } var enumTypeField = new EnumField { Name = name, Value = ii, DisplayName = name }; enumDefinition.Fields.Add(enumTypeField); } return enumDefinition; } /// /// Test for special Bit type used in the binary schema structure definition. /// private static bool IsXmlBitType(this XmlQualifiedName typeName) { return typeName.Namespace is Namespaces.OpcBinarySchema or Namespaces.OpcUa && typeName.Name == "Bit"; } /// /// Look up the node id for a qualified name of a type /// in a binary schema type definition. /// private static NodeId ToNodeId( this XmlQualifiedName typeName, Dictionary typeCollection) { if (typeName.Namespace is Namespaces.OpcBinarySchema or Namespaces.OpcUa) { switch (typeName.Name) { case "CharArray": return DataTypeIds.String; case "Variant": return DataTypeIds.BaseDataType; case "ExtensionObject": return DataTypeIds.Structure; default: System.Reflection.FieldInfo internalField = typeof(DataTypeIds).GetField( typeName.Name); if (internalField == null) { // The type was not found in the internal type factory. return NodeId.Null; } return (NodeId)internalField.GetValue(typeName.Name); } } if (!typeCollection.TryGetValue(typeName, out NodeId referenceId)) { // The type was not found in the namespace return NodeId.Null; } return referenceId; } } }