// Copyright 2016 The Nomulus Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package google.registry.util; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; import javax.annotation.Nonnegative; import javax.annotation.WillNotClose; import javax.annotation.concurrent.NotThreadSafe; /** * Hex Dump Utility. * *
This class takes binary data and prints it out in a way that humans can read. It's just like * the hex dump programs you remembered as a kid. There a column on the left for the address (or * offset) in decimal, the middle columns are each digit in hexadecimal, and the column on the * right is the ASCII representation where non-printable characters are represented by {@code '.'}. * *
It's easiest to generate a simple {@link String} by calling {@link #dumpHex(byte[])}, or you * can stream data with {@link #HexDumper(Writer, int, int)}. * *
Example output: *
{@code * [222 bytes total] * 00000000 90 0d 03 00 08 03 35 58 61 46 fd f3 f3 73 01 88 ......5XaF...s.. * 00000016 cd 04 00 03 08 00 37 05 02 52 09 3f 8d 30 1c 45 ......7..R.?.0.E * 00000032 72 69 63 20 45 63 68 69 64 6e 61 20 28 74 65 73 ric Echidna (tes * 00000048 74 20 6b 65 79 29 20 3c 65 72 69 63 40 62 6f 75 t key).. * 00000080 09 10 35 58 61 46 fd f3 f3 73 4b 5b 03 fe 2e 53 ..5XaF...sK[...S * 00000096 04 28 ab cb 35 3b e2 1b 63 91 65 3a 86 b9 fb 47 .(..5;..c.e:...G * 00000112 d5 4c 6a 21 50 f5 2e 39 76 aa d5 86 d7 96 3b 9a .Lj!P..9v.....;. * 00000128 1a c3 6d c0 50 7f c6 25 9a 04 de 0f 1f 20 ae 70 ..m.P..%..... .p * 00000144 f9 77 c4 8b bf ec 3c 2f 59 58 b8 47 81 6a 59 25 .w.... */ @NotThreadSafe public final class HexDumper extends OutputStream { @Nonnegative public static final int DEFAULT_PER_LINE = 16; @Nonnegative public static final int DEFAULT_PER_GROUP = 4; private Writer upstream; @Nonnegative private final int perLine; @Nonnegative private final int perGroup; private long totalBytes; @Nonnegative private int lineCount; private StringBuilder line; private final char[] asciis; /** * Calls {@link #dumpHex(byte[], int, int)} with {@code perLine} set to * {@value #DEFAULT_PER_LINE} and {@code perGroup} set to {@value #DEFAULT_PER_GROUP}. */ public static String dumpHex(byte[] data) { return dumpHex(data, DEFAULT_PER_LINE, DEFAULT_PER_GROUP); } /** * Convenience static method for generating a hex dump as a {@link String}. * * This method adds an additional line to the beginning with the total number of bytes. * * @see #HexDumper(Writer, int, int) */ public static String dumpHex(byte[] data, @Nonnegative int perLine, @Nonnegative int perGroup) { checkNotNull(data, "data"); StringWriter writer = new StringWriter(); writer.write(String.format("[%d bytes total]\n", data.length)); try (HexDumper hexDump = new HexDumper(writer, perLine, perGroup)) { hexDump.write(data); } catch (IOException e) { throw new RuntimeException(e); } return writer.toString(); } /** * Calls {@link #HexDumper(Writer, int, int)} with {@code perLine} set to * {@value #DEFAULT_PER_LINE} and {@code perGroup} set to {@value #DEFAULT_PER_GROUP}. */ public HexDumper(@WillNotClose Writer writer) { this(writer, DEFAULT_PER_LINE, DEFAULT_PER_GROUP); } /** * Construct a new streaming {@link HexDumper} object. * *
The output is line-buffered so a single write call is made to {@code out} for each line. * This is done to avoid system call overhead {@code out} is a resource, and reduces the chance * of lines being broken by other threads writing to {@code out} (or its underlying resource) at * the same time. * *
This object will not close {@code out}. You must close both this object and * {@code out}, and this object must be closed first. * * @param out is the stream to which the hex dump text is written. It is not closed. * @param perLine determines how many hex characters to show on each line. * @param perGroup how many columns of hex digits should be grouped together. If this value is * {@code > 0}, an extra space will be inserted after each Nth column for readability. * Grouping can be disabled by setting this to {@code 0}. * @see #dumpHex(byte[]) */ public HexDumper(@WillNotClose Writer out, @Nonnegative int perLine, @Nonnegative int perGroup) { checkArgument(0 < perLine, "0 < perLine <= INT32_MAX"); checkArgument(0 <= perGroup && perGroup < perLine, "0 <= perGroup < perLine"); this.upstream = checkNotNull(out, "out"); this.totalBytes = 0L; this.perLine = perLine; this.perGroup = perGroup; this.asciis = new char[perLine]; this.line = newLine(); } /** Initializes member variables at the beginning of a new line. */ private StringBuilder newLine() { lineCount = 0; return new StringBuilder(String.format("%08d ", totalBytes)); } /** * Writes a single byte to the current line buffer, flushing if end of line. * * @throws IOException upon failure to write to upstream {@link Writer#write(String) writer}. * @throws IllegalStateException if this object has been {@link #close() closed}. */ @Override public void write(int b) throws IOException { String flush = null; line.append(String.format("%02x ", (byte) b)); asciis[lineCount] = b >= 32 && b <= 126 ? (char) b : '.'; ++lineCount; ++totalBytes; if (lineCount == perLine) { line.append(' '); line.append(asciis); line.append('\n'); flush = line.toString(); line = newLine(); } else { if (perGroup > 0 && lineCount % perGroup == 0) { line.append(' '); } } // Writing upstream is deferred until the end in order to *somewhat* maintain a correct // internal state in the event that an exception is thrown. This also avoids the need for // a try statement which usually makes code run slower. if (flush != null) { upstream.write(flush); } } /** * Writes partial line buffer (if any) to upstream {@link Writer}. * * @throws IOException upon failure to write to upstream {@link Writer#write(String) writer}. * @throws IllegalStateException if this object has been {@link #close() closed}. */ @Override public void flush() throws IOException { if (line.length() > 0) { upstream.write(line.toString()); line = new StringBuilder(); } } /** * Writes out the final line (if incomplete) and invalidates this object. * *
This object must be closed before you close the upstream writer. Please note that * this method does not close upstream writer for you. * *
If you attempt to write to this object after calling this method, * {@link IllegalStateException} will be thrown. However, it's safe to call close multiple times, * as subsequent calls will be treated as a no-op. * * @throws IOException upon failure to write to upstream {@link Writer#write(String) writer}. */ @Override public void close() throws IOException { if (lineCount > 0) { while (lineCount < perLine) { asciis[lineCount] = ' '; line.append(" "); ++lineCount; if (perGroup > 0 && lineCount % perGroup == 0 && lineCount != perLine) { line.append(' '); } } line.append(' '); line.append(asciis); line.append('\n'); flush(); } } }