Skip to content

Commit 941bdf0

Browse files
authored
Little endian updates, automatic format detection (#35)
* support LE varint nbt, nbt format detection, tagname sanity checks * sanity check reading tag types, update tests * cleanup * update readme, add samples * fix zigzag size reading, api back compat * add EOF checks, handle bedrock level.dat headers * Fix gzipped EOF checks * use BigIntExt, seperate 32/64bit zigzag * fix BigIntExt * api update * simplify zigzag * fix test require protodef from git * zigzag sizeOf fixes * update remote * lint * fix long zigzag overflow
1 parent 370ac08 commit 941bdf0

16 files changed

+622
-56
lines changed

README.md

+45-11
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,35 @@
22
[![NPM version](https://img.shields.io/npm/v/prismarine-nbt.svg)](http://npmjs.com/package/prismarine-nbt)
33
[![Build Status](https://github.com/PrismarineJS/prismarine-nbt/workflows/CI/badge.svg)](https://github.com/PrismarineJS/prismarine-nbt/actions?query=workflow%3A%22CI%22)
44

5-
Prismarine-NBT is a JavaScript parser and serializer for [NBT](http://wiki.vg/NBT) archives, for use with [Node.js](http://nodejs.org/).
5+
Prismarine-NBT is a JavaScript parser and serializer for [NBT](http://wiki.vg/NBT) archives, for use with [Node.js](http://nodejs.org/). It supports big, little, and little-varint encoded NBT files.
66

77

88
## Usage
99

10+
#### as a async promise
11+
12+
```js
13+
const fs = require('fs')
14+
const { parse, writeUncompressed } = require('prismarine-nbt')
15+
16+
async function main(file) {
17+
const buffer = await fs.promises.readFile(file)
18+
const { parsed, type } = await parse(buffer)
19+
const json = JSON.stringify(result, null, 2)
20+
console.log('JSON serialized:', json)
21+
22+
// Write it back
23+
const outBuffer = fs.createWriteStream('file.nbt')
24+
const newBuf = writeUncompressed(result, type)
25+
outBuffer.write(newBuf)
26+
outBuffer.end(() => console.log('written!'))
27+
}
28+
29+
main(process.argv[2])
30+
```
31+
32+
#### as a callback
33+
1034
```js
1135
var fs = require('fs'),
1236
nbt = require('prismarine-nbt');
@@ -21,32 +45,42 @@ fs.readFile('bigtest.nbt', function(error, data) {
2145
});
2246
```
2347

24-
If the data is gzipped, it is automatically decompressed first.
48+
If the data is gzipped, it is automatically decompressed, for the buffer see metadata.buffer
2549

2650
## API
2751

28-
### writeUncompressed(value,[isLittleEndian])
52+
### parse(data, [format]): Promise<{ parsed, type, metadata: { size, buffer? } }>
53+
### parse(data, [format,] callback)
2954

30-
Returns a buffer with a serialized nbt `value`. If isLittleEndian is passed and is true, write little endian nbt (mcpe).
55+
Takes an optionally compressed `data` buffer and reads the nbt data.
3156

32-
### parseUncompressed(data,[isLittleEndian])
57+
If the endian `format` is known, it can be specified as 'big', 'little' or 'littleVarint'. If not specified, the library will
58+
try to sequentially load as big, little and little varint until the parse is successful. The deduced type is returned as `type`.
3359

34-
Takes a buffer `data` and returns a parsed nbt value. If isLittleEndian is passed and is true, read little endian nbt (mcpe).
60+
Minecraft Java Edition uses big-endian format, and Bedrock uses little-endian.
3561

36-
### parse(data,[isLittleEndian], callback)
62+
### writeUncompressed(value, format='big')
63+
64+
Returns a buffer with a serialized nbt `value`.
65+
66+
### parseUncompressed(data, format='big')
67+
68+
Takes a buffer `data` and returns a parsed nbt value.
3769

38-
Takes an optionally compressed `data` and provide a parsed nbt value in the `callback(err,value)`.
39-
If isLittleEndian is passed and is true, read little endian nbt (mcpe).
4070

4171
### simplify(nbt)
4272

4373
Returns a simplified nbt representation : keep only the value to remove one level.
4474
This loses the types so you cannot use the resulting representation to write it back to nbt.
4575

76+
### protos : { big, little, littleVarint }
77+
78+
Provides compiled protodef instances used to parse and serialize nbt
79+
4680
### proto
4781

48-
Provide the protodef instance used to parse and serialize nbt.
82+
Provide the big-endian protodef instance used to parse and serialize nbt.
4983

5084
### protoLE
5185

52-
Provide the protodef instance used to parse and serialize little endian nbt.
86+
Provide the little-endian protodef instance used to parse and serialize little endian nbt.

compiler-compound.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ module.exports = {
66
value: {},
77
size: 0
88
}
9-
while (true) {
9+
while (offset !== buffer.length) {
1010
const typ = ctx.i8(buffer, offset)
1111
if (typ.value === 0) {
1212
results.size += typ.size
1313
break
1414
}
1515

16+
if (typ.value > 20) {
17+
throw new Error(`Invalid tag: ${typ.value} > 20`)
18+
}
19+
1620
const readResults = ctx.nbt(buffer, offset)
1721
offset += readResults.size
1822
results.size += readResults.size

compiler-tagname.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* global ctx */
2+
function readPString (buffer, offset) {
3+
const { value, size } = ctx.shortString(buffer, offset)
4+
for (var c of value) {
5+
if (c === '\0') throw new Error('unexpected tag end')
6+
}
7+
return { value, size }
8+
}
9+
10+
function writePString (...args) {
11+
return ctx.shortString(...args)
12+
}
13+
14+
function sizeOfPString (...args) {
15+
return ctx.shortString(...args)
16+
}
17+
18+
module.exports = {
19+
Read: { nbtTagName: ['context', readPString] },
20+
Write: { nbtTagName: ['context', writePString] },
21+
SizeOf: { nbtTagName: ['context', sizeOfPString] }
22+
}

compiler-zigzag.js

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Reads the length for a VarInt
3+
*/
4+
function sizeOfVarInt (value) {
5+
value = (value << 1) ^ (value >> 63)
6+
let cursor = 0
7+
while (value & ~0x7F) {
8+
value >>>= 7
9+
cursor++
10+
}
11+
return cursor + 1
12+
}
13+
14+
function sizeOfVarLong (value) {
15+
if (typeof value.valueOf() === 'object') {
16+
value = (BigInt(value[0]) << 32n) | BigInt(value[1])
17+
} else if (typeof value !== 'bigint') value = BigInt(value)
18+
19+
value = (value << 1n) ^ (value >> 63n)
20+
let cursor = 0
21+
while (value > 127n) {
22+
value >>= 7n
23+
cursor++
24+
}
25+
return cursor + 1
26+
}
27+
28+
/**
29+
* Reads a zigzag encoded 64-bit VarInt as a BigInt
30+
*/
31+
function readSignedVarLong (buffer, offset) {
32+
let result = BigInt(0)
33+
let shift = 0n
34+
let cursor = offset
35+
let size = 0
36+
37+
while (true) {
38+
if (cursor + 1 > buffer.length) { throw new Error('unexpected buffer end') }
39+
const b = buffer.readUInt8(cursor)
40+
result |= (BigInt(b) & 0x7fn) << shift // Add the bits to our number, except MSB
41+
cursor++
42+
if (!(b & 0x80)) { // If the MSB is not set, we return the number
43+
size = cursor - offset
44+
break
45+
}
46+
shift += 7n // we only have 7 bits, MSB being the return-trigger
47+
if (shift > 63n) throw new Error(`varint is too big: ${shift}`)
48+
}
49+
50+
// in zigzag encoding, the sign bit is the LSB of the value - remove the bit,
51+
// if 1, then flip the rest of the bits (xor) and set to negative
52+
// Note: bigint has no sign bit; instead if we XOR -0 we get no-op, XOR -1 flips and sets negative
53+
const zigzag = (result >> 1n) ^ -(result & 1n)
54+
return { value: zigzag, size }
55+
}
56+
57+
/**
58+
* Writes a zigzag encoded 64-bit VarInt as a BigInt
59+
*/
60+
function writeSignedVarLong (value, buffer, offset) {
61+
// if an array, turn it into a BigInt
62+
if (typeof value.valueOf() === 'object') {
63+
value = BigInt.asIntN(64, (BigInt(value[0]) << 32n)) | BigInt(value[1])
64+
} else if (typeof value !== 'bigint') value = BigInt(value)
65+
66+
// shift value left and flip if negative (no sign bit, but right shifting beyond value will always be -0b1)
67+
value = (value << 1n) ^ (value >> 63n)
68+
let cursor = 0
69+
while (value > 127n) { // keep writing in 7 bit slices
70+
const num = Number(value & 0xFFn)
71+
buffer.writeUInt8(num | 0x80, offset + cursor)
72+
cursor++
73+
value >>= 7n
74+
}
75+
buffer.writeUInt8(Number(value), offset + cursor)
76+
return offset + cursor + 1
77+
}
78+
79+
/**
80+
* Reads a 32-bit zigzag encoded varint as a Number
81+
*/
82+
function readSignedVarInt (buffer, offset) {
83+
let result = 0
84+
let shift = 0
85+
let cursor = offset
86+
let size = 0
87+
88+
while (true) {
89+
if (cursor + 1 > buffer.length) { throw new Error('unexpected buffer end') }
90+
const b = buffer.readUInt8(cursor)
91+
result |= ((b & 0x7f) << shift) // Add the bits to our number, except MSB
92+
cursor++
93+
if (!(b & 0x80)) { // If the MSB is not set, we return the number
94+
size = cursor - offset
95+
break
96+
}
97+
shift += 7 // we only have 7 bits, MSB being the return-trigger
98+
if (shift > 31) throw new Error(`varint is too big: ${shift}`)
99+
}
100+
101+
const zigzag = ((((result << 63) >> 63) ^ result) >> 1) ^ (result & (1 << 63))
102+
return { value: zigzag, size }
103+
}
104+
105+
/**
106+
* Writes a 32-bit zigzag encoded varint
107+
*/
108+
function writeSignedVarInt (value, buffer, offset) {
109+
value = (value << 1) ^ (value >> 31)
110+
let cursor = 0
111+
while (value & ~0x7F) {
112+
const num = Number((value & 0xFF) | 0x80)
113+
buffer.writeUInt8(num, offset + cursor)
114+
cursor++
115+
value >>>= 7
116+
}
117+
buffer.writeUInt8(value, offset + cursor)
118+
return offset + cursor + 1
119+
}
120+
121+
module.exports = {
122+
Read: { zigzag64: ['native', readSignedVarLong], zigzag32: ['native', readSignedVarInt] },
123+
Write: { zigzag64: ['native', writeSignedVarLong], zigzag32: ['native', writeSignedVarInt] },
124+
SizeOf: { zigzag64: ['native', sizeOfVarLong], zigzag32: ['native', sizeOfVarInt] }
125+
}

nbt-varint.json

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
{
2+
"void": "native",
3+
"container": "native",
4+
"i8": "native",
5+
"switch": "native",
6+
"compound": "native",
7+
"zigzag32": "native",
8+
"zigzag64": "native",
9+
"i16": "native",
10+
"i32": "native",
11+
"i64": "native",
12+
"f32": "native",
13+
"f64": "native",
14+
"pstring": "native",
15+
"shortString": [
16+
"pstring",
17+
{
18+
"countType": "varint"
19+
}
20+
],
21+
"byteArray": [
22+
"array",
23+
{
24+
"countType": "zigzag32",
25+
"type": "i8"
26+
}
27+
],
28+
"list": [
29+
"container",
30+
[
31+
{
32+
"name": "type",
33+
"type": "nbtMapper"
34+
},
35+
{
36+
"name": "value",
37+
"type": [
38+
"array",
39+
{
40+
"countType": "zigzag32",
41+
"type": [
42+
"nbtSwitch",
43+
{
44+
"type": "type"
45+
}
46+
]
47+
}
48+
]
49+
}
50+
]
51+
],
52+
"intArray": [
53+
"array",
54+
{
55+
"countType": "zigzag32",
56+
"type": "i32"
57+
}
58+
],
59+
"longArray": [
60+
"array",
61+
{
62+
"countType": "zigzag32",
63+
"type": "i64"
64+
}
65+
],
66+
"nbtMapper": [
67+
"mapper",
68+
{
69+
"type": "i8",
70+
"mappings": {
71+
"0": "end",
72+
"1": "byte",
73+
"2": "short",
74+
"3": "int",
75+
"4": "long",
76+
"5": "float",
77+
"6": "double",
78+
"7": "byteArray",
79+
"8": "string",
80+
"9": "list",
81+
"10": "compound",
82+
"11": "intArray",
83+
"12": "longArray"
84+
}
85+
}
86+
],
87+
"nbtSwitch": [
88+
"switch",
89+
{
90+
"compareTo": "$type",
91+
"fields": {
92+
"end": "void",
93+
"byte": "i8",
94+
"short": "i16",
95+
"int": "zigzag32",
96+
"long": "zigzag64",
97+
"float": "f32",
98+
"double": "f64",
99+
"byteArray": "byteArray",
100+
"string": "shortString",
101+
"list": "list",
102+
"compound": "compound",
103+
"intArray": "intArray",
104+
"longArray": "longArray"
105+
}
106+
}
107+
],
108+
"nbt": [
109+
"container",
110+
[
111+
{
112+
"name": "type",
113+
"type": "nbtMapper"
114+
},
115+
{
116+
"name": "name",
117+
"type": "nbtTagName"
118+
},
119+
{
120+
"name": "value",
121+
"type": [
122+
"nbtSwitch",
123+
{
124+
"type": "type"
125+
}
126+
]
127+
}
128+
]
129+
]
130+
}

0 commit comments

Comments
 (0)