From 040a085a94e66daf18fd8cea6b00d64a067519b7 Mon Sep 17 00:00:00 2001 From: Querz Date: Wed, 15 Jul 2020 17:08:31 +0200 Subject: [PATCH 1/5] add support for little endian input and output --- .../nbt/io/LittleEndianNBTInputStream.java | 240 +++++++++++++++++ .../nbt/io/LittleEndianNBTOutputStream.java | 244 ++++++++++++++++++ .../net/querz/nbt/io/NBTDeserializer.java | 20 +- src/main/java/net/querz/nbt/io/NBTInput.java | 11 + .../java/net/querz/nbt/io/NBTInputStream.java | 2 +- src/main/java/net/querz/nbt/io/NBTOutput.java | 13 + .../net/querz/nbt/io/NBTOutputStream.java | 2 +- .../java/net/querz/nbt/io/NBTSerializer.java | 20 +- src/main/java/net/querz/nbt/io/NBTUtil.java | 54 ++++ 9 files changed, 596 insertions(+), 10 deletions(-) create mode 100644 src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java create mode 100644 src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java create mode 100644 src/main/java/net/querz/nbt/io/NBTInput.java create mode 100644 src/main/java/net/querz/nbt/io/NBTOutput.java diff --git a/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java b/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java new file mode 100644 index 00000000..7c7d8ed2 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java @@ -0,0 +1,240 @@ +package net.querz.nbt.io; + +import net.querz.io.ExceptionBiFunction; +import net.querz.io.MaxDepthIO; +import net.querz.nbt.tag.ByteArrayTag; +import net.querz.nbt.tag.ByteTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.DoubleTag; +import net.querz.nbt.tag.EndTag; +import net.querz.nbt.tag.FloatTag; +import net.querz.nbt.tag.IntArrayTag; +import net.querz.nbt.tag.IntTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.tag.LongArrayTag; +import net.querz.nbt.tag.LongTag; +import net.querz.nbt.tag.ShortTag; +import net.querz.nbt.tag.StringTag; +import net.querz.nbt.tag.Tag; +import java.io.Closeable; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class LittleEndianNBTInputStream implements DataInput, NBTInput, MaxDepthIO, Closeable { + + private final DataInputStream input; + + private static Map, IOException>> readers = new HashMap<>(); + private static Map> idClassMapping = new HashMap<>(); + + static { + put(EndTag.ID, (i, d) -> EndTag.INSTANCE, EndTag.class); + put(ByteTag.ID, (i, d) -> readByte(i), ByteTag.class); + put(ShortTag.ID, (i, d) -> readShort(i), ShortTag.class); + put(IntTag.ID, (i, d) -> readInt(i), IntTag.class); + put(LongTag.ID, (i, d) -> readLong(i), LongTag.class); + put(FloatTag.ID, (i, d) -> readFloat(i), FloatTag.class); + put(DoubleTag.ID, (i, d) -> readDouble(i), DoubleTag.class); + put(ByteArrayTag.ID, (i, d) -> readByteArray(i), ByteArrayTag.class); + put(StringTag.ID, (i, d) -> readString(i), StringTag.class); + put(ListTag.ID, LittleEndianNBTInputStream::readListTag, ListTag.class); + put(CompoundTag.ID, LittleEndianNBTInputStream::readCompound, CompoundTag.class); + put(IntArrayTag.ID, (i, d) -> readIntArray(i), IntArrayTag.class); + put(LongArrayTag.ID, (i, d) -> readLongArray(i), LongArrayTag.class); + } + + private static void put(byte id, ExceptionBiFunction, IOException> reader, Class clazz) { + readers.put(id, reader); + idClassMapping.put(id, clazz); + } + + public LittleEndianNBTInputStream(InputStream in) { + input = new DataInputStream(in); + } + + public LittleEndianNBTInputStream(DataInputStream in) { + input = in; + } + + public NamedTag readTag(int maxDepth) throws IOException { + byte id = readByte(); + return new NamedTag(readUTF(), readTag(id, maxDepth)); + } + + public Tag readRawTag(int maxDepth) throws IOException { + byte id = readByte(); + return readTag(id, maxDepth); + } + + private Tag readTag(byte type, int maxDepth) throws IOException { + ExceptionBiFunction, IOException> f; + if ((f = readers.get(type)) == null) { + throw new IOException("invalid tag id \"" + type + "\""); + } + return f.accept(this, maxDepth); + } + + private static ByteTag readByte(LittleEndianNBTInputStream in) throws IOException { + return new ByteTag(in.readByte()); + } + + private static ShortTag readShort(LittleEndianNBTInputStream in) throws IOException { + return new ShortTag(in.readShort()); + } + + private static IntTag readInt(LittleEndianNBTInputStream in) throws IOException { + return new IntTag(in.readInt()); + } + + private static LongTag readLong(LittleEndianNBTInputStream in) throws IOException { + return new LongTag(in.readLong()); + } + + private static FloatTag readFloat(LittleEndianNBTInputStream in) throws IOException { + return new FloatTag(in.readFloat()); + } + + private static DoubleTag readDouble(LittleEndianNBTInputStream in) throws IOException { + return new DoubleTag(in.readDouble()); + } + + private static StringTag readString(LittleEndianNBTInputStream in) throws IOException { + return new StringTag(in.readUTF()); + } + + private static ByteArrayTag readByteArray(LittleEndianNBTInputStream in) throws IOException { + ByteArrayTag bat = new ByteArrayTag(new byte[in.readInt()]); + in.readFully(bat.getValue()); + return bat; + } + + private static IntArrayTag readIntArray(LittleEndianNBTInputStream in) throws IOException { + int l = in.readInt(); + int[] data = new int[l]; + IntArrayTag iat = new IntArrayTag(data); + for (int i = 0; i < l; i++) { + data[i] = in.readInt(); + } + return iat; + } + + private static LongArrayTag readLongArray(LittleEndianNBTInputStream in) throws IOException { + int l = in.readInt(); + long[] data = new long[l]; + LongArrayTag iat = new LongArrayTag(data); + for (int i = 0; i < l; i++) { + data[i] = in.readLong(); + } + return iat; + } + + private static ListTag readListTag(LittleEndianNBTInputStream in, int maxDepth) throws IOException { + byte listType = in.readByte(); + ListTag list = ListTag.createUnchecked(idClassMapping.get(listType)); + int length = in.readInt(); + if (length < 0) { + length = 0; + } + for (int i = 0; i < length; i++) { + list.addUnchecked(in.readTag(listType, in.decrementMaxDepth(maxDepth))); + } + return list; + } + + private static CompoundTag readCompound(LittleEndianNBTInputStream in, int maxDepth) throws IOException { + CompoundTag comp = new CompoundTag(); + for (int id = in.readByte() & 0xFF; id != 0; id = in.readByte() & 0xFF) { + String key = in.readUTF(); + Tag element = in.readTag((byte) id, in.decrementMaxDepth(maxDepth)); + comp.put(key, element); + } + return comp; + } + + @Override + public void readFully(byte[] b) throws IOException { + input.readFully(b); + } + + @Override + public void readFully(byte[] b, int off, int len) throws IOException { + input.readFully(b, off, len); + } + + @Override + public int skipBytes(int n) throws IOException { + return input.skipBytes(n); + } + + @Override + public boolean readBoolean() throws IOException { + return input.readBoolean(); + } + + @Override + public byte readByte() throws IOException { + return input.readByte(); + } + + @Override + public int readUnsignedByte() throws IOException { + return input.readUnsignedByte(); + } + + @Override + public short readShort() throws IOException { + return Short.reverseBytes(input.readShort()); + } + + public int readUnsignedShort() throws IOException { + return Short.toUnsignedInt(Short.reverseBytes(input.readShort())); + } + + @Override + public char readChar() throws IOException { + return Character.reverseBytes(input.readChar()); + } + + @Override + public int readInt() throws IOException { + return Integer.reverseBytes(input.readInt()); + } + + @Override + public long readLong() throws IOException { + return Long.reverseBytes(input.readLong()); + } + + @Override + public float readFloat() throws IOException { + return Float.intBitsToFloat(Integer.reverseBytes(input.readInt())); + } + + @Override + public double readDouble() throws IOException { + return Double.longBitsToDouble(Long.reverseBytes(input.readLong())); + } + + @Override + @Deprecated + public String readLine() throws IOException { + return input.readLine(); + } + + @Override + public void close() throws IOException { + input.close(); + } + + @Override + public String readUTF() throws IOException { + byte[] bytes = new byte[readUnsignedShort()]; + readFully(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java b/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java new file mode 100644 index 00000000..9cda01b2 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java @@ -0,0 +1,244 @@ +package net.querz.nbt.io; + +import net.querz.io.ExceptionTriConsumer; +import net.querz.io.MaxDepthIO; +import net.querz.nbt.tag.ByteArrayTag; +import net.querz.nbt.tag.ByteTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.DoubleTag; +import net.querz.nbt.tag.EndTag; +import net.querz.nbt.tag.FloatTag; +import net.querz.nbt.tag.IntArrayTag; +import net.querz.nbt.tag.IntTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.tag.LongArrayTag; +import net.querz.nbt.tag.LongTag; +import net.querz.nbt.tag.ShortTag; +import net.querz.nbt.tag.StringTag; +import net.querz.nbt.tag.Tag; +import java.io.Closeable; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class LittleEndianNBTOutputStream implements DataOutput, NBTOutput, MaxDepthIO, Closeable { + + private final DataOutputStream output; + + private static Map, Integer, IOException>> writers = new HashMap<>(); + private static Map, Byte> classIdMapping = new HashMap<>(); + + static { + put(EndTag.ID, (o, t, d) -> {}, EndTag.class); + put(ByteTag.ID, (o, t, d) -> writeByte(o, t), ByteTag.class); + put(ShortTag.ID, (o, t, d) -> writeShort(o, t), ShortTag.class); + put(IntTag.ID, (o, t, d) -> writeInt(o, t), IntTag.class); + put(LongTag.ID, (o, t, d) -> writeLong(o, t), LongTag.class); + put(FloatTag.ID, (o, t, d) -> writeFloat(o, t), FloatTag.class); + put(DoubleTag.ID, (o, t, d) -> writeDouble(o, t), DoubleTag.class); + put(ByteArrayTag.ID, (o, t, d) -> writeByteArray(o, t), ByteArrayTag.class); + put(StringTag.ID, (o, t, d) -> writeString(o, t), StringTag.class); + put(ListTag.ID, LittleEndianNBTOutputStream::writeList, ListTag.class); + put(CompoundTag.ID, LittleEndianNBTOutputStream::writeCompound, CompoundTag.class); + put(IntArrayTag.ID, (o, t, d) -> writeIntArray(o, t), IntArrayTag.class); + put(LongArrayTag.ID, (o, t, d) -> writeLongArray(o, t), LongArrayTag.class); + } + + private static void put(byte id, ExceptionTriConsumer, Integer, IOException> f, Class clazz) { + writers.put(id, f); + classIdMapping.put(clazz, id); + } + + public LittleEndianNBTOutputStream(OutputStream out) { + output = new DataOutputStream(out); + } + + public LittleEndianNBTOutputStream(DataOutputStream out) { + output = out; + } + + public void writeTag(NamedTag tag, int maxDepth) throws IOException { + writeByte(tag.getTag().getID()); + if (tag.getTag().getID() != 0) { + writeUTF(tag.getName() == null ? "" : tag.getName()); + } + writeRawTag(tag.getTag(), maxDepth); + } + + public void writeTag(Tag tag, int maxDepth) throws IOException { + writeByte(tag.getID()); + if (tag.getID() != 0) { + writeUTF(""); + } + writeRawTag(tag, maxDepth); + } + + public void writeRawTag(Tag tag, int maxDepth) throws IOException { + ExceptionTriConsumer, Integer, IOException> f; + if ((f = writers.get(tag.getID())) == null) { + throw new IOException("invalid tag \"" + tag.getID() + "\""); + } + f.accept(this, tag, maxDepth); + } + + static byte idFromClass(Class clazz) { + Byte id = classIdMapping.get(clazz); + if (id == null) { + throw new IllegalArgumentException("unknown Tag class " + clazz.getName()); + } + return id; + } + + private static void writeByte(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeByte(((ByteTag) tag).asByte()); + } + + private static void writeShort(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeShort(((ShortTag) tag).asShort()); + } + + private static void writeInt(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((IntTag) tag).asInt()); + } + + private static void writeLong(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeLong(((LongTag) tag).asLong()); + } + + private static void writeFloat(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeFloat(((FloatTag) tag).asFloat()); + } + + private static void writeDouble(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeDouble(((DoubleTag) tag).asDouble()); + } + + private static void writeString(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeUTF(((StringTag) tag).getValue()); + } + + private static void writeByteArray(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((ByteArrayTag) tag).length()); + out.write(((ByteArrayTag) tag).getValue()); + } + + private static void writeIntArray(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((IntArrayTag) tag).length()); + for (int i : ((IntArrayTag) tag).getValue()) { + out.writeInt(i); + } + } + + private static void writeLongArray(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((LongArrayTag) tag).length()); + for (long l : ((LongArrayTag) tag).getValue()) { + out.writeLong(l); + } + } + + private static void writeList(LittleEndianNBTOutputStream out, Tag tag, int maxDepth) throws IOException { + out.writeByte(idFromClass(((ListTag) tag).getTypeClass())); + out.writeInt(((ListTag) tag).size()); + for (Tag t : ((ListTag) tag)) { + out.writeRawTag(t, out.decrementMaxDepth(maxDepth)); + } + } + + private static void writeCompound(LittleEndianNBTOutputStream out, Tag tag, int maxDepth) throws IOException { + for (Map.Entry> entry : (CompoundTag) tag) { + if (entry.getValue().getID() == 0) { + throw new IOException("end tag not allowed"); + } + out.writeByte(entry.getValue().getID()); + out.writeUTF(entry.getKey()); + out.writeRawTag(entry.getValue(), out.decrementMaxDepth(maxDepth)); + } + out.writeByte(0); + } + + @Override + public void close() throws IOException { + output.close(); + } + + @Override + public void flush() throws IOException { + output.flush(); + } + + @Override + public void write(int b) throws IOException { + output.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + output.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + output.write(b, off, len); + } + + @Override + public void writeBoolean(boolean v) throws IOException { + output.writeBoolean(v); + } + + @Override + public void writeByte(int v) throws IOException { + output.writeByte(v); + } + + @Override + public void writeShort(int v) throws IOException { + output.writeShort(Short.reverseBytes((short) v)); + } + + @Override + public void writeChar(int v) throws IOException { + output.writeChar(Character.reverseBytes((char) v)); + } + + @Override + public void writeInt(int v) throws IOException { + output.writeInt(Integer.reverseBytes(v)); + } + + @Override + public void writeLong(long v) throws IOException { + output.writeLong(Long.reverseBytes(v)); + } + + @Override + public void writeFloat(float v) throws IOException { + output.writeInt(Integer.reverseBytes(Float.floatToIntBits(v))); + } + + @Override + public void writeDouble(double v) throws IOException { + output.writeLong(Long.reverseBytes(Double.doubleToLongBits(v))); + } + + @Override + public void writeBytes(String s) throws IOException { + output.writeBytes(s); + } + + @Override + public void writeChars(String s) throws IOException { + output.writeChars(s); + } + + @Override + public void writeUTF(String s) throws IOException { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + writeShort(bytes.length); + write(bytes); + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTDeserializer.java b/src/main/java/net/querz/nbt/io/NBTDeserializer.java index 2f7289e3..085e37be 100644 --- a/src/main/java/net/querz/nbt/io/NBTDeserializer.java +++ b/src/main/java/net/querz/nbt/io/NBTDeserializer.java @@ -8,7 +8,7 @@ public class NBTDeserializer implements Deserializer { - private boolean compressed; + private boolean compressed, littleEndian; public NBTDeserializer() { this(true); @@ -18,13 +18,25 @@ public NBTDeserializer(boolean compressed) { this.compressed = compressed; } + public NBTDeserializer(boolean compressed, boolean littleEndian) { + this.compressed = compressed; + this.littleEndian = littleEndian; + } + @Override public NamedTag fromStream(InputStream stream) throws IOException { - NBTInputStream nbtIn; + NBTInput nbtIn; + InputStream input; if (compressed) { - nbtIn = new NBTInputStream(new GZIPInputStream(stream)); + input = new GZIPInputStream(stream); + } else { + input = stream; + } + + if (littleEndian) { + nbtIn = new LittleEndianNBTInputStream(input); } else { - nbtIn = new NBTInputStream(stream); + nbtIn = new NBTInputStream(input); } return nbtIn.readTag(Tag.DEFAULT_MAX_DEPTH); } diff --git a/src/main/java/net/querz/nbt/io/NBTInput.java b/src/main/java/net/querz/nbt/io/NBTInput.java new file mode 100644 index 00000000..b17c6123 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTInput.java @@ -0,0 +1,11 @@ +package net.querz.nbt.io; + +import net.querz.nbt.tag.Tag; +import java.io.IOException; + +public interface NBTInput { + + NamedTag readTag(int maxDepth) throws IOException; + + Tag readRawTag(int maxDepth) throws IOException; +} diff --git a/src/main/java/net/querz/nbt/io/NBTInputStream.java b/src/main/java/net/querz/nbt/io/NBTInputStream.java index b3ca6b8a..dd69c3c7 100644 --- a/src/main/java/net/querz/nbt/io/NBTInputStream.java +++ b/src/main/java/net/querz/nbt/io/NBTInputStream.java @@ -22,7 +22,7 @@ import java.util.HashMap; import java.util.Map; -public class NBTInputStream extends DataInputStream implements MaxDepthIO { +public class NBTInputStream extends DataInputStream implements NBTInput, MaxDepthIO { private static Map, IOException>> readers = new HashMap<>(); private static Map> idClassMapping = new HashMap<>(); diff --git a/src/main/java/net/querz/nbt/io/NBTOutput.java b/src/main/java/net/querz/nbt/io/NBTOutput.java new file mode 100644 index 00000000..39f6d688 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTOutput.java @@ -0,0 +1,13 @@ +package net.querz.nbt.io; + +import net.querz.nbt.tag.Tag; +import java.io.IOException; + +public interface NBTOutput { + + void writeTag(NamedTag tag, int maxDepth) throws IOException; + + void writeTag(Tag tag, int maxDepth) throws IOException; + + void flush() throws IOException; +} diff --git a/src/main/java/net/querz/nbt/io/NBTOutputStream.java b/src/main/java/net/querz/nbt/io/NBTOutputStream.java index 4879052e..ae1a16b0 100644 --- a/src/main/java/net/querz/nbt/io/NBTOutputStream.java +++ b/src/main/java/net/querz/nbt/io/NBTOutputStream.java @@ -22,7 +22,7 @@ import java.util.HashMap; import java.util.Map; -public class NBTOutputStream extends DataOutputStream implements MaxDepthIO { +public class NBTOutputStream extends DataOutputStream implements NBTOutput, MaxDepthIO { private static Map, Integer, IOException>> writers = new HashMap<>(); private static Map, Byte> classIdMapping = new HashMap<>(); diff --git a/src/main/java/net/querz/nbt/io/NBTSerializer.java b/src/main/java/net/querz/nbt/io/NBTSerializer.java index fe505992..921d8f47 100644 --- a/src/main/java/net/querz/nbt/io/NBTSerializer.java +++ b/src/main/java/net/querz/nbt/io/NBTSerializer.java @@ -8,7 +8,7 @@ public class NBTSerializer implements Serializer { - private boolean compressed; + private boolean compressed, littleEndian; public NBTSerializer() { this(true); @@ -18,13 +18,25 @@ public NBTSerializer(boolean compressed) { this.compressed = compressed; } + public NBTSerializer(boolean compressed, boolean littleEndian) { + this.compressed = compressed; + this.littleEndian = littleEndian; + } + @Override public void toStream(NamedTag object, OutputStream out) throws IOException { - NBTOutputStream nbtOut; + NBTOutput nbtOut; + OutputStream output; if (compressed) { - nbtOut = new NBTOutputStream(new GZIPOutputStream(out, true)); + output = new GZIPOutputStream(out, true); + } else { + output = out; + } + + if (littleEndian) { + nbtOut = new LittleEndianNBTOutputStream(output); } else { - nbtOut = new NBTOutputStream(out); + nbtOut = new NBTOutputStream(output); } nbtOut.writeTag(object, Tag.DEFAULT_MAX_DEPTH); nbtOut.flush(); diff --git a/src/main/java/net/querz/nbt/io/NBTUtil.java b/src/main/java/net/querz/nbt/io/NBTUtil.java index d8efc150..edd0b869 100644 --- a/src/main/java/net/querz/nbt/io/NBTUtil.java +++ b/src/main/java/net/querz/nbt/io/NBTUtil.java @@ -47,6 +47,40 @@ public static void write(Tag tag, String file) throws IOException { write(new NamedTag(null, tag), new File(file), true); } + public static void writeLE(NamedTag tag, File file, boolean compressed) throws IOException { + try (FileOutputStream fos = new FileOutputStream(file)) { + new NBTSerializer(compressed, true).toStream(tag, fos); + } + } + + public static void writeLE(NamedTag tag, String file, boolean compressed) throws IOException { + writeLE(tag, new File(file), compressed); + } + + public static void writeLE(NamedTag tag, File file) throws IOException { + writeLE(tag, file, true); + } + + public static void writeLE(NamedTag tag, String file) throws IOException { + writeLE(tag, new File(file), true); + } + + public static void writeLE(Tag tag, File file, boolean compressed) throws IOException { + writeLE(new NamedTag(null, tag), file, compressed); + } + + public static void writeLE(Tag tag, String file, boolean compressed) throws IOException { + writeLE(new NamedTag(null, tag), new File(file), compressed); + } + + public static void writeLE(Tag tag, File file) throws IOException { + writeLE(new NamedTag(null, tag), file, true); + } + + public static void writeLE(Tag tag, String file) throws IOException { + writeLE(new NamedTag(null, tag), new File(file), true); + } + public static NamedTag read(File file, boolean compressed) throws IOException { try (FileInputStream fis = new FileInputStream(file)) { return new NBTDeserializer(compressed).fromStream(fis); @@ -67,6 +101,26 @@ public static NamedTag read(String file) throws IOException { return read(new File(file)); } + public static NamedTag readLE(File file, boolean compressed) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + return new NBTDeserializer(compressed, true).fromStream(fis); + } + } + + public static NamedTag readLE(String file, boolean compressed) throws IOException { + return readLE(new File(file), compressed); + } + + public static NamedTag readLE(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + return new NBTDeserializer(false, true).fromStream(detectDecompression(fis)); + } + } + + public static NamedTag readLE(String file) throws IOException { + return readLE(new File(file)); + } + private static InputStream detectDecompression(InputStream is) throws IOException { PushbackInputStream pbis = new PushbackInputStream(is, 2); int signature = (pbis.read() & 0xFF) + (pbis.read() << 8); From 021f1d8d2ef9b2d4b13db5de0188d49fd6a0f3d7 Mon Sep 17 00:00:00 2001 From: Querz Date: Fri, 7 Aug 2020 17:19:13 +0200 Subject: [PATCH 2/5] add load flag to only load raw data and skip loading specifics --- src/main/java/net/querz/mca/Chunk.java | 107 +++++++++++++++++---- src/main/java/net/querz/mca/LoadFlags.java | 37 ++++--- 2 files changed, 107 insertions(+), 37 deletions(-) diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index d305df04..cd7d9853 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -16,9 +16,10 @@ public class Chunk { - public static final int DEFAULT_DATA_VERSION = 1628; + public static final int DEFAULT_DATA_VERSION = 2567; private boolean partial; + private boolean raw; private int lastMCAUpdate; @@ -59,6 +60,12 @@ private void initReferences(long loadFlags) { if (data == null) { throw new NullPointerException("data cannot be null"); } + + if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) { + raw = true; + return; + } + CompoundTag level; if ((level = data.getCompoundTag("Level")) == null) { throw new IllegalArgumentException("data does not contain \"Level\" tag"); @@ -121,8 +128,6 @@ private void initReferences(long loadFlags) { if (loadFlags != ALL_DATA) { data = null; partial = true; - } else { - partial = false; } } @@ -224,6 +229,7 @@ public int getBiomeAt(int blockX, int blockY, int blockZ) { @Deprecated public void setBiomeAt(int blockX, int blockZ, int biomeID) { + checkRaw(); if (dataVersion < 2202) { if (biomes == null || biomes.length != 256) { biomes = new int[256]; @@ -254,6 +260,7 @@ public void setBiomeAt(int blockX, int blockZ, int biomeID) { * When set to a negative number, Minecraft will replace it with the block column's default biome. */ public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { + checkRaw(); if (dataVersion < 2202) { if (biomes == null || biomes.length != 256) { biomes = new int[256]; @@ -297,6 +304,7 @@ public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { * Recalculating the Palette should only be executed once right before saving the Chunk to file. */ public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + checkRaw(); int sectionIndex = MCAUtil.blockToChunk(blockY); Section section = sections[sectionIndex]; if (section == null) { @@ -318,6 +326,7 @@ public int getDataVersion() { * @param dataVersion The DataVersion to be set. */ public void setDataVersion(int dataVersion) { + checkRaw(); this.dataVersion = dataVersion; } @@ -333,6 +342,7 @@ public int getLastMCAUpdate() { * @param lastMCAUpdate The time in seconds since 1970-01-01. */ public void setLastMCAUpdate(int lastMCAUpdate) { + checkRaw(); this.lastMCAUpdate = lastMCAUpdate; } @@ -348,6 +358,7 @@ public String getStatus() { * @param status The generation status of this chunk. */ public void setStatus(String status) { + checkRaw(); this.status = status; } @@ -366,6 +377,7 @@ public Section getSection(int sectionY) { * @param section The section to be set. */ public void setSection(int sectionY, Section section) { + checkRaw(); sections[sectionY] = section; } @@ -381,6 +393,7 @@ public long getLastUpdate() { * @param lastUpdate The UNIX timestamp. */ public void setLastUpdate(long lastUpdate) { + checkRaw(); this.lastUpdate = lastUpdate; } @@ -396,6 +409,7 @@ public long getInhabitedTime() { * @param inhabitedTime The time in ticks. */ public void setInhabitedTime(long inhabitedTime) { + checkRaw(); this.inhabitedTime = inhabitedTime; } @@ -413,6 +427,7 @@ public int[] getBiomes() { * or is null */ public void setBiomes(int[] biomes) { + checkRaw(); if (biomes != null) { if (dataVersion < 2202 && biomes.length != 256 || dataVersion >= 2202 && biomes.length != 1024) { throw new IllegalArgumentException("biomes array must have a length of " + (dataVersion < 2202 ? "256" : "1024")); @@ -433,6 +448,7 @@ public CompoundTag getHeightMaps() { * @param heightMaps The height maps. */ public void setHeightMaps(CompoundTag heightMaps) { + checkRaw(); this.heightMaps = heightMaps; } @@ -448,6 +464,7 @@ public CompoundTag getCarvingMasks() { * @param carvingMasks The carving masks. */ public void setCarvingMasks(CompoundTag carvingMasks) { + checkRaw(); this.carvingMasks = carvingMasks; } @@ -463,6 +480,7 @@ public ListTag getEntities() { * @param entities The entities. */ public void setEntities(ListTag entities) { + checkRaw(); this.entities = entities; } @@ -478,6 +496,7 @@ public ListTag getTileEntities() { * @param tileEntities The tile entities of this chunk. */ public void setTileEntities(ListTag tileEntities) { + checkRaw(); this.tileEntities = tileEntities; } @@ -493,6 +512,7 @@ public ListTag getTileTicks() { * @param tileTicks Thee tile ticks. */ public void setTileTicks(ListTag tileTicks) { + checkRaw(); this.tileTicks = tileTicks; } @@ -508,6 +528,7 @@ public ListTag getLiquidTicks() { * @param liquidTicks The liquid ticks. */ public void setLiquidTicks(ListTag liquidTicks) { + checkRaw(); this.liquidTicks = liquidTicks; } @@ -523,11 +544,12 @@ public ListTag> getLights() { * @param lights The light sources. */ public void setLights(ListTag> lights) { + checkRaw(); this.lights = lights; } /** - * @return THe liquids to be ticked in this chunk. + * @return The liquids to be ticked in this chunk. */ public ListTag> getLiquidsToBeTicked() { return liquidsToBeTicked; @@ -538,6 +560,7 @@ public ListTag> getLiquidsToBeTicked() { * @param liquidsToBeTicked The liquids to be ticked. */ public void setLiquidsToBeTicked(ListTag> liquidsToBeTicked) { + checkRaw(); this.liquidsToBeTicked = liquidsToBeTicked; } @@ -553,6 +576,7 @@ public ListTag> getToBeTicked() { * @param toBeTicked The stuff to be ticked. */ public void setToBeTicked(ListTag> toBeTicked) { + checkRaw(); this.toBeTicked = toBeTicked; } @@ -568,6 +592,7 @@ public ListTag> getPostProcessing() { * @param postProcessing The things to be post processed. */ public void setPostProcessing(ListTag> postProcessing) { + checkRaw(); this.postProcessing = postProcessing; } @@ -583,6 +608,7 @@ public CompoundTag getStructures() { * @param structures The data about structures. */ public void setStructures(CompoundTag structures) { + checkRaw(); this.structures = structures; } @@ -591,6 +617,7 @@ int getBlockIndex(int blockX, int blockZ) { } public void cleanupPalettesAndBlockStates() { + checkRaw(); for (Section section : sections) { if (section != null) { section.cleanupPaletteAndBlockStates(); @@ -598,6 +625,12 @@ public void cleanupPalettesAndBlockStates() { } } + private void checkRaw() { + if (raw) { + throw new UnsupportedOperationException("cannot update field when working with raw data"); + } + } + public static Chunk newChunk() { Chunk c = new Chunk(0); c.dataVersion = DEFAULT_DATA_VERSION; @@ -607,7 +640,19 @@ public static Chunk newChunk() { return c; } + /** + * Provides a reference to the full chunk data. + * @return The full chunk data or null if there is none, e.g. when this chunk has only been loaded partially. + */ + public CompoundTag getHandle() { + return data; + } + public CompoundTag updateHandle(int xPos, int zPos) { + if (raw) { + return data; + } + data.putInt("DataVersion", dataVersion); CompoundTag level = data.getCompoundTag("Level"); level.putInt("xPos", xPos); @@ -615,22 +660,48 @@ public CompoundTag updateHandle(int xPos, int zPos) { level.putLong("LastUpdate", lastUpdate); level.putLong("InhabitedTime", inhabitedTime); if (dataVersion < 2202) { - if (biomes != null && biomes.length == 256) level.putIntArray("Biomes", biomes); + if (biomes != null && biomes.length == 256) { + level.putIntArray("Biomes", biomes); + } } else { - if (biomes != null && biomes.length == 1024) level.putIntArray("Biomes", biomes); - } - if (heightMaps != null) level.put("Heightmaps", heightMaps); - if (carvingMasks != null) level.put("CarvingMasks", carvingMasks); - if (entities != null) level.put("Entities", entities); - if (tileEntities != null) level.put("TileEntities", tileEntities); - if (tileTicks != null) level.put("TileTicks", tileTicks); - if (liquidTicks != null) level.put("LiquidTicks", liquidTicks); - if (lights != null) level.put("Lights", lights); - if (liquidsToBeTicked != null) level.put("LiquidsToBeTicked", liquidsToBeTicked); - if (toBeTicked != null) level.put("ToBeTicked", toBeTicked); - if (postProcessing != null) level.put("PostProcessing", postProcessing); + if (biomes != null && biomes.length == 1024) { + level.putIntArray("Biomes", biomes); + } + } + if (heightMaps != null) { + level.put("Heightmaps", heightMaps); + } + if (carvingMasks != null) { + level.put("CarvingMasks", carvingMasks); + } + if (entities != null) { + level.put("Entities", entities); + } + if (tileEntities != null) { + level.put("TileEntities", tileEntities); + } + if (tileTicks != null) { + level.put("TileTicks", tileTicks); + } + if (liquidTicks != null) { + level.put("LiquidTicks", liquidTicks); + } + if (lights != null) { + level.put("Lights", lights); + } + if (liquidsToBeTicked != null) { + level.put("LiquidsToBeTicked", liquidsToBeTicked); + } + if (toBeTicked != null) { + level.put("ToBeTicked", toBeTicked); + } + if (postProcessing != null) { + level.put("PostProcessing", postProcessing); + } level.putString("Status", status); - if (structures != null) level.put("Structures", structures); + if (structures != null) { + level.put("Structures", structures); + } ListTag sections = new ListTag<>(CompoundTag.class); for (int i = 0; i < this.sections.length; i++) { if (this.sections[i] != null) { diff --git a/src/main/java/net/querz/mca/LoadFlags.java b/src/main/java/net/querz/mca/LoadFlags.java index e4122898..da37a597 100644 --- a/src/main/java/net/querz/mca/LoadFlags.java +++ b/src/main/java/net/querz/mca/LoadFlags.java @@ -1,24 +1,23 @@ package net.querz.mca; -public class LoadFlags { - - public static long BIOMES = 0x0001; - public static long HEIGHTMAPS = 0x0002; - public static long CARVING_MASKS = 0x0004; - public static long ENTITIES = 0x0008; - public static long TILE_ENTITIES = 0x0010; - public static long TILE_TICKS = 0x0040; - public static long LIQUID_TICKS = 0x0080; - public static long TO_BE_TICKED = 0x0100; - public static long POST_PROCESSING = 0x0200; - public static long STRUCTURES = 0x0400; - public static long BLOCK_LIGHTS = 0x0800; - public static long BLOCK_STATES = 0x1000; - public static long SKY_LIGHT = 0x2000; - public static long LIGHTS = 0x4000; - public static long LIQUIDS_TO_BE_TICKED = 0x8000; - - public static long ALL_DATA = 0xffffffffffffffffL; +public final class LoadFlags { + public static final long BIOMES = 0x00001; + public static final long HEIGHTMAPS = 0x00002; + public static final long CARVING_MASKS = 0x00004; + public static final long ENTITIES = 0x00008; + public static final long TILE_ENTITIES = 0x00010; + public static final long TILE_TICKS = 0x00040; + public static final long LIQUID_TICKS = 0x00080; + public static final long TO_BE_TICKED = 0x00100; + public static final long POST_PROCESSING = 0x00200; + public static final long STRUCTURES = 0x00400; + public static final long BLOCK_LIGHTS = 0x00800; + public static final long BLOCK_STATES = 0x01000; + public static final long SKY_LIGHT = 0x02000; + public static final long LIGHTS = 0x04000; + public static final long LIQUIDS_TO_BE_TICKED = 0x08000; + public static final long RAW = 0x10000; + public static final long ALL_DATA = 0xffffffffffffffffL; } From 90646abb469c2c96e16f36fdca5fcb966b2f758a Mon Sep 17 00:00:00 2001 From: Querz Date: Wed, 2 Sep 2020 15:34:52 +0200 Subject: [PATCH 3/5] incorrectly set list type of empty list when casting / fix unit tests --- src/main/java/net/querz/mca/Chunk.java | 8 ++++++-- src/main/java/net/querz/nbt/tag/ListTag.java | 11 +++++------ src/test/java/net/querz/mca/MCAFileTest.java | 11 ++++++----- src/test/java/net/querz/nbt/tag/ListTagTest.java | 11 ++++++----- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index cd7d9853..cfe06ce6 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -281,7 +281,7 @@ public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { } int getBiomeIndex(int biomeX, int biomeY, int biomeZ) { - return biomeY * 64 + biomeZ * 4 + biomeX; + return biomeY * 16 + biomeZ * 4 + biomeX; } public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { @@ -632,8 +632,12 @@ private void checkRaw() { } public static Chunk newChunk() { + return newChunk(DEFAULT_DATA_VERSION); + } + + public static Chunk newChunk(int dataVersion) { Chunk c = new Chunk(0); - c.dataVersion = DEFAULT_DATA_VERSION; + c.dataVersion = dataVersion; c.data = new CompoundTag(); c.data.put("Level", new CompoundTag()); c.status = "mobs_spawned"; diff --git a/src/main/java/net/querz/nbt/tag/ListTag.java b/src/main/java/net/querz/nbt/tag/ListTag.java index 008e3aaa..0dd6970a 100644 --- a/src/main/java/net/querz/nbt/tag/ListTag.java +++ b/src/main/java/net/querz/nbt/tag/ListTag.java @@ -12,8 +12,7 @@ /** * ListTag represents a typed List in the nbt structure. - * An empty {@link ListTag} created using {@link ListTag#createUnchecked(Class)} will be of unknown type - * and returns an {@link EndTag}{@code .class} in {@link ListTag#getTypeClass()}. + * An empty {@link ListTag} will be of type {@link EndTag} (unknown type). * The type of an empty untyped {@link ListTag} can be set by using any of the {@code add()} * methods or any of the {@code as...List()} methods. * */ @@ -123,7 +122,7 @@ public void add(T t) { public void add(int index, T t) { Objects.requireNonNull(t); - if (typeClass == null || typeClass == EndTag.class) { + if (getTypeClass() == EndTag.class) { typeClass = t.getClass(); } else if (typeClass != t.getClass()) { throw new ClassCastException( @@ -203,7 +202,7 @@ public int indexOf(T t) { @SuppressWarnings("unchecked") public > ListTag asTypedList(Class type) { checkTypeClass(type); - typeClass = type; +// typeClass = type; return (ListTag) this; } @@ -309,7 +308,7 @@ public ListTag clone() { //TODO: make private @SuppressWarnings("unchecked") public void addUnchecked(Tag tag) { - if (typeClass != null && typeClass != tag.getClass() && typeClass != EndTag.class) { + if (getTypeClass() != EndTag.class && typeClass != tag.getClass()) { throw new IllegalArgumentException(String.format( "cannot add %s to ListTag<%s>", tag.getClass().getSimpleName(), typeClass.getSimpleName())); @@ -318,7 +317,7 @@ public void addUnchecked(Tag tag) { } private void checkTypeClass(Class clazz) { - if (typeClass != null && typeClass != EndTag.class && typeClass != clazz) { + if (getTypeClass() != EndTag.class && typeClass != clazz) { throw new ClassCastException(String.format( "cannot cast ListTag<%s> to ListTag<%s>", typeClass.getSimpleName(), clazz.getSimpleName())); diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java index 38e09f49..2688001a 100644 --- a/src/test/java/net/querz/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -1,6 +1,7 @@ package net.querz.mca; import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.EndTag; import net.querz.nbt.tag.ListTag; import static net.querz.mca.LoadFlags.*; import java.io.File; @@ -90,7 +91,7 @@ public void testGetters() { assertNotNull(f.getChunk(0).getBiomes()); assertNull(f.getChunk(0).getHeightMaps()); assertNull(f.getChunk(0).getCarvingMasks()); - assertEquals(new ListTag<>(CompoundTag.class), f.getChunk(0).getEntities()); + assertEquals(ListTag.createUnchecked(null), f.getChunk(0).getEntities()); assertNull(f.getChunk(0).getTileEntities()); assertNull(f.getChunk(0).getTileTicks()); assertNull(f.getChunk(0).getLiquidTicks()); @@ -184,7 +185,7 @@ public void testGetBiomeAt() { MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertEquals(21, f.getBiomeAt(1024, 1024)); assertEquals(-1, f.getBiomeAt(1040, 1024)); - f.setChunk(0, 1, Chunk.newChunk()); + f.setChunk(0, 1, Chunk.newChunk(2201)); assertEquals(-1, f.getBiomeAt(1024, 1040)); } @@ -197,9 +198,9 @@ public void testSetBiomeAt() { assertEquals(47, f.getChunk(64, 64).updateHandle(64, 64).getCompoundTag("Level").getIntArray("Biomes")[255]); f.setBiomeAt(1040, 1024, 20); int[] biomes = f.getChunk(65, 64).updateHandle(65, 64).getCompoundTag("Level").getIntArray("Biomes"); - assertEquals(256, biomes.length); - for (int i = 0; i < 256; i++) { - assertTrue(i == 0 ? biomes[i] == 20 : biomes[i] == -1); + assertEquals(1024, biomes.length); + for (int i = 0; i < 1024; i++) { + assertTrue(i % 16 == 0 ? biomes[i] == 20 : biomes[i] == -1); } } diff --git a/src/test/java/net/querz/nbt/tag/ListTagTest.java b/src/test/java/net/querz/nbt/tag/ListTagTest.java index 777d165b..af2b72fb 100644 --- a/src/test/java/net/querz/nbt/tag/ListTagTest.java +++ b/src/test/java/net/querz/nbt/tag/ListTagTest.java @@ -72,11 +72,11 @@ public void testEquals() { ListTag lu2 = ListTag.createUnchecked(null); assertTrue(lu.equals(lu2)); lu2.asIntTagList(); - assertFalse(lu.equals(lu2)); + assertTrue(lu.equals(lu2)); ListTag lie = new ListTag<>(IntTag.class); assertFalse(lu.equals(lie)); lu.asIntTagList(); - assertTrue(lie.equals(lu)); + assertFalse(lie.equals(lu)); } public void testHashCode() { @@ -91,7 +91,8 @@ public void testClone() { ListTag i = new ListTag<>(IntTag.class); ListTag c = i.clone(); assertThrowsRuntimeException(() -> c.addString("wrong type in clone"), IllegalArgumentException.class); - c.addInt(123); + assertThrowsNoRuntimeException(() -> c.addInt(123)); + assertFalse(i.equals(c)); c.clear(); assertTrue(i.equals(c)); @@ -203,8 +204,8 @@ public void testCasting() { ListTag lg = ListTag.createUnchecked(null); ListTag lb = assertThrowsNoRuntimeException(lg::asByteTagList); assertEquals(lb, lg); - //only allow casting once from untyped list to typed list - assertThrowsRuntimeException(lg::asShortTagList, ClassCastException.class); + // allow casting to a different list type if the list is empty + assertThrowsNoException(lg::asShortTagList); } public void testCompareTo() { From 96b13d043a5a5d6aba8b926601cab26003d81540 Mon Sep 17 00:00:00 2001 From: Querz Date: Wed, 2 Sep 2020 15:35:16 +0200 Subject: [PATCH 4/5] remove commented out code --- src/main/java/net/querz/nbt/tag/ListTag.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/net/querz/nbt/tag/ListTag.java b/src/main/java/net/querz/nbt/tag/ListTag.java index 0dd6970a..f26c4b07 100644 --- a/src/main/java/net/querz/nbt/tag/ListTag.java +++ b/src/main/java/net/querz/nbt/tag/ListTag.java @@ -202,7 +202,6 @@ public int indexOf(T t) { @SuppressWarnings("unchecked") public > ListTag asTypedList(Class type) { checkTypeClass(type); -// typeClass = type; return (ListTag) this; } From d985c463ecdbe5e1a73fd9f4d12560796dddec7a Mon Sep 17 00:00:00 2001 From: Querz Date: Mon, 11 Jan 2021 12:33:16 +0100 Subject: [PATCH 5/5] update version to 6.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4d457e77..4e79db37 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ apply plugin: 'jacoco' group = 'net.querz.nbt' archivesBaseName = 'nbt' -version = '5.5' +version = '6.0' sourceCompatibility = '1.8' targetCompatibility = '1.8' compileJava.options.encoding = 'UTF-8'