• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-opcua / node-opcua / 39320269605

19 Oct 2026 08:33PM UTC coverage: 91.329% (-0.06%) from 91.385%
39320269605

push

github

erossignon
v2.187.10

34410 of 42203 branches covered (81.53%)

229485 of 251273 relevant lines covered (91.33%)

223128.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.42
/packages/node-opcua-json/source/json_basic_encoding_decoding_nodeid.ts
1
/**
1 ✔
2
 * ==========================================================================
1 ✔
3
 * Copyright (c) 2021-2026 Sterfive - etienne.rossignon@sterfive.com
1 ✔
4
 *
1 ✔
5
 * Permission is hereby granted, free of charge, to any person obtaining a copy
1 ✔
6
 * of this software and associated documentation files (the "Software"), to deal
1 ✔
7
 * in the Software without restriction, including without limitation the rights
1 ✔
8
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1 ✔
9
 * copies of the Software, and to permit persons to whom the Software is
1 ✔
10
 * furnished to do so, subject to the following conditions:
1 ✔
11
 *
1 ✔
12
 * The above copyright notice and this permission notice shall be included in
1 ✔
13
 * all copies or substantial portions of the Software.
1 ✔
14
 *
1 ✔
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1 ✔
16
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1 ✔
17
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1 ✔
18
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1 ✔
19
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1 ✔
20
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
1 ✔
21
 * IN THE SOFTWARE.
1 ✔
22
 * ==========================================================================
1 ✔
23
 */
1 ✔
24
import { coerceNodeId, ExpandedNodeId, NodeId, NodeIdType } from "node-opcua-nodeid"; // -nodeid";
1 ✔
25
import type { ExtensionObjectBuilder } from "./json_basic_encoding_body_functor.js";
1 ✔
26
import { JsonEncodingScheme } from "./json_encoding_scheme.js";
1 ✔
27
import { OPCUA_NAMESPACE_URI } from "./json_opcua_namespace.js";
1 ✔
28

1 ✔
29
// from https://reference.opcfoundation.org/Core/Part6/v105/docs/5.4
1 ✔
30

1 ✔
31
// NodeId values shall be encoded as a JSON string using the format defined in
1 ✔
32
// https://reference.opcfoundation.org/Core/Part6/v105/docs/5.1.12#_Ref122651839
1 ✔
33
// .
1 ✔
34
//  NodeIds in NamespaceIndex 0 use the <identifier> form.
1 ✔
35
//  All other NodeIds use the <namespace-uri> form.
1 ✔
36
//
1 ✔
37
// The first abnormal state occurs when the encoder cannot map a NamespaceIndex to a NamespaceUri.
1 ✔
38
// In this case, the encoder shall encode the NamespaceIndex using the <namespace-index > form.
1 ✔
39
// The decoder shall pass this NamespaceIndex to the application.
1 ✔
40
//
1 ✔
41
// The second abnormal state occurs when the decoder cannot convert a NamespaceUri to a NamespaceIndex.
1 ✔
42
// If this occurs the decoder shall set the NamespaceIndex to 0, the IdType to String and the
1 ✔
43
// Identifier to the JSON string.
1 ✔
44
export interface NodeIdJSON104 {
1 ✔
45
    IdType?: 0 | 1 | 2 | 3;
1 ✔
46
    Id: number | string;
1 ✔
47
    Namespace?: number | string;
1 ✔
48
    /** ExpandedNodeId only: the ServerIndex, omitted when 0 (Part 6 1.04 clause 5.4.2.11) */
1 ✔
49
    ServerUri?: number;
1 ✔
50
}
1 ✔
51
export type NodeIdJSON105 = string;
1 ✔
52
export type NodeIdJSON = NodeIdJSON104 | NodeIdJSON105;
1 ✔
53

