Files
AeroMc-Launcher/src/nbt.js
T

175 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/*
* nbt.js minimaler NBT-Leser und -Schreiber
* --------------------------------------------
* Nur so viel, wie servers.dat braucht. Diese Datei ist unkomprimiertes NBT
* mit einem Wurzel-Compound, darin eine Liste "servers" aus Compounds mit
* name, ip, icon und acceptTextures.
*
* Bewusst kein Fremdpaket: Der benötigte Ausschnitt ist klein und gut
* prüfbar, eine Abhängigkeit für zwei Datentypen wäre unverhältnismäßig.
*
* Alle Zahlen liegen in NBT als Big-Endian vor, Zeichenketten als
* modifiziertes UTF-8 mit vorangestellter Länge (2 Byte).
*/
const TAG = {
END: 0, BYTE: 1, SHORT: 2, INT: 3, LONG: 4, FLOAT: 5, DOUBLE: 6,
BYTE_ARRAY: 7, STRING: 8, LIST: 9, COMPOUND: 10, INT_ARRAY: 11, LONG_ARRAY: 12,
};
// ---------------------------------------------------------------------------
// Lesen
// ---------------------------------------------------------------------------
class Leser {
constructor(buf) { this.buf = buf; this.pos = 0; }
pruefe(n) { if (this.pos + n > this.buf.length) throw new Error('NBT endet unerwartet'); }
byte() { this.pruefe(1); return this.buf.readInt8(this.pos++); }
ubyte() { this.pruefe(1); return this.buf.readUInt8(this.pos++); }
short() { this.pruefe(2); const v = this.buf.readInt16BE(this.pos); this.pos += 2; return v; }
ushort() { this.pruefe(2); const v = this.buf.readUInt16BE(this.pos); this.pos += 2; return v; }
int() { this.pruefe(4); const v = this.buf.readInt32BE(this.pos); this.pos += 4; return v; }
long() { this.pruefe(8); const v = this.buf.readBigInt64BE(this.pos); this.pos += 8; return v; }
float() { this.pruefe(4); const v = this.buf.readFloatBE(this.pos); this.pos += 4; return v; }
double() { this.pruefe(8); const v = this.buf.readDoubleBE(this.pos); this.pos += 8; return v; }
text() {
const laenge = this.ushort();
this.pruefe(laenge);
const s = this.buf.toString('utf8', this.pos, this.pos + laenge);
this.pos += laenge;
return s;
}
bytes(n) { this.pruefe(n); const b = this.buf.subarray(this.pos, this.pos + n); this.pos += n; return Buffer.from(b); }
}
function leseWert(l, typ) {
switch (typ) {
case TAG.BYTE: return l.byte();
case TAG.SHORT: return l.short();
case TAG.INT: return l.int();
case TAG.LONG: return l.long();
case TAG.FLOAT: return l.float();
case TAG.DOUBLE: return l.double();
case TAG.STRING: return l.text();
case TAG.BYTE_ARRAY: return l.bytes(l.int());
case TAG.INT_ARRAY: { const n = l.int(); const a = []; for (let i = 0; i < n; i++) a.push(l.int()); return a; }
case TAG.LONG_ARRAY: { const n = l.int(); const a = []; for (let i = 0; i < n; i++) a.push(l.long()); return a; }
case TAG.LIST: {
const innerTyp = l.ubyte();
const n = l.int();
const a = [];
for (let i = 0; i < n; i++) a.push(leseWert(l, innerTyp));
a.__typ = innerTyp; // Typ merken, damit Schreiben ihn wiederherstellt
return a;
}
case TAG.COMPOUND: {
const o = {};
o.__typen = {};
for (;;) {
const t = l.ubyte();
if (t === TAG.END) break;
const name = l.text();
o.__typen[name] = t;
o[name] = leseWert(l, t);
}
return o;
}
default: throw new Error('Unbekannter NBT-Typ: ' + typ);
}
}
/*
* Liest unkomprimiertes NBT.
* -> { name, wert }
*/
function lese(buf) {
const l = new Leser(buf);
const typ = l.ubyte();
if (typ !== TAG.COMPOUND) throw new Error('NBT beginnt nicht mit einem Compound');
const name = l.text();
return { name, wert: leseWert(l, TAG.COMPOUND) };
}
// ---------------------------------------------------------------------------
// Schreiben
// ---------------------------------------------------------------------------
class Schreiber {
constructor() { this.teile = []; }
byte(v) { const b = Buffer.alloc(1); b.writeInt8(v, 0); this.teile.push(b); }
ubyte(v) { const b = Buffer.alloc(1); b.writeUInt8(v, 0); this.teile.push(b); }
short(v) { const b = Buffer.alloc(2); b.writeInt16BE(v, 0); this.teile.push(b); }
int(v) { const b = Buffer.alloc(4); b.writeInt32BE(v, 0); this.teile.push(b); }
long(v) { const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(v), 0); this.teile.push(b); }
float(v) { const b = Buffer.alloc(4); b.writeFloatBE(v, 0); this.teile.push(b); }
double(v) { const b = Buffer.alloc(8); b.writeDoubleBE(v, 0); this.teile.push(b); }
text(s) {
const d = Buffer.from(String(s), 'utf8');
const l = Buffer.alloc(2); l.writeUInt16BE(d.length, 0);
this.teile.push(l, d);
}
roh(b) { this.teile.push(b); }
fertig() { return Buffer.concat(this.teile); }
}
// Typ raten, wenn er nicht aus dem Einlesen bekannt ist
function typVon(wert) {
if (typeof wert === 'string') return TAG.STRING;
if (typeof wert === 'bigint') return TAG.LONG;
if (typeof wert === 'boolean') return TAG.BYTE;
if (Buffer.isBuffer(wert)) return TAG.BYTE_ARRAY;
if (Array.isArray(wert)) return TAG.LIST;
if (wert && typeof wert === 'object') return TAG.COMPOUND;
if (Number.isInteger(wert)) return TAG.INT;
return TAG.DOUBLE;
}
function schreibeWert(s, typ, wert) {
switch (typ) {
case TAG.BYTE: s.byte(wert ? Number(wert) : 0); break;
case TAG.SHORT: s.short(Number(wert) || 0); break;
case TAG.INT: s.int(Number(wert) || 0); break;
case TAG.LONG: s.long(wert || 0); break;
case TAG.FLOAT: s.float(Number(wert) || 0); break;
case TAG.DOUBLE: s.double(Number(wert) || 0); break;
case TAG.STRING: s.text(wert == null ? '' : wert); break;
case TAG.BYTE_ARRAY: { const b = Buffer.isBuffer(wert) ? wert : Buffer.alloc(0); s.int(b.length); s.roh(b); break; }
case TAG.INT_ARRAY: { s.int(wert.length); for (const v of wert) s.int(v); break; }
case TAG.LONG_ARRAY: { s.int(wert.length); for (const v of wert) s.long(v); break; }
case TAG.LIST: {
const liste = wert || [];
// leere Listen bekommen END als Typ so macht es Minecraft auch
const innerTyp = liste.__typ || (liste.length ? typVon(liste[0]) : TAG.END);
s.ubyte(innerTyp);
s.int(liste.length);
for (const v of liste) schreibeWert(s, innerTyp, v);
break;
}
case TAG.COMPOUND: {
const typen = (wert && wert.__typen) || {};
for (const schluessel of Object.keys(wert || {})) {
if (schluessel === '__typen' || schluessel === '__typ') continue;
const t = typen[schluessel] || typVon(wert[schluessel]);
s.ubyte(t);
s.text(schluessel);
schreibeWert(s, t, wert[schluessel]);
}
s.ubyte(TAG.END);
break;
}
default: throw new Error('Unbekannter NBT-Typ beim Schreiben: ' + typ);
}
}
function schreibe(name, wert) {
const s = new Schreiber();
s.ubyte(TAG.COMPOUND);
s.text(name || '');
schreibeWert(s, TAG.COMPOUND, wert);
return s.fertig();
}
module.exports = { lese, schreibe, TAG };