Print a hex dump in the case of very large arrays.

This commit is contained in:
Kristian S. Stangeland 2014-05-02 03:49:33 +02:00
parent d8e8a88076
commit 2244f986bb
4 changed files with 315 additions and 5 deletions

View File

@ -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<Object> 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<Object> converter = findConverter(value.getClass());
if (converter != null) {
output.append(converter.getSpecific(value));
return true;

View File

@ -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:
* <table border="1">
* <tr>
* <th>Property</th>
* <th>Value</th>
* </tr>
* <tr>
* <td>Position Length</td>
* <td>6</td>
* </tr>
* <tr>
* <td>Position Suffix</td>
* <td>": "</td>
* </tr>
* <tr>
* <td>Delimiter</td>
* <td>" "</td>
* </tr>
* <tr>
* <td>Group Length</td>
* <td>2</td>
* </tr>
* <tr>
* <td>Group Count</td>
* <td>24</td>
* </tr>
* <tr>
* <td>Line Delimiter</td>
* <td>"\n"</td>
* </tr>
* </table>
* @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.
* <p>
* 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;
}
}

View File

@ -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.

View File

@ -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<Object> 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<World> getWorldConverter() {
return new IgnoreNullConverter<World>() {
@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<World> 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());