1 ✔
54
const nodeIdValueToJson = (value: NodeId): string | number => {
1 ✔
55
    switch (value.identifierType) {
×
56
        case NodeIdType.NUMERIC:
×
57
            return value.value as number;
×
58
        case NodeIdType.STRING:
×
59
            return value.value as string;
×
60
        case NodeIdType.GUID:
×
61
            return value.value as string;
×
62
        case NodeIdType.BYTESTRING:
×
63
            return (value.value as Buffer).toString("base64");
×
64
        default:
×
65
            throw new Error(`Unknown NodeId identifier type: ${value.identifierType}`);
×
66
    }
×
67
};
×
68

1 ✔
69
const nodeIdValueToString = (value: NodeId): string => {
1 ✔
70
    switch (value.identifierType) {
1,038 ✔
71
        case NodeIdType.NUMERIC:
1,038 ✔
72
            return `i=${value.value as number}`;
1,038 ✔
73

1,038 ✔
74
        case NodeIdType.STRING:
1,038 !
75
            return `s=${value.value as string}`;
×
76

1,038 ✔
77
        case NodeIdType.GUID:
1,038 !
78
            return `g=${value.value as string}`;
×
79
        case NodeIdType.BYTESTRING:
1,038 !
80
            return `b=${(value.value as Buffer).toString("base64")}`;
×
81
        default:
1,038 !
82
            throw new Error(`Unknown NodeId identifier type: ${value.identifierType}`);
×
83
    }
1,038 ✔
84
};
1,038 ✔
85
/**
1 ✔
86
 *
1 ✔
87
 * IdType           The IdentifierType encoded as a JSON number.
1 ✔
88
 *             Allowed values are:
1 ✔
89
 *               0 - UInt32 Identifier encoded as a JSON number.
1 ✔
90
 *               1 - A String Identifier encoded as a JSON string.
1 ✔
91
 *               2 - A Guid Identifier.
1 ✔
92
 *               3 - A ByteString Identifier
1 ✔
93
 *
1 ✔
94
 *             This field is omitted for UInt32 identifiers.
1 ✔
95
 *
1 ✔
96
 *  Id         The Identifier.
1 ✔
97
 *             The value of the id field specifies the encoding of this field.
1 ✔
98
 *
1 ✔
99
 *  Namespace        The NamespaceIndex for the NodeId.
1 ✔
100
 *              The field is encoded as a JSON number for the reversible encoding.
1 ✔
101
 *              The field is omitted if the NamespaceIndex equals 0.
1 ✔
102
 *              For the non-reversible encoding, the field is the NamespaceUri associated with the NamespaceIndex,
1 ✔
103
 *              encoded as a JSON string.
1 ✔
104
 *  A NamespaceIndex of 1 is always encoded as a JSON number.
1 ✔
105
 */
1 ✔
106

1 ✔
107
export function opcuaJsonEncodeNodeId104(
1 ✔
108
    value: NodeId,
×
109
    scheme = JsonEncodingScheme.DeprecatedReversible,
×
110
    namespaceArray?: string[]
×
111
): NodeIdJSON104 {
×
112
    const nodeIdIdentifierTypeToJSON = () => {
×
113
        switch (value.identifierType) {
×
114
            case NodeIdType.NUMERIC:
×
115
                return 0;
×
116
            case NodeIdType.STRING:
×
117
                return 1;
×
118
            case NodeIdType.GUID:
×
119
                return 2;
×
120
            default:
×
121
                return 3;
×
122
        }
×
123
    };
×
124
    const IdType = nodeIdIdentifierTypeToJSON();
×
125

×
126
    const Id = nodeIdValueToJson(value);
×
127

×
128
    const pojo: NodeIdJSON = { Id };
×
129
    if (IdType !== 0) {
×
130
        pojo.IdType = IdType as 1 | 2 | 3;
×
131
    }
×
132
    if (value.namespace === 0) {
×
133
        return pojo;
×
134
    }
×
135
    const reversible = !(scheme === JsonEncodingScheme.DeprecatedNonReversible);
×
136

×
137
    if (value.namespace === 1 || reversible || !namespaceArray) {
×
138
        pojo.Namespace = value.namespace;
×
139
        return pojo;
×
140
    }
×
141
    pojo.Namespace = namespaceArray[value.namespace];
×
142
    return pojo;
×
143
}
×
144

