protodef
Version:
A simple yet powerful way to define binary protocols
169 lines (148 loc) • 5.3 kB
JavaScript
const { getFieldInfo, tryCatch } = require('./utils')
const reduce = require('lodash.reduce')
const Validator = require('protodef-validator')
function isFieldInfo (type) {
return typeof type === 'string' ||
(Array.isArray(type) && typeof type[0] === 'string') ||
type.type
}
function findArgs (acc, v, k) {
if (typeof v === 'string' && v.charAt(0) === '$') { acc.push({ path: k, val: v.substr(1) }) } else if (Array.isArray(v) || typeof v === 'object') { acc = acc.concat(reduce(v, findArgs, []).map((v) => ({ path: k + '.' + v.path, val: v.val }))) }
return acc
}
function setField (path, val, into) {
const c = path.split('.').reverse()
while (c.length > 1) {
into = into[c.pop()]
}
into[c.pop()] = val
}
function extendType (functions, defaultTypeArgs) {
const json = JSON.stringify(defaultTypeArgs)
const argPos = reduce(defaultTypeArgs, findArgs, [])
function produceArgs (typeArgs) {
const args = JSON.parse(json)
argPos.forEach((v) => {
setField(v.path, typeArgs[v.val], args)
})
return args
}
return [function read (buffer, offset, typeArgs, context) {
return functions[0].call(this, buffer, offset, produceArgs(typeArgs), context)
}, function write (value, buffer, offset, typeArgs, context) {
return functions[1].call(this, value, buffer, offset, produceArgs(typeArgs), context)
}, function sizeOf (value, typeArgs, context) {
if (typeof functions[2] === 'function') { return functions[2].call(this, value, produceArgs(typeArgs), context) } else { return functions[2] }
}]
}
class ProtoDef {
constructor (validation = true) {
this.types = {}
this.validator = validation ? new Validator() : null
this.addDefaultTypes()
}
addDefaultTypes () {
this.addTypes(require('./datatypes/numeric'))
this.addTypes(require('./datatypes/utils'))
this.addTypes(require('./datatypes/structures'))
this.addTypes(require('./datatypes/conditional'))
}
addProtocol (protocolData, path) {
const self = this
function recursiveAddTypes (protocolData, path) {
if (protocolData === undefined) { return }
if (protocolData.types) { self.addTypes(protocolData.types) }
recursiveAddTypes(protocolData?.[path[0]], path.slice(1))
}
if (this.validator) { this.validator.validateProtocol(protocolData) }
recursiveAddTypes(protocolData, path)
}
addType (name, functions, validate = true) {
if (functions === 'native') {
if (this.validator) { this.validator.addType(name) }
return
}
if (isFieldInfo(functions)) {
if (this.validator) {
if (validate) { this.validator.validateType(functions) }
this.validator.addType(name)
}
const { type, typeArgs } = getFieldInfo(functions)
this.types[name] = typeArgs ? extendType(this.types[type], typeArgs) : this.types[type]
} else {
if (this.validator) {
if (functions[3]) {
this.validator.addType(name, functions[3])
} else { this.validator.addType(name) }
}
this.types[name] = functions
}
}
addTypes (types) {
Object.keys(types).forEach((name) => this.addType(name, types[name], false))
if (this.validator) {
Object.keys(types).forEach((name) => {
if (isFieldInfo(types[name])) {
this.validator.validateType(types[name])
}
})
}
}
setVariable (key, val) {
this.types[key] = val
}
read (buffer, cursor, _fieldInfo, rootNodes) {
const { type, typeArgs } = getFieldInfo(_fieldInfo)
const typeFunctions = this.types[type]
if (!typeFunctions) { throw new Error('missing data type: ' + type) }
return typeFunctions[0].call(this, buffer, cursor, typeArgs, rootNodes)
}
write (value, buffer, offset, _fieldInfo, rootNode) {
const { type, typeArgs } = getFieldInfo(_fieldInfo)
const typeFunctions = this.types[type]
if (!typeFunctions) { throw new Error('missing data type: ' + type) }
return typeFunctions[1].call(this, value, buffer, offset, typeArgs, rootNode)
}
sizeOf (value, _fieldInfo, rootNode) {
const { type, typeArgs } = getFieldInfo(_fieldInfo)
const typeFunctions = this.types[type]
if (!typeFunctions) {
throw new Error('missing data type: ' + type)
}
if (typeof typeFunctions[2] === 'function') {
return typeFunctions[2].call(this, value, typeArgs, rootNode)
} else {
return typeFunctions[2]
}
}
createPacketBuffer (type, packet) {
const length = tryCatch(() => this.sizeOf(packet, type, {}),
(e) => {
e.message = `SizeOf error for ${e.field} : ${e.message}`
throw e
})
const buffer = Buffer.allocUnsafe(length)
tryCatch(() => this.write(packet, buffer, 0, type, {}),
(e) => {
e.message = `Write error for ${e.field} : ${e.message}`
throw e
})
return buffer
}
parsePacketBuffer (type, buffer, offset = 0) {
const { value, size } = tryCatch(() => this.read(buffer, offset, type, {}),
(e) => {
e.message = `Read error for ${e.field} : ${e.message}`
throw e
})
return {
data: value,
metadata: {
size
},
buffer: buffer.slice(0, size),
fullBuffer: buffer
}
}
}
module.exports = ProtoDef