import { type Byte, coerceInt64, coerceUInt64, type Int16, type Int32, type Int64, type SByte, type UInt16, type UInt32, type UInt64 } from "node-opcua-basic-types"; import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug"; import { coerceNodeId, ExpandedNodeId, type INodeId, type NodeId, NodeIdType } from "node-opcua-nodeid"; import { coerceStatusCode, StatusCodes } from "node-opcua-status-code"; import { EnumDefinition, StructureDefinition } from "node-opcua-types"; import { lowerFirstLetter } from "node-opcua-utils"; import { DataType, Variant, type VariantOptions } from "node-opcua-variant"; import { type IReaderState, ReaderState, type ReaderStateParser, type ReaderStateParserLike, type Xml2Json, type XmlAttributes } from "node-opcua-xml2json"; import { localizedText_parser } from "./parsers/localized_text_parser.js"; import { makeQualifiedNameParser } from "./parsers/qualified_name_parser.js"; import { makeVariantReader, withoutXmlFragments } from "./parsers/variant_parser.js"; const warningLog = make_warningLog("make_xml_extension_object_parser"); const debugLog = make_debugLog("make_xml_extension_object_parser"); const doDebug = checkDebugFlag("make_xml_extension_object_parser"); // textual form of an ExpandedNodeId, as found in ...: // an optional server index, an optional namespace uri, then a plain nodeId. see OPC UA part 6. const regexServerIndex = /^svr=([0-9]+);(.*)$/; const regexNamespaceUri = /^nsu=(.*?);(.*)$/; function parseExpandedNodeId(text: string): ExpandedNodeId { let remaining = text.trim(); let serverIndex = 0; let namespaceUri: string | null = null; const serverIndexMatch = remaining.match(regexServerIndex); if (serverIndexMatch) { serverIndex = parseInt(serverIndexMatch[1], 10); remaining = serverIndexMatch[2]; } const namespaceUriMatch = remaining.match(regexNamespaceUri); if (namespaceUriMatch) { namespaceUri = namespaceUriMatch[1]; remaining = namespaceUriMatch[2]; } const nodeId = coerceNodeId(remaining); return new ExpandedNodeId(nodeId.identifierType, nodeId.value, nodeId.namespace, namespaceUri, serverIndex); } function clamp(value: number, minValue: number, maxValue: number) { if (value < minValue) { warningLog(`invalid value range : ${value} < ${minValue} but should be [${minValue} , ${maxValue}]`); return minValue; } if (value > maxValue) { warningLog(`invalid value range : ${value} > ${maxValue} but should be [${minValue} , ${maxValue}]`); return maxValue; } return value; } interface Parser extends ReaderStateParserLike { value: T | null; parent: Parser; text: string; } // generic loose parser-state shape used by the Xml2Json engine: `this` here is a // dynamically-populated reader state whose fields (value/parent/text/...) vary by parser, // see the `this: any` justification in ReaderStateParserLike itself (node-opcua-xml2json). interface AnyParserState extends ReaderStateParserLike { value: unknown; parent: AnyParserState; text: string; name?: string; obj?: unknown; parser: Record; } // 2153644032 const statusCode_parser: ReaderStateParserLike = { init(this: AnyParserState) { this.value = StatusCodes.Good; }, parser: { Code: { finish(this: AnyParserState) { this.parent.value = coerceStatusCode(parseInt(this.text, 10)); } } } }; // a DiagnosticInfo field is stored on the enclosing DiagnosticInfo, which is only // created when at least one field is present ( stays empty ). function _diagnosticInfoField(name: string, convert: (text: string) => unknown): ReaderStateParserLike { return { finish(this: AnyParserState) { this.parent.value = this.parent.value || {}; (this.parent.value as Record)[name] = convert(this.text); } }; } // 1...... const diagnosticInfo_parser: ReaderStateParserLike = { init(this: AnyParserState) { this.value = undefined; }, parser: { SymbolicId: _diagnosticInfoField("symbolicId", (text) => parseInt(text, 10)), NamespaceUri: _diagnosticInfoField("namespaceUri", (text) => parseInt(text, 10)), Locale: _diagnosticInfoField("locale", (text) => parseInt(text, 10)), LocalizedText: _diagnosticInfoField("localizedText", (text) => parseInt(text, 10)), AdditionalInfo: _diagnosticInfoField("additionalInfo", (text) => text), InnerStatusCode: { ...statusCode_parser, finish(this: AnyParserState) { this.parent.value = this.parent.value || {}; (this.parent.value as Record).innerStatusCode = this.value; } } } }; // InnerDiagnosticInfo is a DiagnosticInfo in turn: the parser is registered after the // fact so that it can refer to the reader being built. if (!diagnosticInfo_parser.parser) { throw new Error("internal error: diagnosticInfo_parser.parser must be defined"); } diagnosticInfo_parser.parser.InnerDiagnosticInfo = { ...diagnosticInfo_parser, finish(this: AnyParserState) { this.parent.value = this.parent.value || {}; (this.parent.value as Record).innerDiagnosticInfo = this.value; } }; const partials = { LocalizedText: localizedText_parser.LocalizedText, QualifiedName: makeQualifiedNameParser((nodeId: string) => coerceNodeId(nodeId)).QualifiedName, String: >{ finish(this: Parser) { this.value = this.text; } }, Guid: { parser: { String: >{ finish(this: Parser) { this.parent.value = this.text; } } } }, Boolean: >{ finish(this: Parser) { this.value = this.text.toLowerCase() === "true"; } }, ByteString: >{ init(this: Parser, _name: string, _attrs: XmlAttributes, _parent: IReaderState, _engine: Xml2Json) { this.value = null; }, finish(this: Parser) { const base64text = this.text; const byteString = Buffer.from(base64text, "base64"); this.value = byteString; } }, Float: >{ finish(this: Parser) { this.value = parseFloat(this.text); } }, Double: >{ finish(this: Parser) { this.value = parseFloat(this.text); } }, Byte: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), 0, 255); } }, SByte: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), -128, 127); } }, Int8: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), -128, 127); } }, Int16: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), -32768, 32767); } }, Int32: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), -2147483648, 2147483647); } }, Int64: >{ finish(this: Parser) { this.value = coerceInt64(parseInt(this.text, 10)); } }, UInt8: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), 0, 255); } }, UInt16: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), 0, 65535); } }, UInt32: >{ finish(this: Parser) { this.value = clamp(parseInt(this.text, 10), 0, 4294967295); } }, UInt64: >{ finish(this: Parser) { this.value = coerceUInt64(parseInt(this.text, 10)); } }, DateTime: >{ finish(this: Parser) { // to do check Local or GMT this.value = new Date(this.text); } }, Variant: { finish(this: AnyParserState) { /** to do */ warningLog(" Missing Implemntation contact sterfive.com!"); } }, NodeId: >{ finish(this: Parser) { // to do check Local or GMT this.value = coerceNodeId(this.text); } }, StatusCode: statusCode_parser, DiagnosticInfo: diagnosticInfo_parser, // svr=1;nsu=http://acme.com/UA/;i=42 ExpandedNodeId: >{ finish(this: Parser) { this.value = parseExpandedNodeId(this.text); } } }; export interface TypeInfo1 { name: string; definition: StructureDefinition; } export interface TypeInfo2 { name: string; definition: EnumDefinition; } export interface TypeInfo3 { name: string; definition: { dataType: DataType }; } export type TypeInfo = TypeInfo1 | TypeInfo2 | TypeInfo3; export interface DefinitionMap2 { findDefinition(dataTypeNodeId: NodeId): TypeInfo; } function _clone(a: unknown): unknown { if (typeof a === "string" || typeof a === "number" || typeof a === "boolean") { return a; } if (Buffer.isBuffer(a)) { return Buffer.from(a); } if (a instanceof Date) { return new Date(a); } if (Array.isArray(a)) { return a.map((x) => _clone(x)); } return { ...(a as Record) }; } function _makeTypeReader( dataTypeNodeId1: NodeId, definitionMap: DefinitionMap2, readerMap: Map, translateNodeId: (nodeId: string) => NodeId ): { name: string; reader: ReaderStateParserLike } { const n = dataTypeNodeId1 as INodeId; if (n.identifierType === NodeIdType.NUMERIC && n.namespace === 0 && n.value === 0) { // a generic Extension Object return { name: "Variant", reader: partials.Variant }; } if (n.namespace === 0 && n.identifierType === NodeIdType.NUMERIC && n.value < DataType.ExtensionObject) { const name = DataType[n.value as number] as string; const reader = partials[name as keyof typeof partials] as ReaderStateParserLike; return { name, reader }; } const { name, definition } = definitionMap.findDefinition(n); const dataTypeName = name; let reader: ReaderStateParserLike | undefined = readerMap.get(dataTypeName); if (reader) { return { name, reader }; } reader = { init(this: AnyParserState) { // the same reader instance is used for every element of that data type: the value must be // reset here, or the element being read would inherit the fields of the previously read one // ( or of the enclosing one, when the structure refers to itself ). this.value = undefined; }, finish(this: AnyParserState) { /** empty */ }, parser: { /** empty */ } }; // `reader.parser` is guaranteed defined: it was just set as a literal above, and the // ReaderStateParserLike interface only declares it optional for other unrelated call sites. const readerParser = reader.parser; if (!readerParser) { throw new Error("internal error: reader.parser must be defined"); } if (definition instanceof StructureDefinition) { // the reader must be registered *before* its fields are explored: a structure may refer to // itself, directly or through one of its fields, and the recursion below would never end. // `reader` and `reader.parser` keep their identity while being filled, so a nested reference // that picks the reader up from the map ends up pointing at the completed reader. readerMap.set(dataTypeName, reader); for (const field of definition.fields || []) { const typeReader = _makeTypeReader(field.dataType, definitionMap, readerMap, translateNodeId); const fieldParser = typeReader.reader; const fieldTypename = typeReader.name; // c8 ignore next if (!fieldParser) { throw new Error(` Cannot find reader for dataType ${field.dataType} fieldTypename=${fieldTypename}`); } if (field.valueRank === undefined || field.valueRank === -1) { // scalar const parser = fieldParser; if (!parser) { throw new Error(`??? ${field.dataType} ${field.name}`); } readerParser[field.name || ""] = { parser: fieldParser.parser, // the field reader borrows the partial's sub-parsers: it must borrow its init // too, or the state those sub-parsers write into is never set up. init(this: AnyParserState, elementName: string, attrs: XmlAttributes, parent: IReaderState, engine: Xml2Json) { if (fieldParser.init) { fieldParser.init.call(this, elementName, attrs, parent, engine); } }, // endElement: fieldReader.endElement, finish(this: AnyParserState) { const elName = lowerFirstLetter(field.name || ""); if (fieldParser.finish) { fieldParser.finish.call(this); } else { // c8 ignore next doDebug && debugLog(`xxx check ${fieldTypename}`); } this.parent.value = this.parent.value || Object.create(null); (this.parent.value as Record)[elName] = _clone(this.value); } }; } else if (field.valueRank === 1) { const listReader: ReaderStateParserLike = { init(this: AnyParserState) { this.value = []; }, parser: { /** empty */ }, finish(this: AnyParserState) { const elName = lowerFirstLetter(this.name || ""); this.parent.value = this.parent.value || Object.create(null); (this.parent.value as Record)[elName] = this.value; this.value = undefined; }, startElement(_name: string, _attrs: XmlAttributes) { // empty }, endElement(this: AnyParserState, element: string) { (this.value as unknown[]).push(_clone(this.parser[element].value)); } }; const listReaderParser = listReader.parser; if (!listReaderParser) { throw new Error("internal error: listReader.parser must be defined"); } listReaderParser[fieldTypename] = fieldParser; readerParser[field.name || ""] = listReader; } else { throw new Error("Unsupported ValueRank !"); } } return { name, reader }; } else if (definition instanceof EnumDefinition) { const turnToInt = (value: string) => { // Green_100 return parseInt(value.split("_")[1], 10); }; return { name, reader: { finish(this: AnyParserState) { this.value = turnToInt(this.text); } } }; } else if (definition?.dataType === DataType.Variant) { // Foo let variantOptions: VariantOptions = Object.create(null); // an extension object the reader cannot decode inside a Variant field is dropped, as before const variantReader = makeVariantReader( (_self, data: VariantOptions) => (variantOptions = withoutXmlFragments(data)), translateNodeId ); return { name, reader: { init(this: AnyParserState, _name: string, _attrs: XmlAttributes, _parent: IReaderState, _engine: Xml2Json) { this.obj = {}; }, ...variantReader, finish(this: AnyParserState) { this.value = new Variant(variantOptions); } } }; } else { // basic datatype const typeName: string = DataType[definition.dataType]; const reader = partials[typeName as keyof typeof partials] as ReaderStateParserLike; // c8 ignore next if (!reader) { throw new Error(`missing parse for ${typeName}`); } return { name, reader }; } } export function makeXmlExtensionObjectReader( dataTypeNodeId: NodeId, definitionMap: DefinitionMap2, readerMap: Map, translateNodeId: (nodeId: string) => NodeId ): ReaderState { const { name, definition } = definitionMap.findDefinition(dataTypeNodeId); // c8 ignore next if (!(definition instanceof StructureDefinition)) { throw new Error("Expecting StructureDefinition"); } // interface Reader1State extends IReaderState { _pojo?: unknown; parser: Record; } const reader1 = { parser: {} as Record, endElement(this: Reader1State) { this._pojo = this.parser[name].value; } }; const { reader } = _makeTypeReader(dataTypeNodeId, definitionMap, readerMap, translateNodeId); const reader1Parser = reader1.parser; if (!reader1Parser) { throw new Error("internal error: reader1.parser must be defined"); } reader1Parser[name] = reader as unknown as AnyParserState; return new ReaderState(reader1 as unknown as ReaderStateParser); }