diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 8a252a7c..c014d96e 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -71,7 +71,8 @@ if (n !== 0) { } `.trim()) }], - lpVec3: ['native', minecraft.lpVec3[0]] + lpVec3: ['native', minecraft.lpVec3[0]], + nbtOptionalLengthPrefixed: ['native', minecraft.nbtOptionalLengthPrefixed[0]] }, Write: { varlong: ['native', minecraft.varlong[1]], @@ -137,7 +138,8 @@ if (${baseName} != null) { return offset `.trim()) }], - lpVec3: ['native', minecraft.lpVec3[1]] + lpVec3: ['native', minecraft.lpVec3[1]], + nbtOptionalLengthPrefixed: ['native', minecraft.nbtOptionalLengthPrefixed[1]] }, SizeOf: { varlong: ['native', minecraft.varlong[2]], @@ -197,6 +199,7 @@ if (${baseName} != null) { return size `.trim()) }], - lpVec3: ['native', minecraft.lpVec3[2]] + lpVec3: ['native', minecraft.lpVec3[2]], + nbtOptionalLengthPrefixed: ['native', minecraft.nbtOptionalLengthPrefixed[2]] } } diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 1b616186..9570c539 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -13,7 +13,8 @@ module.exports = { restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray], - lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3] + lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3], + nbtOptionalLengthPrefixed: [readNbtOptionalLengthPrefixed, writeNbtOptionalLengthPrefixed, sizeOfNbtOptionalLengthPrefixed] } const PartialReadError = require('protodef').utils.PartialReadError @@ -187,3 +188,25 @@ function sizeOfTopBitSetTerminatedArray (value, { type }) { } return size } + +// A network (anonymous) optional NBT tag preceded by its byte length as a VarInt. +// This is Mojang's `ByteBufCodecs.optionalTagCodec(...).apply(ByteBufCodecs.lengthPrefixed(N))` +// (e.g. the payload of ServerboundCustomClickActionPacket / the dialog system). An empty tag +// is a single TAG_END (0) byte, so an absent value encodes as `01 00`. +function readNbtOptionalLengthPrefixed (buffer, offset) { + const { value: length, size: lengthSize } = readVarInt(buffer, offset) + if (offset + lengthSize + length > buffer.length) { throw new PartialReadError() } + const tag = nbt.proto.read(buffer, offset + lengthSize, 'anonOptionalNbt') + return { value: tag.value, size: lengthSize + tag.size } +} + +function writeNbtOptionalLengthPrefixed (value, buffer, offset) { + const innerSize = nbt.proto.sizeOf(value, 'anonOptionalNbt') + offset = writeVarInt(innerSize, buffer, offset) + return nbt.proto.write(value, buffer, offset, 'anonOptionalNbt') +} + +function sizeOfNbtOptionalLengthPrefixed (value) { + const innerSize = nbt.proto.sizeOf(value, 'anonOptionalNbt') + return sizeOfVarInt(innerSize) + innerSize +} diff --git a/test/nbtOptionalLengthPrefixedTest.js b/test/nbtOptionalLengthPrefixedTest.js new file mode 100644 index 00000000..6fe370e9 --- /dev/null +++ b/test/nbtOptionalLengthPrefixedTest.js @@ -0,0 +1,38 @@ +/* eslint-env mocha */ +const assert = require('assert') +const nbt = require('prismarine-nbt') +const { nbtOptionalLengthPrefixed } = require('../src/datatypes/minecraft') +const [read, write, sizeOf] = nbtOptionalLengthPrefixed + +describe('nbtOptionalLengthPrefixed', () => { + // The NBT payload of a real ServerboundCustomClickActionPacket (a dialog form submit), + // captured from a 1.21.11 client: a single VarInt byte-length (0x1a = 26) followed by the + // anonymous compound { password: "hello world" }. + const compound = nbt.comp({ password: nbt.string('hello world') }) + // 1a | 0a(compound) 08(string) 0008 "password" 000b "hello world" 00(end) + const expectedHex = '1a' + '0a' + '08' + '0008' + Buffer.from('password').toString('hex') + + '000b' + Buffer.from('hello world').toString('hex') + '00' + + it('writes a present tag as VarInt(byteLength) + anonymous NBT', () => { + const buf = Buffer.alloc(sizeOf(compound)) + const end = write(compound, buf, 0) + assert.strictEqual(buf.subarray(0, end).toString('hex'), expectedHex) + assert.strictEqual(sizeOf(compound), end) + }) + + it('round-trips a present tag', () => { + const buf = Buffer.alloc(sizeOf(compound)) + write(compound, buf, 0) + const { value, size } = read(buf, 0) + assert.strictEqual(size, buf.length) + assert.strictEqual(value.value.password.value, 'hello world') + }) + + it('encodes an absent tag as 01 00 (length 1, TAG_END)', () => { + const buf = Buffer.alloc(8) + const end = write(undefined, buf, 0) + assert.strictEqual(buf.subarray(0, end).toString('hex'), '0100') + assert.strictEqual(sizeOf(undefined), 2) + assert.strictEqual(read(buf, 0).value, undefined) + }) +})