Fix the StreamSerializer

String-based methods will still work, but deserializing from a data
input stream has been deprecated since it depends on hacky code

Fixes #31, Fixes #125
This commit is contained in:
Dan Mulloy 2015-11-02 23:56:41 -05:00
parent 4bb917e596
commit 7dff86cb48
10 changed files with 293 additions and 191 deletions

View File

@ -61,10 +61,6 @@ public class Netty {
return getCompat().createPacketBuffer();
}
public static WrappedByteBuf allocateUnpooled() {
return getCompat().allocateUnpooled();
}
public static Class<?> getGenericFutureListenerArray() {
return getCompat().getGenericFutureListenerArray();
}
@ -92,4 +88,12 @@ public class Netty {
public static WrappedByteBuf packetWriter(DataOutputStream output) {
return getCompat().packetWriter(output);
}
public static WrappedByteBuf copiedBuffer(byte[] array) {
return getCompat().copiedBuffer(array);
}
public static WrappedByteBuf buffer() {
return getCompat().buffer();
}
}

View File

@ -30,11 +30,14 @@ import com.comphenix.protocol.wrappers.WrappedServerPing.CompressedImage;
* @author dmulloy2
*/
// TODO: Sort out packet readers/writers
public interface NettyCompat {
WrappedByteBuf createPacketBuffer();
WrappedByteBuf allocateUnpooled();
WrappedByteBuf copiedBuffer(byte[] array);
WrappedByteBuf buffer();
Class<?> getGenericFutureListenerArray();

View File

@ -41,4 +41,6 @@ public interface WrappedByteBuf {
void writeByte(int i);
void writeBytes(byte[] bytes);
byte[] array();
}

View File

@ -55,11 +55,6 @@ public class IndependentNetty implements NettyCompat {
}
}
@Override
public WrappedByteBuf allocateUnpooled() {
return new NettyByteBuf(UnpooledByteBufAllocator.DEFAULT.buffer());
}
@Override
public Class<?> getGenericFutureListenerArray() {
return GenericFutureListener[].class;
@ -97,4 +92,14 @@ public class IndependentNetty implements NettyCompat {
public WrappedByteBuf packetWriter(DataOutputStream output) {
return new NettyByteBuf(NettyByteBufAdapter.packetWriter(output));
}
@Override
public WrappedByteBuf copiedBuffer(byte[] array) {
return new NettyByteBuf(Unpooled.copiedBuffer(array));
}
@Override
public WrappedByteBuf buffer() {
return new NettyByteBuf(Unpooled.buffer());
}
}

View File

@ -75,4 +75,9 @@ public class NettyByteBuf implements WrappedByteBuf {
public void writeBytes(byte[] bytes) {
handle.get().writeBytes(bytes);
}
@Override
public byte[] array() {
return handle.get().array();
}
}

View File

@ -174,7 +174,7 @@ public class MinecraftMethods {
// Create our proxy object
Object javaProxy = enhancer.create(
new Class<?>[] { MinecraftReflection.getByteBufClass() },
new Object[] { Netty.allocateUnpooled().getHandle() }
new Object[] { Netty.buffer().getHandle() }
);
Object lookPacket = new PacketContainer(PacketType.Play.Client.CLOSE_WINDOW).getHandle();

View File

@ -10,6 +10,7 @@ import java.io.IOException;
import javax.annotation.Nonnull;
import org.apache.commons.lang.Validate;
import org.bukkit.inventory.ItemStack;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
@ -49,7 +50,23 @@ public class StreamSerializer {
public static StreamSerializer getDefault() {
return DEFAULT;
}
/**
* Write a variable integer to an output stream.
* @param destination - the destination.
* @param value - the value to write.
* @throws IOException The destination stream threw an exception.
*/
public void serializeVarInt(@Nonnull DataOutputStream destination, int value) throws IOException {
Preconditions.checkNotNull(destination, "source cannot be NULL");
while ((value & 0xFFFFFF80) != 0) {
destination.writeByte(value & 0x7F | 0x80);
value >>>= 7;
}
destination.writeByte(value);
}
/**
* Read a variable integer from an input stream.
* @param source - the source.
@ -73,66 +90,49 @@ public class StreamSerializer {
}
/**
* Write a variable integer to an output stream.
* @param destination - the destination.
* @param value - the value to write.
* @throws IOException The destination stream threw an exception.
*/
public void serializeVarInt(@Nonnull DataOutputStream destination, int value) throws IOException {
Preconditions.checkNotNull(destination, "source cannot be NULL");
while ((value & 0xFFFFFF80) != 0) {
destination.writeByte(value & 0x7F | 0x80);
value >>>= 7;
}
destination.writeByte(value);
}
/**
* Read or deserialize an item stack from an underlying input stream.
* Write or serialize a NBT compound to the given output stream.
* <p>
* To supply a byte array, wrap it in a {@link java.io.ByteArrayInputStream ByteArrayInputStream}
* and {@link java.io.DataInputStream DataInputStream}.
* Note: An NBT compound can be written to a stream even if it's NULL.
*
* @param input - the target input stream.
* @return The resulting item stack, or NULL if the serialized item stack was NULL.
* @throws IOException If the operation failed due to reflection or corrupt data.
* @param output - the target output stream.
* @param compound - the NBT compound to be serialized, or NULL to represent nothing.
* @throws IOException If the operation fails due to reflection problems.
*/
public ItemStack deserializeItemStack(@Nonnull DataInputStream input) throws IOException {
if (input == null)
throw new IllegalArgumentException("Input stream cannot be NULL.");
Object nmsItem = null;
public void serializeCompound(@Nonnull DataOutputStream output, NbtCompound compound) throws IOException {
if (output == null)
throw new IllegalArgumentException("Output stream cannot be NULL.");
// Get the NMS version of the compound
Object handle = compound != null ? NbtFactory.fromBase(compound).getHandle() : null;
if (MinecraftReflection.isUsingNetty()) {
if (READ_ITEM_METHOD == null) {
READ_ITEM_METHOD = Accessors.getMethodAccessor(
if (WRITE_NBT_METHOD == null) {
WRITE_NBT_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketDataSerializerClass(), true).
getMethodByParameters("readItemStack", /* i */
MinecraftReflection.getItemStackClass(), new Class<?>[0])
getMethodByParameters("writeNbtCompound", /* a */
MinecraftReflection.getNBTCompoundClass())
);
}
nmsItem = READ_ITEM_METHOD.invoke(Netty.packetReader(input).getHandle());
WrappedByteBuf buf = Netty.packetWriter(output);
buf.writeByte(NbtType.TAG_COMPOUND.getRawID());
WRITE_NBT_METHOD.invoke(buf.getHandle(), handle);
} else {
if (READ_ITEM_METHOD == null) {
READ_ITEM_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()).getMethod(
if (WRITE_NBT_METHOD == null) {
WRITE_NBT_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true).getMethod(
FuzzyMethodContract.newBuilder().
parameterCount(1).
parameterDerivedOf(DataInput.class).
returnDerivedOf(MinecraftReflection.getItemStackClass()).
parameterCount(2).
parameterDerivedOf(MinecraftReflection.getNBTBaseClass(), 0).
parameterDerivedOf(DataOutput.class, 1).
returnTypeVoid().
build())
);
);
}
nmsItem = READ_ITEM_METHOD.invoke(null, input);
WRITE_NBT_METHOD.invoke(null, handle, output);
}
// Convert back to a Bukkit item stack
if (nmsItem != null)
return MinecraftReflection.getBukkitItemStack(nmsItem);
else
return null;
}
/**
@ -182,6 +182,47 @@ public class StreamSerializer {
else
return null;
}
/**
* Serialize a string using the standard Minecraft UTF-16 encoding.
* <p>
* Note that strings cannot exceed 32767 characters, regardless if maximum lenght.
* @param output - the output stream.
* @param text - the string to serialize.
* @throws IOException If the data in the string cannot be written.
*/
public void serializeString(@Nonnull DataOutputStream output, String text) throws IOException {
if (output == null)
throw new IllegalArgumentException("output stream cannot be NULL.");
if (text == null)
throw new IllegalArgumentException("text cannot be NULL.");
if (MinecraftReflection.isUsingNetty()) {
if (WRITE_STRING_METHOD == null) {
WRITE_STRING_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketDataSerializerClass(), true).
getMethodByParameters("writeString", /* a */
String.class)
);
}
WRITE_STRING_METHOD.invoke(Netty.packetWriter(output).getHandle(), text);
} else {
if (WRITE_STRING_METHOD == null) {
WRITE_STRING_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()).getMethod(
FuzzyMethodContract.newBuilder().
parameterCount(2).
parameterExactType(String.class, 0).
parameterDerivedOf(DataOutput.class, 1).
returnTypeVoid().
build())
);
}
WRITE_STRING_METHOD.invoke(null, text, output);
}
}
/**
* Deserialize a string using the standard Minecraft UTF-16 encoding.
@ -226,21 +267,101 @@ public class StreamSerializer {
return (String) READ_STRING_METHOD.invoke(null, input, maximumLength);
}
}
/**
* Serialize an item stack as a base-64 encoded string.
* <p>
* Note: An ItemStack can be written to the serialized text even if it's NULL.
*
* @param stack - the item stack to serialize, or NULL to represent air/nothing.
* @return A base-64 representation of the given item stack.
* @throws IOException If the operation fails due to reflection problems.
*/
public String serializeItemStack(ItemStack stack) throws IOException {
Object nmsItem = MinecraftReflection.getMinecraftItemStack(stack);
byte[] bytes = null;
if (MinecraftReflection.isUsingNetty()) {
WrappedByteBuf buf = Netty.buffer();
Object serializer = MinecraftReflection.getPacketDataSerializer(buf.getHandle());
if (WRITE_ITEM_METHOD == null) {
WRITE_ITEM_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketDataSerializerClass(), true).
getMethodByParameters("writeStack", // a()
MinecraftReflection.getItemStackClass()));
}
WRITE_ITEM_METHOD.invoke(serializer, nmsItem);
bytes = buf.array();
} else {
if (WRITE_ITEM_METHOD == null) {
WRITE_ITEM_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()).getMethod(
FuzzyMethodContract.newBuilder().
parameterCount(2).
parameterDerivedOf(MinecraftReflection.getItemStackClass(), 0).
parameterDerivedOf(DataOutput.class, 1).
build())
);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
DataOutputStream dataOutput = new DataOutputStream(outputStream);
WRITE_ITEM_METHOD.invoke(null, nmsItem, dataOutput);
bytes = outputStream.toByteArray();
}
return Base64Coder.encodeLines(bytes);
}
/**
* Deserialize an item stack from a base-64 encoded string.
* @param input - base-64 encoded string.
* @return A deserialized item stack, or NULL if the serialized ItemStack was also NULL.
* @throws IOException If the operation failed due to reflection or corrupt data.
*/
public ItemStack deserializeItemStack(@Nonnull String input) throws IOException {
if (input == null)
throw new IllegalArgumentException("Input text cannot be NULL.");
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(input));
return deserializeItemStack(new DataInputStream(inputStream));
public ItemStack deserializeItemStack(String input) throws IOException {
Validate.notNull(input, "input cannot be null!");
Object nmsItem = null;
byte[] bytes = Base64Coder.decodeLines(input);
if (MinecraftReflection.isUsingNetty()) {
WrappedByteBuf buf = Netty.copiedBuffer(bytes);
Object serializer = MinecraftReflection.getPacketDataSerializer(buf.getHandle());
if (READ_ITEM_METHOD == null) {
READ_ITEM_METHOD = Accessors.getMethodAccessor(FuzzyReflection.fromClass(MinecraftReflection.getPacketDataSerializerClass(), true).
getMethodByParameters("readItemStack", // i(ItemStack)
MinecraftReflection.getItemStackClass(), new Class<?>[0]));
}
nmsItem = READ_ITEM_METHOD.invoke(serializer);
} else {
if (READ_ITEM_METHOD == null) {
READ_ITEM_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()).getMethod(
FuzzyMethodContract.newBuilder().
parameterCount(1).
parameterDerivedOf(DataInput.class).
returnDerivedOf(MinecraftReflection.getItemStackClass()).
build())
);
}
ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
DataInputStream inputStream = new DataInputStream(byteStream);
nmsItem = READ_ITEM_METHOD.invoke(null, inputStream);
}
return nmsItem != null ? MinecraftReflection.getBukkitItemStack(nmsItem) : null;
}
/**
* Write or serialize an item stack to the given output stream.
* <p>
@ -253,9 +374,8 @@ public class StreamSerializer {
* @param stack - the item stack that will be written, or NULL to represent air/nothing.
* @throws IOException If the operation fails due to reflection problems.
*/
public void serializeItemStack(@Nonnull DataOutputStream output, ItemStack stack) throws IOException {
if (output == null)
throw new IllegalArgumentException("Output stream cannot be NULL.");
public void serializeItemStack(DataOutputStream output, ItemStack stack) throws IOException {
Validate.notNull("output cannot be null!");
// Get the NMS version of the ItemStack
Object nmsItem = MinecraftReflection.getMinecraftItemStack(stack);
@ -269,7 +389,12 @@ public class StreamSerializer {
);
}
WRITE_ITEM_METHOD.invoke(Netty.packetWriter(output).getHandle(), nmsItem);
WrappedByteBuf buf = Netty.buffer();
Object serializer = MinecraftReflection.getPacketDataSerializer(buf.getHandle());
WRITE_ITEM_METHOD.invoke(serializer, nmsItem);
output.write(buf.array());
} else {
if (WRITE_ITEM_METHOD == null)
WRITE_ITEM_METHOD = Accessors.getMethodAccessor(
@ -284,110 +409,58 @@ public class StreamSerializer {
WRITE_ITEM_METHOD.invoke(null, nmsItem, output);
}
}
/**
* Write or serialize a NBT compound to the given output stream.
* Read or deserialize an item stack from an underlying input stream.
* <p>
* Note: An NBT compound can be written to a stream even if it's NULL.
* To supply a byte array, wrap it in a {@link java.io.ByteArrayInputStream ByteArrayInputStream}
* and {@link java.io.DataInputStream DataInputStream}.
*
* @param output - the target output stream.
* @param compound - the NBT compound to be serialized, or NULL to represent nothing.
* @throws IOException If the operation fails due to reflection problems.
* @param input - the target input stream.
* @return The resulting item stack, or NULL if the serialized item stack was NULL.
* @throws IOException If the operation failed due to reflection or corrupt data.
* @deprecated This is a pretty hacky solution for backwards compatibility. See {@link #deserializeItemStack(DataInputStream)}
*/
public void serializeCompound(@Nonnull DataOutputStream output, NbtCompound compound) throws IOException {
if (output == null)
throw new IllegalArgumentException("Output stream cannot be NULL.");
// Get the NMS version of the compound
Object handle = compound != null ? NbtFactory.fromBase(compound).getHandle() : null;
@Deprecated
public ItemStack deserializeItemStack(DataInputStream input) throws IOException {
Validate.notNull(input, "input cannot be null!");
Object nmsItem = null;
if (MinecraftReflection.isUsingNetty()) {
if (WRITE_NBT_METHOD == null) {
WRITE_NBT_METHOD = Accessors.getMethodAccessor(
if (READ_ITEM_METHOD == null) {
READ_ITEM_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketDataSerializerClass(), true).
getMethodByParameters("writeNbtCompound", /* a */
MinecraftReflection.getNBTCompoundClass())
getMethodByParameters("readItemStack", /* i */
MinecraftReflection.getItemStackClass(), new Class<?>[0])
);
}
WrappedByteBuf buf = Netty.packetWriter(output);
buf.writeByte(NbtType.TAG_COMPOUND.getRawID());
byte[] bytes = new byte[8192];
input.read(bytes);
WRITE_NBT_METHOD.invoke(buf.getHandle(), handle);
WrappedByteBuf buf = Netty.copiedBuffer(bytes);
Object serializer = MinecraftReflection.getPacketDataSerializer(buf.getHandle());
nmsItem = READ_ITEM_METHOD.invoke(serializer);
} else {
if (WRITE_NBT_METHOD == null) {
WRITE_NBT_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true).getMethod(
FuzzyMethodContract.newBuilder().
parameterCount(2).
parameterDerivedOf(MinecraftReflection.getNBTBaseClass(), 0).
parameterDerivedOf(DataOutput.class, 1).
returnTypeVoid().
build())
);
}
WRITE_NBT_METHOD.invoke(null, handle, output);
}
}
/**
* Serialize a string using the standard Minecraft UTF-16 encoding.
* <p>
* Note that strings cannot exceed 32767 characters, regardless if maximum lenght.
* @param output - the output stream.
* @param text - the string to serialize.
* @throws IOException If the data in the string cannot be written.
*/
public void serializeString(@Nonnull DataOutputStream output, String text) throws IOException {
if (output == null)
throw new IllegalArgumentException("output stream cannot be NULL.");
if (text == null)
throw new IllegalArgumentException("text cannot be NULL.");
if (MinecraftReflection.isUsingNetty()) {
if (WRITE_STRING_METHOD == null) {
WRITE_STRING_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketDataSerializerClass(), true).
getMethodByParameters("writeString", /* a */
String.class)
);
}
WRITE_STRING_METHOD.invoke(Netty.packetWriter(output).getHandle(), text);
} else {
if (WRITE_STRING_METHOD == null) {
WRITE_STRING_METHOD = Accessors.getMethodAccessor(
if (READ_ITEM_METHOD == null) {
READ_ITEM_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()).getMethod(
FuzzyMethodContract.newBuilder().
parameterCount(2).
parameterExactType(String.class, 0).
parameterDerivedOf(DataOutput.class, 1).
returnTypeVoid().
parameterCount(1).
parameterDerivedOf(DataInput.class).
returnDerivedOf(MinecraftReflection.getItemStackClass()).
build())
);
);
}
WRITE_STRING_METHOD.invoke(null, text, output);
nmsItem = READ_ITEM_METHOD.invoke(null, input);
}
}
/**
* Serialize an item stack as a base-64 encoded string.
* <p>
* Note: An ItemStack can be written to the serialized text even if it's NULL.
*
* @param stack - the item stack to serialize, or NULL to represent air/nothing.
* @return A base-64 representation of the given item stack.
* @throws IOException If the operation fails due to reflection problems.
*/
public String serializeItemStack(ItemStack stack) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
DataOutputStream dataOutput = new DataOutputStream(outputStream);
serializeItemStack(dataOutput, stack);
// Serialize that array
return Base64Coder.encodeLines(outputStream.toByteArray());
// Convert back to a Bukkit item stack
if (nmsItem != null)
return MinecraftReflection.getBukkitItemStack(nmsItem);
else
return null;
}
}
}

View File

@ -10,8 +10,11 @@ import java.io.IOException;
import net.minecraft.server.v1_8_R3.IntHashMap;
import org.bukkit.ChatColor;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -30,37 +33,24 @@ public class StreamSerializerTest {
public static void initializeBukkit() throws IllegalAccessException {
BukkitInitialization.initializeItemMeta();
}
@Test
public void testMinecraftReflection() {
assertEquals(IntHashMap.class, MinecraftReflection.getIntHashMapClass());
}
@Test
public void testSerializer() throws IOException {
ItemStack before = new ItemStack(Material.GOLD_AXE);
StreamSerializer serializer = new StreamSerializer();
String data = serializer.serializeItemStack(before);
ItemStack after = serializer.deserializeItemStack(data);
assertEquals(before.getType(), after.getType());
assertEquals(before.getAmount(), after.getAmount());
}
@Test
public void testStrings() throws IOException {
StreamSerializer serializer = new StreamSerializer();
String initial = "Hello - this is a ÆØÅ test.";
// Buffer
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
serializer.serializeString(new DataOutputStream(buffer), initial);
DataInputStream input = new DataInputStream(
new ByteArrayInputStream(buffer.toByteArray()));
DataInputStream input = new DataInputStream(new ByteArrayInputStream(buffer.toByteArray()));
String deserialized = serializer.deserializeString(input, 50);
assertEquals(initial, deserialized);
}
@ -75,11 +65,10 @@ public class StreamSerializerTest {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
serializer.serializeCompound(new DataOutputStream(buffer), initial);
DataInputStream input = new DataInputStream(
new ByteArrayInputStream(buffer.toByteArray()));
DataInputStream input = new DataInputStream(new ByteArrayInputStream(buffer.toByteArray()));
NbtCompound deserialized = serializer.deserializeCompound(input);
assertEquals(initial, deserialized);
}
@ -88,13 +77,24 @@ public class StreamSerializerTest {
StreamSerializer serializer = new StreamSerializer();
ItemStack initial = new ItemStack(Material.STRING);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
serializer.serializeItemStack(new DataOutputStream(buffer), initial);
DataInputStream input = new DataInputStream(
new ByteArrayInputStream(buffer.toByteArray()));
ItemStack deserialized = serializer.deserializeItemStack(input);
String serialized = serializer.serializeItemStack(initial);
ItemStack deserialized = serializer.deserializeItemStack(serialized);
assertEquals(initial, deserialized);
}
@Test
public void testItemMeta() throws IOException {
StreamSerializer serializer = new StreamSerializer();
ItemStack initial = new ItemStack(Material.WOOL, 2, DyeColor.BLUE.getWoolData());
ItemMeta meta = initial.getItemMeta();
meta.setDisplayName(ChatColor.BLUE + "Blue Wool");
initial.setItemMeta(meta);
String serialized = serializer.serializeItemStack(initial);
ItemStack deserialized = serializer.deserializeItemStack(serialized);
assertEquals(initial, deserialized);
}
}

View File

@ -75,4 +75,9 @@ public class ShadedByteBuf implements WrappedByteBuf {
public void writeBytes(byte[] bytes) {
handle.get().writeBytes(bytes);
}
@Override
public byte[] array() {
return handle.get().array();
}
}

View File

@ -55,11 +55,6 @@ public class ShadedNetty implements NettyCompat {
}
}
@Override
public WrappedByteBuf allocateUnpooled() {
return new ShadedByteBuf(UnpooledByteBufAllocator.DEFAULT.buffer());
}
@Override
public Class<?> getGenericFutureListenerArray() {
return GenericFutureListener[].class;
@ -97,4 +92,14 @@ public class ShadedNetty implements NettyCompat {
public WrappedByteBuf packetWriter(DataOutputStream output) {
return new ShadedByteBuf(ShadedByteBufAdapter.packetWriter(output));
}
@Override
public WrappedByteBuf copiedBuffer(byte[] array) {
return new ShadedByteBuf(Unpooled.copiedBuffer(array));
}
@Override
public WrappedByteBuf buffer() {
return new ShadedByteBuf(Unpooled.buffer());
}
}