Buffer Parser
Buffer Parser
MIT License
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
/**
* Generate a spec item from users input
* @param {object} item - a spec item with properties name, type, offset
and length
* @param {Number} itemNumber - which item is this
* @returns An object with expected properties that has been (kinda)
validated
*/
function parseSpecificationItem(item, itemNumber) {
if (!item)
throw new Error("Spec item is invalid");
let isObject = (item != null && typeof item === 'object' &&
(Array.isArray(item) === false));
if (!isObject)
throw new Error("Spec item is invalid");
let formattedSpecItem = Object.assign({}, item, {
"name": item.name || "item" + itemNumber,
"type": item.type,
"offset": item.offset,
"offsetbit": item.offsetbit,
"scale": item.scale,
"length": item.length || 1,
"id": itemNumber - 1
});
//validate type
if (!TYPEOPTS.includes(formattedSpecItem.type.toLowerCase())) {
throw new Error("'" + formattedSpecItem.type + "' is not a valid
type (item '" + (formattedSpecItem.name || "unnamed") + "')");
}
//compile scaler
let scale = null;
if (formattedSpecItem.scale) {
if (typeof formattedSpecItem.scale == "number") {
scale = formattedSpecItem.scale.toString();
} else {
scale = formattedSpecItem.scale &&
formattedSpecItem.scale.trim();
}
}
if (scale) {
try {
if (scale != "1" && scale != "0") {
if (isNumber(scale)) {
formattedSpecItem.scaler = { operator: '*', operand:
Number(scale) };
} else {
if (scale == "!" || scale == "!!") scale += "0"
let matches = [];
if(!scale.matchAll) {
//throw new Error("Scaling equations not supported
by the running version of node-js. It is recommended you upgrade nodejs to V12 or
greater. Alternatively, do your own scaling on the output.")
//TEMP: emulate matchAll
while ((match = scalerRegex.exec(scale)) !== null)
{
matches.push([...match])
break;
}
} else {
matches = scale.matchAll(scalerRegex);
}
for (const match of matches) {
formattedSpecItem.scaler = {
operator: match["1"].trim(),
operand: Number(match["2"])
}
break;
}
if (!formattedSpecItem.scaler || !
formattedSpecItem.scaler.operator || !scalingOps[formattedSpecItem.scaler.operator]
|| !isNumber(formattedSpecItem.scaler.operand)) {
throw new Error("scaling equation '" +
formattedSpecItem.scale + "' is not valid (item " + itemNumber + " '" +
(formattedSpecItem.name || "unnamed") + "')");
}
}
}
} catch (e) {
throw e
}
}
return formattedSpecItem;
}
/**
* Check the provided specification is valid & set any defaults. Throws an
error if the specification is invalid.
* @param {object | string} specification
* @returns correctly formated and validate specification object
*/
function parseSpecification(specification) {
if (typeof specification == "string") {
specification = JSON.parse();
}
let _spec = {
options: {
byteSwap: false,
resultType: "value",
singleResult: true
},
items: []
};
//validate resultType
if (!RESULTYPEOPTS.includes(_spec.options.resultType)) {
throw new Error("resultType property is invalid");
}
//validate byteSwap
if (Array.isArray(_spec.options.byteSwap)) {
let allFound = _spec.options.byteSwap.every(ai =>
SWAPOPTS.includes(ai));
if (!allFound) {
throw new Error("byteSwap property contains unsupported
option");
}
}
return _spec;
}
/**
* parser function reads the provided `specification` (json or JS object)
and converts the items in the `data` to the type specified in each element of
`specification.items`
*
* @param {Buffer|integer[]} data - The data to parse. Must be either an
array of `integer` or a `Buffer`
* @param {object} specification - an object with `{options:{byteSwap:
boolean}}` and `{items[ {name: string, offset: number, length: number, type:
string} ]}`
* @returns result object containing . `objectResults:{}`,
`arrayResults[]`, `values[]`
*/
function parser(data, validatedSpec, msg) {
let result = {
objectResults: {},
keyvalues: {},
arrayResults: [],
values: [],
specification: validatedSpec
}
//get buffer
if (isBuffer) {
buf = data;
}
}
} else {
try {
buf.swap16();
} catch (error) {
throw new Error("Cannot swap16: " + error.message);
}
}
}
return value;
result.buffer = buf;
if (validatedSpec.options.resultType === "buffer") {
return result;
}
case 'int16':
case 'int16be':
itemReader(item, buf, "readInt16BE", 2);
break;
case 'uint16le':
itemReader(item, buf, "readUInt16LE", 2);
break;
case 'uint16':
case 'uint16be':
itemReader(item, buf, "readUInt16BE", 2);
break;
case 'int32le':
itemReader(item, buf, "readInt32LE", 4);
break;
case 'int32':
case 'int32be':
itemReader(item, buf, "readInt32BE", 4);
break;
case 'uint32le':
itemReader(item, buf, "readUInt32LE", 4);
break;
case 'uint32':
case 'uint32be':
itemReader(item, buf, "readUInt32BE", 4);
break;
case 'bigint64le':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0
or greater")
}
itemReader(item, buf, "readBigInt64LE", 8);
break;
case 'bigint64':
case 'bigint64be':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0
or greater")
}
itemReader(item, buf, "readBigInt64BE", 8);
break;
case 'biguint64le':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0
or greater")
}
itemReader(item, buf, "readBigUInt64LE", 8);
break;
case 'biguint64':
case 'biguint64be':
if(!SUPPORTS_BIGINT) {
throw new Error("BigInt operations require NODE v10.4.0
or greater")
}
itemReader(item, buf, "readBigUInt64BE", 8);
break;
if (Array.isArray(data) == false) {
data = [data]
}
for (let index = 0; index < data.length; index++) {
const thisByte = data[index];
let bits = byteToBits(thisByte);
bitData.push(...bits.bits.map(e => e ? true :
false));
}
if (length === 1) {
item.value = bitData[item.offsetbit];
} else if (length === -1) {
item.value = bitData.slice(item.offsetbit); // -1 -
return all to the end.
} else {
item.value = bitData.slice(item.offsetbit,
item.offsetbit + length);
}
setObjectProperty(result.objectResults, item.name,
item, "=>");
setObjectProperty(result.keyvalues, item.name,
item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "8bit":
{
let data = dataGetter(buf, item.offset, length,
"readUInt8", 1, item.mask)
let bitData = [];
if (Array.isArray(data) === false) {
data = [data]
}
for (let index = 0; index < data.length; index++) {
const thisByte = data[index];
let bits = byteToBits(thisByte);
bitData.push(bits);
}
item.value = bitData;
setObjectProperty(result.objectResults, item.name,
item, "=>");
setObjectProperty(result.keyvalues, item.name,
item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "16bit":
case "16bitle":
case "16bitbe":
{
let fn = type == "16bitle" ? "readUInt16LE" :
"readUInt16BE";
let data = dataGetter(buf, item.offset, length, fn, 2,
item.mask)
let bitData = [];
if (Array.isArray(data) == false) {
data = [data];
}
for (let index = 0; index < data.length; index++) {
const thisByte = data[index];
let bits = wordToBits(thisByte);
bitData.push(bits);
}
item.value = bitData;
setObjectProperty(result.objectResults, item.name,
item, "=>");
setObjectProperty(result.keyvalues, item.name,
item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "bcd":
case "bcdle":
case "bcdbe":
{
let fn = type == "bcdle" ? "readUInt16LE" :
"readUInt16BE";
let data = dataGetter(buf, item.offset, length, fn, 2,
item.mask)
if (item.length > 1) {
dataBCD = data.map(e => bcd2number(e));
} else {
dataBCD = bcd2number(data)
}
item.value = dataBCD;
setObjectProperty(result.objectResults, item.name,
item, "=>");
setObjectProperty(result.keyvalues, item.name,
item.value, "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
case "buffer":
{
let _end = length === -1 ? undefined : offset + length;
item.value = buf.slice(offset, _end);
setObjectProperty(result.objectResults, item.name,
item, "=>", "=>");
setObjectProperty(result.keyvalues, item.name,
item.value, "=>", "=>");
result.arrayResults.push(item);
result.values.push(item.value);
}
break;
default: {
let errmsg = `type '${item.type}' is not a recognised parse
specification`;
console.warn(errmsg);
throw new Error(errmsg);
break;
}
}
if (validatedSpec.options.singleResult === false) {
let m = { topic: msg.topic, specification: item };
if (validatedSpec.options.setTopic) m.topic = item.name;
switch (validatedSpec.options.resultType) {
case "value":
case "keyvalue":
setObjectProperty(m, validatedSpec.options.msgProperty,
item.value, ".")
break;
case "object":
setObjectProperty(m, validatedSpec.options.msgProperty,
item, ".")
break;
}
if (node.fanOutMultipleResult) {
fanOut[itemIndex] = m;
} else {
node.send(m);
}
}
}
if (node.fanOutMultipleResult) {
return fanOut;
}
return result;
}
if (node.specificationType == "ui") {
specification = {};
var swap1;
RED.util.evaluateNodeProperty(node.swap1, node.swap1Type, node,
msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap1", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to
evaluate swap1" });
return;//halt flow!
} else {
if (node.swap1Type == "env") {
swap1 = value.split(",");
swap1 = swap1.map(e => e.trim());
} else {
swap1 = value;
}
}
});
var swap2;
var swap3;
if (node.swap1Type == "swap") {
RED.util.evaluateNodeProperty(node.swap2, node.swap2Type, node,
msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap2", msg);
node.status({ fill: "red", shape: "ring", text: "Unable
to evaluate swap2" });
return;//halt flow!
} else {
swap2 = value;
}
});
RED.util.evaluateNodeProperty(node.swap3, node.swap3Type, node,
msg, (err, value) => {
if (err) {
node.error("Unable to evaluate swap3", msg);
node.status({ fill: "red", shape: "ring", text: "Unable
to evaluate swap3" });
return;//halt flow!
} else {
swap3 = value;
}
});
}
var resultType;
RED.util.evaluateNodeProperty(node.resultType, node.resultTypeType,
node, msg, (err, value) => {
if (err) {
node.error("Unable to evaluate resultType", msg);
node.status({ fill: "red", shape: "ring", text: "Unable to
evaluate resultType" });
return;//halt flow!
} else {
resultType = value;
}
});
var msgProperty = node.msgProperty;
let validatedSpec;
try {
validatedSpec = parseSpecification(specification)
} catch (error) {
node.error(error, msg);
node.status({ fill: "red", shape: "dot", text: error.message });
return;//halt flow
}
switch (validatedSpec.options.resultType) {
case "buffer":
setObjectProperty(msg,
validatedSpec.options.msgProperty, msg.buffer, ".");
break;
case "value":
setObjectProperty(msg,
validatedSpec.options.msgProperty, msg.values, ".");
break;
case "object":
setObjectProperty(msg,
validatedSpec.options.msgProperty, msg.objectResults, ".");
break;
case "keyvalue":
case "keyvalues":
setObjectProperty(msg,
validatedSpec.options.msgProperty, msg.keyvalues, ".");
break;
case "array":
setObjectProperty(msg,
validatedSpec.options.msgProperty, msg.arrayResults, ".");
break;
}
node.send(msg);
} else if (node.fanOutMultipleResult) {
node.send(results);
}
} catch (error) {
node.error(error, msg);
node.status({ fill: "red", shape: "dot", text: "Error parsing data"
});
return;//halt flow
}
});
}
RED.nodes.registerType("buffer-parser", bufferParserNode);
}