--- /dev/null 2018-10-27 20:27:18.925084831 -0700 +++ new/src/java.base/share/classes/java/util/HexFormat.java 2018-12-11 12:52:38.424224957 -0800 @@ -0,0 +1,803 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.util; + +import java.io.*; +import java.nio.*; +import java.util.*; +import java.util.stream.*; +import static java.nio.charset.StandardCharsets.*; + +/** + * Converts binary data to and from its hexadecimal (base 16) string + * representation. It can also generate the classic Unix {@code hexdump(1)} + * format. + *

+ * Example usages: + *

{@code    // Initialize a 16-byte array from a hexadecimal string
+ *   byte[] bytes = HexFormat.fromString("a1a2a3a4a5a6a7a8a9aaabacadaeaf");
+ *
+ *   // Display the hexadecimal representation of a file's 256-bit hash code
+ *   MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+ *   System.out.println(
+ *       HexFormat.toString(sha256.digest(Files.readAllBytes(Paths.get("mydata")))));
+ *
+ *   // Write the printable representation of a file to the standard output stream
+ *   // in 64-byte chunks formatted according to the supplied Formatter function
+ *   try (InputStream is = Files.newInputStream(Paths.get("mydata"))) {
+ *       HexFormat.dumpAsStream(is, 64,
+ *           (offset, chunk, fromIndex, toIndex) ->
+ *               String.format("%d %s",
+ *                   offset / 64 + 1,
+ *                   HexFormat.toPrintableString(chunk, fromIndex, toIndex)))
+ *           .forEachOrdered(System.out::println);
+ *   } catch (IOException ioe) {
+ *       ...
+ *   }
+ *
+ *   // Write the standard input stream to the standard output stream in hexdump format
+ *   HexFormat.dump(System.in, System.out);
+ * }
+ * + * @since 12 + */ +public final class HexFormat { + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + private static final String NEWLINE = System.lineSeparator(); + private static final int NEWLINE_LENGTH = NEWLINE.length(); + private static final int DEFAULT_CHUNK_SIZE = 16; + + /** + * A formatter that generates the classic Unix {@code hexdump(1)} format. + * It behaves as if: + *
{@code
+     *     String.format("%08x  %s  |%s|",
+     *         offset,
+     *         HexFormat.toFormattedString(chunk, from, to),
+     *         HexFormat.toPrintableString(chunk, from, to));
+     * }
+ */ + public static final Formatter HEXDUMP_FORMATTER = new Formatter() { + public String format(long offset, byte[] chunk, int fromIndex, int toIndex) { + return String.format("%08x %s |%s|", + offset, + HexFormat.toFormattedString(chunk, fromIndex, toIndex), + HexFormat.toPrintableString(chunk, fromIndex, toIndex)); + } + }; + + private HexFormat() {} + + /** + * Returns a hexadecimal string representation of the contents of the + * provided byte array, with no additional formatting. + *

+ * The binary value is converted to a string comprising pairs of + * hexadecimal digits that use only the following ASCII characters: + *

+ * {@code 0123456789abcdef} + *
+ * + * @param bytes a byte array + * @return a hexadecimal string representation of the byte array. + * The string length is twice the array length. + * @throws NullPointerException if {@code bytes} is {@code null} + */ + public static String toString(byte[] bytes) { + Objects.requireNonNull(bytes, "bytes"); + return toString(bytes, 0, bytes.length); + } + + /** + * Returns a hexadecimal string representation of a range within the + * provided byte array, with no additional formatting. + *

+ * The binary value is converted to a string comprising pairs of + * hexadecimal digits that use only the following ASCII characters: + *

+ * {@code 0123456789abcdef} + *
+ * The range to be converted extends from index {@code fromIndex}, + * inclusive, to index {@code toIndex}, exclusive. + * If {@code fromIndex==toIndex}, the range to be converted is empty. + * + * @param bytes a byte array + * @param fromIndex the index of the first byte (inclusive) to be converted + * @param toIndex the index of the last byte (exclusive) to be converted + * @return a hexadecimal string representation of the byte array. + * The string length is twice the number of bytes converted. + * @throws NullPointerException if {@code bytes} is {@code null} + * @throws IllegalArgumentException if {@code fromIndex > toIndex} + * @throws ArrayIndexOutOfBoundsException + * if {@code fromIndex < 0} or {@code toIndex > bytes.length} + */ + public static String toString(byte[] bytes, int fromIndex, int toIndex) { + Objects.requireNonNull(bytes, "bytes"); + Arrays.rangeCheck(bytes.length, fromIndex, toIndex); + return toChunkedString(bytes, fromIndex, toIndex, toIndex - fromIndex, + 1, false); + } + + /** + * Returns a formatted hexadecimal string representation of the contents of + * the provided byte array. + *

+ * The binary value is converted to a string in the canonical hexdump + * format of two columns of eight space-separated pairs of hexadecimal + * digits that use only the following ASCII characters: + *

+ * {@code 0123456789abcdef} + *
+ *

+ * If the number of bytes to be converted is greater than 16 then + * {@link System#lineSeparator()} characters are inserted after each 16-byte chunk. + * If the final chunk is less than 16 bytes then the result is padded with spaces + * to match the length of the preceding chunks. + * The general output format is as follows: + *

+     * 00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff
+     * 
+ * + * @param bytes a byte array + * @return a formatted hexadecimal string representation of the byte array + * @throws NullPointerException if {@code bytes} is {@code null} + */ + public static String toFormattedString(byte[] bytes) { + Objects.requireNonNull(bytes, "bytes"); + return toFormattedString(bytes, 0, bytes.length); + } + + /** + * Returns a formatted hexadecimal string representation of the contents of + * a range within the provided byte array. + *

+ * The binary value is converted to a string in the canonical hexdump + * format of two columns of eight space-separated pairs of hexadecimal + * digits that use only the following ASCII characters: + *

+ * {@code 0123456789abcdef} + *
+ *

+ * The range to be converted extends from index {@code fromIndex}, + * inclusive, to index {@code toIndex}, exclusive. + * If {@code fromIndex==toIndex}, the range to be converted is empty. + *

+ * If the number of bytes to be converted is greater than 16 then + * {@link System#lineSeparator()} characters are inserted after each 16-byte chunk. + * If the final chunk is less than 16 bytes then the result is padded with spaces + * to match the length of the preceding chunks. + * The general output format is as follows: + *

+     * 00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff
+     * 
+ * + * @param bytes a byte array + * @param fromIndex the index of the first byte (inclusive) to be converted + * @param toIndex the index of the last byte (exclusive) to be converted + * @return a formatted hexadecimal string representation of the byte array + * @throws NullPointerException if {@code bytes} is {@code null} + * @throws IllegalArgumentException if {@code fromIndex > toIndex} + * @throws ArrayIndexOutOfBoundsException + * if {@code fromIndex < 0} or {@code toIndex > bytes.length} + */ + public static String toFormattedString(byte[] bytes, int fromIndex, + int toIndex) { + Objects.requireNonNull(bytes, "bytes"); + Arrays.rangeCheck(bytes.length, fromIndex, toIndex); + return toChunkedString(bytes, fromIndex, toIndex, DEFAULT_CHUNK_SIZE, 2, true); + } + + /** + * Returns a printable representation of the contents of the + * provided byte array. + *

+ * The binary value is converted to a string comprising printable + * {@link java.nio.charset.StandardCharsets#ISO_8859_1} + * characters, or {@code '.'} if the byte maps to a non-printable character. + * A non-printable character is one outside of the range + * {@code '\u005Cu0020'} through {@code '\u005Cu007E'} and + * {@code '\u005Cu00A0'} through {@code '\u005Cu00FF'}. + * + * @param bytes a byte array + * @return a printable representation of the byte array + * @throws NullPointerException if {@code bytes} is {@code null} + */ + public static String toPrintableString(byte[] bytes) { + Objects.requireNonNull(bytes, "bytes"); + return toPrintableString(bytes, 0, bytes.length); + } + + /** + * Returns a printable representation of the contents of a + * range within the provided byte array. + *

+ * The binary value is converted to a string comprising printable + * {@link java.nio.charset.StandardCharsets#ISO_8859_1} + * characters, or {@code '.'} if the byte maps to a non-printable character. + * A non-printable character is one outside of the range + * {@code '\u005Cu0020'} through {@code '\u005Cu007E'} and + * {@code '\u005Cu00A0'} through {@code '\u005Cu00FF'}. + * + * @param bytes a byte array + * @param fromIndex the index of the first byte (inclusive) to be converted + * @param toIndex the index of the last byte (exclusive) to be converted + * @return a printable representation of the byte array + * @throws NullPointerException if {@code bytes} is {@code null} + * @throws IllegalArgumentException if {@code fromIndex > toIndex} + * @throws ArrayIndexOutOfBoundsException + * if {@code fromIndex < 0} or {@code toIndex > bytes.length} + */ + public static String toPrintableString(byte[] bytes, int fromIndex, + int toIndex) { + Objects.requireNonNull(bytes, "bytes"); + Arrays.rangeCheck(bytes.length, fromIndex, toIndex); + + StringBuilder printable = new StringBuilder(toIndex - fromIndex); + for (int i = fromIndex; i < toIndex; i++) { + if (bytes[i] > 0x1F && bytes[i] < 0x7F) { + printable.append((char) bytes[i]); + } else if (bytes[i] > (byte)0x9F && bytes[i] <= (byte)0xFF) { + printable.append(new String(new byte[]{bytes[i]}, ISO_8859_1)); + + } else { + printable.append('.'); + } + } + + return printable.toString(); + } + + /** + * Returns a byte array containing the provided sequence of hexadecimal + * digits. The sequence may be prefixed with the hexadecimal indicator + * {@code "0x"}. + * The optional prefix of {@code "0x"} is ignored. + *

+ * The binary value is generated from pairs of hexadecimal digits that use + * only the following ASCII characters: + *

+ * {@code 0123456789abcdefABCDEF} + *
+ * + * @param hexString an even numbered sequence of hexadecimal digits. + * If this is a {@link CharBuffer} then its position does not get + * advanced. + * @return a byte array + * @throws IllegalArgumentException if {@code hexString} has an odd number + * of digits or contains an illegal hexadecimal character + * @throws NullPointerException if {@code hexString} is {@code null} + */ + public static byte[] fromString(CharSequence hexString) { + Objects.requireNonNull(hexString, "hexString"); + return hexToBytes(hexString, 0, hexString.length()); + } + + /** + * Returns a byte array containing a range within the provided + * sequence of hexadecimal digits. The sequence may be prefixed with the + * hexadecimal indicator {@code "0x"}. + * The optional prefix of {@code "0x"} is ignored. + *

+ * The binary value is generated from pairs of hexadecimal digits that use + * only the following ASCII characters: + *

+ * {@code 0123456789abcdefABCDEF} + *
+ * + * @param hexString an even numbered sequence of hexadecimal digits. + * If this is a {@link CharBuffer} then its position does not get + * advanced. + * @param fromIndex the index of the first digit (inclusive) to be converted + * @param toIndex the index of the last digit (exclusive) to be converted + * @return a byte array + * @throws IllegalArgumentException if {@code hexString} has an odd number + * of digits or contains an illegal hexadecimal character, + * or if {@code fromIndex > toIndex} + * @throws NullPointerException if {@code hexString} is {@code null} + * @throws ArrayIndexOutOfBoundsException + * if {@code fromIndex < 0} or {@code toIndex > hexString.length()} + */ + public static byte[] fromString(CharSequence hexString, int fromIndex, + int toIndex) { + Objects.requireNonNull(hexString, "hexString"); + Arrays.rangeCheck(hexString.length(), fromIndex, toIndex); + return hexToBytes(hexString, fromIndex, toIndex); + } + + /** + * Generates a dump of the contents of the provided input stream, as a + * stream of hexadecimal strings in hexdump format. + * This method outputs the same format as + * {@link #dump(byte[],PrintStream)}, + * without the {@link System#lineSeparator()} characters. + *

+ * If the input is not a multiple of 16 bytes then the final chunk will + * be shorter than the preceding chunks. The result will be padded with + * spaces to match the length of the preceding chunks. + *

+ * On return, the generated stream lazily consumes the input stream. + * This method does not close the input stream and may block indefinitely + * reading from it. The behavior for the case where it is + * asynchronously closed, or the thread interrupted, + * is highly input stream specific, and therefore not specified. + *

+ * If an I/O error occurs reading from the input stream then it may not be + * at end-of-stream and may be in an inconsistent state. It is strongly + * recommended that the input stream be promptly closed if an I/O error + * occurs. + * + * @param in the input stream, non-null + * @return a new infinite sequential ordered stream of hexadecimal strings + * @throws NullPointerException if {@code in} is {@code null} + */ + public static Stream dumpAsStream(InputStream in) { + return dumpAsStream(in, DEFAULT_CHUNK_SIZE, null); + } + + /** + * Generates a dump of the contents of the provided input stream, as a + * stream of formatted hexadecimal strings. Each string is formatted + * according to the {@code formatter} function, if not {@code null}. + * Otherwise, this method outputs the same format as + * {@link #dump(byte[],PrintStream)}, + * without the {@link System#lineSeparator()} characters. + *

+ * On return, the generated stream lazily consumes the input stream. + * This method does not close the input stream and may block indefinitely + * reading from it. The behavior for the case where it is + * asynchronously closed, or the thread interrupted, + * is highly input stream specific, and therefore not specified. + *

+ * If an I/O error occurs reading from the input stream then it may not be + * at end-of-stream and may be in an inconsistent state. It is strongly + * recommended that the input stream be promptly closed if an I/O error + * occurs. + *

+ * If an error occurs in the {@code formatter} then an unchecked exception + * will be thrown from the underlying {@code Stream} method. + * + * @param in the input stream, non-null + * @param chunkSize the number of bytes-per-chunk (typically 16) + * @param formatter a hexdump formatting function, or {@code null} + * @return a new infinite sequential ordered stream of hexadecimal strings + * @throws IllegalArgumentException if {@code chunkSize <= 0} + * @throws NullPointerException if {@code in} is {@code null} + */ + public static Stream dumpAsStream(InputStream in, int chunkSize, + Formatter formatter) { + Objects.requireNonNull(in, "in"); + if (chunkSize <= 0) { + throw new IllegalArgumentException("chunkSize(" + chunkSize + ") <= 0"); + } + final Formatter f = formatter == null ? HEXDUMP_FORMATTER : formatter; + + Iterator iterator = new Iterator<>() { + byte[] nextChunk = null; + int counter = 0; + + @Override + public boolean hasNext() { + if (nextChunk != null) { + return true; + } else { + try { + nextChunk = readChunk(in, chunkSize); + return (nextChunk != null); + + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + @Override + public String next() { + if (nextChunk != null || hasNext()) { + String formattedChunk = + f.format(counter * chunkSize, nextChunk, 0, + nextChunk.length); + nextChunk = null; + counter++; + return formattedChunk; + + } else { + throw new NoSuchElementException(); + } + } + }; + + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + iterator, Spliterator.ORDERED | Spliterator.NONNULL), + false); + } + + /** + * Generates a dump of the contents of the provided byte array, as a stream + * of hexadecimal strings in hexdump format. + * This method outputs the same format as + * {@link #dump(byte[],PrintStream)}, + * without the {@link System#lineSeparator()} characters. + *

+ * If the input is not a multiple of 16 bytes then the final chunk will + * be shorter than the preceding chunks. The result will be padded with + * spaces to match the length of the preceding chunks. + * + * @param bytes a byte array, assumed to be unmodified during use + * @return a new sequential ordered stream of hexadecimal strings + * @throws NullPointerException if {@code bytes} is {@code null} + */ + public static Stream dumpAsStream(byte[] bytes) { + Objects.requireNonNull(bytes, "bytes"); + return dumpAsStream(bytes, 0, bytes.length, DEFAULT_CHUNK_SIZE, null); + } + + /** + * Generates a dump of the contents of a range within the provided + * byte array, as a stream of formatted hexadecimal strings. Each string is + * formatted according to the {@code formatter} function, if not {@code null}. + * Otherwise, this method outputs the same format as + * {@link #dump(byte[],PrintStream)}, + * without the {@link System#lineSeparator()} characters. + *

+ * The range to be converted extends from index {@code fromIndex}, + * inclusive, to index {@code toIndex}, exclusive. + * If {@code fromIndex==toIndex}, the range to be converted is empty. + * If the input is not a multiple of {@code chunkSize} then the final chunk + * will be shorter than the preceding chunks. The result may be padded with + * spaces to match the length of the preceding chunks. + *

+ * If an error occurs in the {@code formatter} then an unchecked exception + * will be thrown from the underlying {@code Stream} method. + * + * @param bytes a byte array, assumed to be unmodified during use + * @param fromIndex the index of the first byte (inclusive) to be converted + * @param toIndex the index of the last byte (exclusive) to be converted + * @param chunkSize the number of bytes-per-chunk (typically 16) + * @param formatter a hexdump formatting function, or {@code null} + * @return a new sequential ordered stream of hexadecimal strings + * @throws NullPointerException if {@code bytes} is {@code null} + * @throws IllegalArgumentException if {@code fromIndex > toIndex} + * or {@code chunkSize <= 0} + * @throws ArrayIndexOutOfBoundsException + * if {@code fromIndex < 0} or {@code toIndex > bytes.length} + */ + public static Stream dumpAsStream(byte[] bytes, int fromIndex, + int toIndex, int chunkSize, Formatter formatter) { + Objects.requireNonNull(bytes, "bytes"); + Arrays.rangeCheck(bytes.length, fromIndex, toIndex); + if (chunkSize <= 0) { + throw new IllegalArgumentException("chunkSize(" + chunkSize + ") <= 0"); + } + final Formatter f = formatter == null ? HEXDUMP_FORMATTER : formatter; + + int range = toIndex - fromIndex; + if (range == 0) { + return Stream.empty(); + } + final int length = chunkSize > range ? range : chunkSize; + + return IntStream.range(0, roundUp(range, length)) + .mapToObj(i -> { + int from = fromIndex + (i * length); + int to = from + length; + if (to > toIndex) { + to = toIndex; + } + return f.format(i * chunkSize, bytes, from, to); + }); + } + + /** + * Generates a dump of the contents of the provided ByteBuffer, + * as a stream of formatted hexadecimal strings. Each string is + * formatted according to the {@code formatter} function, if not {@code null}. + * Otherwise, this method outputs the same format as + * {@link #dump(byte[],PrintStream)}, + * without the {@link System#lineSeparator()} characters. + *

+ * If the input is not a multiple of {@code chunkSize} then the final chunk + * will be shorter than the preceding chunks. The result may be padded with + * spaces to match the length of the preceding chunks. + *

+ * Access to the ByteBuffer is relative and its position gets advanced to + * the buffer's limit. + *

+ * If an error occurs in the {@code formatter} then an unchecked exception + * will be thrown from the underlying {@code Stream} method. + * + * @param buffer a byte buffer, assumed to be unmodified during use + * @param chunkSize the number of bytes-per-chunk (typically 16) + * @param formatter a hexdump formatting function, or {@code null} + * @return a new sequential ordered stream of hexadecimal strings + * @throws IllegalArgumentException if {@code chunkSize <= 0} + * @throws NullPointerException if {@code buffer} is {@code null} + */ + public static Stream dumpAsStream(ByteBuffer buffer, int chunkSize, + Formatter formatter) { + Objects.requireNonNull(buffer, "buffer"); + if (chunkSize <= 0) { + throw new IllegalArgumentException("chunkSize(" + chunkSize + ") <= 0"); + } + byte[] bytes = new byte[buffer.remaining()]; + try { + buffer.get(bytes); + } catch (BufferUnderflowException e) { + // Safe to ignore + } + + return dumpAsStream(bytes, 0, bytes.length, chunkSize, formatter); + } + + /** + * Generates a hexadecimal dump of the contents of the provided byte array + * and writes it to the provided output stream. + * This method behaves as if: + *

{@code
+     *     byte[] bytes = ...
+     *     PrintStream out = ...
+     *     HexFormat.dumpAsStream(bytes, 16,
+     *         (offset, chunk, from, to) ->
+     *             String.format("%08x  %s  |%s|",
+     *                 offset,
+     *                 HexFormat.toFormattedString(chunk, from, to),
+     *                 HexFormat.toPrintableString(chunk, from, to)))
+     *         .forEachOrdered(out::println);
+     * }
+ *

+ * This method does not close the output stream and may block indefinitely + * writing to it. The behavior for the case where it is + * asynchronously closed, or the thread interrupted, + * is highly output stream specific, and therefore not specified. + *

+ * If an I/O error occurs writing to the output stream, then it may be + * in an inconsistent state. It is strongly recommended that the output + * stream be promptly closed if an I/O error occurs. + * + * @param bytes the byte array, assumed to be unmodified during use + * @param out the output stream, non-null + * @throws IOException if an I/O error occurs when writing + * @throws NullPointerException if {@code bytes} or {@code out} is + * {@code null} + */ + public static void dump(byte[] bytes, PrintStream out) throws IOException { + Objects.requireNonNull(bytes, "bytes"); + dump(bytes, 0, bytes.length, out); + } + + /** + * Generates a hexadecimal dump of the contents of a range within the + * provided byte array and writes it to the provided output stream. + * This method outputs the same format as + * {@link #dump(byte[],PrintStream)}. + *

+ * The range to be converted extends from index {@code fromIndex}, + * inclusive, to index {@code toIndex}, exclusive. + * If {@code fromIndex==toIndex}, the range to be converted is empty. + *

+ * This method does not close the output stream and may block indefinitely + * writing to it. The behavior for the case where it is + * asynchronously closed, or the thread interrupted, + * is highly output stream specific, and therefore not specified. + *

+ * If an I/O error occurs writing to the output stream, then it may be + * in an inconsistent state. It is strongly recommended that the output + * stream be promptly closed if an I/O error occurs. + * + * @param bytes the byte array, assumed to be unmodified during use + * @param fromIndex the index of the first byte (inclusive) to be converted + * @param toIndex the index of the last byte (exclusive) to be converted + * @param out the output stream, non-null + * @throws IOException if an I/O error occurs when writing + * @throws NullPointerException if {@code bytes} or {@code out} is + * {@code null} + * @throws IllegalArgumentException if {@code fromIndex > toIndex} + * @throws ArrayIndexOutOfBoundsException + * if {@code fromIndex < 0} or {@code toIndex > bytes.length} + */ + public static void dump(byte[] bytes, int fromIndex, int toIndex, + PrintStream out) throws IOException { + + dumpAsStream(bytes, fromIndex, toIndex, DEFAULT_CHUNK_SIZE, null) + .forEachOrdered(getPrintStream(out)::println); + } + + /** + * Generates a hexadecimal dump of the contents of the provided input stream + * and writes it to the provided output stream. + * This method outputs the same format as + * {@link #dump(byte[],PrintStream)}. + *

+ * Reads all bytes from the input stream. + * On return, the input stream will be at end-of-stream. This method does + * not close either stream and may block indefinitely reading from the + * input stream, or writing to the output stream. The behavior for the case + * where the input and/or output stream is asynchronously closed, + * or the thread interrupted, is highly input stream and output stream + * specific, and therefore not specified. + *

+ * If an I/O error occurs reading from the input stream or writing to the + * output stream, then it may do so after some bytes have been read or + * written. Consequently the input stream may not be at end-of-stream and + * one, or both, streams may be in an inconsistent state. It is strongly + * recommended that both streams be promptly closed if an I/O error occurs. + * + * @param in the input stream, non-null + * @param out the output stream, non-null + * @throws IOException if an I/O error occurs when reading or writing + * @throws NullPointerException if {@code in} or {@code out} is {@code null} + */ + public static void dump(InputStream in, PrintStream out) + throws IOException { + dumpAsStream(in, DEFAULT_CHUNK_SIZE, null) + .forEachOrdered(getPrintStream(out)::println); + } + + // Returns a hexadecimal string formatted according to the specified number + // of columns and with/without space separators between pairs of hexadecimal + // digits. Newlines are added when the chunkSize is exceeded. If the final + // line is less than chunkSize then it is padded with spaces. + private static String toChunkedString(byte[] bytes, int fromIndex, + int toIndex, int chunkSize, int columns, boolean useSeparators) { + + int range = toIndex - fromIndex; + if (range == 0) { + return ""; + } + int columnWidth = chunkSize / columns; + int lineLength = useSeparators + ? chunkSize * 3 + (columns - 1) - 1 + : chunkSize * 2 + (columns - 1); + + StringBuilder hexString = + new StringBuilder(lineLength + lineLength * (range / chunkSize)); + int position = 1; + int newlineCount = 0; + for (int i = fromIndex; i < toIndex; i++, position++) { + // add the pair of hex. digits + hexString.append(HEX_DIGITS[(bytes[i] >> 4) & 0xF]); + hexString.append(HEX_DIGITS[(bytes[i] & 0xF)]); + // add a space between pairs of hex. digits + if (useSeparators && position != chunkSize) { + hexString.append(' '); + } + // add a space between columns + if (position % columnWidth == 0 && position != chunkSize) { + hexString.append(' '); + } + // handle end-of-line + if (position == chunkSize && (i + 1 < toIndex)) { + hexString.append(NEWLINE); + newlineCount++; + position = 0; + } + } + // add final line padding, if needed + if (position <= chunkSize) { + int len = hexString.length() - (newlineCount * NEWLINE_LENGTH); + for (int i = len % lineLength; i < lineLength; i++) { + hexString.append(' '); + } + } + + return hexString.toString(); + } + + private static byte[] hexToBytes(CharSequence hexString, int fromIndex, + int toIndex) { + + int len = toIndex - fromIndex; + if (len % 2 != 0) { + throw new IllegalArgumentException( + "contains an odd number of digits: " + hexString); + } + // Skip the '0x' prefix, if present + if (len > 2 && + hexString.charAt(fromIndex) == '0' && + hexString.charAt(fromIndex + 1) == 'x') { + fromIndex += 2; + len -= 2; + } + byte[] bytes = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + int hexIndex = fromIndex + i; + int high = Character.digit(hexString.charAt(hexIndex), 16); + int low = Character.digit(hexString.charAt(hexIndex + 1), 16); + if (high == -1 || low == -1) { + throw new IllegalArgumentException( + "contains an illegal hexadecimal character: " + hexString); + } + + bytes[i / 2] = (byte) (high * 16 + low); + } + return bytes; + } + + private static int roundUp(int total, int chunkSize) { + return (total + chunkSize - 1) / chunkSize; + } + + private static byte[] readChunk(InputStream inStream, int chunkSize) + throws IOException { + byte[] buffer = new byte[chunkSize]; + + int n = inStream.readNBytes(buffer, 0, buffer.length); + if (n == 0) { + return null; + } else if (n < chunkSize) { + return Arrays.copyOf(buffer, n); + } else { + return buffer; + } + } + + private static PrintStream getPrintStream(OutputStream out) + throws IOException { + Objects.requireNonNull(out, "out"); + PrintStream ps = null; + if (out instanceof PrintStream) { + ps = (PrintStream) out; + } else { + ps = new PrintStream(out, true); // auto flush + } + return ps; + } + + /** + * Represents a function that formats a byte array as a hexadecimal + * string. + * + *

This is a functional interface + * whose functional method is + * {@link #format}. + * + * @see java.util.function.Function + * @since 12 + */ + @FunctionalInterface + public interface Formatter { + /** + * Returns a formatted hexadecimal string representation of the contents + * of a chunk within a byte array. + * + * @param offsetField is the offset into the byte array + * @param chunk a byte array + * @param fromIndex the index of the first byte (inclusive) of the + * chunk to be converted + * @param toIndex the index of the last byte (exclusive) of the + * chunk to be converted + * @return a hexadecimal string representation of a chunk of the byte + * array + * @throws NullPointerException if {@code chunk} is {@code null} + * @throws IllegalArgumentException if {@code fromIndex > toIndex} + * @throws ArrayIndexOutOfBoundsException + * if {@code fromIndex < 0} or {@code toIndex > chunk.length} + */ + String format(long offsetField, byte[] chunk, int fromIndex, int toIndex); + } +}