node-opcua-address-space 2.181.0 → 2.182.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/api/helpers/ensure_secure_access.ts +53 -2
  2. package/api/index.ts +30 -1
  3. package/api/loader/generateAddressSpaceRaw.ts +44 -139
  4. package/api/loader/load_nodeset2.ts +5 -0
  5. package/api/loader/narrow_instance_datatypes.ts +264 -0
  6. package/api/loader/nodeset_builtin_formats.ts +143 -0
  7. package/api/loader/nodeset_format.ts +153 -0
  8. package/api/loader/nodeset_image.ts +37 -4
  9. package/api/loader/nodeset_image_codec.ts +153 -34
  10. package/api/loader/nodeset_record.ts +26 -2
  11. package/api/loader/nodeset_record_applier.ts +54 -3
  12. package/api/loader/nodeset_records_to_xml.ts +620 -0
  13. package/api/loader/nodeset_source.ts +185 -26
  14. package/api/loader/nodeset_to_image.ts +16 -5
  15. package/api/loader/nodeset_xml_header.ts +119 -0
  16. package/api/loader/nodeset_xml_primitives.ts +209 -0
  17. package/api/loader/nodeset_xml_producer.ts +61 -2
  18. package/api/loader/parsers/variant_parser.ts +9 -0
  19. package/bin/nodeset_equivalence.js +750 -0
  20. package/bin/nodeset_tool_common.js +72 -0
  21. package/bin/opcua-nodeset-image.js +93 -103
  22. package/bin/opcua-nodeset-live.js +139 -0
  23. package/dist/api/helpers/ensure_secure_access.d.ts +20 -0
  24. package/dist/api/helpers/ensure_secure_access.js +39 -0
  25. package/dist/api/helpers/ensure_secure_access.js.map +1 -1
  26. package/dist/api/index.d.ts +7 -2
  27. package/dist/api/index.js +21 -1
  28. package/dist/api/index.js.map +1 -1
  29. package/dist/api/loader/generateAddressSpaceRaw.d.ts +1 -7
  30. package/dist/api/loader/generateAddressSpaceRaw.js +32 -125
  31. package/dist/api/loader/generateAddressSpaceRaw.js.map +1 -1
  32. package/dist/api/loader/load_nodeset2.js +4 -0
  33. package/dist/api/loader/load_nodeset2.js.map +1 -1
  34. package/dist/api/loader/narrow_instance_datatypes.d.ts +30 -0
  35. package/dist/api/loader/narrow_instance_datatypes.js +217 -0
  36. package/dist/api/loader/narrow_instance_datatypes.js.map +1 -0
  37. package/dist/api/loader/nodeset_builtin_formats.d.ts +27 -0
  38. package/dist/api/loader/nodeset_builtin_formats.js +133 -0
  39. package/dist/api/loader/nodeset_builtin_formats.js.map +1 -0
  40. package/dist/api/loader/nodeset_format.d.ts +123 -0
  41. package/dist/api/loader/nodeset_format.js +63 -0
  42. package/dist/api/loader/nodeset_format.js.map +1 -0
  43. package/dist/api/loader/nodeset_image.d.ts +17 -1
  44. package/dist/api/loader/nodeset_image.js +32 -4
  45. package/dist/api/loader/nodeset_image.js.map +1 -1
  46. package/dist/api/loader/nodeset_image_codec.d.ts +4 -0
  47. package/dist/api/loader/nodeset_image_codec.js +142 -53
  48. package/dist/api/loader/nodeset_image_codec.js.map +1 -1
  49. package/dist/api/loader/nodeset_record.d.ts +26 -2
  50. package/dist/api/loader/nodeset_record.js +11 -2
  51. package/dist/api/loader/nodeset_record.js.map +1 -1
  52. package/dist/api/loader/nodeset_record_applier.js +52 -3
  53. package/dist/api/loader/nodeset_record_applier.js.map +1 -1
  54. package/dist/api/loader/nodeset_records_to_xml.d.ts +16 -0
  55. package/dist/api/loader/nodeset_records_to_xml.js +536 -0
  56. package/dist/api/loader/nodeset_records_to_xml.js.map +1 -0
  57. package/dist/api/loader/nodeset_source.d.ts +68 -7
  58. package/dist/api/loader/nodeset_source.js +157 -20
  59. package/dist/api/loader/nodeset_source.js.map +1 -1
  60. package/dist/api/loader/nodeset_to_image.d.ts +4 -4
  61. package/dist/api/loader/nodeset_to_image.js +14 -4
  62. package/dist/api/loader/nodeset_to_image.js.map +1 -1
  63. package/dist/api/loader/nodeset_xml_header.d.ts +22 -0
  64. package/dist/api/loader/nodeset_xml_header.js +113 -0
  65. package/dist/api/loader/nodeset_xml_header.js.map +1 -0
  66. package/dist/api/loader/nodeset_xml_primitives.d.ts +37 -0
  67. package/dist/api/loader/nodeset_xml_primitives.js +196 -0
  68. package/dist/api/loader/nodeset_xml_primitives.js.map +1 -0
  69. package/dist/api/loader/nodeset_xml_producer.js +56 -1
  70. package/dist/api/loader/nodeset_xml_producer.js.map +1 -1
  71. package/dist/api/loader/parsers/variant_parser.js +7 -0
  72. package/dist/api/loader/parsers/variant_parser.js.map +1 -1
  73. package/dist/impl/_instantiate_helpers.js +1 -0
  74. package/dist/impl/_instantiate_helpers.js.map +1 -1
  75. package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js +4 -0
  76. package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js.map +1 -1
  77. package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js +14 -0
  78. package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js.map +1 -1
  79. package/dist/impl/base_node_impl.d.ts +9 -0
  80. package/dist/impl/base_node_impl.js +11 -0
  81. package/dist/impl/base_node_impl.js.map +1 -1
  82. package/dist/impl/base_node_private.js +67 -97
  83. package/dist/impl/base_node_private.js.map +1 -1
  84. package/dist/impl/nodeset_tools/construct_namespace_dependency.js +5 -1
  85. package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
  86. package/dist/impl/nodeset_tools/nodeset_to_records.js +61 -15
  87. package/dist/impl/nodeset_tools/nodeset_to_records.js.map +1 -1
  88. package/dist/impl/nodeset_tools/nodeset_to_xml.d.ts +1 -8
  89. package/dist/impl/nodeset_tools/nodeset_to_xml.js +174 -190
  90. package/dist/impl/nodeset_tools/nodeset_to_xml.js.map +1 -1
  91. package/dist/impl/ua_data_type_impl.d.ts +21 -0
  92. package/dist/impl/ua_data_type_impl.js +27 -2
  93. package/dist/impl/ua_data_type_impl.js.map +1 -1
  94. package/dist/impl/ua_object_type_impl.d.ts +9 -0
  95. package/dist/impl/ua_object_type_impl.js +12 -1
  96. package/dist/impl/ua_object_type_impl.js.map +1 -1
  97. package/dist/impl/ua_variable_impl.js +27 -10
  98. package/dist/impl/ua_variable_impl.js.map +1 -1
  99. package/dist/impl/ua_variable_impl_ext_obj.js +19 -2
  100. package/dist/impl/ua_variable_impl_ext_obj.js.map +1 -1
  101. package/dist/impl/ua_variable_type_impl.d.ts +8 -1
  102. package/dist/impl/ua_variable_type_impl.js +39 -2
  103. package/dist/impl/ua_variable_type_impl.js.map +1 -1
  104. package/dist/tsconfig_base.tsbuildinfo +1 -1
  105. package/distNodeJS/generate_address_space.js +4 -0
  106. package/distNodeJS/generate_address_space.js.map +1 -1
  107. package/impl/_instantiate_helpers.ts +3 -0
  108. package/impl/alarms_and_conditions/ua_acknowledgeable_condition_impl.ts +4 -4
  109. package/impl/alarms_and_conditions/ua_alarm_condition_impl.ts +19 -9
  110. package/impl/alarms_and_conditions/ua_base_event_impl.ts +9 -9
  111. package/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.ts +17 -0
  112. package/impl/alarms_and_conditions/ua_condition_impl.ts +18 -18
  113. package/impl/alarms_and_conditions/ua_exclusive_deviation_alarm_impl.ts +2 -2
  114. package/impl/alarms_and_conditions/ua_exclusive_limit_alarm_impl.ts +1 -1
  115. package/impl/alarms_and_conditions/ua_limit_alarm_impl.ts +16 -16
  116. package/impl/alarms_and_conditions/ua_non_exclusive_deviation_alarm_impl.ts +2 -2
  117. package/impl/alarms_and_conditions/ua_non_exclusive_limit_alarm_impl.ts +4 -4
  118. package/impl/alarms_and_conditions/ua_off_normal_alarm_impl.ts +1 -1
  119. package/impl/base_node_impl.ts +15 -0
  120. package/impl/base_node_private.ts +97 -146
  121. package/impl/data_access/ua_multistate_discrete_impl.ts +1 -1
  122. package/impl/data_access/ua_multistate_value_discrete_impl.ts +2 -2
  123. package/impl/data_access/ua_two_state_discrete_impl.ts +2 -2
  124. package/impl/nodeset_tools/construct_namespace_dependency.ts +5 -1
  125. package/impl/nodeset_tools/nodeset_to_records.ts +66 -16
  126. package/impl/nodeset_tools/nodeset_to_xml.ts +127 -160
  127. package/impl/state_machine/finite_state_machine.ts +3 -3
  128. package/impl/state_machine/ua_shelving_state_machine_ex.ts +20 -20
  129. package/impl/ua_data_type_impl.ts +42 -4
  130. package/impl/ua_method_impl.ts +1 -1
  131. package/impl/ua_object_type_impl.ts +16 -3
  132. package/impl/ua_variable_impl.ts +27 -10
  133. package/impl/ua_variable_impl_ext_obj.ts +20 -2
  134. package/impl/ua_variable_type_impl.ts +49 -3
  135. package/package.json +29 -27
  136. package/source_nodejs/generate_address_space.ts +7 -1
@@ -1,6 +1,13 @@
1
1
  import type { BaseNode, IChannelBase, UAMethod, UAObject, UAVariable } from "node-opcua-address-space-base";
2
- import { allPermissions, BrowseDirection, makeAccessRestrictionsFlag, makePermissionFlag, NodeClass } from "node-opcua-data-model";
3
- import { MessageSecurityMode } from "node-opcua-types";
2
+ import {
3
+ allPermissions,
4
+ BrowseDirection,
5
+ makeAccessRestrictionsFlag,
6
+ makePermissionFlag,
7
+ NodeClass,
8
+ PermissionFlag
9
+ } from "node-opcua-data-model";
10
+ import { MessageSecurityMode, type RolePermissionType } from "node-opcua-types";
4
11
  import { WellKnownRoles } from "../session_context.js";
5
12
 
6
13
  function _isChannelSecure(channel: IChannelBase): boolean {
@@ -76,3 +83,47 @@ export function ensureObjectIsSecure(node: BaseNode, options: EnsureObjectIsSecu
76
83
  ensureObjectIsSecure(child, options);
77
84
  }
78
85
  }
86
+
87
+ /** the two Roles every Session's permissions are evaluated against, granted Browse and nothing else */
88
+ const browseForEverySession = [
89
+ { roleId: WellKnownRoles.Anonymous, permissions: makePermissionFlag("Browse") },
90
+ { roleId: WellKnownRoles.AuthenticatedUser, permissions: makePermissionFlag("Browse") }
91
+ ];
92
+
93
+ function grantsBrowseToEverySession(rolePermissions: RolePermissionType[]): boolean {
94
+ return rolePermissions.some(
95
+ ({ roleId, permissions }) =>
96
+ (permissions & PermissionFlag.Browse) !== 0 &&
97
+ roleId.namespace === 0 &&
98
+ (roleId.value === WellKnownRoles.Anonymous || roleId.value === WellKnownRoles.AuthenticatedUser)
99
+ );
100
+ }
101
+
102
+ /**
103
+ * keep the structure of `node` and its aggregates browsable by every session, leaving every
104
+ * other permission as declared.
105
+ *
106
+ * A node whose RolePermissions grant Browse to neither Anonymous nor AuthenticatedUser is
107
+ * hidden from any session that holds none of the roles it names. Where those roles are ones
108
+ * the server never assigns, nobody sees the node, and its parent - browsable by everyone -
109
+ * shows an instance missing the mandatory children of its type. `Opc.Ua.NodeSet2.xml` does
110
+ * this to eleven nodes under Server/PublishSubscribe (the methods of SecurityGroups and
111
+ * KeyPushTargets, with their arguments), reserved to the SecurityKeyServerAdmin role
112
+ * (CTT Base Info Core Structure 002, FEAT-30).
113
+ *
114
+ * Such a node gains Browse for Anonymous and AuthenticatedUser, which every session's
115
+ * permissions are evaluated against; Read, Write and Call stay with the roles it named. A
116
+ * node without RolePermissions, or that already grants Browse to one of the two, is left as
117
+ * it is.
118
+ *
119
+ * @param node the node whose structure, and whose aggregates' structure, must stay visible
120
+ */
121
+ export function ensureStructureIsBrowsable(node: BaseNode): void {
122
+ const rolePermissions = node.rolePermissions;
123
+ if (rolePermissions && !grantsBrowseToEverySession(rolePermissions)) {
124
+ node.setRolePermissions([...rolePermissions, ...browseForEverySession]);
125
+ }
126
+ for (const child of node.findReferencesExAsObject("Aggregates", BrowseDirection.Forward)) {
127
+ ensureStructureIsBrowsable(child);
128
+ }
129
+ }
package/api/index.ts CHANGED
@@ -156,7 +156,26 @@ export * from "./interfaces/state_machine/ua_transition_ex.js";
156
156
  export * from "./interfaces/ua_subscription_diagnostics_variable_ex.js";
