package com.intellectualcrafters.jnbt;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class reads <strong>NBT</strong>, or <strong>Named Binary Tag</strong> streams, and produces an object graph of
* subclasses of the {@code Tag} object. The NBT format was created by Markus Persson, and the specification
* may be found at @linktourl http://www.minecraft.net/docs/NBT.txt"> http://www.minecraft.net/docs/NBT.txt.
*/
public final class NBTInputStream implements Closeable {
private final DataInputStream is;
private int count;
/**
* Creates a new {@code NBTInputStream}, which will source its data from the specified input stream.
*
* @param is the input stream
*
* @throws IOException if an I/O error occurs
*/
public NBTInputStream(InputStream is) {
this.is = new DataInputStream(is);
}
/**
* Reads an NBT tag from the stream.
*
* @return The tag that was read.
*
* @throws IOException if an I/O error occurs.
*/
public Tag readTag() throws IOException {
return readTag(0, Integer.MAX_VALUE);
}
/**
* Reads an NBT tag from the stream.
*
* @return The tag that was read.
*
* @throws IOException if an I/O error occurs.
*/
public Tag readTag(int maxDepth) throws IOException {
return readTag(0, maxDepth);
}
/**
* Reads an NBT from the stream.
*
* @param depth the depth of this tag
*
* @return The tag that was read.
*
* @throws IOException if an I/O error occurs.
*/
private Tag readTag(int depth, int maxDepth) throws IOException {
if (this.count++ > maxDepth) {
throw new IOException("Exceeds max depth: " + this.count);
}
int type = this.is.readByte() & 0xFF;
String name;
if (type != NBTConstants.TYPE_END) {
int nameLength = this.is.readShort() & 0xFFFF;
byte[] nameBytes = new byte[nameLength];
this.is.readFully(nameBytes);
name = new String(nameBytes, NBTConstants.CHARSET);
} else {
name = "";
}
return readTagPayload(type, name, depth, maxDepth);
}
/**
* Reads the payload of a tag, given the name and type.
*
* @param type the type
* @param name the name
* @param depth the depth
*
* @return the tag
*
* @throws IOException if an I/O error occurs.
*/
private Tag readTagPayload(int type, String name, int depth, int maxDepth) throws IOException {
if (this.count++ > maxDepth) {
throw new IOException("Exceeds max depth: " + this.count);
}
this.count++;
switch (type) {
case NBTConstants.TYPE_END:
if (depth == 0) {
throw new IOException("TAG_End found without a TAG_Compound/TAG_List tag preceding it.");
} else {
return new EndTag();
}
case NBTConstants.TYPE_BYTE:
return new ByteTag(name, this.is.readByte());
case NBTConstants.TYPE_SHORT:
return new ShortTag(name, this.is.readShort());
case NBTConstants.TYPE_INT:
return new IntTag(name, this.is.readInt());
case NBTConstants.TYPE_LONG:
return new LongTag(name, this.is.readLong());
case NBTConstants.TYPE_FLOAT:
return new FloatTag(name, this.is.readFloat());
case NBTConstants.TYPE_DOUBLE:
return new DoubleTag(name, this.is.readDouble());
case NBTConstants.TYPE_BYTE_ARRAY:
int length = this.is.readInt();
// Max depth
if ((this.count += length) > maxDepth) {
throw new IOException("Exceeds max depth: " + this.count);
//
}
byte[] bytes = new byte[length];
this.is.readFully(bytes);
return new ByteArrayTag(name, bytes);
case NBTConstants.TYPE_STRING:
length = this.is.readShort();
// Max depth
if ((this.count += length) > maxDepth) {
throw new IOException("Exceeds max depth: " + this.count);
//
}
bytes = new byte[length];
this.is.readFully(bytes);
return new StringTag(name, new String(bytes, NBTConstants.CHARSET));
case NBTConstants.TYPE_LIST:
int childType = this.is.readByte();
length = this.is.readInt();
// Max depth
if ((this.count += length) > maxDepth) {
throw new IOException("Exceeds max depth: " + this.count);
//
}
List<Tag> tagList = new ArrayList<Tag>();
for (int i = 0; i < length; ++i) {
Tag tag = readTagPayload(childType, "", depth + 1, maxDepth);
if (tag instanceof EndTag) {
throw new IOException("TAG_End not permitted in a list.");
}
tagList.add(tag);
}
return new ListTag(name, NBTUtils.getTypeClass(childType), tagList);
case NBTConstants.TYPE_COMPOUND:
Map<String, Tag> tagMap = new HashMap<String, Tag>();
while (true) {
Tag tag = readTag(depth + 1, maxDepth);
if (tag instanceof EndTag) {
break;
} else {
tagMap.put(tag.getName(), tag);
}
}
return new CompoundTag(name, tagMap);
case NBTConstants.TYPE_INT_ARRAY:
length = this.is.readInt();
// Max depth
if ((this.count += length) > maxDepth) {
throw new IOException("Exceeds max depth: " + this.count);
}
//
int[] data = new int[length];
for (int i = 0; i < length; i++) {
data[i] = this.is.readInt();
}
return new IntArrayTag(name, data);
default:
throw new IOException("Invalid tag type: " + type + '.');
}
}
@Override
public void close() throws IOException {
this.is.close();
}
}