node-opcua-address-space 2.181.1 → 2.182.2
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
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { BaseNode, IChannelBase, UAMethod, UAObject, UAVariable } from "node-opcua-address-space-base";
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
|
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 {
|
|
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
|
|
120
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
175
|
-
const namespaceModel = await
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
354
|
+
await nodesetLoader.addRecords(nodeset.format.records(nodeset.document));
|
|
450
355
|
} finally {
|
|
451
|
-
releaseInflatedImageLines(nodeset.
|
|
356
|
+
releaseInflatedImageLines(await nodeset.document.rawBytes());
|
|
452
357
|
}
|
|
453
358
|
} else {
|
|
454
|
-
await
|
|
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
|
+
}
|