1 ✔
145
export function opcuaJsonEncodeNodeIdAsString(value: NodeId, namespaceArray: string[]): string {
1 ✔
146
    const namespace = namespaceArray ? namespaceArray[value.namespace] : undefined;
1,038 !
147
    if (namespace) {
1,038 ✔
148
        if (namespace === OPCUA_NAMESPACE_URI) {
1,038 ✔
149
            return `${nodeIdValueToString(value)}`;
1,026 ✔
150
        }
1,026 ✔
151
        const encodedNamespace = opcuaEncodeURIComponent(namespace);
12 ✔
152
        return `nsu=${encodedNamespace};${nodeIdValueToString(value)}`;
12 ✔
153
    } else {
1,038 !
154
        if (value.namespace === 0) {
×
155
            return nodeIdValueToString(value);
×
156
        }
×
157
        // If the namespace is not found, we assume it is a new namespace
×
158
        return `ns=${value.namespace};${nodeIdValueToString(value)}`;
×
159
    }
×
160
}
1,038 ✔
161
export function opcuaJsonEncodeExpandedNodeIdAsString(value: ExpandedNodeId, namespaceArray: string[]): string {
1 ✔
162
    // the NamespaceUri of an ExpandedNodeId names its namespace (nsu=); svu= is the URI of a
×
163
    // server, which is only known through a ServerArray this encoder does not have
×
164
    let result: string;
×
165
    if (value.namespaceUri && value.namespaceUri !== OPCUA_NAMESPACE_URI) {
×
166
        result = `nsu=${opcuaEncodeURIComponent(value.namespaceUri)};${nodeIdValueToString(value)}`;
×
167
    } else if (value.namespaceUri) {
×
168
        result = nodeIdValueToString(value);
×
169
    } else {
×
170
        // the NamespaceIndex of a remote node refers to the remote server's namespace table, which
×
171
        // is not the local one: it stays an index (OPC 10000-4 7.16, OPC 10000-6 5.4.2.10)
×
172
        result = opcuaJsonEncodeNodeIdAsString(value, value.serverIndex !== 0 ? [] : namespaceArray);
×
173
    }
×
174
    if (value.serverIndex !== 0) {
×
175
        result = `svr=${value.serverIndex};${result}`;
×
176
    }
×
177
    return result;
×
178
}
×
179

1 ✔
180
export function opcuaJsonEncodeNodeId105(
1 ✔
181
    value: NodeId,
1,038 ✔
182
    scheme: JsonEncodingScheme,
1,038 ✔
183
    namespaceArray?: string[]
1,038 ✔
184
): NodeIdJSON105 | undefined {
1,038 ✔
185
    if (value.namespace === 0 && value.identifierType === NodeIdType.NUMERIC && value.value === 0) {
1,038 ✔
186
        if (scheme === JsonEncodingScheme.Compact) {
30 !
187
            return undefined;
×
188
        }
×
189
    }
30 ✔
190

1,038 ✔
191
    return opcuaJsonEncodeNodeIdAsString(value, namespaceArray || []);
1,038 !
192
}
1,038 ✔
193

1 ✔
194
export function opcuaJsonEncodeExpandedNodeId105(
1 ✔
195
    value: ExpandedNodeId,
×
196
    scheme: JsonEncodingScheme,
×
197
    namespaceArray?: string[]
×
198
): NodeIdJSON105 | undefined {
×
199
    if (value.namespace === 0 && value.identifierType === NodeIdType.NUMERIC && value.value === 0 && value.serverIndex === 0) {
×
200
        if (scheme === JsonEncodingScheme.Compact) {
×
201
            return undefined;
×
202
        }
×
203
    }
×
204
    return opcuaJsonEncodeExpandedNodeIdAsString(value, namespaceArray || []);
×
205
}
×
206

