191 lines
5.5 KiB
JavaScript
191 lines
5.5 KiB
JavaScript
const zlib = require('zlib')
|
|
|
|
const { ProtoDefCompiler } = require('protodef').Compiler
|
|
|
|
const beNbtJson = JSON.stringify(require('./nbt.json'))
|
|
const leNbtJson = beNbtJson.replace(/([iuf][0-7]+)/g, 'l$1')
|
|
const varintJson = JSON.stringify(require('./nbt-varint.json')).replace(/([if][0-7]+)/g, 'l$1')
|
|
|
|
function createProto (type) {
|
|
const compiler = new ProtoDefCompiler()
|
|
compiler.addTypes(require('./compiler-compound'))
|
|
compiler.addTypes(require('./compiler-tagname'))
|
|
let proto = beNbtJson
|
|
if (type === 'littleVarint') {
|
|
compiler.addTypes(require('./compiler-zigzag'))
|
|
proto = varintJson
|
|
} else if (type === 'little') {
|
|
proto = leNbtJson
|
|
}
|
|
compiler.addTypesToCompile(JSON.parse(proto))
|
|
return compiler.compileProtoDefSync()
|
|
}
|
|
|
|
const protoBE = createProto('big')
|
|
const protoLE = createProto('little')
|
|
const protoVarInt = createProto('littleVarint')
|
|
|
|
const protos = {
|
|
big: protoBE,
|
|
little: protoLE,
|
|
littleVarint: protoVarInt
|
|
}
|
|
|
|
function writeUncompressed (value, proto = 'big') {
|
|
if (proto === true) proto = 'little'
|
|
return protos[proto].createPacketBuffer('nbt', value)
|
|
}
|
|
|
|
function parseUncompressed (data, proto = 'big') {
|
|
if (proto === true) proto = 'little'
|
|
return protos[proto].parsePacketBuffer('nbt', data, data.startOffset).data
|
|
}
|
|
|
|
const hasGzipHeader = function (data) {
|
|
let result = true
|
|
if (data[0] !== 0x1f) result = false
|
|
if (data[1] !== 0x8b) result = false
|
|
return result
|
|
}
|
|
|
|
const hasBedrockLevelHeader = (data) =>
|
|
data[1] === 0 && data[2] === 0 && data[3] === 0
|
|
|
|
async function parseAs (data, type) {
|
|
if (hasGzipHeader(data)) {
|
|
data = await new Promise((resolve, reject) => {
|
|
zlib.gunzip(data, (error, uncompressed) => {
|
|
if (error) reject(error)
|
|
else resolve(uncompressed)
|
|
})
|
|
})
|
|
}
|
|
const parsed = protos[type].parsePacketBuffer('nbt', data, data.startOffset)
|
|
parsed.metadata.buffer = data
|
|
parsed.type = type
|
|
return parsed
|
|
}
|
|
|
|
async function parse (data, format, callback) {
|
|
let fmt = null
|
|
if (typeof format === 'function') {
|
|
callback = format
|
|
} else if (format === true || format === 'little') {
|
|
fmt = 'little'
|
|
} else if (format === 'big') {
|
|
fmt = 'big'
|
|
} else if (format === 'littleVarint') {
|
|
fmt = 'littleVarint'
|
|
} else if (format) {
|
|
throw new Error('Unrecognized format: ' + format)
|
|
}
|
|
|
|
data.startOffset = data.startOffset || 0
|
|
|
|
if (!fmt && !data.startOffset) {
|
|
if (hasBedrockLevelHeader(data)) { // bedrock level.dat header
|
|
data.startOffset += 8 // skip + 8 bytes
|
|
fmt = 'little'
|
|
}
|
|
}
|
|
|
|
// if the format is specified, parse
|
|
if (fmt) {
|
|
try {
|
|
const res = await parseAs(data, fmt)
|
|
if (callback) callback(null, res.data, res.type, res.metadata)
|
|
return { parsed: res.data, type: res.type, metadata: res.metadata }
|
|
} catch (e) {
|
|
if (callback) return callback(e)
|
|
else throw e
|
|
}
|
|
}
|
|
|
|
// else try to deduce file type
|
|
|
|
// Check if we decoded properly: the EOF should match end of the buffer,
|
|
// or there should be more tags to read, else throw unexpected EOF
|
|
const verifyEOF = ({ buffer, size }) => {
|
|
const readLen = size
|
|
const bufferLen = buffer.length - buffer.startOffset
|
|
const lastByte = buffer[readLen + buffer.startOffset]
|
|
const nextNbtTag = lastByte === 0x0A
|
|
if (readLen < bufferLen && !nextNbtTag) {
|
|
throw new Error(`Unexpected EOF at ${readLen}: still have ${bufferLen - readLen} bytes to read !`)
|
|
}
|
|
}
|
|
|
|
// Try to parse as all formats until something passes
|
|
let ret = null
|
|
try {
|
|
ret = await parseAs(data, 'big')
|
|
verifyEOF(ret.metadata)
|
|
} catch (e) {
|
|
try {
|
|
ret = await parseAs(data, 'little')
|
|
verifyEOF(ret.metadata)
|
|
} catch (e2) {
|
|
try {
|
|
ret = await parseAs(data, 'littleVarint')
|
|
verifyEOF(ret.metadata)
|
|
} catch (e3) {
|
|
if (callback) return callback(e)
|
|
else throw e // throw error decoding as big endian
|
|
}
|
|
}
|
|
}
|
|
|
|
if (callback) callback(null, ret.data, ret.type, ret.metadata)
|
|
return { parsed: ret.data, type: ret.type, metadata: ret.metadata }
|
|
}
|
|
|
|
function simplify (data) {
|
|
function transform (value, type) {
|
|
if (type === 'compound') {
|
|
return Object.keys(value).reduce(function (acc, key) {
|
|
acc[key] = simplify(value[key])
|
|
return acc
|
|
}, {})
|
|
}
|
|
if (type === 'list') {
|
|
return value.value.map(function (v) { return transform(v, value.type) })
|
|
}
|
|
return value
|
|
}
|
|
return transform(data.value, data.type)
|
|
}
|
|
|
|
const builder = {
|
|
bool (value = false) { return { type: 'bool', value } },
|
|
short (value) { return { type: 'short', value } },
|
|
byte (value) { return { type: 'byte', value } },
|
|
string (value) { return { type: 'string', value } },
|
|
comp (value, name = '') { return { type: 'compound', name, value } },
|
|
int (value) { return { type: 'int', value } },
|
|
float (value) { return { type: 'float', value } },
|
|
double (value) { return { type: 'double', value } },
|
|
long (value) { return { type: 'long', value } },
|
|
list (value) {
|
|
const type = value?.type ?? 'end'
|
|
return { type: 'list', value: { type, value: value?.value ?? [] } }
|
|
},
|
|
byteArray (value = []) { return { type: 'byteArray', value } },
|
|
shortArray (value = []) { return { type: 'shortArray', value } },
|
|
intArray (value = []) { return { type: 'intArray', value } },
|
|
longArray (value = []) { return { type: 'longArray', value } }
|
|
}
|
|
|
|
module.exports = {
|
|
writeUncompressed,
|
|
parseUncompressed,
|
|
simplify,
|
|
hasBedrockLevelHeader,
|
|
parse,
|
|
parseAs,
|
|
proto: protoBE,
|
|
protoLE,
|
|
protos,
|
|
TagType: require('./typings/tag-type'),
|
|
...builder
|
|
}
|