29 | /*
30 | NBT.js - a JavaScript parser for NBT archives
31 | by Sijmen Mulder
32 |
33 | I, the copyright holder of this work, hereby release it into the public
34 | domain. This applies worldwide.
35 |
36 | In case this is not legally possible: I grant anyone the right to use this
37 | work for any purpose, without any conditions, unless such conditions are
38 | required by law.
39 | */
40 |
41 | (function() {
42 | 'use strict';
43 |
44 | if (typeof ArrayBuffer === 'undefined') {
45 | throw new Error('Missing required type ArrayBuffer');
46 | }
47 | if (typeof DataView === 'undefined') {
48 | throw new Error('Missing required type DataView');
49 | }
50 | if (typeof Uint8Array === 'undefined') {
51 | throw new Error('Missing required type Uint8Array');
52 | }
53 |
54 | /** @exports nbt */
55 |
56 | var nbt = this;
57 | var zlib = typeof require !== 'undefined' ? require('zlib') : window.zlib;
58 |
59 | /**
60 | * A mapping from type names to NBT type numbers.
61 | * {@link module:nbt.Writer} and {@link module:nbt.Reader}
62 | * have correspoding methods (e.g. {@link module:nbt.Writer#int})
63 | * for every type.
64 | *
65 | * @type Object<string, number>
66 | * @see module:nbt.tagTypeNames */
67 | nbt.tagTypes = {
68 | 'end': 0,
69 | 'byte': 1,
70 | 'short': 2,
71 | 'int': 3,
72 | 'long': 4,
73 | 'float': 5,
74 | 'double': 6,
75 | 'byteArray': 7,
76 | 'string': 8,
77 | 'list': 9,
78 | 'compound': 10,
79 | 'intArray': 11,
80 | 'longArray': 12
81 | };
82 |
83 | /**
84 | * A mapping from NBT type numbers to type names.
85 | *
86 | * @type Object<number, string>
87 | * @see module:nbt.tagTypes */
88 | nbt.tagTypeNames = {};
89 | (function() {
90 | for (var typeName in nbt.tagTypes) {
91 | if (nbt.tagTypes.hasOwnProperty(typeName)) {
92 | nbt.tagTypeNames[nbt.tagTypes[typeName]] = typeName;
93 | }
94 | }
95 | })();
96 |
97 | function hasGzipHeader(data) {
98 | var head = new Uint8Array(data.slice(0, 2));
99 | return head.length === 2 && head[0] === 0x1f && head[1] === 0x8b;
100 | }
101 |
102 | function encodeUTF8(str) {
103 | var array = [], i, c;
104 | for (i = 0; i < str.length; i++) {
105 | c = str.charCodeAt(i);
106 | if (c < 0x80) {
107 | array.push(c);
108 | } else if (c < 0x800) {
109 | array.push(0xC0 | c >> 6);
110 | array.push(0x80 | c & 0x3F);
111 | } else if (c < 0x10000) {
112 | array.push(0xE0 | c >> 12);
113 | array.push(0x80 | (c >> 6) & 0x3F);
114 | array.push(0x80 | c & 0x3F);
115 | } else {
116 | array.push(0xF0 | (c >> 18) & 0x07);
117 | array.push(0x80 | (c >> 12) & 0x3F);
118 | array.push(0x80 | (c >> 6) & 0x3F);
119 | array.push(0x80 | c & 0x3F);
120 | }
121 | }
122 | return array;
123 | }
124 |
125 | function decodeUTF8(array) {
126 | var codepoints = [], i;
127 | for (i = 0; i < array.length; i++) {
128 | if ((array[i] & 0x80) === 0) {
129 | codepoints.push(array[i] & 0x7F);
130 | } else if (i+1 < array.length &&
131 | (array[i] & 0xE0) === 0xC0 &&
132 | (array[i+1] & 0xC0) === 0x80) {
133 | codepoints.push(
134 | ((array[i] & 0x1F) << 6) |
135 | ( array[i+1] & 0x3F));
136 | } else if (i+2 < array.length &&
137 | (array[i] & 0xF0) === 0xE0 &&
138 | (array[i+1] & 0xC0) === 0x80 &&
139 | (array[i+2] & 0xC0) === 0x80) {
140 | codepoints.push(
141 | ((array[i] & 0x0F) << 12) |
142 | ((array[i+1] & 0x3F) << 6) |
143 | ( array[i+2] & 0x3F));
144 | } else if (i+3 < array.length &&
145 | (array[i] & 0xF8) === 0xF0 &&
146 | (array[i+1] & 0xC0) === 0x80 &&
147 | (array[i+2] & 0xC0) === 0x80 &&
148 | (array[i+3] & 0xC0) === 0x80) {
149 | codepoints.push(
150 | ((array[i] & 0x07) << 18) |
151 | ((array[i+1] & 0x3F) << 12) |
152 | ((array[i+2] & 0x3F) << 6) |
153 | ( array[i+3] & 0x3F));
154 | }
155 | }
156 | return String.fromCharCode.apply(null, codepoints);
157 | }
158 |
159 | /* Not all environments, in particular PhantomJS, supply
160 | Uint8Array.slice() */
161 | function sliceUint8Array(array, begin, end) {
162 | if ('slice' in array) {
163 | return array.slice(begin, end);
164 | } else {
165 | return new Uint8Array([].slice.call(array, begin, end));
166 | }
167 | }
168 |
169 | /**
170 | * In addition to the named writing methods documented below,
171 | * the same methods are indexed by the NBT type number as well,
172 | * as shown in the example below.
173 | *
174 | * @constructor
175 | * @see module:nbt.Reader
176 | *
177 | * @example
178 | * var writer = new nbt.Writer();
179 | *
180 | * // all equivalent
181 | * writer.int(42);
182 | * writer[3](42);
183 | * writer(nbt.tagTypes.int)(42);
184 | *
185 | * // overwrite the second int
186 | * writer.offset = 0;
187 | * writer.int(999);
188 | *
189 | * return writer.buffer; */
190 | nbt.Writer = function() {
191 | var self = this;
192 |
193 | /* Will be resized (x2) on write if necessary. */
194 | var buffer = new ArrayBuffer(1024);
195 |
196 | /* These are recreated when the buffer is */
197 | var dataView = new DataView(buffer);
198 | var arrayView = new Uint8Array(buffer);
199 |
200 | /**
201 | * The location in the buffer where bytes are written or read.
202 | * This increases after every write, but can be freely changed.
203 | * The buffer will be resized when necessary.
204 | *
205 | * @type number */
206 | this.offset = 0;
207 |
208 | // Ensures that the buffer is large enough to write `size` bytes
209 | // at the current `self.offset`.
210 | function accommodate(size) {
211 | var requiredLength = self.offset + size;
212 | if (buffer.byteLength >= requiredLength) {
213 | return;
214 | }
215 |
216 | var newLength = buffer.byteLength;
217 | while (newLength < requiredLength) {
218 | newLength *= 2;
219 | }
220 |
221 | var newBuffer = new ArrayBuffer(newLength);
222 | var newArrayView = new Uint8Array(newBuffer);
223 | newArrayView.set(arrayView);
224 |
225 | // If there's a gap between the end of the old buffer
226 | // and the start of the new one, we need to zero it out
227 | if (self.offset > buffer.byteLength) {
228 | newArrayView.fill(0, buffer.byteLength, self.offset);
229 | }
230 |
231 | buffer = newBuffer;
232 | dataView = new DataView(newBuffer);
233 | arrayView = newArrayView;
234 | }
235 |
236 | function write(dataType, size, value) {
237 | accommodate(size);
238 | dataView['set' + dataType](self.offset, value);
239 | self.offset += size;
240 | return self;
241 | }
242 |
243 | /**
244 | * Returns the writen data as a slice from the internal buffer,
245 | * cutting off any padding at the end.
246 | *
247 | * @returns {ArrayBuffer} a [0, offset] slice of the interal buffer */
248 | this.getData = function() {
249 | accommodate(0); /* make sure the offset is inside the buffer */
250 | return buffer.slice(0, self.offset);
251 | };
252 |
253 | /**
254 | * @method module:nbt.Writer#byte
255 | * @param {number} value - a signed byte
256 | * @returns {module:nbt.Writer} itself */
257 | this[nbt.tagTypes.byte] = write.bind(this, 'Int8', 1);
258 |
259 | /**
260 | * @method module:nbt.Writer#ubyte
261 | * @param {number} value - an unsigned byte
262 | * @returns {module:nbt.Writer} itself */
263 | this.ubyte = write.bind(this, 'Uint8', 1);
264 |
265 | /**
266 | * @method module:nbt.Writer#short
267 | * @param {number} value - a signed 16-bit integer
268 | * @returns {module:nbt.Writer} itself */
269 | this[nbt.tagTypes.short] = write.bind(this, 'Int16', 2);
270 |
271 | /**
272 | * @method module:nbt.Writer#int
273 | * @param {number} value - a signed 32-bit integer
274 | * @returns {module:nbt.Writer} itself */
275 | this[nbt.tagTypes.int] = write.bind(this, 'Int32', 4);
276 |
277 | /**
278 | * @method module:nbt.Writer#float
279 | * @param {number} value - a signed 32-bit float
280 | * @returns {module:nbt.Writer} itself */
281 | this[nbt.tagTypes.float] = write.bind(this, 'Float32', 4);
282 |
283 | /**
284 | * @method module:nbt.Writer#float
285 | * @param {number} value - a signed 64-bit float
286 | * @returns {module:nbt.Writer} itself */
287 | this[nbt.tagTypes.double] = write.bind(this, 'Float64', 8);
288 |
289 | /**
290 | * As JavaScript does not support 64-bit integers natively, this
291 | * method takes an array of two 32-bit integers that make up the
292 | * upper and lower halves of the long.
293 | *
294 | * @method module:nbt.Writer#long
295 | * @param {Array.<number>} value - [upper, lower]
296 | * @returns {module:nbt.Writer} itself */
297 | this[nbt.tagTypes.long] = function(value) {
298 | self.int(value[0]);
299 | self.int(value[1]);
300 | return self;
301 | };
302 |
303 | /**
304 | * @method module:nbt.Writer#byteArray
305 | * @param {Array.<number>|Uint8Array|Buffer} value
306 | * @returns {module:nbt.Writer} itself */
307 | this[nbt.tagTypes.byteArray] = function(value) {
308 | this.int(value.length);
309 | accommodate(value.length);
310 | arrayView.set(value, this.offset);
311 | this.offset += value.length;
312 | return this;
313 | };
314 |
315 | /**
316 | * @method module:nbt.Writer#intArray
317 | * @param {Array.<number>} value
318 | * @returns {module:nbt.Writer} itself */
319 | this[nbt.tagTypes.intArray] = function(value) {
320 | this.int(value.length);
321 | var i;
322 | for (i = 0; i < value.length; i++) {
323 | this.int(value[i]);
324 | }
325 | return this;
326 | };
327 |
328 | /**
329 | * @method module:nbt.Writer#longArray
330 | * @param {Array.<number>} value
331 | * @returns {module:nbt.Writer} itself */
332 | this[nbt.tagTypes.longArray] = function(value) {
333 | this.int(value.length);
334 | var i;
335 | for (i = 0; i < value.length; i++) {
336 | this.long(value[i]);
337 | }
338 | return this;
339 | };
340 |
341 | /**
342 | * @method module:nbt.Writer#string
343 | * @param {string} value
344 | * @returns {module:nbt.Writer} itself */
345 | this[nbt.tagTypes.string] = function(value) {
346 | var bytes = encodeUTF8(value);
347 | this.short(bytes.length);
348 | accommodate(bytes.length);
349 | arrayView.set(bytes, this.offset);
350 | this.offset += bytes.length;
351 | return this;
352 | };
353 |
354 | /**
355 | * @method module:nbt.Writer#list
356 | * @param {Object} value
357 | * @param {number} value.type - the NBT type number
358 | * @param {Array} value.value - an array of values
359 | * @returns {module:nbt.Writer} itself */
360 | this[nbt.tagTypes.list] = function(value) {
361 | this.byte(nbt.tagTypes[value.type]);
362 | this.int(value.value.length);
363 | var i;
364 | for (i = 0; i < value.value.length; i++) {
365 | this[value.type](value.value[i]);
366 | }
367 | return this;
368 | };
369 |
370 | /**
371 | * @method module:nbt.Writer#compound
372 | * @param {Object} value - a key/value map
373 | * @param {Object} value.KEY
374 | * @param {string} value.KEY.type - the NBT type number
375 | * @param {Object} value.KEY.value - a value matching the type
376 | * @returns {module:nbt.Writer} itself
377 | *
378 | * @example
379 | * writer.compound({
380 | * foo: { type: 'int', value: 12 },
381 | * bar: { type: 'string', value: 'Hello, World!' }
382 | * }); */
383 | this[nbt.tagTypes.compound] = function(value) {
384 | var self = this;
385 | Object.keys(value).map(function (key) {
386 | self.byte(nbt.tagTypes[value[key].type]);
387 | self.string(key);
388 | self[value[key].type](value[key].value);
389 | });
390 | this.byte(nbt.tagTypes.end);
391 | return this;
392 | };
393 |
394 | var typeName;
395 | for (typeName in nbt.tagTypes) {
396 | if (nbt.tagTypes.hasOwnProperty(typeName)) {
397 | this[typeName] = this[nbt.tagTypes[typeName]];
398 | }
399 | }
400 | };
401 |
402 | /**
403 | * In addition to the named writing methods documented below,
404 | * the same methods are indexed by the NBT type number as well,
405 | * as shown in the example below.
406 | *
407 | * @constructor
408 | * @see module:nbt.Writer
409 | *
410 | * @example
411 | * var reader = new nbt.Reader(buf);
412 | * int x = reader.int();
413 | * int y = reader[3]();
414 | * int z = reader[nbt.tagTypes.int](); */
415 | nbt.Reader = function(buffer) {
416 | if (!buffer) { throw new Error('Argument "buffer" is falsy'); }
417 |
418 | var self = this;
419 |
420 | /**
421 | * The current location in the buffer. Can be freely changed
422 | * within the bounds of the buffer.
423 | *
424 | * @type number */
425 | this.offset = 0;
426 |
427 | var arrayView = new Uint8Array(buffer);
428 | var dataView = new DataView(arrayView.buffer);
429 |
430 | function read(dataType, size) {
431 | var val = dataView['get' + dataType](self.offset);
432 | self.offset += size;
433 | return val;
434 | }
435 |
436 | /**
437 | * @method module:nbt.Reader#byte
438 | * @returns {number} the read byte */
439 | this[nbt.tagTypes.byte] = read.bind(this, 'Int8', 1);
440 |
441 | /**
442 | * @method module:nbt.Reader#byte
443 | * @returns {number} the read unsigned byte */
444 | this.ubyte = read.bind(this, 'Uint8', 1);
445 |
446 | /**
447 | * @method module:nbt.Reader#short
448 | * @returns {number} the read signed 16-bit short */
449 | this[nbt.tagTypes.short] = read.bind(this, 'Int16', 2);
450 |
451 | /**
452 | * @method module:nbt.Reader#int
453 | * @returns {number} the read signed 32-bit integer */
454 | this[nbt.tagTypes.int] = read.bind(this, 'Int32', 4);
455 |
456 | /**
457 | * @method module:nbt.Reader#float
458 | * @returns {number} the read signed 32-bit float */
459 | this[nbt.tagTypes.float] = read.bind(this, 'Float32', 4);
460 |
461 | /**
462 | * @method module:nbt.Reader#double
463 | * @returns {number} the read signed 64-bit float */
464 | this[nbt.tagTypes.double] = read.bind(this, 'Float64', 8);
465 |
466 | /**
467 | * As JavaScript does not not natively support 64-bit
468 | * integers, the value is returned as an array of two
469 | * 32-bit integers, the upper and the lower.
470 | *
471 | * @method module:nbt.Reader#long
472 | * @returns {Array.<number>} [upper, lower] */
473 | this[nbt.tagTypes.long] = function() {
474 | return [this.int(), this.int()];
475 | };
476 |
477 | /**
478 | * @method module:nbt.Reader#byteArray
479 | * @returns {Array.<number>} the read array */
480 | this[nbt.tagTypes.byteArray] = function() {
481 | var length = this.int();
482 | var bytes = [];
483 | var i;
484 | for (i = 0; i < length; i++) {
485 | bytes.push(this.byte());
486 | }
487 | return bytes;
488 | };
489 |
490 | /**
491 | * @method module:nbt.Reader#intArray
492 | * @returns {Array.<number>} the read array of 32-bit ints */
493 | this[nbt.tagTypes.intArray] = function() {
494 | var length = this.int();
495 | var ints = [];
496 | var i;
497 | for (i = 0; i < length; i++) {
498 | ints.push(this.int());
499 | }
500 | return ints;
501 | };
502 |
503 | /**
504 | * As JavaScript does not not natively support 64-bit
505 | * integers, the value is returned as an array of arrays of two
506 | * 32-bit integers, the upper and the lower.
507 | *
508 | * @method module:nbt.Reader#longArray
509 | * @returns {Array.<number>} the read array of 64-bit ints
510 | * split into [upper, lower] */
511 | this[nbt.tagTypes.longArray] = function() {
512 | var length = this.int();
513 | var longs = [];
514 | var i;
515 | for (i = 0; i < length; i++) {
516 | longs.push(this.long());
517 | }
518 | return longs;
519 | };
520 |
521 | /**
522 | * @method module:nbt.Reader#string
523 | * @returns {string} the read string */
524 | this[nbt.tagTypes.string] = function() {
525 | var length = this.short();
526 | var slice = sliceUint8Array(arrayView, this.offset,
527 | this.offset + length);
528 | this.offset += length;
529 | return decodeUTF8(slice);
530 | };
531 |
532 | /**
533 | * @method module:nbt.Reader#list
534 | * @returns {{type: string, value: Array}}
535 | *
536 | * @example
537 | * reader.list();
538 | * // -> { type: 'string', values: ['foo', 'bar'] } */
539 | this[nbt.tagTypes.list] = function() {
540 | var type = this.byte();
541 | var length = this.int();
542 | var values = [];
543 | var i;
544 | for (i = 0; i < length; i++) {
545 | values.push(this[type]());
546 | }
547 | return { type: nbt.tagTypeNames[type], value: values };
548 | };
549 |
550 | /**
551 | * @method module:nbt.Reader#compound
552 | * @returns {Object.<string, { type: string, value }>}
553 | *
554 | * @example
555 | * reader.compound();
556 | * // -> { foo: { type: int, value: 42 },
557 | * // bar: { type: string, value: 'Hello! }} */
558 | this[nbt.tagTypes.compound] = function() {
559 | var values = {};
560 | while (true) {
561 | var type = this.byte();
562 | if (type === nbt.tagTypes.end) {
563 | break;
564 | }
565 | var name = this.string();
566 | var value = this[type]();
567 | values[name] = { type: nbt.tagTypeNames[type], value: value };
568 | }
569 | return values;
570 | };
571 |
572 | var typeName;
573 | for (typeName in nbt.tagTypes) {
574 | if (nbt.tagTypes.hasOwnProperty(typeName)) {
575 | this[typeName] = this[nbt.tagTypes[typeName]];
576 | }
577 | }
578 | };
579 |
580 | /**
581 | * @param {Object} value - a named compound
582 | * @param {string} value.name - the top-level name
583 | * @param {Object} value.value - a compound
584 | * @returns {ArrayBuffer}
585 | *
586 | * @see module:nbt.parseUncompressed
587 | * @see module:nbt.Writer#compound
588 | *
589 | * @example
590 | * nbt.writeUncompressed({
591 | * name: 'My Level',
592 | * value: {
593 | * foo: { type: int, value: 42 },
594 | * bar: { type: string, value: 'Hi!' }
595 | * }
596 | * }); */
597 | nbt.writeUncompressed = function(value) {
598 | if (!value) { throw new Error('Argument "value" is falsy'); }
599 |
600 | var writer = new nbt.Writer();
601 |
602 | writer.byte(nbt.tagTypes.compound);
603 | writer.string(value.name);
604 | writer.compound(value.value);
605 |
606 | return writer.getData();
607 | };
608 |
609 | /**
610 | * @param {ArrayBuffer|Buffer} data - an uncompressed NBT archive
611 | * @returns {{name: string, value: Object.<string, Object>}}
612 | * a named compound
613 | *
614 | * @see module:nbt.parse
615 | * @see module:nbt.writeUncompressed
616 | *
617 | * @example
618 | * nbt.readUncompressed(buf);
619 | * // -> { name: 'My Level',
620 | * // value: { foo: { type: int, value: 42 },
621 | * // bar: { type: string, value: 'Hi!' }}} */
622 | nbt.parseUncompressed = function(data) {
623 | if (!data) { throw new Error('Argument "data" is falsy'); }
624 |
625 | var reader = new nbt.Reader(data);
626 |
627 | var type = reader.byte();
628 | if (type !== nbt.tagTypes.compound) {
629 | throw new Error('Top tag should be a compound');
630 | }
631 |
632 | return {
633 | name: reader.string(),
634 | value: reader.compound()
635 | };
636 | };
637 |
638 | /**
639 | * @callback parseCallback
640 | * @param {Object} error
641 | * @param {Object} result - a named compound
642 | * @param {string} result.name - the top-level name
643 | * @param {Object} result.value - the top-level compound */
644 |
645 | /**
646 | * This accepts both gzipped and uncompressd NBT archives.
647 | * If the archive is uncompressed, the callback will be
648 | * called directly from this method. For gzipped files, the
649 | * callback is async.
650 | *
651 | * For use in the browser, window.zlib must be defined to decode
652 | * compressed archives. It will be passed a Buffer if the type is
653 | * available, or an Uint8Array otherwise.
654 | *
655 | * @param {ArrayBuffer|Buffer} data - gzipped or uncompressed data
656 | * @param {parseCallback} callback
657 | *
658 | * @see module:nbt.parseUncompressed
659 | * @see module:nbt.Reader#compound
660 | *
661 | * @example
662 | * nbt.parse(buf, function(error, results) {
663 | * if (error) {
664 | * throw error;
665 | * }
666 | * console.log(result.name);
667 | * console.log(result.value.foo);
668 | * }); */
669 | nbt.parse = function(data, callback) {
670 | if (!data) { throw new Error('Argument "data" is falsy'); }
671 |
672 | var self = this;
673 |
674 | if (!hasGzipHeader(data)) {
675 | callback(null, self.parseUncompressed(data));
676 | } else if (!zlib) {
677 | callback(new Error('NBT archive is compressed but zlib is not ' +
678 | 'available'), null);
679 | } else {
680 | /* zlib.gunzip take a Buffer, at least in Node, so try to convert
681 | if possible. */
682 | var buffer;
683 | if (data.length) {
684 | buffer = data;
685 | } else if (typeof Buffer !== 'undefined') {
686 | buffer = new Buffer(data);
687 | } else {
688 | /* In the browser? Unknown zlib library. Let's settle for
689 | Uint8Array and see what happens. */
690 | buffer = new Uint8Array(data);
691 | }
692 |
693 | zlib.gunzip(buffer, function(error, uncompressed) {
694 | if (error) {
695 | callback(error, null);
696 | } else {
697 | callback(null, self.parseUncompressed(uncompressed));
698 | }
699 | });
700 | }
701 | };
702 | }).apply(typeof exports !== 'undefined' ? exports : (window.nbt = {}));
703 |
704 |
705 |