1 ✔
207
export function opcuaJsonEncodeNodeId(
1 ✔
208
    value: NodeId,
1,038 ✔
209
    scheme: JsonEncodingScheme,
1,038 ✔
210
    namespaceArray?: string[]
1,038 ✔
211
): NodeIdJSON | undefined {
1,038 ✔
212
    switch (scheme) {
1,038 ✔
213
        case JsonEncodingScheme.DeprecatedReversible:
1,038 !
214
            return opcuaJsonEncodeNodeId104(value, scheme, namespaceArray);
×
215
        case JsonEncodingScheme.DeprecatedNonReversible:
1,038 !
216
            return opcuaJsonEncodeNodeId104(value, scheme, namespaceArray);
×
217
        case JsonEncodingScheme.Compact:
1,038 !
218
            return opcuaJsonEncodeNodeId105(value, scheme, namespaceArray);
×
219
        case JsonEncodingScheme.Verbose:
1,038 ✔
220
            return opcuaJsonEncodeNodeId105(value, scheme, namespaceArray);
1,038 ✔
221
        default:
1,038 !
222
            throw new Error(`Invalid JsonEncodingScheme: ${scheme}`);
×
223
    }
1,038 ✔
224
}
1,038 ✔
225
export function opcuaJsonEncodeExpandedNodeId(
1 ✔
226
    value: ExpandedNodeId,
×
227
    scheme: JsonEncodingScheme,
×
228
    namespaceArray?: string[]
×
229
): NodeIdJSON | undefined {
×
230
    switch (scheme) {
×
231
        case JsonEncodingScheme.DeprecatedReversible:
×
232
        case JsonEncodingScheme.DeprecatedNonReversible:
×
233
            return opcuaJsonEncodeExpandedNodeId104(value, scheme, namespaceArray);
×
234
        case JsonEncodingScheme.Compact:
×
235
        case JsonEncodingScheme.Verbose:
×
236
            return opcuaJsonEncodeExpandedNodeId105(value, scheme, namespaceArray);
×
237
        default:
×
238
            throw new Error(`Invalid JsonEncodingScheme: ${scheme}`);
×
239
    }
×
240
}
×
241

1 ✔
242
/**
1 ✔
243
 * Part 6 1.04 clause 5.4.2.11: an ExpandedNodeId is the NodeId object plus a ServerUri field
1 ✔
244
 * carrying the ServerIndex, omitted when 0. A NamespaceUri set on the ExpandedNodeId wins over
1 ✔
245
 * the index in the non-reversible form, where the namespace is written as a URI anyway.
1 ✔
246
 */
1 ✔
247
export function opcuaJsonEncodeExpandedNodeId104(
1 ✔
248
    value: ExpandedNodeId,
×
249
    scheme: JsonEncodingScheme,
×
250
    namespaceArray?: string[]
×
251
): NodeIdJSON104 {
×
252
    // a remote node's NamespaceIndex is not in the local table: it stays a number
×
253
    const pojo = opcuaJsonEncodeNodeId104(value, scheme, value.serverIndex !== 0 ? undefined : namespaceArray);
×
254
    if (value.namespaceUri) {
×
255
        // 1.04 Table 24, Annex H.3: a NamespaceUri that is specified is written as a JSON string,
×
256
        // in the reversible form too
×
257
        pojo.Namespace = value.namespaceUri;
×
258
    }
×
259
    if (value.serverIndex !== 0) {
×
260
        pojo.ServerUri = value.serverIndex;
×
261
    }
×
262
    return pojo;
×
263
}
×
264

1 ✔
265
export function opcuaEncodeURIComponent(value: string): string {
1 ✔
266
    return value.replace(/;/g, (match) => {
24 ✔
267
        // Encode semicolon as %3B
×
268
        return encodeURIComponent(match);
×
269
    });
24 ✔
270
}
24 ✔
271
export function opcuaDecodeURIComponent(value: string): string {
1 ✔
272
    return value.replace(/%3B/g, (match) => {
×
273
        // Decode %3B back to semicolon
×
274
        return decodeURIComponent(match);
×
275
    });
×
276
}
×
277

