From 2244f986bb60962f9197b56c653cc1acec45ad71 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Fri, 2 May 2014 03:49:33 +0200 Subject: [PATCH] Print a hex dump in the case of very large arrays. --- .../com/comphenix/protocol/CommandPacket.java | 20 +- .../comphenix/protocol/utility/HexDumper.java | 234 ++++++++++++++++++ .../protocol/utility/MinecraftReflection.java | 20 ++ .../protocol/wrappers/BukkitConverters.java | 46 +++- 4 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/utility/HexDumper.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index ce7e6189..45f25556 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -49,6 +49,7 @@ import com.comphenix.protocol.reflect.EquivalentConverter; import com.comphenix.protocol.reflect.PrettyPrinter; import com.comphenix.protocol.reflect.PrettyPrinter.ObjectPrinter; import com.comphenix.protocol.utility.ChatExtensions; +import com.comphenix.protocol.utility.HexDumper; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.BukkitConverters; import com.google.common.collect.MapMaker; @@ -76,6 +77,11 @@ class CommandPacket extends CommandBase { */ public static final int PAGE_LINE_COUNT = 9; + /** + * Number of bytes before we do a hex dump. + */ + private static final int HEX_DUMP_THRESHOLD = 256; + private Plugin plugin; private Logger logger; private ProtocolManager manager; @@ -465,9 +471,19 @@ class CommandPacket extends CommandBase { return PrettyPrinter.printObject(packet, clazz, MinecraftReflection.getPacketClass(), PrettyPrinter.RECURSE_DEPTH, new ObjectPrinter() { @Override public boolean print(StringBuilder output, Object value) { - if (value != null) { - EquivalentConverter converter = findConverter(value.getClass()); + // Special case + if (value instanceof byte[]) { + byte[] data = (byte[]) value; + if (data.length > HEX_DUMP_THRESHOLD) { + output.append("["); + HexDumper.defaultDumper().appendTo(output, data); + output.append("]"); + return true; + } + } else if (value != null) { + EquivalentConverter converter = findConverter(value.getClass()); + if (converter != null) { output.append(converter.getSpecific(value)); return true; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/HexDumper.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/HexDumper.java new file mode 100644 index 00000000..53d91303 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/HexDumper.java @@ -0,0 +1,234 @@ +package com.comphenix.protocol.utility; + +import java.io.IOException; + +import com.google.common.base.Preconditions; + +/** + * Represents a class for printing hexadecimal dumps. + * + * @author Kristian + */ +public class HexDumper { + private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); + + // Default values + private int positionLength = 6; + private char[] positionSuffix = ": ".toCharArray(); + private char[] delimiter = " ".toCharArray(); + private int groupLength = 2; + private int groupCount = 24; + private char[] lineDelimiter = "\n".toCharArray(); + + /** + * Retrieve a hex dumper tuned for lines of 80 characters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PropertyValue
Position Length6
Position Suffix": "
Delimiter" "
Group Length2
Group Count24
Line Delimiter"\n"
+ * @return The default dumper. + */ + public static HexDumper defaultDumper() { + return new HexDumper(); + } + + /** + * Set the delimiter between each new line. + * @param lineDelimiter - the line delimiter. + * @return This instance, for chaining. + */ + public HexDumper lineDelimiter(String lineDelimiter) { + this.lineDelimiter = Preconditions.checkNotNull(lineDelimiter, "lineDelimiter cannot be NULL").toCharArray(); + return this; + } + + /** + * Set the number of hex characters in the position. + * @param positionLength - number of characters, from 0 to 8. + * @return This instance, for chaining. + */ + public HexDumper positionLength(int positionLength) { + if (positionLength < 0) + throw new IllegalArgumentException("positionLength cannot be less than zero."); + if (positionLength > 8) + throw new IllegalArgumentException("positionLength cannot be greater than eight."); + this.positionLength = positionLength; + return this; + } + + /** + * Set a suffix to write after each position. + * @param positionSuffix - non-null string to write after the positions. + * @return This instance, for chaining. + */ + public HexDumper positionSuffix(String positionSuffix) { + this.positionSuffix = Preconditions.checkNotNull(positionSuffix, "positionSuffix cannot be NULL").toCharArray(); + return this; + } + + /** + * Set the delimiter to write in between each group of hexadecimal characters. + * @param delimiter - non-null string to write between each group. + * @return This instance, for chaining. + */ + public HexDumper delimiter(String delimiter) { + this.delimiter = Preconditions.checkNotNull(delimiter, "delimiter cannot be NULL").toCharArray(); + return this; + } + + /** + * Set the length of each group in hexadecimal characters. + * @param groupLength - the length of each group. + * @return This instance, for chaining. + */ + public HexDumper groupLength(int groupLength) { + if (groupLength < 1) + throw new IllegalArgumentException("groupLength cannot be less than one."); + this.groupLength = groupLength; + return this; + } + + /** + * Set the number of groups in each line. This is limited by the supply of bytes in the byte array. + *

+ * Use {@link Integer#MAX_VALUE} to effectively disable lines. + * @param groupLength - the length of each group. + * @return This instance, for chaining. + */ + public HexDumper groupCount(int groupCount) { + if (groupCount < 1) + throw new IllegalArgumentException("groupCount cannot be less than one."); + this.groupCount = groupCount; + return this; + } + + /** + * Append the hex dump of the given data to the string builder, using the current formatting settings. + * @param appendable - appendable source. + * @param data - the data to dump. + * @param start - the starting index of the data. + * @param length - the number of bytes to dump. + * @throws IOException Any underlying IO exception. + */ + public void appendTo(Appendable appendable, byte[] data) throws IOException { + appendTo(appendable, data, 0, data.length); + } + + /** + * Append the hex dump of the given data to the string builder, using the current formatting settings. + * @param appendable - appendable source. + * @param data - the data to dump. + * @param start - the starting index of the data. + * @param length - the number of bytes to dump. + * @throws IOException Any underlying IO exception. + */ + public void appendTo(Appendable appendable, byte[] data, int start, int length) throws IOException { + StringBuilder output = new StringBuilder(); + appendTo(output, data, start, length); + appendable.append(output.toString()); + } + + /** + * Append the hex dump of the given data to the string builder, using the current formatting settings. + * @param builder - the builder. + * @param data - the data to dump. + * @param start - the starting index of the data. + * @param length - the number of bytes to dump. + */ + public void appendTo(StringBuilder builder, byte[] data) { + appendTo(builder, data, 0, data.length); + } + + /** + * Append the hex dump of the given data to the string builder, using the current formatting settings. + * @param builder - the builder. + * @param data - the data to dump. + * @param start - the starting index of the data. + * @param length - the number of bytes to dump. + */ + public void appendTo(StringBuilder builder, byte[] data, int start, int length) { + // Positions + int dataIndex = start; + int dataEnd = start + length; + int groupCounter = 0; + int currentGroupLength = 0; + + // Current niblet in the byte + int value = 0; + boolean highNiblet = true; + + while (dataIndex < dataEnd || !highNiblet) { + // Prefix + if (groupCounter == 0 && currentGroupLength == 0) { + // Print the current dataIndex (print in reverse) + for (int i = positionLength - 1; i >= 0; i--) { + builder.append(HEX_DIGITS[(dataIndex >>> (4 * i)) & 0xF]); + } + builder.append(positionSuffix); + } + + // Print niblet + if (highNiblet) { + value = data[dataIndex++] & 0xFF; + builder.append(HEX_DIGITS[value >>> 4]); + } else { + builder.append(HEX_DIGITS[value & 0x0F]); + } + highNiblet = !highNiblet; + currentGroupLength++; + + // See if we're dealing with the last element + if (currentGroupLength >= groupLength) { + currentGroupLength = 0; + + // See if we've reached the last element in the line + if (++groupCounter >= groupCount) { + builder.append(lineDelimiter); + groupCounter = 0; + } else { + // Write delimiter + builder.append(delimiter); + } + } + } + } + + /** + * Calculate the length of each line. + * @param byteCount - the maximum number of bytes + * @return The lenght of the final line. + */ + public int getLineLength(int byteCount) { + int constant = positionLength + positionSuffix.length + lineDelimiter.length; + int groups = Math.min((2 * byteCount) / groupLength, groupCount); + + // Total expected length of each line + return constant + delimiter.length * (groups - 1) + groupLength * groups; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index ee3f3c8b..8b869c38 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -627,6 +627,18 @@ public class MinecraftReflection { } } + /** + * Retrieve the World (NMS) class. + * @return The world class. + */ + public static Class getNmsWorldClass() { + try { + return getMinecraftClass("World"); + } catch (RuntimeException e) { + return setMinecraftClass("World", getWorldServerClass().getSuperclass()); + } + } + /** * Fallback on the return value of a named method in order to get a NMS class. * @param nmsClass - the expected name of the Minecraft class. @@ -1675,6 +1687,14 @@ public class MinecraftReflection { return getCraftBukkitClass("entity.CraftPlayer"); } + /** + * Retrieve the CraftWorld class. + * @return The CraftWorld class. + */ + public static Class getCraftWorldClass() { + return getCraftBukkitClass("CraftWorld"); + } + /** * Retrieve the CraftEntity class. * @return CraftEntity class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index a78f0097..f61dcec8 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.WorldType; @@ -39,6 +40,7 @@ import org.bukkit.potion.PotionEffectType; import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.injector.BukkitUnwrapper; import com.comphenix.protocol.injector.PacketConstructor; import com.comphenix.protocol.injector.PacketConstructor.Unwrapper; import com.comphenix.protocol.reflect.EquivalentConverter; @@ -46,6 +48,7 @@ import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; import com.comphenix.protocol.reflect.accessors.MethodAccessor; import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.reflect.instances.DefaultInstances; @@ -86,6 +89,9 @@ public class BukkitConverters { private static volatile Constructor mobEffectConstructor; private static volatile StructureModifier mobEffectModifier; + // Used for fetching the CraftWorld associated with a WorldServer + private static FieldAccessor craftWorldField; + static { try { MinecraftReflection.getWorldTypeClass(); @@ -98,6 +104,15 @@ public class BukkitConverters { hasAttributeSnapshot = true; } catch (Exception e) { } + + // Fetch CraftWorld field + try { + craftWorldField = Accessors.getFieldAccessor( + MinecraftReflection.getNmsWorldClass(), + MinecraftReflection.getCraftWorldClass(), true); + } catch (Exception e) { + e.printStackTrace(); + } } /** @@ -711,6 +726,29 @@ public class BukkitConverters { }; } + /** + * Retrieve the converter used to convert between a NMS World and a Bukkit world. + * @return The potion effect converter. + */ + public static EquivalentConverter getWorldConverter() { + return new IgnoreNullConverter() { + @Override + protected Object getGenericValue(Class genericType, World specific) { + return BukkitUnwrapper.getInstance().unwrapItem(specific); + } + + @Override + protected World getSpecificValue(Object generic) { + return (World) craftWorldField.get(generic); + } + + @Override + public Class getSpecificType() { + return World.class; + } + }; + } + /** * Retrieve the converter used to convert between a PotionEffect and the equivalent NMS Mobeffect. * @return The potion effect converter. @@ -827,8 +865,9 @@ public class BukkitConverters { put(NbtBase.class, (EquivalentConverter) getNbtConverter()). put(NbtCompound.class, (EquivalentConverter) getNbtConverter()). put(WrappedWatchableObject.class, (EquivalentConverter) getWatchableObjectConverter()). - put(PotionEffect.class, (EquivalentConverter) getPotionEffectConverter()); - + put(PotionEffect.class, (EquivalentConverter) getPotionEffectConverter()). + put(World.class, (EquivalentConverter) getWorldConverter()); + // Types added in 1.7.2 if (MinecraftReflection.isUsingNetty()) { builder.put(Material.class, (EquivalentConverter) getBlockConverter()); @@ -866,7 +905,8 @@ public class BukkitConverters { put(MinecraftReflection.getNBTBaseClass(), (EquivalentConverter) getNbtConverter()). put(MinecraftReflection.getNBTCompoundClass(), (EquivalentConverter) getNbtConverter()). put(MinecraftReflection.getWatchableObjectClass(), (EquivalentConverter) getWatchableObjectConverter()). - put(MinecraftReflection.getMobEffectClass(), (EquivalentConverter) getPotionEffectConverter()); + put(MinecraftReflection.getMobEffectClass(), (EquivalentConverter) getPotionEffectConverter()). + put(MinecraftReflection.getNmsWorldClass(), (EquivalentConverter) getWorldConverter()); if (hasWorldType) builder.put(MinecraftReflection.getWorldTypeClass(), (EquivalentConverter) getWorldTypeConverter());