node-opcua-variant
Version:
pure nodejs OPCUA SDK - module variant
1,110 lines (998 loc) • 37.3 kB
text/typescript
/**
* @module node-opcua-variant
*/
import { assert } from "node-opcua-assert";
import { NodeId } from "node-opcua-nodeid";
import {
coerceInt64,
coerceUInt64,
decodeUInt32,
decodeUInt8,
encodeUInt32,
encodeUInt8,
isValidBoolean,
isValidByteString,
isValidInt16,
isValidInt32,
isValidInt64,
isValidInt8,
isValidNodeId,
isValidUInt16,
isValidUInt32,
isValidUInt64,
isValidUInt8
} from "node-opcua-basic-types";
import { LocalizedText, QualifiedName } from "node-opcua-data-model";
import {
BaseUAObject,
buildStructuredType,
findBuiltInType,
initialize_field,
registerSpecialVariantEncoder,
registerType,
DecodeDebugOptions,
FieldCategory
} from "node-opcua-factory";
import { isNullOrUndefined } from "node-opcua-utils";
import { make_warningLog } from "node-opcua-debug";
import { BinaryStream, OutputBinaryStream } from "node-opcua-binary-stream";
import { _enumerationDataType, DataType } from "./DataType_enum";
import { _enumerationVariantArrayType, VariantArrayType } from "./VariantArrayType_enum";
// tslint:disable:no-bitwise
const warningLog = make_warningLog(__filename);
const schemaVariant = buildStructuredType({
baseType: "BaseUAObject",
category: FieldCategory.basic,
fields: [
{
defaultValue: DataType.Null,
documentation: "the variant type.",
fieldType: "DataType",
name: "dataType"
},
{
defaultValue: VariantArrayType.Scalar,
fieldType: "VariantArrayType",
name: "arrayType"
},
{
defaultValue: null,
fieldType: "Any",
name: "value"
},
{
defaultValue: null,
documentation: "the matrix dimensions",
fieldType: "UInt32",
isArray: true,
name: "dimensions"
}
],
name: "Variant"
});
function _coerceVariant(variantLike: VariantOptions | Variant): Variant {
const value = variantLike instanceof Variant ? variantLike : new Variant(variantLike);
return value;
}
export interface VariantOptions {
dataType?: DataType | string;
arrayType?: VariantArrayType | string;
value?: any;
dimensions?: number[] | null;
}
export interface VariantOptions2 {
dataType: DataType;
arrayType: VariantArrayType;
value: any;
dimensions: number[] | null;
}
export class Variant extends BaseUAObject {
public static maxTypedArrayLength = 16 * 1024 * 1024;
public static maxArrayLength = 1 * 1024 * 1024;
public static schema = schemaVariant;
public static coerce = _coerceVariant;
public static computer_default_value = (): Variant => new Variant({ dataType: DataType.Null });
public dataType: DataType;
public arrayType: VariantArrayType;
public value: any;
public dimensions: number[] | null;
constructor(options?: VariantOptions | null) {
super();
if (options === null) {
this.dataType = DataType.Null;
this.arrayType = VariantArrayType.Scalar;
this.value = null;
this.dimensions = null;
return;
}
const options2 = constructHook(options || {});
this.dataType = DataType.Null;
this.arrayType = VariantArrayType.Scalar;
const schema = schemaVariant;
this.dataType = options2.dataType;
this.arrayType = options2.arrayType;
this.value = initialize_field(schema.fields[2], options2.value);
this.dimensions = options2.dimensions || null;
if (this.dataType === DataType.ExtensionObject) {
if (this.arrayType === VariantArrayType.Scalar) {
/* istanbul ignore next */
if (this.value && !(this.value instanceof BaseUAObject)) {
throw new Error(
`A variant with DataType.ExtensionObject must have a ExtensionObject value.\nMake sure that you specify a valid ExtensionObject to the value options in the Variant Constructor`
);
}
} else {
if (this.value) {
for (const e of this.value) {
/* istanbul ignore next */
if (e && !(e instanceof BaseUAObject)) {
throw new Error(
"A variant with DataType.ExtensionObject must have a ExtensionObject value\nMake sure that you specify a valid ExtensionObject for all element of the value array passed to the Variant Constructor`"
);
}
}
}
}
}
}
public encode(stream: OutputBinaryStream): void {
encodeVariant(this, stream);
}
public decode(stream: BinaryStream): void {
internalDecodeVariant(this, stream);
}
public decodeDebug(stream: BinaryStream, options: DecodeDebugOptions): void {
decodeDebugVariant(this, stream, options);
}
public toString(): string {
return variantToString(this);
}
public isValid(): boolean {
return isValidVariant(this.arrayType, this.dataType, this.value, this.dimensions);
}
public clone(): Variant {
return new Variant(this);
}
}
Variant.prototype.schema = schemaVariant;
export type VariantLike = VariantOptions;
function variantToString(self: Variant, options?: any) {
function toString(value: any): string {
switch (self.dataType) {
case DataType.ByteString:
return value ? "0x" + value.toString("hex") : "<null>";
case DataType.NodeId:
return value instanceof NodeId ? (value as NodeId).displayText() : value ? value.toString(options) : "";
case DataType.Boolean:
return value.toString();
case DataType.DateTime:
return value ? (value.toISOString ? value.toISOString() : value.toString()) : "<null>";
default:
return value ? value.toString(options) : "0";
}
}
function f(value: any) {
if (value === undefined || (value === null && typeof value === "object")) {
return "<null>";
}
return toString(value);
}
let data = VariantArrayType[self.arrayType];
if (self.dimensions && self.dimensions.length >= 0) {
data += "[ " + self.dimensions.join(",") + " ]";
}
data += "<" + DataType[self.dataType] + ">";
if (self.arrayType === VariantArrayType.Scalar) {
data += ", value: " + f(self.value);
} else if (self.arrayType === VariantArrayType.Array || self.arrayType === VariantArrayType.Matrix) {
if (!self.value) {
data += ", null";
} else {
const a = [];
assert(Array.isArray(self.value) || self.value.buffer instanceof ArrayBuffer);
for (let i = 0; i < Math.min(10, self.value.length); i++) {
a[i] = self.value[i];
}
if (self.value.length > 10) {
a.push("...");
}
data += ", l= " + self.value.length + ", value=[" + a.map(f).join(",") + "]";
}
}
return "Variant(" + data + ")";
}
/***
* @private
*/
export const VARIANT_ARRAY_MASK = 0x80;
/***
* @private
*/
export const VARIANT_ARRAY_DIMENSIONS_MASK = 0x40;
/***
* @private
*/
export const VARIANT_TYPE_MASK = 0x3f;
const nullVariant = new Variant({ dataType: DataType.Null });
/***
* @private
*/
export function encodeVariant(variant: Variant | undefined | null, stream: OutputBinaryStream): void {
if (!variant) {
variant = nullVariant;
}
let encodingByte = variant.dataType;
if (variant.arrayType === VariantArrayType.Array || variant.arrayType === VariantArrayType.Matrix) {
encodingByte |= VARIANT_ARRAY_MASK;
}
if (variant.dimensions && variant.arrayType === VariantArrayType.Matrix) {
assert(variant.dimensions.length >= 0);
encodingByte |= VARIANT_ARRAY_DIMENSIONS_MASK;
}
encodeUInt8(encodingByte, stream);
if (variant.arrayType === VariantArrayType.Array || variant.arrayType === VariantArrayType.Matrix) {
encodeVariantArray(variant.dataType, stream, variant.value);
} else {
const encode = get_encoder(variant.dataType || DataType.Null);
encode(variant.value, stream);
}
if ((encodingByte & VARIANT_ARRAY_DIMENSIONS_MASK) === VARIANT_ARRAY_DIMENSIONS_MASK && variant.dimensions) {
encodeDimension(variant.dimensions, stream);
}
}
/***
* @private
*
* istanbul ignore function
*/
function decodeDebugVariant(self: Variant, stream: BinaryStream, options: DecodeDebugOptions): void {
const tracer = options.tracer;
const encodingByte = decodeUInt8(stream);
const isArray = (encodingByte & VARIANT_ARRAY_MASK) === VARIANT_ARRAY_MASK;
const hasDimension = (encodingByte & VARIANT_ARRAY_DIMENSIONS_MASK) === VARIANT_ARRAY_DIMENSIONS_MASK;
self.dataType = (encodingByte & VARIANT_TYPE_MASK) as DataType;
tracer.dump("dataType: ", self.dataType);
tracer.dump("isArray: ", isArray ? "true" : "false");
tracer.dump("dimension: ", hasDimension);
const decode = findBuiltInType(DataType[self.dataType]).decode;
/* istanbul ignore next */
if (!decode) {
throw new Error("Variant.decode : cannot find decoder for type " + DataType[self.dataType]);
}
const cursorBefore = stream.length;
if (isArray) {
self.arrayType = hasDimension ? VariantArrayType.Matrix : VariantArrayType.Array;
_decodeVariantArrayDebug(stream, decode, tracer, self.dataType);
} else {
self.arrayType = VariantArrayType.Scalar;
self.value = decode(stream);
tracer.trace("member", "Variant", self.value, cursorBefore, stream.length, DataType[self.dataType]);
}
// ArrayDimensions
// Int32[]
// The length of each dimension.
// This field is only present if the array dimensions flag is set in the encoding mask.
// The lower rank dimensions appear first in the array.
// All dimensions shall be specified and shall be greater than zero.
// If ArrayDimensions are inconsistent with the ArrayLength then the decoder shall
// stop and raise a BadDecodingError.
if (hasDimension) {
self.dimensions = decodeDimension(stream);
const verification = calculate_product(self.dimensions);
}
}
function internalDecodeVariant(self: Variant, stream: BinaryStream) {
const encodingByte = decodeUInt8(stream);
const isArray: boolean = (encodingByte & VARIANT_ARRAY_MASK) === VARIANT_ARRAY_MASK;
const hasDimension: boolean = (encodingByte & VARIANT_ARRAY_DIMENSIONS_MASK) === VARIANT_ARRAY_DIMENSIONS_MASK;
self.dataType = (encodingByte & VARIANT_TYPE_MASK) as DataType;
if (isArray) {
self.arrayType = hasDimension ? VariantArrayType.Matrix : VariantArrayType.Array;
self.value = decodeVariantArray(self.dataType, stream);
} else {
self.arrayType = VariantArrayType.Scalar;
const decode = get_decoder(self.dataType);
self.value = decode(stream);
}
if (hasDimension) {
self.dimensions = decodeDimension(stream);
const verification = calculate_product(self.dimensions);
/* istanbul ignore next */
if (verification !== self.value.length) {
throw new Error("internalDecodeVariant: BadDecodingError: inconsistent matrix ");
}
}
}
/***
* @private
*/
export function decodeVariant(stream: BinaryStream, value?: Variant): Variant {
value = value || new Variant(null);
value.decode(stream);
return value;
}
function constructHook(options: VariantOptions | Variant): VariantOptions2 {
let isArrayTypeUnspecified = options.arrayType === undefined;
if (options instanceof Variant) {
const opts: VariantOptions2 = {
arrayType: options.arrayType,
dataType: options.dataType,
dimensions: options.dimensions,
value: options.value
};
if (opts.dataType === DataType.ExtensionObject) {
if (opts.arrayType === VariantArrayType.Scalar) {
if (opts.value && opts.value.constructor) {
opts.value = new opts.value.constructor(opts.value);
}
} else {
if (opts.value) {
opts.value = opts.value.map((e: any) => {
if (e && e.constructor) {
return new e.constructor(e);
}
return null;
});
}
}
} else if (opts.arrayType !== VariantArrayType.Scalar) {
opts.value = coerceVariantArray(opts.dataType, options.value);
}
return opts;
}
options.dataType = options.dataType || DataType.Null;
// dataType could be a string
if (typeof options.dataType === "string") {
const d = findBuiltInType(options.dataType);
/* istanbul ignore next */
if (!d) {
throw new Error("Cannot find Built-In data type or any DataType resolving to " + options.dataType);
}
options.dataType = DataType[d.name as keyof typeof DataType];
}
// array type could be a string
if (typeof options.arrayType === "string") {
const at: VariantArrayType | undefined = (VariantArrayType as any)[options.arrayType];
/* istanbul ignore next */
if (at === undefined) {
throw new Error("ArrayType: invalid " + options.arrayType);
}
options.arrayType = at;
}
if (isArrayTypeUnspecified && Array.isArray(options.value)) {
// when using UInt64 ou Int64 arrayType must be specified , as automatic detection cannot be made
/* istanbul ignore next */
if (options.dataType === DataType.UInt64 || options.dataType === DataType.Int64) {
// we do nothing here ....
throw new Error(
"Variant#constructor : when using UInt64 ou Int64" +
" arrayType must be specified , as automatic detection cannot be made"
);
} else {
options.arrayType = VariantArrayType.Array;
isArrayTypeUnspecified = false;
}
}
if (options.arrayType !== VariantArrayType.Scalar && !isArrayTypeUnspecified) {
assert(options.arrayType === VariantArrayType.Array || options.arrayType === VariantArrayType.Matrix);
/* istanbul ignore else */
if (options.arrayType === VariantArrayType.Array) {
const value1 = coerceVariantArray(options.dataType, options.value);
assert(value1 === null || value1 !== options.value);
options.value = value1;
} else {
assert(options.arrayType === VariantArrayType.Matrix);
options.value = options.value || [];
options.value = coerceVariantArray(options.dataType, options.value);
/* istanbul ignore next */
if (!options.dimensions) {
throw new Error("Matrix Variant : missing dimensions");
}
/* istanbul ignore next */
if (options.value.length != 0 && options.value.length !== calculate_product(options.dimensions)) {
throw new Error(
"Matrix Variant : invalid value size = options.value.length " +
options.value.length +
"!=" +
calculate_product(options.dimensions) +
" => " +
JSON.stringify(options.dimensions)
);
}
}
} else {
assert(options.arrayType === VariantArrayType.Scalar || options.arrayType === undefined);
options.arrayType = VariantArrayType.Scalar;
const tmp = options.value;
// scalar
options.value = coerceVariantType(options.dataType, options.value);
/* istanbul ignore next */
if (!isValidVariant(options.arrayType, options.dataType, options.value, null)) {
throw new Error(
"Invalid variant arrayType: " +
VariantArrayType[options.arrayType] +
" dataType: " +
DataType[options.dataType] +
" value:" +
options.value +
" (javascript type = " +
typeof options.value +
" )"
);
}
}
if (options.dimensions) {
assert(options.arrayType === VariantArrayType.Matrix, "dimension can only provided if variant is a matrix");
}
return options as VariantOptions2;
}
function calculate_product(array: number[] | null): number {
/* istanbul ignore next */
if (!array || array.length === 0) {
return 0;
}
return array.reduce((n: number, p: number) => n * p, 1);
}
function get_encoder(dataType: DataType) {
const dataTypeAsString = typeof dataType === "string" ? dataType : DataType[dataType];
/* istanbul ignore next */
if (!dataTypeAsString) {
throw new Error("invalid dataType " + dataType);
}
const encode = findBuiltInType(dataTypeAsString).encode;
/* istanbul ignore next */
if (!encode) {
throw new Error("Cannot find encode function for dataType " + dataTypeAsString);
}
return encode;
}
function get_decoder(dataType: DataType) {
const dataTypeAsString = DataType[dataType];
const decode = findBuiltInType(dataTypeAsString).decode;
/* istanbul ignore next */
if (!decode) {
throw new Error("Variant.decode : cannot find decoder for type " + dataTypeAsString);
}
return decode;
}
const displayWarning = true;
/***
* @private
*/
export type BufferedArray2 =
| Float32Array
| Float64Array
| Int8Array
| Int16Array
| Int32Array
| Uint8Array
| Uint16Array
| Uint32Array;
interface BufferedArrayConstructor {
BYTES_PER_ELEMENT: number;
new (buffer: any): any;
}
function convertTo(dataType: DataType, arrayTypeConstructor: BufferedArrayConstructor | null, value: any) {
// istanbul ignore next
if (value === undefined || value === null) {
return null;
}
if (arrayTypeConstructor && value instanceof arrayTypeConstructor) {
const newArray = new value.constructor(value.length); // deep copy
/* istanbul ignore if */
if (newArray instanceof Buffer) {
// required for nodejs 4.x
value.copy(newArray);
} else {
newArray.set(value);
}
return newArray;
}
const coerceFunc = coerceVariantType.bind(null, dataType);
const n = value.length;
const newArr: any = arrayTypeConstructor ? new arrayTypeConstructor(n) : new Array(n);
for (let i = 0; i < n; i++) {
newArr[i] = coerceFunc(value[i]);
}
// istanbul ignore next
if (arrayTypeConstructor && displayWarning && n > 10) {
warningLog("Warning ! an array containing " + DataType[dataType] + " elements has been provided as a generic array. ");
warningLog(
" This is inefficient as every array value will " + "have to be coerced and verified against the expected type"
);
warningLog(
" It is highly recommended that you use a " + " typed array ",
arrayTypeConstructor.constructor.name,
" instead"
);
}
return newArr;
}
interface DataTypeHelper {
coerce: (value: any) => any;
encode: (stream: OutputBinaryStream, value: any) => void;
decode: (stream: BinaryStream) => any;
}
const typedArrayHelpers: { [key: string]: DataTypeHelper } = {};
function _getHelper(dataType: DataType) {
return typedArrayHelpers[DataType[dataType]];
}
function coerceVariantArray(dataType: DataType, value: any) {
const helper = _getHelper(dataType);
if (helper) {
return helper.coerce(value);
} else {
return convertTo(dataType, null, value);
}
}
function encodeTypedArray(arrayTypeConstructor: BufferedArrayConstructor, stream: OutputBinaryStream, value: any) {
assert(value instanceof arrayTypeConstructor);
assert(value.buffer instanceof ArrayBuffer);
encodeUInt32(value.length, stream);
stream.writeArrayBuffer(value.buffer, value.byteOffset, value.byteLength);
}
function encodeGeneralArray(dataType: DataType, stream: OutputBinaryStream, value: any[] | null) {
if (!value) {
assert(value === null);
encodeUInt32(0xffffffff, stream);
return;
}
encodeUInt32(value.length, stream);
const encode = get_encoder(dataType);
for (const e of value) {
encode(e, stream);
}
}
function encodeVariantArray(dataType: DataType, stream: OutputBinaryStream, value: any) {
if (value && value.buffer) {
return _getHelper(dataType).encode(stream, value);
}
return encodeGeneralArray(dataType, stream, value);
}
function decodeTypedArray(arrayTypeConstructor: BufferedArrayConstructor, stream: BinaryStream) {
const length = decodeUInt32(stream);
/* istanbul ignore next */
if (length === 0xffffffff) {
return null;
}
if (length > Variant.maxTypedArrayLength) {
throw new Error(
`maxTypedArrayLength(${Variant.maxTypedArrayLength}) has been exceeded in Variant.decodeArray (typed Array) len=${length}`
);
}
const byteLength = length * arrayTypeConstructor.BYTES_PER_ELEMENT;
const arr = stream.readArrayBuffer(byteLength);
const value = new arrayTypeConstructor(arr.buffer);
assert(value.length === length);
return value;
}
function decodeGeneralArray(dataType: DataType, stream: BinaryStream) {
const length = decodeUInt32(stream);
/* istanbul ignore next */
if (length === 0xffffffff) {
return null;
}
if (length > Variant.maxArrayLength) {
throw new Error(`maxArrayLength(${Variant.maxArrayLength}) has been exceeded in Variant.decodeArray len=${length}`);
}
const decode = get_decoder(dataType);
const arr = [];
for (let i = 0; i < length; i++) {
arr.push(decode(stream));
}
return arr;
}
function decodeVariantArray(dataType: DataType, stream: BinaryStream) {
const helper = _getHelper(dataType);
if (helper) {
return helper.decode(stream);
} else {
return decodeGeneralArray(dataType, stream);
}
}
function _declareTypeArrayHelper(dataType: DataType, typedArrayConstructor: any) {
typedArrayHelpers[DataType[dataType]] = {
coerce: convertTo.bind(null, dataType, typedArrayConstructor),
decode: decodeTypedArray.bind(null, typedArrayConstructor),
encode: encodeTypedArray.bind(null, typedArrayConstructor)
};
}
_declareTypeArrayHelper(DataType.Float, Float32Array);
_declareTypeArrayHelper(DataType.Double, Float64Array);
_declareTypeArrayHelper(DataType.SByte, Int8Array);
_declareTypeArrayHelper(DataType.Byte, Uint8Array);
_declareTypeArrayHelper(DataType.Int16, Int16Array);
_declareTypeArrayHelper(DataType.Int32, Int32Array);
_declareTypeArrayHelper(DataType.UInt16, Uint16Array);
_declareTypeArrayHelper(DataType.UInt32, Uint32Array);
function _decodeVariantArrayDebug(stream: BinaryStream, decode: any, tracer: any, dataType: DataType) {
let cursorBefore = stream.length;
const length = decodeUInt32(stream);
let i;
let element;
tracer.trace("start_array", "Variant", -1, cursorBefore, stream.length);
if (length === 0xffffffff) {
// empty array
tracer.trace("end_array", "Variant", stream.length);
return;
}
const n1 = Math.min(10, length);
// display a maximum of 10 elements
for (i = 0; i < n1; i++) {
tracer.trace("start_element", "", i);
cursorBefore = stream.length;
element = decode(stream);
// arr.push(element);
tracer.trace("member", "Variant", element, cursorBefore, stream.length, DataType[dataType]);
tracer.trace("end_element", "", i);
}
// keep reading
if (length >= n1) {
for (i = n1; i < length; i++) {
decode(stream);
}
tracer.trace("start_element", "", n1);
tracer.trace("member", "Variant", "...", cursorBefore, stream.length, DataType[dataType]);
tracer.trace("end_element", "", n1);
}
tracer.trace("end_array", "Variant", stream.length);
}
function decodeDimension(stream: BinaryStream) {
return decodeGeneralArray(DataType.UInt32, stream);
}
function encodeDimension(dimensions: number[], stream: OutputBinaryStream) {
return encodeGeneralArray(DataType.UInt32, stream, dimensions);
}
function isEnumerationItem(value: any): boolean {
return (
value instanceof Object &&
Object.prototype.hasOwnProperty.call(value, "value") &&
Object.prototype.hasOwnProperty.call(value, "key") &&
value.constructor.name === "EnumValueType"
);
}
export function coerceVariantType(dataType: DataType, value: undefined | any): any {
/* eslint max-statements: ["error",1000], complexity: ["error",1000]*/
if (value === undefined) {
value = null;
}
if (isEnumerationItem(value)) {
// OPCUA Specification 1.0.3 5.8.2 encoding rules for various dataType:
// [...]Enumeration are always encoded as Int32 on the wire [...]
/* istanbul ignore next */
if (dataType !== DataType.Int32 && dataType !== DataType.ExtensionObject) {
throw new Error(
"expecting DataType.Int32 for enumeration values ;" + " got DataType." + dataType.toString() + " instead"
);
}
}
switch (dataType) {
case DataType.Null:
value = null;
break;
case DataType.LocalizedText:
if (!value || !value.schema || value.schema !== LocalizedText.schema) {
value = new LocalizedText(value);
}
break;
case DataType.QualifiedName:
if (!value || !value.schema || value.schema !== QualifiedName.schema) {
value = new QualifiedName(value);
}
break;
case DataType.Int32:
case DataType.Int16:
case DataType.UInt32:
case DataType.UInt16:
assert(value !== undefined);
value = parseInt(value, 10);
/* istanbul ignore next */
if (!isFinite(value)) {
throw new Error("expecting a number " + value);
}
break;
case DataType.UInt64:
value = coerceUInt64(value);
break;
case DataType.Int64:
value = coerceInt64(value);
break;
case DataType.ExtensionObject:
break;
case DataType.DateTime:
assert(value === null || !!value.getTime);
break;
case DataType.String:
assert(typeof value === "string" || value === null);
break;
case DataType.ByteString:
value = typeof value === "string" ? Buffer.from(value) : value;
// istanbul ignore next
if (!(value === null || value instanceof Buffer)) {
throw new Error("ByteString should be null or a Buffer");
}
assert(value === null || value instanceof Buffer);
break;
default:
assert(dataType !== undefined && dataType !== null, "Invalid DataType");
break;
case DataType.NodeId:
break;
}
return value;
}
function isValidScalarVariant(dataType: DataType, value: any): boolean {
assert(
value === null ||
DataType.Int64 === dataType ||
DataType.ByteString === dataType ||
DataType.UInt64 === dataType ||
!(value instanceof Array)
);
assert(value === null || !(value instanceof Int32Array));
assert(value === null || !(value instanceof Uint32Array));
switch (dataType) {
case DataType.NodeId:
return isValidNodeId(value);
case DataType.String:
return typeof value === "string" || isNullOrUndefined(value);
case DataType.Int64:
return isValidInt64(value);
case DataType.UInt64:
return isValidUInt64(value);
case DataType.UInt32:
return isValidUInt32(value);
case DataType.Int32:
return isValidInt32(value);
case DataType.UInt16:
return isValidUInt16(value);
case DataType.Int16:
return isValidInt16(value);
case DataType.Byte:
return isValidUInt8(value);
case DataType.SByte:
return isValidInt8(value);
case DataType.Boolean:
return isValidBoolean(value);
case DataType.ByteString:
return isValidByteString(value);
default:
return true;
}
}
function isValidArrayVariant(dataType: DataType, value: any): boolean {
if (value === null) {
return true;
}
if (dataType === DataType.Float && value instanceof Float32Array) {
return true;
} else if (dataType === DataType.Double && value instanceof Float64Array) {
return true;
} else if (dataType === DataType.SByte && value instanceof Int8Array) {
return true;
} else if (dataType === DataType.Byte && (value instanceof Buffer || value instanceof Uint8Array)) {
return true;
} else if (dataType === DataType.Int16 && value instanceof Int16Array) {
return true;
} else if (dataType === DataType.Int32 && value instanceof Int32Array) {
return true;
} else if (dataType === DataType.UInt16 && value instanceof Uint16Array) {
return true;
} else if (dataType === DataType.UInt32 && value instanceof Uint32Array) {
return true;
}
// array values can be store in Buffer, Float32Array
assert(Array.isArray(value));
for (const valueItem of value) {
if (!isValidScalarVariant(dataType, valueItem)) {
return false;
}
}
return true;
}
/*istanbul ignore next*/
function isValidMatrixVariant(dataType: DataType, value: any, dimensions: number[] | null) {
if (!dimensions) {
return false;
}
if (!isValidArrayVariant(dataType, value)) {
return false;
}
return true;
}
export function isValidVariant(
arrayType: VariantArrayType,
dataType: DataType,
value: unknown,
dimensions?: number[] | null
): boolean {
switch (arrayType) {
case VariantArrayType.Scalar:
return isValidScalarVariant(dataType, value);
case VariantArrayType.Array:
return isValidArrayVariant(dataType, value);
default:
assert(arrayType === VariantArrayType.Matrix);
return isValidMatrixVariant(dataType, value, dimensions!);
}
}
export function buildVariantArray(
dataType: DataType,
nbElements: number,
defaultValue: unknown
): Float32Array | Float64Array | Uint32Array | Int32Array | Uint16Array | Int16Array | Uint8Array | Int8Array | Array<unknown> {
let value;
switch (dataType) {
case DataType.Float:
value = new Float32Array(nbElements);
break;
case DataType.Double:
value = new Float64Array(nbElements);
break;
case DataType.UInt32:
value = new Uint32Array(nbElements);
break;
case DataType.Int32:
value = new Int32Array(nbElements);
break;
case DataType.UInt16:
value = new Uint16Array(nbElements);
break;
case DataType.Int16:
value = new Int16Array(nbElements);
break;
case DataType.Byte:
value = new Uint8Array(nbElements);
break;
case DataType.SByte:
value = new Int8Array(nbElements);
break;
default:
value = new Array(nbElements);
}
if (defaultValue !== undefined) {
for (let i = 0; i < nbElements; i++) {
value[i] = defaultValue;
}
}
return value;
}
// old version of nodejs do not provide a Buffer#equals test
const oldNodeVersion =
typeof process === "object" && process.versions && process.versions.node && process.versions.node.substring(0, 1) === "0";
function __type(a: any): string {
return Object.prototype.toString.call(a);
}
function __check_same_object(o1: any, o2: any): boolean {
if (o1 === o2) return true;
if ((!o1 && o2) || (!o2 && o1)) return false;
const t1 = __type(o1);
const t2 = __type(o2);
if (t1 !== t2) return false;
switch (t1) {
case "[object Array]":
return __check_same_array(o1, o2);
case "[object Object]": {
if (o1.constructor?.name !== o2.constructor?.name) {
return false;
}
const keys1 = Object.keys(o1);
const keys2 = Object.keys(o2);
// istanbul ignore next
if (keys1.length !== keys2.length) {
return false;
}
for (const k of Object.keys(o1)) {
if (!__check_same_object(o1[k], o2[k])) {
return false;
}
}
return true;
}
case "[object Float32Array]":
case "[object Float64Array]":
case "[object Int32Array]":
case "[object Int16Array]":
case "[object Int8Array]":
case "[object Uint32Array]":
case "[object Uint16Array]":
case "[object Uint8Array]": {
const b1 = Buffer.from(o1.buffer, o1.byteOffset, o1.byteLength);
const b2 = Buffer.from(o2.buffer, o2.byteOffset, o2.byteLength);
return b1.equals(b2);
}
default:
return o1 === o2;
}
}
function __check_same_array(arr1: any, arr2: any) {
if (!arr1 || !arr2) {
return !arr1 && !arr2;
}
if (arr1.length !== arr2.length) {
return false;
}
if (arr1.length === 0 && 0 === arr2.length) {
return true;
}
if (!oldNodeVersion && arr1.buffer) {
// v1 and v2 are TypedArray (such as Int32Array...)
// this is the most efficient way to compare 2 buffers but it doesn't work with node <= 0.12
assert(arr2.buffer && __type(arr2.buffer) === "[object ArrayBuffer]");
// compare byte by byte
const b1 = Buffer.from(arr1.buffer, arr1.byteOffset, arr1.byteLength);
const b2 = Buffer.from(arr2.buffer, arr2.byteOffset, arr2.byteLength);
return b1.equals(b2);
}
const n = arr1.length;
for (let i = 0; i < n; i++) {
if (!__check_same_object(arr1[i], arr2[i])) {
return false;
}
}
return true;
}
/***
* returns true if the two variant represent the same value
* @param v1 the first variant to compare
* @param v2 the variant to compare with
*/
export function sameVariant(v1: Variant, v2: Variant): boolean {
if (v1 === v2) {
return true;
}
if ((!v1 && v2) || (v1 && !v2)) {
return false;
}
if (v1.arrayType !== v2.arrayType) {
return false;
}
if (v1.dataType !== v2.dataType) {
return false;
}
if (v1.value === v2.value) {
return true;
}
if (v1.arrayType === VariantArrayType.Scalar) {
if (v1.dataType === DataType.ExtensionObject) {
// compare two extension objects
return __check_same_object(v1.value, v2.value);
}
if (Array.isArray(v1.value) && Array.isArray(v2.value)) {
return __check_same_array(v1.value, v2.value);
}
if (Buffer.isBuffer(v1.value) && Buffer.isBuffer(v2.value)) {
return v1.value.equals(v2.value);
}
}
if (v1.arrayType === VariantArrayType.Array) {
return __check_same_array(v1.value, v2.value);
} else if (v1.arrayType === VariantArrayType.Matrix) {
if (!__check_same_array(v1.dimensions, v2.dimensions)) {
return false;
}
return __check_same_array(v1.value, v2.value);
}
return false;
}
// ---------------------------------------------------------------------------------------------------------
registerSpecialVariantEncoder(Variant);
export interface VariantOptionsT<T, DT extends DataType> extends VariantOptions {
dataType: DT;
arrayType?: VariantArrayType | string;
value: T;
dimensions?: number[] | null;
}
export interface VariantT<T, DT extends DataType> extends Variant {
value: T;
dataType: DT;
}
export declare type VariantByteString = VariantT<Buffer, DataType.ByteString>;
export declare type VariantDouble = VariantT<number, DataType.Double>;
registerType({
name: "Variant",
subType: "",
coerce: _coerceVariant,
encode: encodeVariant,
decode: decodeVariant
});