1 ✔
278
function makeNodeIdFromString(identifierType: string, identifierValue: string, namespaceIndex: number, text: string): NodeId {
×
279
    switch (identifierType) {
×
280
        case "i=":
×
281
            return new NodeId(NodeIdType.NUMERIC, parseInt(identifierValue, 10), namespaceIndex);
×
282
        case "s=":
×
283
            return new NodeId(NodeIdType.STRING, identifierValue, namespaceIndex);
×
284
        case "g=":
×
285
            return new NodeId(NodeIdType.GUID, identifierValue, namespaceIndex);
×
286
        case "b=":
×
287
            return new NodeId(NodeIdType.BYTESTRING, Buffer.from(identifierValue, "base64"), namespaceIndex);
×
288
        default:
×
289
            throw new Error(`Invalid NodeId format: ${text}`);
×
290
    }
×
291
}
×
292

1 ✔
293
/**
1 ✔
294
 * The NamespaceIndex of a NamespaceUri, or -1 when it cannot be mapped.
1 ✔
295
 * The OPC UA namespace is index 0 whatever the table says.
1 ✔
296
 */
1 ✔
297
function namespaceUriToIndex(namespaceUri: string, namespaceArray?: string[]): number {
×
298
    if (namespaceUri === OPCUA_NAMESPACE_URI) {
×
299
        return 0;
×
300
    }
×
301
    return namespaceArray ? namespaceArray.indexOf(namespaceUri) : -1;
×
302
}
×
303

1 ✔
304
interface ParsedNodeIdString {
1 ✔
305
    /** the NamespaceUri of the nsu= form, percent-decoded */
1 ✔
306
    namespaceUri?: string;
1 ✔
307
    identifierType: string;
1 ✔
308
    identifierValue: string;
1 ✔
309
}
1 ✔
310
function parseNodeIdString(nodeIdAsString: string): ParsedNodeIdString | null {
×
311
    //nsu=tag:acme.com,2023:schemas:data#off%3B;b=M/RbKBsRVkePCePcx24oRA==
×
312
    const match = nodeIdAsString.match(/^(nsu=([^;]+);)?(g=|s=|i=|b=)(.*)$/);
×
313
    if (!match) {
×
314
        return null;
×
315
    }
×
316
    // A URI which has the RFC 3986 Percent - Encoding applied to it.
×
317
    //   Any ‘;’ in the URI shall be percent encoded.
×
318
    const namespaceUri = match[2] ? opcuaDecodeURIComponent(match[2]) : undefined;
×
319
    return { namespaceUri, identifierType: match[3], identifierValue: match[4] };
×
320
}
×
321

