package net.glowstone.util.nbt; import java.io.Closeable; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPInputStream; /** * This class reads NBT, or Named Binary Tag streams, and produces an object * graph of subclasses of the {@link Tag} object. * <p/> * The NBT format was created by Markus Persson, and the specification may * be found at <a href="http://www.minecraft.net/docs/NBT.txt"> * http://www.minecraft.net/docs/NBT.txt</a>. */ public final class NBTInputStream implements Closeable { /** * The data input stream. */ private final DataInputStream is; /** * Creates a new NBTInputStream, which will source its data * from the specified input stream. This assumes the stream is compressed. * @param is The input stream. * @throws IOException if an I/O error occurs. */ public NBTInputStream(InputStream is) throws IOException { this(is, true); } /** * Creates a new NBTInputStream, which sources its data from the * specified input stream. A flag must be passed which indicates if the * stream is compressed with GZIP or not. * @param is The input stream. * @param compressed A flag indicating if the stream is compressed. * @throws IOException if an I/O error occurs. */ @SuppressWarnings("resource") public NBTInputStream(InputStream is, boolean compressed) throws IOException { this.is = new DataInputStream(compressed ? new GZIPInputStream(is) : is); } /** * Reads the root NBT {@link CompoundTag} from the stream. * @return The tag that was read. * @throws IOException if an I/O error occurs. */ public CompoundTag readCompound() throws IOException { // read type TagType type = TagType.byIdOrError(is.readUnsignedByte()); if (type != TagType.COMPOUND) { throw new IOException("Root of NBTInputStream was " + type + ", not COMPOUND"); } // for now, throw away name int nameLength = is.readUnsignedShort(); is.skipBytes(nameLength); // read tag return (CompoundTag) readTagPayload(type, 0); } private CompoundTag readCompound(int depth) throws IOException { CompoundTag result = new CompoundTag(); while (true) { // read type TagType type = TagType.byIdOrError(is.readUnsignedByte()); if (type == TagType.END) { break; } // read name int nameLength = is.readUnsignedShort(); byte[] nameBytes = new byte[nameLength]; is.readFully(nameBytes); String name = new String(nameBytes, StandardCharsets.UTF_8); // read tag Tag tag = readTagPayload(type, depth + 1); result.put(name, tag); } return result; } /** * Reads the payload of a {@link Tag}, given the name and type. * @param type The type. * @param depth The depth. * @return The tag. * @throws IOException if an I/O error occurs. */ @SuppressWarnings("unchecked") private Tag readTagPayload(TagType type, int depth) throws IOException { switch (type) { case BYTE: return new ByteTag(is.readByte()); case SHORT: return new ShortTag(is.readShort()); case INT: return new IntTag(is.readInt()); case LONG: return new LongTag(is.readLong()); case FLOAT: return new FloatTag(is.readFloat()); case DOUBLE: return new DoubleTag(is.readDouble()); case BYTE_ARRAY: int length = is.readInt(); byte[] bytes = new byte[length]; is.readFully(bytes); return new ByteArrayTag(bytes); case STRING: length = is.readShort(); bytes = new byte[length]; is.readFully(bytes); return new StringTag(new String(bytes, StandardCharsets.UTF_8)); case LIST: TagType childType = TagType.byIdOrError(is.readUnsignedByte()); length = is.readInt(); List<Tag> tagList = new ArrayList<>(); for (int i = 0; i < length; i++) { tagList.add(readTagPayload(childType, depth + 1)); } return new ListTag(childType, tagList); case COMPOUND: return readCompound(depth + 1); case INT_ARRAY: length = is.readInt(); int[] ints = new int[length]; for (int i = 0; i < length; ++i) { ints[i] = is.readInt(); } return new IntArrayTag(ints); default: throw new IOException("Invalid tag type: " + type + "."); } } @Override public void close() throws IOException { is.close(); } }