protodef
Version:
A simple yet powerful way to define binary protocols
248 lines (241 loc) • 9.57 kB
JavaScript
module.exports = {
Read: {
pstring: ['parametrizable', (compiler, string) => {
let code = ''
if (string.countType) {
code += 'const { value: count, size: countSize } = ' + compiler.callType(string.countType) + '\n'
} else if (string.count) {
code += 'const count = ' + string.count + '\n'
code += 'const countSize = 0\n'
} else {
throw new Error('pstring must contain either count or countType')
}
code += 'offset += countSize\n'
code += 'if (offset + count > buffer.length) {\n'
code += ' throw new PartialReadError("Missing characters in string, found size is " + buffer.length + " expected size was " + (offset + count))\n'
code += '}\n'
code += `return { value: buffer.toString("${string.encoding || 'utf8'}", offset, offset + count), size: count + countSize }`
return compiler.wrapCode(code)
}],
buffer: ['parametrizable', (compiler, buffer) => {
let code = ''
if (buffer.countType) {
code += 'const { value: count, size: countSize } = ' + compiler.callType(buffer.countType) + '\n'
} else if (buffer.count) {
code += 'const count = ' + buffer.count + '\n'
code += 'const countSize = 0\n'
} else {
throw new Error('buffer must contain either count or countType')
}
code += 'offset += countSize\n'
code += 'if (offset + count > buffer.length) {\n'
code += ' throw new PartialReadError()\n'
code += '}\n'
code += 'return { value: buffer.slice(offset, offset + count), size: count + countSize }'
return compiler.wrapCode(code)
}],
bitfield: ['parametrizable', (compiler, values) => {
let code = ''
const totalBytes = Math.ceil(values.reduce((acc, { size }) => acc + size, 0) / 8)
code += `if ( offset + ${totalBytes} > buffer.length) { throw new PartialReadError() }\n`
const names = []
let totalSize = 8
code += 'let bits = buffer[offset++]\n'
for (const i in values) {
const { name, size, signed } = values[i]
const trueName = compiler.getField(name)
while (totalSize < size) {
totalSize += 8
code += 'bits = (bits << 8) | buffer[offset++]\n'
}
code += `let ${trueName} = (bits >> ` + (totalSize - size) + ') & 0x' + ((1 << size) - 1).toString(16) + '\n'
if (signed) code += `${trueName} -= (${trueName} & 0x` + (1 << (size - 1)).toString(16) + ') << 1\n'
totalSize -= size
if (name === trueName) names.push(name)
else names.push(`${name}: ${trueName}`)
}
code += 'return { value: { ' + names.join(', ') + ` }, size: ${totalBytes} }`
return compiler.wrapCode(code)
}],
bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => {
let fstr = JSON.stringify(flags)
if (Array.isArray(flags)) {
fstr = '{'
for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',')
fstr += '}'
} else if (shift) {
fstr = '{'
for (const key in flags) fstr += `"${key}": ${1 << flags[key]}${big ? 'n,' : ','}`
fstr += '}'
}
return compiler.wrapCode(`
const { value: _value, size } = ${compiler.callType(type, 'offset')}
const value = { _value }
const flags = ${fstr}
for (const key in flags) {
value[key] = (_value & flags[key]) == flags[key]
}
return { value, size }
`.trim())
}],
mapper: ['parametrizable', (compiler, mapper) => {
let code = 'const { value, size } = ' + compiler.callType(mapper.type) + '\n'
code += 'return { value: ' + JSON.stringify(sanitizeMappings(mapper.mappings)) + '[value] || value, size }'
return compiler.wrapCode(code)
}]
},
Write: {
pstring: ['parametrizable', (compiler, string) => {
let code = `const length = Buffer.byteLength(value, "${string.encoding || 'utf8'}")\n`
if (string.countType) {
code += 'offset = ' + compiler.callType('length', string.countType) + '\n'
} else if (string.count === null) {
throw new Error('pstring must contain either count or countType')
}
code += `buffer.write(value, offset, length, "${string.encoding || 'utf8'}")\n`
code += 'return offset + length'
return compiler.wrapCode(code)
}],
buffer: ['parametrizable', (compiler, buffer) => {
let code = 'if (!(value instanceof Buffer)) value = Buffer.from(value)\n'
if (buffer.countType) {
code += 'offset = ' + compiler.callType('value.length', buffer.countType) + '\n'
} else if (buffer.count === null) {
throw new Error('buffer must contain either count or countType')
}
code += 'value.copy(buffer, offset)\n'
code += 'return offset + value.length'
return compiler.wrapCode(code)
}],
bitfield: ['parametrizable', (compiler, values) => {
let toWrite = ''
let bits = 0
let code = ''
for (const i in values) {
let { name, size } = values[i]
const trueName = compiler.getField(name)
code += `let ${trueName} = value.${name}\n`
while (size > 0) {
const writeBits = Math.min(8 - bits, size)
const mask = ((1 << writeBits) - 1)
if (toWrite !== '') toWrite = `((${toWrite}) << ${writeBits}) | `
toWrite += `((${trueName} >> ` + (size - writeBits) + ') & 0x' + mask.toString(16) + ')'
size -= writeBits
bits += writeBits
if (bits === 8) {
code += 'buffer[offset++] = ' + toWrite + '\n'
bits = 0
toWrite = ''
}
}
}
if (bits !== 0) {
code += 'buffer[offset++] = (' + toWrite + ') << ' + (8 - bits) + '\n'
}
code += 'return offset'
return compiler.wrapCode(code)
}],
bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => {
let fstr = JSON.stringify(flags)
if (Array.isArray(flags)) {
fstr = '{'
for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',')
fstr += '}'
} else if (shift) {
fstr = '{'
for (const key in flags) fstr += `"${key}": ${1 << flags[key]}${big ? 'n,' : ','}`
fstr += '}'
}
return compiler.wrapCode(`
const flags = ${fstr}
let val = value._value ${big ? '|| 0n' : ''}
for (const key in flags) {
if (value[key]) val |= flags[key]
}
return (ctx.${type})(val, buffer, offset)
`.trim())
}],
mapper: ['parametrizable', (compiler, mapper) => {
const mappings = JSON.stringify(swapMappings(mapper.mappings))
const code = 'return ' + compiler.callType(`${mappings}[value] || value`, mapper.type)
return compiler.wrapCode(code)
}]
},
SizeOf: {
pstring: ['parametrizable', (compiler, string) => {
let code = `let size = Buffer.byteLength(value, "${string.encoding || 'utf8'}")\n`
if (string.countType) {
code += 'size += ' + compiler.callType('size', string.countType) + '\n'
} else if (string.count === null) {
throw new Error('pstring must contain either count or countType')
}
code += 'return size'
return compiler.wrapCode(code)
}],
buffer: ['parametrizable', (compiler, buffer) => {
let code = 'let size = value instanceof Buffer ? value.length : Buffer.from(value).length\n'
if (buffer.countType) {
code += 'size += ' + compiler.callType('size', buffer.countType) + '\n'
} else if (buffer.count === null) {
throw new Error('buffer must contain either count or countType')
}
code += 'return size'
return compiler.wrapCode(code)
}],
bitfield: ['parametrizable', (compiler, values) => {
const totalBytes = Math.ceil(values.reduce((acc, { size }) => acc + size, 0) / 8)
return `${totalBytes}`
}],
bitflags: ['parametrizable', (compiler, { type, flags, shift, big }) => {
let fstr = JSON.stringify(flags)
if (Array.isArray(flags)) {
fstr = '{'
for (const [k, v] of Object.entries(flags)) fstr += `"${v}": ${big ? (1n << BigInt(k)) : (1 << k)}` + (big ? 'n,' : ',')
fstr += '}'
} else if (shift) {
fstr = '{'
for (const key in flags) fstr += `"${key}": ${1 << flags[key]}${big ? 'n,' : ','}`
fstr += '}'
}
return compiler.wrapCode(`
const flags = ${fstr}
let val = value._value ${big ? '|| 0n' : ''}
for (const key in flags) {
if (value[key]) val |= flags[key]
}
return (ctx.${type})(val)
`.trim())
}],
mapper: ['parametrizable', (compiler, mapper) => {
const mappings = JSON.stringify(swapMappings(mapper.mappings))
const code = 'return ' + compiler.callType(`${mappings}[value] || value`, mapper.type)
return compiler.wrapCode(code)
}]
}
}
// Convert hexadecimal keys to decimal
function sanitizeMappings (json) {
const ret = {}
for (let key in json) {
let val = json[key]
key = hex2dec(key)
if (!isNaN(val)) val = Number(val)
if (val === 'true') val = true
if (val === 'false') val = false
ret[key] = val
}
return ret
}
function swapMappings (json) {
const ret = {}
for (let key in json) {
const val = json[key]
key = hex2dec(key)
ret[val] = (isNaN(key)) ? key : parseInt(key, 10)
}
return ret
}
function hex2dec (num) {
if ((num.match(/^0x[0-9a-f]+$/i))) { return parseInt(num.substring(2), 16) }
return num
}