157
157
  export { ensureDatatypeExtracted, ensureDatatypeExtractedWithCallback } from "./loader/ensure_datatype_extracted.js";
158
158
  export * from "./loader/generateAddressSpaceRaw.js";
159
+ export { type NarrowedDataType, narrowInstanceDataTypes } from "./loader/narrow_instance_datatypes.js";
159
160
  export {
161
+ isNdjsonHeaderLine,
162
+ NDJSON_IMAGE_FORMAT,
163
+ NODESET2_XML_FORMAT,
164
+ registerBuiltinNodesetFormats
165
+ } from "./loader/nodeset_builtin_formats.js";
166
+ export {
167
+ findNodesetFormat,
168
+ type NodesetDocument,
169
+ type NodesetFormat,
170
+ type NodesetHead,
171
+ type NodesetModel,
172
+ type NodesetModelInfo,
173
+ nodesetFormatByName,
174
+ nodesetFormats,
175
+ registerNodesetFormat
176
+ } from "./loader/nodeset_format.js";
177
+ export {
178
+ type ImageDeflater,
160
179
  imageLinesToRecords,
161
180
  imageNodesetRecords,
162
181
  inflatedImageLines,
@@ -169,6 +188,7 @@ export {
169
188
  nodesetImageProblem,
170
189
  type ReadNodesetImageOptions,
171
190
  readNodesetImageInfo,
191
+ setImageDeflater,
172
192
  setImageInflater
173
193
  } from "./loader/nodeset_image.js";
174
194
  export { decodeValue, encodeValue, type JsonNodeId, type JsonQualifiedName, type JsonValue } from "./loader/nodeset_image_codec.js";
@@ -193,10 +213,19 @@ export {
193
213
  recordBytes,
194
214
  XmlExtensionObjectFragment
195
215
  } from "./loader/nodeset_record.js";
196
- export type { NamedNodesetSource, NodesetChunk, NodesetChunkStream, NodesetSource } from "./loader/nodeset_source.js";
216
+ export { type RecordsToNodeset2XMLOptions, recordsToNodeset2XML } from "./loader/nodeset_records_to_xml.js";
217
+ export type {
218
+ NamedNodesetSource,
219
+ NodesetChunk,
220
+ NodesetChunkStream,
221
+ NodesetContainer,
222
+ NodesetSource,
223
+ NodesetSourceKind
224
+ } from "./loader/nodeset_source.js";
197
225
  export { sha256Hex } from "./loader/nodeset_source.js";
198
226
  export * from "./loader/nodeset_source_helpers.js";
199
227
  export { type NodesetToImageOptions, nodesetToImage } from "./loader/nodeset_to_image.js";
228
+ export { headerComplete, parseDependencies, sliceHeader } from "./loader/nodeset_xml_header.js";
200
229
  export { makeXmlNodesetRecordReader, type XmlNodesetRecordReader, xmlNodesetRecords } from "./loader/nodeset_xml_producer.js";
201
230
  export * from "./loader/register_node_promoter.js";
202
231
  export * from "./namespace.js";
@@ -1,14 +1,20 @@
1
1
  import type { IAddressSpace, RequiredModel } from "node-opcua-address-space-base";
2
- import { getMinOPCUADate } from "node-opcua-date-time";
3
2
  import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
4
3
  import type { CallbackT } from "node-opcua-status-code";
5
- import { type ReaderStateParser, type ReaderStateParserLike, Xml2Json, type XmlAttributes } from "node-opcua-xml2json";
6
4
  import semver from "semver";
7
5
  import type { NamespacePrivate } from "../../impl/namespace_private.js";
8
6
  import { adjustNamespaceArray } from "../../impl/nodeset_tools/adjust_namespace_array.js";
9
7
  import type { NodeSetLoaderOptions } from "../interfaces/nodeset_loader_options.js";
10
8
  import { NodeSetLoader } from "./load_nodeset2.js";
11
9
  import { makeSemverCompatible } from "./make_semver_compatible.js";
10
+ import { NDJSON_IMAGE_FORMAT, registerBuiltinNodesetFormats } from "./nodeset_builtin_formats.js";
11
+ import {
12
+ type NodesetModel as Model,
13
+ type NodesetDocument,
14
+ type NodesetFormat,
15
+ type NodesetModelInfo as NodesetInfo,
16
+ nodesetFormatByName
17
+ } from "./nodeset_format.js";
12
18
  import {
13
19
  imageLinesToRecords,
14
20
  inflatedImageLines,
@@ -21,92 +27,17 @@ import {
21
27
  import { decodeHeader } from "./nodeset_image_codec.js";
22
28
  import { type NodesetImageStore, nodesetImageKey, sharedMemoryNodesetImageStore } from "./nodeset_image_store.js";
23
29
  import type { NodesetRecord, NodesetRecordConsumer } from "./nodeset_record.js";
24
- import { type NodesetReader, type NodesetSource, openNodesetSource } from "./nodeset_source.js";
25
- import { xmlNodesetRecords } from "./nodeset_xml_producer.js";
30
+ import { type NodesetReader, type NodesetSource, nodesetDocumentOf, openNodesetSource } from "./nodeset_source.js";
31
+ import { parseDependencies, sliceHeader } from "./nodeset_xml_header.js";
32
+
33
+ // the loader reads NodeSet2 XML and its own NDJSON images out of the box: registering them is
34
+ // what makes that true, and it has to happen before any source is sniffed
35
+ registerBuiltinNodesetFormats();
26
36
 
27
37
  const doDebug = checkDebugFlag("generateAddressSpaceRaw");
28
38
  const debugLog = make_debugLog("generateAddressSpaceRaw");
29
39
  const errorLog = make_errorLog("generateAddressSpaceRaw");
30
40
 
31
- interface Model extends RequiredModel {
32
- requiredModel: RequiredModel[];
33
- }
34
- interface NodesetInfo {
35
- namespaceUris: string[];
36
- models: Model[];
37
- }
38
-
39
- async function parseDependencies(xmlData: string): Promise<NodesetInfo> {
40
- const namespaceUris: string[] = [];
41
-
42
- const models: Model[] = [];
43
- let currentModel: Model | undefined;
44
- const state0: ReaderStateParser = {
45
- parser: {
46
- UANodeSet: {
47
- parser: {
48
- NamespaceUris: {
49
- parser: {
50
- Uri: <ReaderStateParserLike & { text: string }>{
51
- finish(this: ReaderStateParserLike & { text: string }) {
52
- namespaceUris.push(this.text);
53
- }
54
- }
55
- }
56
- },
57
- Models: {
58
- parser: {
59
- Model: {
60
- init(_elementName: string, attrs: XmlAttributes) {
61
- const modelUri = attrs.ModelUri;
62
- const version = attrs.Version;
63
- const publicationDate = new Date(Date.parse(attrs.PublicationDate));
64
- currentModel = {
65
- modelUri,
66
- version,
67
- publicationDate,
68
- requiredModel: []
69
- };
70
- doDebug && console.log(`currentModel = ${JSON.stringify(currentModel)}`);
71
- models.push(currentModel);
72
- },
73
- parser: {
74
- RequiredModel: {
75
- init(_elementName: string, attrs: XmlAttributes) {
76
- const modelUri = attrs.ModelUri;
77
- const version = attrs.Version;
78
- const publicationDate = new Date(Date.parse(attrs.PublicationDate));
79
-
80
- if (!currentModel) {
81
- throw new Error("Internal Error");
82
- }
83
- currentModel.requiredModel.push({
84
- modelUri,
85
- version,
86
- publicationDate
87
- });
88
- }
89
- }
90
- }
91
- }
92
- }
93
- }
94
- }
95
- }
96
- }
97
- };
98
- const parser = new Xml2Json(state0);
99
- parser.parseString(xmlData);
100
- if (models.length === 0 && namespaceUris.length >= 1) {
101
- models.push({
102
- modelUri: namespaceUris[0],
103
- version: "1",
104
- publicationDate: getMinOPCUADate(),
105
- requiredModel: []
106
- });
107
- }
108
- return { models, namespaceUris: namespaceUris };
109
- }
110
41
  interface NodesetDesc {
111
42
  index: number;
112
43
  xmlData: string;
@@ -116,30 +47,10 @@ interface NodesetSourceDesc {
116
47
  index: number;
117
48
  reader: NodesetReader;
118
49
  namespaceModel: NodesetInfo;
119
- /** the bytes of a source that holds an image, read whole */
120
- image?: Uint8Array;
121
- }
122
-
123
- /**
124
- * the header of a NodeSet2 file (`NamespaceUris`, `Models`) precedes the aliases and the nodes: the
125
- * dependency pre-pass has read enough once both closers were seen, or once the body has begun
126
- */
127
- const BODY_START =
128
- /<(Aliases|Extensions|UAObject|UAVariable|UADataType|UAReferenceType|UAObjectType|UAVariableType|UAMethod|UAView)[\s/>]/;
129
- function headerComplete(text: string): boolean {
130
- return (text.includes("</Models>") && text.includes("</NamespaceUris>")) || BODY_START.test(text);
131
- }
132
-
133
- /** the `<UANodeSet ...>` opener up to the end of `</Models>` or `</NamespaceUris>`, whichever comes last */
134
- function sliceHeader(xmlData: string, name: string): string {
135
- const indexStart = xmlData.match(/<UANodeSet/m)?.index;
136
- const i1 = (xmlData.match(/<\/Models>/m)?.index || 0) + "</Models>".length;
137
- const i2 = (xmlData.match(/<\/NamespaceUris>/m)?.index || 0) + "</NamespaceUris>".length;
138
- const indexEnd = Math.max(i1, i2);
139
- if (indexStart === undefined) {
140
- throw new Error(`nodeset source ${name}: no <UANodeSet> element found`);
141
- }
142
- return xmlData.substring(indexStart, indexEnd);
50
+ /** the format that claimed this source */
51
+ format: NodesetFormat;
52
+ /** the source seen as a document, so the read and the inflate are shared with the pre-pass */
53
+ document: NodesetDocument;
143
54
  }
144
55
 
145
56
  /**
@@ -150,30 +61,15 @@ async function preLoadSources(readers: NodesetReader[]): Promise<NodesetSourceDe
150
61
  for (let index = 0; index < readers.length; index++) {
151
62
  const reader = readers[index];
152
63
  doDebug && console.log("---------------------------------------------", reader.name);
153
- if ((await reader.probe()) === "image") {
154
- // an image is small (a few hundred KB) and its header is line 1: read it whole
155
- const image = await reader.allBytes();
156
- const { header } = await readNodesetImageInfo(image);
157
- const models: Model[] = header.models.map((m) => ({
158
- modelUri: m.modelUri,
159
- version: m.version,
160
- publicationDate: m.publicationDate ? new Date(m.publicationDate) : getMinOPCUADate(),
161
- requiredModel: m.requiredModels.map((r) => ({ ...r, publicationDate: new Date(r.publicationDate ?? Number.NaN) }))
162
- }));
163
- if (models.length === 0 && header.namespaceUris.length >= 1) {
164
- models.push({
165
- modelUri: header.namespaceUris[0],
166
- version: "1",
167
- publicationDate: getMinOPCUADate(),
168
- requiredModel: []
169
- });
170
- }
171
- namespaceDesc.push({ reader, namespaceModel: { models, namespaceUris: header.namespaceUris }, index, image });
172
- continue;
64
+ const format = nodesetFormatByName(await reader.probe());
65
+ // probe() only returns a name it found in the registry, so this cannot be missing; the
66
+ // check is here because a caller may unregister a format between the probe and the load
67
+ if (!format) {
68
+ throw new Error(`nodeset source ${reader.name}: its format is no longer registered`);
173
69
  }
174
- const head = await reader.readHead(headerComplete);
175
- const namespaceModel = await parseDependencies(sliceHeader(head, reader.name));
176
- namespaceDesc.push({ reader, namespaceModel, index });
70
+ const document = nodesetDocumentOf(reader);
71
+ const namespaceModel = await format.readModels(document);
72
+ namespaceDesc.push({ reader, document, format, namespaceModel, index });
177
73
  }
178
74
  return namespaceDesc;
179
75
  }
@@ -217,16 +113,23 @@ async function isReplayable(image: Uint8Array, digest: string, name: string): Pr
217
113
  }
218
114
 
219
115
  /**
220
- * load one XML source: from its image when the store holds a valid one, from the XML otherwise,
221
- * writing the image on the way when there is a store
116
+ * load one source through the image cache: from its image when the store holds a valid one, by
117
+ * parsing it otherwise, writing the image on the way when there is a store.
118
+ *
119
+ * This used to be the XML path, because XML was the only thing worth caching a parse of. It is
120
+ * now every format except the image itself: the cache is keyed on the digest of the bytes that
121
+ * were read, and nothing about that reasoning was ever specific to XML. A published nodeset in
122
+ * any format therefore gets a cache sibling on first read and loads at image speed after it.
222
123
  */
223
- async function loadXmlSource(
124
+ async function loadThroughCache(
224
125
  nodesetLoader: NodeSetLoader,
126
+ format: NodesetFormat,
127
+ document: NodesetDocument,
225
128
  reader: NodesetReader,
226
129
  store: NodesetImageStore | undefined
227
130
  ): Promise<void> {
228
131
  if (!store) {
229
- await nodesetLoader.addNodeSetStream(reader.chunks());
132
+ await nodesetLoader.addRecords(format.records(document));
230
133
  return;
231
134
  }
232
135
  // the digest is known up front for a document given whole; a stream must be read first
@@ -247,7 +150,7 @@ async function loadXmlSource(
247
150
  }
248
151
  }
249
152
  const writer = new NodesetImageWriter();
250
- await nodesetLoader.addRecords(tee(xmlNodesetRecords(reader.chunks()), writer));
153
+ await nodesetLoader.addRecords(tee(format.records(document), writer));
251
154
  const sourceDigest = digest ?? (await reader.digest());
252
155
  try {
253
156
  await store.put(nodesetImageKey(sourceDigest), await writer.finish(sourceDigest, reader.length));
@@ -444,14 +347,16 @@ export async function generateAddressSpaceRaw(
444
347
  // c8 ignore next
445
348
  doDebug && debugLog(" loading ", nodesetIndex, nodeset.reader.name);
446
349
  try {
447
- if (nodeset.image) {
350
+ if (nodeset.format.name === NDJSON_IMAGE_FORMAT) {
351
+ // an image is already the cache: replaying it through the cache would be storing
352
+ // a copy of what was just read
448
353
  try {
449
- await nodesetLoader.addRecords(imageLinesToRecords(await inflatedImageLines(nodeset.image)));
354
+ await nodesetLoader.addRecords(nodeset.format.records(nodeset.document));
450
355
  } finally {
451
- releaseInflatedImageLines(nodeset.image);
356
+ releaseInflatedImageLines(await nodeset.document.rawBytes());
452
357
  }
453
358
  } else {
454
- await loadXmlSource(nodesetLoader, nodeset.reader, store);
359
+ await loadThroughCache(nodesetLoader, nodeset.format, nodeset.document, nodeset.reader, store);
455
360
  }
456
361
  } catch (err) {
457
362
  const cause = err instanceof Error ? err.message : String(err);
@@ -15,6 +15,7 @@ import type { NamespacePrivate } from "../../impl/namespace_private.js";
15
15
  import type { NodeSetLoaderOptions } from "../interfaces/nodeset_loader_options.js";
16
16
  import { ensureDatatypeExtracted } from "./ensure_datatype_extracted.js";
17
17
  import { promoteObjectsAndVariables } from "./namespace_post_step.js";
18
+ import { narrowInstanceDataTypes } from "./narrow_instance_datatypes.js";
18
19
  import { type NodesetRecord, type NodesetRecordProducer, type NodesetRecordWithBytes, recordBytes } from "./nodeset_record.js";
19
20
  import type { PendingBackReferences } from "./nodeset_record_applier.js";
20
21
  import { type LoaderTaskQueues, makeLoaderTaskQueues, NodesetRecordApplier, type Task } from "./nodeset_record_applier.js";
@@ -257,6 +258,10 @@ export class NodeSetLoader {
257
258
  queues.postTasks2_AssignedExtensionObjectToDataValue
258
259
  );
259
260
 
261
+ doDebug && debugLog(chalk.bgGreenBright("Narrowing instance DataTypes to their declaration ---------------------"));
262
+ // after the values are in: a value that would not fit the declared type keeps the variable as it is
263
+ narrowInstanceDataTypes(addressSpace1);
264
+
260
265
  doDebug && debugLog(chalk.bgGreenBright("Performing post variable initialization ---------------------"));
261
266
  // awaited: a promoter that throws must reject generateAddressSpace, not surface later
262
267
  // as an unhandled rejection once the caller believes the address space is complete
@@ -0,0 +1,264 @@
1
+ /**
2
+ * @module node-opcua-address-space
3
+ */
4
+ import type {
5
+ BaseNode,
6
+ IAddressSpace,
7
+ UADataType,
8
+ UAObject,
9
+ UAObjectType,
10
+ UAReferenceType,
11
+ UAVariable,
12
+ UAVariableType
13
+ } from "node-opcua-address-space-base";
14
+ import { BrowseDirection, NodeClass, type QualifiedName } from "node-opcua-data-model";
15
+ import { checkDebugFlag, make_debugLog } from "node-opcua-debug";
16
+ import { type NodeId, sameNodeId } from "node-opcua-nodeid";
17
+ import { DataType } from "node-opcua-variant";
18
+ import type { NamespacePrivate } from "../../impl/namespace_private.js";
19
+
20
+ const doDebug = checkDebugFlag("narrow_instance_datatypes");
21
+ const debugLog = make_debugLog("narrow_instance_datatypes");
22
+
23
+ type UAType = UAObjectType | UAVariableType;
24
+ type UAInstance = UAObject | UAVariable;
25
+
26
+ function isType(node: BaseNode): node is UAType {
27
+ return node.nodeClass === NodeClass.ObjectType || node.nodeClass === NodeClass.VariableType;
28
+ }
29
+ function isInstance(node: BaseNode): node is UAInstance {
30
+ return node.nodeClass === NodeClass.Object || node.nodeClass === NodeClass.Variable;
31
+ }
32
+
33
+ /**
34
+ * the type definition of an instance, resolved without the ObjectType/VariableType guards of the
35
+ * typed getters: a companion nodeset with a wrong type definition must not abort the sweep
36
+ */
37
+ function typeDefinitionOf(addressSpace: IAddressSpace, node: UAInstance): UAType | null {
38
+ const typeDefinition: NodeId | undefined = node.typeDefinition;
39
+ if (!typeDefinition || typeDefinition.isEmpty()) {
40
+ return null;
41
+ }
42
+ const typeNode = addressSpace.findNode(typeDefinition);
43
+ return typeNode && isType(typeNode) ? typeNode : null;
44
+ }
45
+
46
+ /**
47
+ * the members a type declares, by browse name, looked up along its supertype chain and its
48
+ * interfaces the way `instantiate` collects them (see impl/_instantiate_helpers.ts); the answer
49
+ * is cached per (type, browse name) so a sweep over thousands of instances of the same type
50
+ * walks the chain once per member
51
+ */
52
+ class DeclarationIndex {
53
+ private readonly cache = new Map<string, BaseNode | null>();
54
+ /** absent from a standard nodeset older than 1.04: then no type has interfaces to look into */
55
+ private readonly hasInterface: UAReferenceType | null;
56
+ private readonly hasModellingRule: UAReferenceType;
57
+
58
+ constructor(
59
+ private readonly addressSpace: IAddressSpace,
60
+ hasModellingRule: UAReferenceType
61
+ ) {
62
+ this.hasInterface = addressSpace.findReferenceType("HasInterface") ?? null;
63
+ this.hasModellingRule = hasModellingRule;
64
+ }
65
+
66
+ /**
67
+ * whether `declared` is a member declaration a variable instance must honour: a variable with
68
+ * a modelling rule (Mandatory, Optional, ...). A type child without one is not instantiated by
69
+ * Part 3 and is left alone.
70
+ */
71
+ public isVariableDeclaration(declared: BaseNode): declared is UAVariable {
72
+ return (
73
+ declared.nodeClass === NodeClass.Variable &&
74
+ declared.findReferencesEx(this.hasModellingRule, BrowseDirection.Forward).length > 0
75
+ );
76
+ }
77
+
78
+ public find(type: UAType, browseName: QualifiedName): BaseNode | null {
79
+ const key = `${type.nodeId.toString()}|${browseName.toString()}`;
80
+ const cached = this.cache.get(key);
81
+ if (cached !== undefined) {
82
+ return cached;
83
+ }
84
+ const found = this.walk(type, browseName, new Set<string>());
85
+ this.cache.set(key, found);
86
+ return found;
87
+ }
88
+
89
+ private walk(type: UAType, browseName: QualifiedName, visited: Set<string>): BaseNode | null {
90
+ const typeKey = type.nodeId.toString();
91
+ if (visited.has(typeKey)) {
92
+ return null;
93
+ }
94
+ visited.add(typeKey);
95
+ const child = type.getChildByName(browseName);
96
+ if (child) {
97
+ return child;
98
+ }
99
+ const interfaces = this.hasInterface ? type.findReferencesEx(this.hasInterface, BrowseDirection.Forward) : [];
100
+ for (const reference of interfaces) {
101
+ const interfaceType = reference.node ?? this.addressSpace.findNode(reference.nodeId);
102
+ if (interfaceType && isType(interfaceType)) {
103
+ const declared = this.walk(interfaceType, browseName, visited);
104
+ if (declared) {
105
+ return declared;
106
+ }
107
+ }
108
+ }
109
+ const superType = type.subtypeOfObj as UAType | null;
110
+ return superType ? this.walk(superType, browseName, visited) : null;
111
+ }
112
+ }
113
+
114
+ // a parent chain deeper than this is a cycle in a broken nodeset, not a model
115
+ const MAX_DEPTH = 64;
116
+
117
+ /**
118
+ * the node of a type definition that declares this instance member, or null when no type in
119
+ * the instance's ancestry declares one under its browse name.
120
+ *
121
+ * The declaration is looked up in the type definition of the containing instance first; a
122
+ * member nested below a declared member (an object declared by the type with children of its
123
+ * own, whose own type definition says nothing about them) is found through the declaration of
124
+ * its container instead.
125
+ */
126
+ function declarationOf(addressSpace: IAddressSpace, index: DeclarationIndex, node: UAInstance, depth: number): BaseNode | null {
127
+ if (depth > MAX_DEPTH) {
128
+ return null;
129
+ }
130
+ const parent = node.parent;
131
+ // a node under a type is a declaration itself, never an instance to narrow
132
+ if (!parent || isType(parent)) {
133
+ return null;
134
+ }
135
+ const parentIsInstance = isInstance(parent);
136
+ if (parentIsInstance) {
137
+ const parentType = typeDefinitionOf(addressSpace, parent);
138
+ if (parentType) {
139
+ const declared = index.find(parentType, node.browseName);
140
+ if (declared) {
141
+ return declared;
142
+ }
143
+ }
144
+ }
145
+ // the container may itself be a declared member (an instance, or a method) whose declaration
146
+ // carries the member; a method has no type definition and is resolved the same way
147
+ if (parentIsInstance || parent.nodeClass === NodeClass.Method) {
148
+ const parentDeclaration = declarationOf(addressSpace, index, parent as UAInstance, depth + 1);
149
+ if (parentDeclaration) {
150
+ return parentDeclaration.getChildByName(node.browseName);
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function findDataTypeNode(addressSpace: IAddressSpace, nodeId: NodeId): UADataType | null {
157
+ if (!nodeId || nodeId.isEmpty()) {
158
+ return null;
159
+ }
160
+ return addressSpace.findDataType(nodeId) ?? null;
161
+ }
162
+
163
+ /** where a variable keeps the built-in type it derived from its DataType (see impl/get_basic_datatype.ts) */
164
+ interface VariableWithBasicDataTypeCache {
165
+ _basicDataType?: DataType | null;
166
+ }
167
+
168
+ export interface NarrowedDataType {
169
+ node: UAVariable;
170
+ from: NodeId;
171
+ to: NodeId;
172
+ }
173
+
174
+ /**
175
+ * make every instance variable's DataType at least as narrow as its declaration.
176
+ *
177
+ * OPC 10000-3 lets an instance use the DataType its type definition declares for the member, or
178
+ * a subtype of it; a supertype is not conformant. Companion nodesets that predate a narrowing
179
+ * of the standard types carry the supertype anyway (every NamespaceMetadata object written
180
+ * against UA 1.04 declares `ModelVersion` as String, which UA 1.05 narrowed to
181
+ * SemanticVersionString), so every server loading them fails the type-conformance check of
182
+ * the CTT (Base Info Core Structure) and no server can fix it per nodeset. The declared type is
183
+ * what the model means: an instance whose DataType is a strict supertype of the declared one is
184
+ * moved to the declared one.
185
+ *
186
+ * Nothing else moves: an instance whose DataType is the declared one, a subtype of it, or
187
+ * unrelated is left alone, values are never touched, and the children of types are declarations,
188
+ * not instances. A variable whose current value would not fit the declared built-in type is
189
+ * left alone as well, and reported: narrowing it would make the value lie about its type.
190
+ *
191
+ * @returns the variables that were narrowed
192
+ */
193
+ export function narrowInstanceDataTypes(addressSpace: IAddressSpace): NarrowedDataType[] {
194
+ const narrowed: NarrowedDataType[] = [];
195
+ // a nodeset without these cannot declare a member, let alone the DataType of one: a
196
+ // minimal fixture, or a standard nodeset still to come. Nothing to narrow, and the lookups
197
+ // below would throw on the first missing reference type.
198
+ const hasModellingRule = addressSpace.findReferenceType("HasModellingRule");
199
+ if (
200
+ !hasModellingRule ||
201
+ !addressSpace.findReferenceType("HasSubtype") ||
202
+ !addressSpace.findReferenceType("HasChild") ||
203
+ !addressSpace.findReferenceType("HierarchicalReferences")
204
+ ) {
205
+ return narrowed;
206
+ }
207
+ const index = new DeclarationIndex(addressSpace, hasModellingRule);
208
+
209
+ for (const namespace of addressSpace.getNamespaceArray()) {
210
+ for (const node of (namespace as NamespacePrivate).nodeIterator()) {
211
+ if (node.nodeClass !== NodeClass.Variable) {
212
+ continue;
213
+ }
214
+ const variable = node as UAVariable;
215
+ const declared = declarationOf(addressSpace, index, variable, 0);
216
+ if (!declared || !index.isVariableDeclaration(declared)) {
217
+ continue;
218
+ }
219
+ if (sameNodeId(variable.dataType, declared.dataType)) {
220
+ continue;
221
+ }
222
+ const instanceDataType = findDataTypeNode(addressSpace, variable.dataType);
223
+ const declaredDataType = findDataTypeNode(addressSpace, declared.dataType);
224
+ if (!instanceDataType || !declaredDataType) {
225
+ continue;
226
+ }
227
+ // a subtype of the declaration, or an unrelated type: not this sweep's business
228
+ if (!declaredDataType.isSubtypeOf(instanceDataType)) {
229
+ continue;
230
+ }
231
+ const value = variable.readValue().value;
232
+ const declaredBuiltInType = declaredDataType.basicDataType;
233
+ const valueFits =
234
+ value.dataType === DataType.Null ||
235
+ declaredBuiltInType === DataType.Variant ||
236
+ value.dataType === declaredBuiltInType;
237
+ if (!valueFits) {
238
+ debugLog(
239
+ `not narrowing ${variable.nodeId.toString()} ${variable.browseName.toString()}: ` +
240
+ `its value is a ${DataType[value.dataType]}, the declared ${declaredDataType.browseName.toString()} ` +
241
+ `is a ${DataType[declaredBuiltInType]}`
242
+ );
243
+ continue;
244
+ }
245
+ const from = variable.dataType;
246
+ variable.dataType = declaredDataType.nodeId;
247
+ (variable as unknown as VariableWithBasicDataTypeCache)._basicDataType = null;
248
+ narrowed.push({ node: variable, from, to: declaredDataType.nodeId });
249
+ // c8 ignore next
250
+ if (doDebug) {
251
+ debugLog(
252
+ `narrowed the DataType of ${variable.nodeId.toString()} ${variable.browseName.toString()} ` +
253
+ `from ${instanceDataType.browseName.toString()} (${from.toString()}) ` +
254
+ `to the declared ${declaredDataType.browseName.toString()} (${declaredDataType.nodeId.toString()})`
255
+ );
256
+ }
257
+ }
258
+ }
259
+ // c8 ignore next
260
+ if (doDebug && narrowed.length > 0) {
261
+ debugLog(`narrowed the DataType of ${narrowed.length} instance variable(s) to the one their type declares`);
262
+ }
263
+ return narrowed;
264
+ }