1 ✔
322
function coerceNodeIdEx(nodeIdAsString: string, namespaceArray?: string[]): NodeId {
×
323
    const parsed = parseNodeIdString(nodeIdAsString);
×
324
    if (parsed) {
×
325
        const namespaceIndex = parsed.namespaceUri ? namespaceUriToIndex(parsed.namespaceUri, namespaceArray) : 0;
×
326
        if (namespaceIndex < 0) {
×
327
            // second abnormal state of 1.05 clause 5.4.2.10: NamespaceIndex 0, IdType String,
×
328
            // and the JSON string as Identifier, so the node is not mistaken for one of namespace 0
×
329
            return new NodeId(NodeIdType.STRING, nodeIdAsString, 0);
×
330
        }
×
331
        return makeNodeIdFromString(parsed.identifierType, parsed.identifierValue, namespaceIndex, nodeIdAsString);
×
332
    }
×
333
    // Fallback to coerceNodeId
×
334
    return coerceNodeId(nodeIdAsString);
×
335
}
×
336
export function opcuaJsonDecodeNodeId(
1 ✔
337
    pojoOrString: NodeIdJSON | undefined,
×
338
    _builder?: ExtensionObjectBuilder | undefined,
×
339
    namespaceArray?: string[]
×
340
): NodeId {
×
341
    if (pojoOrString === null || pojoOrString === undefined) {
×
342
        throw new Error("opcuaJsonDecodeNodeId: pojoOrString cannot be null or undefined");
×
343
    }
×
344
    if (typeof pojoOrString === "string") {
×
345
        return coerceNodeIdEx(pojoOrString, namespaceArray);
×
346
    }
×
347
    const pojo = pojoOrString as NodeIdJSON104;
×
348
    const identifierType: NodeIdType = (() => {
×
349
        if (!pojo.IdType) {
×
350
            return NodeIdType.NUMERIC;
×
351
        }
×
352
        switch (pojo.IdType) {
×
353
            case 2:
×
354
                return NodeIdType.GUID;
×
355
            case 3:
×
356
                return NodeIdType.BYTESTRING;
×
357
            default:
×
358
                return NodeIdType.STRING;
×
359
        }
×
360
    })();
×
361

×
362
    const namespace: number = (() => {
×
363
        if (!pojo.Namespace) return 0;
×
364
        if (typeof pojo.Namespace === "number") {
×
365
            return pojo.Namespace;
×
366
        }
×
367
        const NAMESPACE_NOT_FOUND = 0xffff;
×
368
        if (!namespaceArray) {
×
369
            return NAMESPACE_NOT_FOUND;
×
370
        }
×
371
        const index = namespaceArray.indexOf(pojo.Namespace);
×
372
        if (index < 0) {
×
373
            return NAMESPACE_NOT_FOUND;
×
374
        }
×
375
        return index;
×
376
    })();
×
377

×
378
    const value: string | number | Buffer = (() => {
×
379
        switch (identifierType) {
×
380
            case NodeIdType.NUMERIC:
×
381
                return typeof pojo.Id === "string" ? parseInt(pojo.Id, 10) : (pojo.Id as number | 0);
×
382
            case NodeIdType.BYTESTRING:
×
383
                return Buffer.from(pojo.Id as string, "base64");
×
384
            default:
×
385
                return pojo.Id;
×
386
        }
×
387
    })();
×
388

×
389
    return new NodeId(identifierType, value, namespace);
×
390
}
×
391

1 ✔
392
export function opcuaJsonDecodeExpandedNodeId(
1 ✔
393
    pojoOrString: NodeIdJSON | undefined,
×
394
    builder?: ExtensionObjectBuilder | undefined,
×
395
    namespaceArray?: string[]
×
396
): ExpandedNodeId {
×
397
    if (typeof pojoOrString === "string") {
×
398
        // 1.05 clause 5.4.2.11: with a ServerIndex of 0, a NamespaceUri that cannot be mapped
×
399
        // to an index is kept in the NamespaceUri field of the ExpandedNodeId
×
400
        const parsed = parseNodeIdString(pojoOrString);
×
401
        if (parsed?.namespaceUri && namespaceUriToIndex(parsed.namespaceUri, namespaceArray) < 0) {
×
402
            const nodeId = makeNodeIdFromString(parsed.identifierType, parsed.identifierValue, 0, pojoOrString);
×
403
            return ExpandedNodeId.fromNodeId(nodeId, parsed.namespaceUri, 0);
×
404
        }
×
405
    }
×
406
    const nodeId = opcuaJsonDecodeNodeId(pojoOrString, builder, namespaceArray);
×
407
    // the 1.05 string form carries svr=/svu=, which coerceNodeId does not parse; the 1.04 object
×
408
    // form carries ServerUri as the ServerIndex
×
409
    const serverIndex = typeof pojoOrString === "object" && pojoOrString ? (pojoOrString.ServerUri ?? 0) : 0;
×
410
    return ExpandedNodeId.fromNodeId(nodeId, undefined, serverIndex);
×
411
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc