node-opcua-address-space 2.181.1 → 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.
- package/api/helpers/ensure_secure_access.ts +53 -2
- package/api/index.ts +30 -1
- package/api/loader/generateAddressSpaceRaw.ts +44 -139
- package/api/loader/load_nodeset2.ts +5 -0
- package/api/loader/narrow_instance_datatypes.ts +264 -0
- package/api/loader/nodeset_builtin_formats.ts +143 -0
- package/api/loader/nodeset_format.ts +153 -0
- package/api/loader/nodeset_image.ts +37 -4
- package/api/loader/nodeset_image_codec.ts +153 -34
- package/api/loader/nodeset_record.ts +26 -2
- package/api/loader/nodeset_record_applier.ts +54 -3
- package/api/loader/nodeset_records_to_xml.ts +620 -0
- package/api/loader/nodeset_source.ts +185 -26
- package/api/loader/nodeset_to_image.ts +16 -5
- package/api/loader/nodeset_xml_header.ts +119 -0
- package/api/loader/nodeset_xml_primitives.ts +209 -0
- package/api/loader/nodeset_xml_producer.ts +61 -2
- package/api/loader/parsers/variant_parser.ts +9 -0
- package/bin/nodeset_equivalence.js +750 -0
- package/bin/nodeset_tool_common.js +72 -0
- package/bin/opcua-nodeset-image.js +93 -103
- package/bin/opcua-nodeset-live.js +139 -0
- package/dist/api/helpers/ensure_secure_access.d.ts +20 -0
- package/dist/api/helpers/ensure_secure_access.js +39 -0
- package/dist/api/helpers/ensure_secure_access.js.map +1 -1
- package/dist/api/index.d.ts +7 -2
- package/dist/api/index.js +21 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/loader/generateAddressSpaceRaw.d.ts +1 -7
- package/dist/api/loader/generateAddressSpaceRaw.js +32 -125
- package/dist/api/loader/generateAddressSpaceRaw.js.map +1 -1
- package/dist/api/loader/load_nodeset2.js +4 -0
- package/dist/api/loader/load_nodeset2.js.map +1 -1
- package/dist/api/loader/narrow_instance_datatypes.d.ts +30 -0
- package/dist/api/loader/narrow_instance_datatypes.js +217 -0
- package/dist/api/loader/narrow_instance_datatypes.js.map +1 -0
- package/dist/api/loader/nodeset_builtin_formats.d.ts +27 -0
- package/dist/api/loader/nodeset_builtin_formats.js +133 -0
- package/dist/api/loader/nodeset_builtin_formats.js.map +1 -0
- package/dist/api/loader/nodeset_format.d.ts +123 -0
- package/dist/api/loader/nodeset_format.js +63 -0
- package/dist/api/loader/nodeset_format.js.map +1 -0
- package/dist/api/loader/nodeset_image.d.ts +17 -1
- package/dist/api/loader/nodeset_image.js +32 -4
- package/dist/api/loader/nodeset_image.js.map +1 -1
- package/dist/api/loader/nodeset_image_codec.d.ts +4 -0
- package/dist/api/loader/nodeset_image_codec.js +142 -53
- package/dist/api/loader/nodeset_image_codec.js.map +1 -1
- package/dist/api/loader/nodeset_record.d.ts +26 -2
- package/dist/api/loader/nodeset_record.js +11 -2
- package/dist/api/loader/nodeset_record.js.map +1 -1
- package/dist/api/loader/nodeset_record_applier.js +52 -3
- package/dist/api/loader/nodeset_record_applier.js.map +1 -1
- package/dist/api/loader/nodeset_records_to_xml.d.ts +16 -0
- package/dist/api/loader/nodeset_records_to_xml.js +536 -0
- package/dist/api/loader/nodeset_records_to_xml.js.map +1 -0
- package/dist/api/loader/nodeset_source.d.ts +68 -7
- package/dist/api/loader/nodeset_source.js +157 -20
- package/dist/api/loader/nodeset_source.js.map +1 -1
- package/dist/api/loader/nodeset_to_image.d.ts +4 -4
- package/dist/api/loader/nodeset_to_image.js +14 -4
- package/dist/api/loader/nodeset_to_image.js.map +1 -1
- package/dist/api/loader/nodeset_xml_header.d.ts +22 -0
- package/dist/api/loader/nodeset_xml_header.js +113 -0
- package/dist/api/loader/nodeset_xml_header.js.map +1 -0
- package/dist/api/loader/nodeset_xml_primitives.d.ts +37 -0
- package/dist/api/loader/nodeset_xml_primitives.js +196 -0
- package/dist/api/loader/nodeset_xml_primitives.js.map +1 -0
- package/dist/api/loader/nodeset_xml_producer.js +56 -1
- package/dist/api/loader/nodeset_xml_producer.js.map +1 -1
- package/dist/api/loader/parsers/variant_parser.js +7 -0
- package/dist/api/loader/parsers/variant_parser.js.map +1 -1
- package/dist/impl/_instantiate_helpers.js +1 -0
- package/dist/impl/_instantiate_helpers.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js +4 -0
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js +14 -0
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js.map +1 -1
- package/dist/impl/base_node_impl.d.ts +9 -0
- package/dist/impl/base_node_impl.js +11 -0
- package/dist/impl/base_node_impl.js.map +1 -1
- package/dist/impl/base_node_private.js +67 -97
- package/dist/impl/base_node_private.js.map +1 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js +5 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_records.js +61 -15
- package/dist/impl/nodeset_tools/nodeset_to_records.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_xml.d.ts +1 -8
- package/dist/impl/nodeset_tools/nodeset_to_xml.js +174 -190
- package/dist/impl/nodeset_tools/nodeset_to_xml.js.map +1 -1
- package/dist/impl/ua_data_type_impl.d.ts +21 -0
- package/dist/impl/ua_data_type_impl.js +27 -2
- package/dist/impl/ua_data_type_impl.js.map +1 -1
- package/dist/impl/ua_object_type_impl.d.ts +9 -0
- package/dist/impl/ua_object_type_impl.js +12 -1
- package/dist/impl/ua_object_type_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl.js +27 -10
- package/dist/impl/ua_variable_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl_ext_obj.js +19 -2
- package/dist/impl/ua_variable_impl_ext_obj.js.map +1 -1
- package/dist/impl/ua_variable_type_impl.d.ts +8 -1
- package/dist/impl/ua_variable_type_impl.js +39 -2
- package/dist/impl/ua_variable_type_impl.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/distNodeJS/generate_address_space.js +4 -0
- package/distNodeJS/generate_address_space.js.map +1 -1
- package/impl/_instantiate_helpers.ts +3 -0
- package/impl/alarms_and_conditions/ua_alarm_condition_impl.ts +10 -0
- package/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.ts +17 -0
- package/impl/base_node_impl.ts +15 -0
- package/impl/base_node_private.ts +97 -146
- package/impl/nodeset_tools/construct_namespace_dependency.ts +5 -1
- package/impl/nodeset_tools/nodeset_to_records.ts +66 -16
- package/impl/nodeset_tools/nodeset_to_xml.ts +127 -160
- package/impl/ua_data_type_impl.ts +40 -2
- package/impl/ua_object_type_impl.ts +16 -3
- package/impl/ua_variable_impl.ts +27 -10
- package/impl/ua_variable_impl_ext_obj.ts +20 -2
- package/impl/ua_variable_type_impl.ts +48 -2
- package/package.json +29 -27
- package/source_nodejs/generate_address_space.ts +7 -1
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* first bytes.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { findNodesetFormat, type NodesetDocument, type NodesetHead } from "./nodeset_format.js";
|
|
11
|
+
import { inflatedImageLines, isGzip, isNodesetImage } from "./nodeset_image.js";
|
|
11
12
|
|
|
12
13
|
/** one piece of a NodeSet2 document: text, or UTF-8 bytes */
|
|
13
14
|
export type NodesetChunk = string | Uint8Array;
|
|
@@ -57,8 +58,26 @@ export interface NamedNodesetSource {
|
|
|
57
58
|
|
|
58
59
|
const BOM = 0xfeff;
|
|
59
60
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
61
|
+
/** how many characters of a document a format sniffer is shown */
|
|
62
|
+
const HEAD_CHARS = 4096;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* how a document arrived, before anything has been decided about what it says: as text, or as
|
|
66
|
+
* bytes that are not text until something inflates or decodes them.
|
|
67
|
+
*
|
|
68
|
+
* This is not the format. It is the one thing that can be settled synchronously on a first
|
|
69
|
+
* chunk, and it decides only whether the reader decodes as it goes or keeps the bytes intact.
|
|
70
|
+
*/
|
|
71
|
+
export type NodesetContainer = "text" | "bytes";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* the name of the format a source turned out to be written in.
|
|
75
|
+
*
|
|
76
|
+
* This used to be a closed pair, XML or image. It is now whatever is registered, so that a
|
|
77
|
+
* format the loader has never heard of can identify itself; the two built-ins go on reading the
|
|
78
|
+
* documents they always did.
|
|
79
|
+
*/
|
|
80
|
+
export type NodesetSourceKind = string;
|
|
62
81
|
|
|
63
82
|
export interface NodesetReaderOptions {
|
|
64
83
|
/** keep the bytes read so that {@link NodesetReader.digest} can be computed */
|
|
@@ -82,7 +101,9 @@ export class NodesetReader {
|
|
|
82
101
|
private bodyStarted = false;
|
|
83
102
|
private decoder: TextDecoder | null = null;
|
|
84
103
|
private atStart = true;
|
|
85
|
-
private
|
|
104
|
+
private container: NodesetContainer | undefined;
|
|
105
|
+
private formatName: NodesetSourceKind | undefined;
|
|
106
|
+
private allBytesValue: Uint8Array | undefined;
|
|
86
107
|
private readonly hashed: Uint8Array[] | null;
|
|
87
108
|
private bytesRead = 0;
|
|
88
109
|
private readonly encoder = new TextEncoder();
|
|
@@ -103,12 +124,66 @@ export class NodesetReader {
|
|
|
103
124
|
return this.options.imageKey;
|
|
104
125
|
}
|
|
105
126
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
/**
|
|
128
|
+
* how this document arrived: as text, or as bytes. Settled on the first chunk, and never a
|
|
129
|
+
* statement about which format the document is written in.
|
|
130
|
+
*/
|
|
131
|
+
public async probeContainer(): Promise<NodesetContainer> {
|
|
132
|
+
if (this.container === undefined) {
|
|
109
133
|
await this.pull();
|
|
110
134
|
}
|
|
111
|
-
return this.
|
|
135
|
+
return this.container ?? "text";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* which registered format claims this document.
|
|
140
|
+
*
|
|
141
|
+
* Two stages, because one is not enough. The container is decided synchronously on the first
|
|
142
|
+
* chunk; the format is decided from the head of the document *after* it has been inflated,
|
|
143
|
+
* which is the only way to tell one gzipped serialisation from another. The loader used to
|
|
144
|
+
* skip that second stage and take any gzip stream for one of its own images, so a foreign
|
|
145
|
+
* compressed nodeset was not rejected but misread.
|
|
146
|
+
*/
|
|
147
|
+
public async probe(): Promise<NodesetSourceKind> {
|
|
148
|
+
if (this.formatName !== undefined) {
|
|
149
|
+
return this.formatName;
|
|
150
|
+
}
|
|
151
|
+
const head = await this.sniffHead();
|
|
152
|
+
const format = findNodesetFormat(head);
|
|
153
|
+
if (!format) {
|
|
154
|
+
throw new Error(`nodeset source ${this.name}: no registered format recognises this document`);
|
|
155
|
+
}
|
|
156
|
+
this.formatName = format.name;
|
|
157
|
+
return this.formatName;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* the beginning of the document as a sniffer sees it: inflated when it arrived gzipped, so
|
|
162
|
+
* that no format has to know how a document happened to be stored.
|
|
163
|
+
*
|
|
164
|
+
* A byte document is read whole. That is not a concession: every format that arrives as bytes
|
|
165
|
+
* is line-delimited or an archive, and reads whole anyway, and the bytes are kept so the read
|
|
166
|
+
* is not repeated. Text streams on, and is sniffed on as much of the head as has arrived.
|
|
167
|
+
*/
|
|
168
|
+
private async sniffHead(): Promise<NodesetHead> {
|
|
169
|
+
if ((await this.probeContainer()) === "bytes") {
|
|
170
|
+
const all = await this.allBytes();
|
|
171
|
+
const gzip = isGzip(all);
|
|
172
|
+
const text = gzip
|
|
173
|
+
? ((await inflatedImageLines(all))[0] ?? "")
|
|
174
|
+
: new TextDecoder("utf-8").decode(all.subarray(0, HEAD_CHARS));
|
|
175
|
+
return makeHead(this.name, text, gzip);
|
|
176
|
+
}
|
|
177
|
+
while (this.headTextLength() < HEAD_CHARS) {
|
|
178
|
+
if ((await this.pull()) === undefined) break;
|
|
179
|
+
}
|
|
180
|
+
return makeHead(this.name, (this.head as string[]).join("").slice(0, HEAD_CHARS), false);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private headTextLength(): number {
|
|
184
|
+
let total = 0;
|
|
185
|
+
for (const chunk of this.head as string[]) total += chunk.length;
|
|
186
|
+
return total;
|
|
112
187
|
}
|
|
113
188
|
|
|
114
189
|
/**
|
|
@@ -119,8 +194,8 @@ export class NodesetReader {
|
|
|
119
194
|
if (this.bodyStarted) {
|
|
120
195
|
throw new Error(`nodeset source ${this.name}: the head cannot be read once the body has been`);
|
|
121
196
|
}
|
|
122
|
-
if ((await this.
|
|
123
|
-
throw new Error(`nodeset source ${this.name}: not
|
|
197
|
+
if ((await this.probeContainer()) !== "text") {
|
|
198
|
+
throw new Error(`nodeset source ${this.name}: the head of a byte document is not text`);
|
|
124
199
|
}
|
|
125
200
|
let text = (this.head as string[]).join("");
|
|
126
201
|
while (!complete(text)) {
|
|
@@ -133,13 +208,14 @@ export class NodesetReader {
|
|
|
133
208
|
return text;
|
|
134
209
|
}
|
|
135
210
|
|
|
136
|
-
/**
|
|
137
|
-
|
|
211
|
+
/**
|
|
212
|
+
* the whole document from the start, usable once: text chunks for a document that arrived as
|
|
213
|
+
* text, byte chunks for one that did not. A caller that wants text either way decodes.
|
|
214
|
+
*/
|
|
215
|
+
public async *chunks(): AsyncGenerator<NodesetChunk> {
|
|
138
216
|
this.startBody();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
const head = this.head as string[];
|
|
217
|
+
await this.probeContainer();
|
|
218
|
+
const head = this.head as NodesetChunk[];
|
|
143
219
|
this.head = [];
|
|
144
220
|
for (const chunk of head) {
|
|
145
221
|
yield chunk;
|
|
@@ -149,15 +225,15 @@ export class NodesetReader {
|
|
|
149
225
|
if (chunk === undefined) {
|
|
150
226
|
return;
|
|
151
227
|
}
|
|
152
|
-
yield chunk
|
|
228
|
+
yield chunk;
|
|
153
229
|
}
|
|
154
230
|
}
|
|
155
231
|
|
|
156
|
-
/** the whole document as raw bytes, from the start, for a source holding
|
|
232
|
+
/** the whole document as raw bytes, from the start, for a source holding bytes; usable once */
|
|
157
233
|
public async *bytes(): AsyncGenerator<Uint8Array> {
|
|
158
234
|
this.startBody();
|
|
159
|
-
if ((await this.
|
|
160
|
-
throw new Error(`nodeset source ${this.name}: not
|
|
235
|
+
if ((await this.probeContainer()) !== "bytes") {
|
|
236
|
+
throw new Error(`nodeset source ${this.name}: not a byte document`);
|
|
161
237
|
}
|
|
162
238
|
const head = this.head as Uint8Array[];
|
|
163
239
|
this.head = [];
|
|
@@ -175,6 +251,9 @@ export class NodesetReader {
|
|
|
175
251
|
|
|
176
252
|
/** the whole document as one byte array; reads the source to its end and keeps it */
|
|
177
253
|
public async allBytes(): Promise<Uint8Array> {
|
|
254
|
+
if (this.allBytesValue !== undefined) {
|
|
255
|
+
return this.allBytesValue;
|
|
256
|
+
}
|
|
178
257
|
if (this.bodyStarted) {
|
|
179
258
|
throw new Error(`nodeset source ${this.name}: a source is read once`);
|
|
180
259
|
}
|
|
@@ -182,7 +261,10 @@ export class NodesetReader {
|
|
|
182
261
|
/* read to the end */
|
|
183
262
|
}
|
|
184
263
|
const parts = this.head.map((chunk) => (typeof chunk === "string" ? this.encoder.encode(chunk) : chunk));
|
|
185
|
-
|
|
264
|
+
// kept, so that the sniff and the format that follows it share one array: the inflated
|
|
265
|
+
// lines are cached against the identity of these bytes, and a second concat would miss
|
|
266
|
+
this.allBytesValue = concat(parts);
|
|
267
|
+
return this.allBytesValue;
|
|
186
268
|
}
|
|
187
269
|
|
|
188
270
|
/** the bytes read so far; the whole document once it has been read */
|
|
@@ -214,6 +296,16 @@ export class NodesetReader {
|
|
|
214
296
|
return this.digestValue;
|
|
215
297
|
}
|
|
216
298
|
|
|
299
|
+
/**
|
|
300
|
+
* the digest if it has already been computed, without computing one.
|
|
301
|
+
*
|
|
302
|
+
* A format may want to record what it read from, but must never be the thing that forces a
|
|
303
|
+
* stream to be drained to find out.
|
|
304
|
+
*/
|
|
305
|
+
public digestIfKnown(): string | undefined {
|
|
306
|
+
return this.digestValue;
|
|
307
|
+
}
|
|
308
|
+
|
|
217
309
|
private startBody(): void {
|
|
218
310
|
if (this.bodyStarted) {
|
|
219
311
|
throw new Error(`nodeset source ${this.name}: a source is read once`);
|
|
@@ -236,8 +328,8 @@ export class NodesetReader {
|
|
|
236
328
|
const result = await this.iterator.next();
|
|
237
329
|
if (result.done) {
|
|
238
330
|
this.exhausted = true;
|
|
239
|
-
if (this.
|
|
240
|
-
this.
|
|
331
|
+
if (this.container === undefined) {
|
|
332
|
+
this.container = "text";
|
|
241
333
|
}
|
|
242
334
|
const tail = this.decoder ? this.decoder.decode() : "";
|
|
243
335
|
if (tail.length > 0) {
|
|
@@ -266,15 +358,16 @@ export class NodesetReader {
|
|
|
266
358
|
if (chunk.length === 0) {
|
|
267
359
|
return undefined;
|
|
268
360
|
}
|
|
269
|
-
if (this.
|
|
270
|
-
|
|
361
|
+
if (this.container === undefined) {
|
|
362
|
+
// text unless the bytes say otherwise; a string chunk is text by construction
|
|
363
|
+
this.container = typeof chunk !== "string" && isNodesetImage(chunk) ? "bytes" : "text";
|
|
271
364
|
}
|
|
272
365
|
const raw = typeof chunk === "string" ? this.encoder.encode(chunk) : chunk;
|
|
273
366
|
this.bytesRead += raw.length;
|
|
274
367
|
if (this.hashed) {
|
|
275
368
|
this.hashed.push(raw);
|
|
276
369
|
}
|
|
277
|
-
if (this.
|
|
370
|
+
if (this.container === "bytes") {
|
|
278
371
|
const bytes = typeof chunk === "string" ? raw : chunk;
|
|
279
372
|
if (!this.bodyStarted) this.head.push(bytes);
|
|
280
373
|
return bytes;
|
|
@@ -365,3 +458,69 @@ export function openNodesetSource(
|
|
|
365
458
|
};
|
|
366
459
|
return new NodesetReader(name, open, whole, { hash: options.hash, imageKey });
|
|
367
460
|
}
|
|
461
|
+
|
|
462
|
+
/** the head a sniffer is shown, with its first line split out for the line-delimited formats */
|
|
463
|
+
function makeHead(name: string, text: string, gzip: boolean): NodesetHead {
|
|
464
|
+
const newline = text.indexOf("\n");
|
|
465
|
+
const firstLine = newline < 0 ? text : text.slice(0, newline);
|
|
466
|
+
return { name, text, gzip, firstLine: firstLine.endsWith("\r") ? firstLine.slice(0, -1) : firstLine };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* a {@link NodesetDocument} over a reader: what a format is handed.
|
|
471
|
+
*
|
|
472
|
+
* The point of the facade is that a format never touches the reader. It asks for the document in
|
|
473
|
+
* the shape it wants -- lines, text, bytes, a stream -- and the reader answers each of those once
|
|
474
|
+
* and shares the answer, so a format that wants both the lines and the raw bytes pays for one
|
|
475
|
+
* read and one inflate rather than two.
|
|
476
|
+
*
|
|
477
|
+
* @internal
|
|
478
|
+
*/
|
|
479
|
+
export function nodesetDocumentOf(reader: NodesetReader): NodesetDocument {
|
|
480
|
+
let inflatedBytes: Promise<Uint8Array> | undefined;
|
|
481
|
+
let textValue: Promise<string> | undefined;
|
|
482
|
+
let linesValue: Promise<string[]> | undefined;
|
|
483
|
+
|
|
484
|
+
const rawBytes = () => reader.allBytes();
|
|
485
|
+
|
|
486
|
+
const bytes = () => {
|
|
487
|
+
if (!inflatedBytes) {
|
|
488
|
+
inflatedBytes = (async () => {
|
|
489
|
+
const raw = await rawBytes();
|
|
490
|
+
if (!isGzip(raw)) return raw;
|
|
491
|
+
// one inflate, shared with lines(): both go through the cache keyed on these bytes
|
|
492
|
+
return new TextEncoder().encode((await inflatedImageLines(raw)).join("\n"));
|
|
493
|
+
})();
|
|
494
|
+
}
|
|
495
|
+
return inflatedBytes;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const lines = () => {
|
|
499
|
+
if (!linesValue) {
|
|
500
|
+
linesValue = (async () => {
|
|
501
|
+
const raw = await rawBytes();
|
|
502
|
+
if (isGzip(raw)) return await inflatedImageLines(raw);
|
|
503
|
+
return new TextDecoder("utf-8").decode(raw).split("\n");
|
|
504
|
+
})();
|
|
505
|
+
}
|
|
506
|
+
return linesValue;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const text = () => {
|
|
510
|
+
if (!textValue) {
|
|
511
|
+
textValue = (async () => (await lines()).join("\n"))();
|
|
512
|
+
}
|
|
513
|
+
return textValue;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
name: reader.name,
|
|
518
|
+
bytes,
|
|
519
|
+
text,
|
|
520
|
+
lines,
|
|
521
|
+
rawBytes,
|
|
522
|
+
chunks: () => reader.chunks(),
|
|
523
|
+
readHead: (complete: (t: string) => boolean) => reader.readHead(complete),
|
|
524
|
+
digest: () => reader.digestIfKnown()
|
|
525
|
+
};
|
|
526
|
+
}
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opcua-address-space
|
|
3
3
|
*/
|
|
4
|
+
import { NDJSON_IMAGE_FORMAT, registerBuiltinNodesetFormats } from "./nodeset_builtin_formats.js";
|
|
5
|
+
import { nodesetFormatByName } from "./nodeset_format.js";
|
|
4
6
|
import { imageNodesetRecords, NodesetImageWriter, type NodesetImageWriterOptions } from "./nodeset_image.js";
|
|
5
|
-
import { type NodesetSource, openNodesetSource } from "./nodeset_source.js";
|
|
6
|
-
|
|
7
|
+
import { type NodesetSource, nodesetDocumentOf, openNodesetSource } from "./nodeset_source.js";
|
|
8
|
+
|
|
9
|
+
registerBuiltinNodesetFormats();
|
|
7
10
|
|
|
8
11
|
export interface NodesetToImageOptions extends Pick<NodesetImageWriterOptions, "addressSpaceVersion" | "createdAt"> {}
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
|
-
* the precompiled image of a
|
|
14
|
+
* the precompiled image of a nodeset document, built from the source alone: converting a file
|
|
12
15
|
* needs neither its dependencies nor an address space. A source that already holds an image is
|
|
13
16
|
* returned as it is, once read through to check it.
|
|
17
|
+
*
|
|
18
|
+
* Any registered format can be the input, not only NodeSet2 XML: an image is a cache of records,
|
|
19
|
+
* and records are what every format produces.
|
|
14
20
|
*/
|
|
15
21
|
export async function nodesetToImage(source: NodesetSource, options: NodesetToImageOptions = {}): Promise<Uint8Array> {
|
|
16
22
|
const reader = openNodesetSource(source, 0, { hash: true });
|
|
17
|
-
|
|
23
|
+
const formatName = await reader.probe();
|
|
24
|
+
if (formatName === NDJSON_IMAGE_FORMAT) {
|
|
18
25
|
const bytes = await reader.allBytes();
|
|
19
26
|
for await (const _record of imageNodesetRecords(bytes)) {
|
|
20
27
|
/* a corrupt image is refused here rather than at load time */
|
|
21
28
|
}
|
|
22
29
|
return bytes;
|
|
23
30
|
}
|
|
31
|
+
const format = nodesetFormatByName(formatName);
|
|
32
|
+
if (!format) {
|
|
33
|
+
throw new Error(`nodeset source ${reader.name}: its format is no longer registered`);
|
|
34
|
+
}
|
|
24
35
|
const writer = new NodesetImageWriter({ ...options });
|
|
25
|
-
for await (const record of
|
|
36
|
+
for await (const record of format.records(nodesetDocumentOf(reader))) {
|
|
26
37
|
writer.apply(record);
|
|
27
38
|
}
|
|
28
39
|
return writer.finish(await reader.digest(), reader.length);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-address-space
|
|
3
|
+
*
|
|
4
|
+
* Reading the header of a NodeSet2 XML document: its `NamespaceUris` and its `Models`, and
|
|
5
|
+
* nothing else.
|
|
6
|
+
*
|
|
7
|
+
* This is the dependency pre-pass. Before any document is loaded the loader has to know what each
|
|
8
|
+
* one defines and what each one needs, so that it can sort them; it does that by reading every
|
|
9
|
+
* source no further than the end of its header, which on the standard nodeset is a few hundred
|
|
10
|
+
* bytes of a four megabyte file.
|
|
11
|
+
*
|
|
12
|
+
* It lives apart from the loader because the built-in XML format needs it too, and a format
|
|
13
|
+
* cannot import the loader that drives it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getMinOPCUADate } from "node-opcua-date-time";
|
|
17
|
+
import { checkDebugFlag } from "node-opcua-debug";
|
|
18
|
+
import { type ReaderStateParser, type ReaderStateParserLike, Xml2Json, type XmlAttributes } from "node-opcua-xml2json";
|
|
19
|
+
import type { NodesetModel, NodesetModelInfo } from "./nodeset_format.js";
|
|
20
|
+
|
|
21
|
+
const doDebug = checkDebugFlag("generateAddressSpaceRaw");
|
|
22
|
+
|
|
23
|
+
type Model = NodesetModel;
|
|
24
|
+
type NodesetInfo = NodesetModelInfo;
|
|
25
|
+
|
|
26
|
+
/** the `NamespaceUris` and `Models` a NodeSet2 header declares */
|
|
27
|
+
export async function parseDependencies(xmlData: string): Promise<NodesetInfo> {
|
|
28
|
+
const namespaceUris: string[] = [];
|
|
29
|
+
|
|
30
|
+
const models: Model[] = [];
|
|
31
|
+
let currentModel: Model | undefined;
|
|
32
|
+
const state0: ReaderStateParser = {
|
|
33
|
+
parser: {
|
|
34
|
+
UANodeSet: {
|
|
35
|
+
parser: {
|
|
36
|
+
NamespaceUris: {
|
|
37
|
+
parser: {
|
|
38
|
+
Uri: <ReaderStateParserLike & { text: string }>{
|
|
39
|
+
finish(this: ReaderStateParserLike & { text: string }) {
|
|
40
|
+
namespaceUris.push(this.text);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
Models: {
|
|
46
|
+
parser: {
|
|
47
|
+
Model: {
|
|
48
|
+
init(_elementName: string, attrs: XmlAttributes) {
|
|
49
|
+
const modelUri = attrs.ModelUri;
|
|
50
|
+
const version = attrs.Version;
|
|
51
|
+
const publicationDate = new Date(Date.parse(attrs.PublicationDate));
|
|
52
|
+
currentModel = {
|
|
53
|
+
modelUri,
|
|
54
|
+
version,
|
|
55
|
+
publicationDate,
|
|
56
|
+
requiredModel: []
|
|
57
|
+
};
|
|
58
|
+
doDebug && console.log(`currentModel = ${JSON.stringify(currentModel)}`);
|
|
59
|
+
models.push(currentModel);
|
|
60
|
+
},
|
|
61
|
+
parser: {
|
|
62
|
+
RequiredModel: {
|
|
63
|
+
init(_elementName: string, attrs: XmlAttributes) {
|
|
64
|
+
const modelUri = attrs.ModelUri;
|
|
65
|
+
const version = attrs.Version;
|
|
66
|
+
const publicationDate = new Date(Date.parse(attrs.PublicationDate));
|
|
67
|
+
|
|
68
|
+
if (!currentModel) {
|
|
69
|
+
throw new Error("Internal Error");
|
|
70
|
+
}
|
|
71
|
+
currentModel.requiredModel.push({
|
|
72
|
+
modelUri,
|
|
73
|
+
version,
|
|
74
|
+
publicationDate
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const parser = new Xml2Json(state0);
|
|
87
|
+
parser.parseString(xmlData);
|
|
88
|
+
if (models.length === 0 && namespaceUris.length >= 1) {
|
|
89
|
+
models.push({
|
|
90
|
+
modelUri: namespaceUris[0],
|
|
91
|
+
version: "1",
|
|
92
|
+
publicationDate: getMinOPCUADate(),
|
|
93
|
+
requiredModel: []
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return { models, namespaceUris: namespaceUris };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* the header of a NodeSet2 file (`NamespaceUris`, `Models`) precedes the aliases and the nodes: the
|
|
101
|
+
* dependency pre-pass has read enough once both closers were seen, or once the body has begun
|
|
102
|
+
*/
|
|
103
|
+
const BODY_START =
|
|
104
|
+
/<(Aliases|Extensions|UAObject|UAVariable|UADataType|UAReferenceType|UAObjectType|UAVariableType|UAMethod|UAView)[\s/>]/;
|
|
105
|
+
export function headerComplete(text: string): boolean {
|
|
106
|
+
return (text.includes("</Models>") && text.includes("</NamespaceUris>")) || BODY_START.test(text);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** the `<UANodeSet ...>` opener up to the end of `</Models>` or `</NamespaceUris>`, whichever comes last */
|
|
110
|
+
export function sliceHeader(xmlData: string, name: string): string {
|
|
111
|
+
const indexStart = xmlData.match(/<UANodeSet/m)?.index;
|
|
112
|
+
const i1 = (xmlData.match(/<\/Models>/m)?.index || 0) + "</Models>".length;
|
|
113
|
+
const i2 = (xmlData.match(/<\/NamespaceUris>/m)?.index || 0) + "</NamespaceUris>".length;
|
|
114
|
+
const indexEnd = Math.max(i1, i2);
|
|
115
|
+
if (indexStart === undefined) {
|
|
116
|
+
throw new Error(`nodeset source ${name}: no <UANodeSet> element found`);
|
|
117
|
+
}
|
|
118
|
+
return xmlData.substring(indexStart, indexEnd);
|
|
119
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-address-space
|
|
3
|
+
*
|
|
4
|
+
* The parts of writing a NodeSet2 document that are about XML and nothing else: the namespace
|
|
5
|
+
* prefix bookkeeping an `XmlWriter` carries around, the id and browse-name spellings, and the few
|
|
6
|
+
* `uax:` elements a value is built from.
|
|
7
|
+
*
|
|
8
|
+
* They live here, below the address space, because there are two writers and neither should own
|
|
9
|
+
* them: `impl/nodeset_tools/nodeset_to_xml.ts` writes a live namespace, and
|
|
10
|
+
* `nodeset_records_to_xml.ts` writes a record stream with no address space anywhere in sight. A
|
|
11
|
+
* helper that reached for a node would be usable by only one of them.
|
|
12
|
+
*/
|
|
13
|
+
import { assert } from "node-opcua-assert";
|
|
14
|
+
import type { Int64, UInt64 } from "node-opcua-basic-types";
|
|
15
|
+
import { type LocalizedText, QualifiedName } from "node-opcua-data-model";
|
|
16
|
+
import { make_warningLog } from "node-opcua-debug";
|
|
17
|
+
import { NodeId } from "node-opcua-nodeid";
|
|
18
|
+
import type { XmlWriter } from "../xml_writer.js";
|
|
19
|
+
|
|
20
|
+
const warningLog = make_warningLog("nodeset_xml_primitives");
|
|
21
|
+
|
|
22
|
+
export type XmlNamespaceUri = string;
|
|
23
|
+
export type NamespaceUri = string;
|
|
24
|
+
export type XmlNs = string;
|
|
25
|
+
|
|
26
|
+
export const UAX_TYPES_XSD = "http://opcfoundation.org/UA/2008/02/Types.xsd";
|
|
27
|
+
|
|
28
|
+
/** the `<xmlns:nsN>` a namespace's own types are declared under, derived from its uri */
|
|
29
|
+
export function makeTypeXsd(namespaceUri: string): string {
|
|
30
|
+
return `${namespaceUri.replace(/\/$/, "")}/Type.xsd`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface XmlWriterEx extends XmlWriter {
|
|
34
|
+
map: Record<XmlNamespaceUri, XmlNs>;
|
|
35
|
+
stackMap: Record<XmlNamespaceUri, XmlNs>[];
|
|
36
|
+
namespaceArray: NamespaceUri[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function initXmlWriterEx(xw: XmlWriter, map: Record<XmlNamespaceUri, XmlNs>, namespaceArray: NamespaceUri[]): void {
|
|
40
|
+
const xwe = xw as XmlWriterEx;
|
|
41
|
+
xwe.map = map;
|
|
42
|
+
xwe.stackMap = [];
|
|
43
|
+
xwe.namespaceArray = namespaceArray;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function findXsdNamespaceUri(xw: XmlWriter, nodeId: NodeId): string {
|
|
47
|
+
const xwe = xw as XmlWriterEx;
|
|
48
|
+
if (!xwe.namespaceArray) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
const namespace = xwe.namespaceArray[nodeId.namespace];
|
|
52
|
+
if (namespace === "http://opcfoundation.org/UA/") {
|
|
53
|
+
return UAX_TYPES_XSD;
|
|
54
|
+
}
|
|
55
|
+
// c8 ignore next
|
|
56
|
+
if (!namespace) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
return `${namespace.replace(/\/$/, "")}/Types.xsd`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getPrefix(xw: XmlWriter, namespace: XmlNamespaceUri): XmlNs {
|
|
63
|
+
const xwe = xw as XmlWriterEx;
|
|
64
|
+
if (!xwe.map) return "";
|
|
65
|
+
const p = xwe.map[namespace] || "";
|
|
66
|
+
return p ? `${p}:` : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function restoreDefaultNamespace(xw: XmlWriter): void {
|
|
70
|
+
const xwe = xw as XmlWriterEx;
|
|
71
|
+
if (!xwe.map) return;
|
|
72
|
+
const previousMap = xwe.stackMap.pop();
|
|
73
|
+
if (previousMap) {
|
|
74
|
+
xwe.map = previousMap;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function setDefaultNamespace(xw: XmlWriter, namespace: XmlNamespaceUri): void {
|
|
79
|
+
const xwe = xw as XmlWriterEx;
|
|
80
|
+
if (!xwe.map) return;
|
|
81
|
+
if (xwe.map[namespace] !== "") {
|
|
82
|
+
xw.writeAttribute("xmlns", namespace);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
xwe.stackMap.push({
|
|
86
|
+
...xwe.map
|
|
87
|
+
});
|
|
88
|
+
xwe.map[namespace] = "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function startElementEx(xw: XmlWriter, _ns: XmlNs, name: string, defaultNamespace: XmlNamespaceUri): void {
|
|
92
|
+
xw.startElement(name);
|
|
93
|
+
setDefaultNamespace(xw, defaultNamespace);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// #region ids and names
|
|
97
|
+
//
|
|
98
|
+
// every id written goes through the writer's translationTable, which maps the index a node carries
|
|
99
|
+
// to the index the document being written declares. A writer whose document keeps the indices it
|
|
100
|
+
// was given -- the record writer -- installs the identity and these are then spelling functions.
|
|
101
|
+
|
|
102
|
+
export function translateNodeId(xw: XmlWriter, nodeId: NodeId): NodeId {
|
|
103
|
+
assert(nodeId instanceof NodeId);
|
|
104
|
+
const nn = xw.translationTable.get(nodeId.namespace);
|
|
105
|
+
return new NodeId(nodeId.identifierType, nodeId.value, nn);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** an id as a document spells it: translated, and with the `ns=0;` that every document leaves out */
|
|
109
|
+
export function n(xw: XmlWriter, nodeId: NodeId): string {
|
|
110
|
+
return translateNodeId(xw, nodeId).toString().replace("ns=0;", "");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function translateBrowseName(xw: XmlWriter, browseName: QualifiedName): QualifiedName {
|
|
114
|
+
assert(browseName instanceof QualifiedName);
|
|
115
|
+
const nn = xw.translationTable.get(browseName.namespaceIndex);
|
|
116
|
+
return new QualifiedName({ namespaceIndex: nn, name: browseName.name });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function b(xw: XmlWriter, browseName: QualifiedName): string {
|
|
120
|
+
return translateBrowseName(xw, browseName).toString().replace("ns=0;", "");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** the identity table, for a writer that keeps the namespace indices its input already carries */
|
|
124
|
+
export function identityTranslationTable(size: number): Map<number, number> {
|
|
125
|
+
const table = new Map<number, number>();
|
|
126
|
+
for (let i = 0; i < size; i++) {
|
|
127
|
+
table.set(i, i);
|
|
128
|
+
}
|
|
129
|
+
return table;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// #endregion
|
|
133
|
+
|
|
134
|
+
const TWO_64 = 1n << 64n;
|
|
135
|
+
const TWO_63 = 1n << 63n;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* a 64-bit integer as one decimal number, the only spelling that says what it means.
|
|
139
|
+
*
|
|
140
|
+
* In memory node-opcua holds one as `[high, low]` with unsigned halves, so Int64 -1 and UInt64
|
|
141
|
+
* 18446744073709551615 are the very same pair and only `signed` tells them apart. Writing the low
|
|
142
|
+
* half alone -- which both XML writers used to do -- silently truncates anything past 2^32 and
|
|
143
|
+
* turns -1 into 4294967295.
|
|
144
|
+
*/
|
|
145
|
+
export function int64ToDecimalString(value: Int64 | UInt64 | number | string, signed: boolean): string {
|
|
146
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
147
|
+
return value.toString();
|
|
148
|
+
}
|
|
149
|
+
const [high, low] = value as [number, number];
|
|
150
|
+
const unsigned = (BigInt(high >>> 0) << 32n) | BigInt(low >>> 0);
|
|
151
|
+
return (signed && unsigned >= TWO_63 ? unsigned - TWO_64 : unsigned).toString();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function coerceInt64ToInt32(int64: Int64): number {
|
|
155
|
+
if (typeof int64 === "number") {
|
|
156
|
+
return int64;
|
|
157
|
+
}
|
|
158
|
+
if (int64[0] === 0xffffffff && int64[1] === 0xffffffff) {
|
|
159
|
+
return 0xffffffff;
|
|
160
|
+
}
|
|
161
|
+
if (int64[0] !== 0) {
|
|
162
|
+
warningLog("coerceInt64ToInt32 , loosing high word in conversion");
|
|
163
|
+
}
|
|
164
|
+
return int64[1];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// #region the `uax:` elements a value is built from
|
|
168
|
+
|
|
169
|
+
export function _dumpLocalizedText(xw: XmlWriter, v: LocalizedText): void {
|
|
170
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
171
|
+
if (v.locale?.length) {
|
|
172
|
+
xw.startElement(`${uax}Locale`);
|
|
173
|
+
xw.text(v.locale);
|
|
174
|
+
xw.endElement();
|
|
175
|
+
}
|
|
176
|
+
xw.startElement(`${uax}Text`);
|
|
177
|
+
if (v.text) {
|
|
178
|
+
xw.text(v.text);
|
|
179
|
+
}
|
|
180
|
+
xw.endElement();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function _dumpQualifiedName(xw: XmlWriter, v: QualifiedName): void {
|
|
184
|
+
const uax = getPrefix(xw, UAX_TYPES_XSD);
|
|
185
|
+
const t = translateBrowseName(xw, v);
|
|
186
|
+
if (t.name) {
|
|
187
|
+
xw.startElement(`${uax}Name`);
|
|
188
|
+
xw.text(t.name);
|
|
189
|
+
xw.endElement();
|
|
190
|
+
}
|
|
191
|
+
if (t.namespaceIndex) {
|
|
192
|
+
xw.startElement(`${uax}NamespaceIndex`);
|
|
193
|
+
xw.text(t.namespaceIndex.toString());
|
|
194
|
+
xw.endElement();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function _dumpXmlElement(xw: XmlWriter, v: string): void {
|
|
199
|
+
xw.text(v);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function _dumpNodeId(xw: XmlWriter, v: NodeId): void {
|
|
203
|
+
const xmlns = getPrefix(xw, UAX_TYPES_XSD);
|
|
204
|
+
xw.startElement(`${xmlns}Identifier`);
|
|
205
|
+
xw.text(n(xw, v));
|
|
206
|
+
xw.endElement();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// #endregion
|