UNPKG

nimcodec

Version:

Encoder/decoder for satellite IoT using Non-IP Messages

347 lines (333 loc) 10 kB
/** * Codec functions for Non-IP Modems based on ORBCOMM protocols * @namespace cbc */ require('dotenv').config(); const mathjs = require('mathjs'); const { isInt } = require('../../bitman'); const { FieldType, MessageDirection } = require('./types'); const { decodeMessage, encodeMessage } = require('./message'); /** * Check if a variable is a valid object (non-null, non-array) * @param {*} candidate * @returns */ function isObject_(candidate) { if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) return false; return true; } /** * Check if a variable is a valid non-empty string * @param {*} candidate * @returns */ function isValidString_(candidate) { if (typeof candidate !== 'string' || candidate.trim().length === 0) { console.error(`Invalid string`); return false; } return true; } /** * Check if a string conforms to semantic versioning e.g. 1.2.3 * @param {*} candidate * @returns */ function isValidSemVer_(candidate) { if (!isValidString_(candidate)) return false; let result = true; if (candidate.includes('.')) { let parts = candidate.split('.'); for (const part of parts) { if (!isInt(part, true)) result = false; } } else { if (!isInt(candidate, true)) result = false; } if (!result) console.error(`Invalid semver`); return result; } /** * Check if an object meets the Field structural definition * @param {*} candidate * @returns */ function isValidFieldDef_(candidate) { if (!isObject_(candidate)) return false; const mFieldKeys = ['name', 'type']; const oFieldKeys = ['description', 'optional', 'fixed', 'calc']; const cFieldKeys = { // bool: [], // float: [], int: ['size'], uint: ['size'], enum: ['size', 'enum'], bitmask: ['size', 'enum'], string: ['size'], data: ['size'], array: ['size', 'fields'], struct: ['fields'], bitmaskarray: ['size', 'enum', 'fields'], }; for (const key of mFieldKeys) { if (!Object.hasOwn(candidate, key)) { console.error(`Missing key: ${key}`); return false; } const v = candidate[key]; if (key === 'name') { if (!isValidString_(v)) { console.error(`Invalid field name`); return false; } } else if (key === 'type') { if (!Object.values(FieldType).includes(v)) { console.error(`Invalid type: ${v}`); return false; } if (Object.keys(cFieldKeys).includes(v)) { const mKeys = cFieldKeys[v]; for (const fKey of mKeys) { if (!Object.hasOwn(candidate, fKey)) { console.error(`Missing type-conditional key ${fKey}`); return false; } const fv = candidate[fKey]; if (fKey === 'size') { if (!isInt(fv) || fv <= 0) { console.error(`Invalid size`); return false; } } else if (fKey === 'enum') { if (!isObject_(fv)) { console.error(`Invalid enum definition`); return false; } if (!candidate.size) { console.error(`Enum missing size`); return false; } let maxEntries = candidate.size; if (candidate.type === 'enum') { maxEntries = 2**maxEntries; } if (Object.keys(fv).length > maxEntries) { console.error(`Enum entries larger than max size`); return false; } for (const [ek, ev] of Object.entries(fv)) { if (!isInt(ek, true)) { console.error(`Enum key must be parsable as integer`); return false; } if (parseInt(ek) < 0 || parseInt(ek) >= maxEntries) { console.error(`Enum key out of range`); return false; } if (!isValidString_(ev)) { console.error(`Invalid enum value must be non-empty string`); return false; } } const eValues = Object.values(fv); const duplicates = eValues.filter((item, idx) => { return eValues.indexOf(item) !== idx; }); if (duplicates.length > 0) { console.error(`Duplicate enum value`); return false; } } else if (fKey === 'fields') { if (!Array.isArray(fv)) { console.error(`Invalid fields must be non-empty array`); return false; } for (const field of fv) { if (!isValidFieldDef_(field)) return false; } } } } } } for (const key of oFieldKeys) { if (Object.hasOwn(candidate, key)) { if (key === 'description') { if (!isValidString_(candidate[key])) return false; } else if (['optional', 'fixed'].includes(key)) { if (typeof candidate[key] !== 'boolean') return false; if (key === 'fixed' && !['string', 'data', 'array'].includes(candidate.type)) return false; } else if (['decalc', 'encalc'].includes(key)) { if (!isValidString_(candidate[key]) || !candidate[key].includes('v') || !['int', 'uint'].includes(candidate.type)) return false; try { let expr = candidate[key].replaceAll('v', 1); if (isNaN(mathjs.evaluate(expr))) return false; } catch { return false; } } } } return true; } /** * Check if an object meets the Message structural definition * @param {*} candidate * @returns */ function isValidMessageDef_(candidate) { if (!isObject_(candidate)) return false; const mMessageKeys = ['direction', 'name', 'messageKey', 'fields']; const oMessageKeys = ['description']; // const coapMessageKeys = ['coapVersion', 'coapType', 'coapTokenLength', // 'coapCodeClass', 'coapCodeMethod' ]; for (const key of mMessageKeys) { if (!Object.hasOwn(candidate, key)) { console.error(`Missing mandatory key ${key}`); return false; } const v = candidate[key]; if (key === 'name') { if (!isValidString_(v)) { console.error('Invalid message name'); return false; } } else if (key === 'direction') { if (![MessageDirection.MO, MessageDirection.MT].includes(v)) { console.error('Invalid message direction'); return false; } } else if (key === 'messageKey') { if (typeof v !== 'number' || v < 49152 || v > 65535) { console.error(`Invalid or out-of-range messageKey value`); return false; } if (v > 65279) { const viasatKey = process.env.VIASAT_KEY; if (!Object.hasOwn(candidate, 'viasatKey') || candidate.viasatKey !== viasatKey) { console.error(`Reserved messageKey`); return false; } } } else if (key === 'fields') { if (!Array.isArray(v) || v.length === 0) { console.error(`Invalid fields must be non-empty array`); return false; } for (const field of v) { if (!isValidFieldDef_(field)) return false; } } } for (const key of oMessageKeys) { if (Object.hasOwn(candidate, key)) { const v = candidate[key]; if (key === 'description') { if (!isValidString_(v)) { console.error(`Invalid description must be non-empty string if present`); return false; } } } } // if (Object.keys(candidate).filter(k => k.startsWith('coap')).length > 0) { // for (const key of coapMessageKeys) { // if (!Object.hasOwn(candidate, key)) // return false; // const v = candidate[key]; // if (key === 'coapVersion') { // if (v !== 1) // return false; // } else if (key === 'coapType') { // if (![0, 1, 2, 3].includes(v)) // return false; // } else if (key === 'coapTokenLength') { // if (typeof v !== 'number' || v < 0 || v > 8) // return false; // } else if (key === 'coapCodeClass') { // if (typeof v !== 'number' || v < 0 || v > 8) // return false; // } else if (key === 'coapCodeMethod') { // if (typeof v !== 'number' || v < 0 || v > 31) // return false; // } // } // } return true; } /** * Check if an object meets the Codec structural definition * @param {*} candidate * @returns */ function isValidCodec(candidate) { if (!isObject_(candidate)) return false; const mMetaKeys = ['application', 'messages']; const oMetaKeys = ['version', 'description']; for (const key of mMetaKeys) { if (!Object.hasOwn(candidate, key)) return false; const v = candidate[key]; if (key === 'application') { if (!isValidString_(v)) return false; } else if (key === 'messages') { if (!Array.isArray(v) || v.length === 0) return false; for (const message of v) { if (!isValidMessageDef_(message)) return false; } // check for duplicate names const nameCount = {}; v.forEach(message => { const name = message.name; if (!nameCount[name]) nameCount[name] = 0; nameCount[name]++; }); if (Object.values(nameCount).some(e => e > 1)) return false; } } for (const key of oMetaKeys) { if (Object.hasOwn(candidate, key)) { if (key === 'version') { if (!isValidSemVer_(candidate[key])) return false; } else if (key === 'description') { if (!isValidString_(candidate[key])) return false; } } } return true; } module.exports = { FieldType, MessageDirection, isValidCodec, decodeMessage, encodeMessage, };