/* * Copyright (c) 2017, 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.util.*; import java.util.stream.*; /** * Converts hexadecimal (base 16) string representations to and from * binary data. It can also generate the classic Unix hexdump(1) format. *

* Example usage: *

{@code
 *   // Display the hexadecimal representation of the local Internet Protocol (IP) address
 *   System.out.println(Hex.toString(InetAddress.getLocalHost().getAddress()));
 *
 *   // Initialize a 16-byte array from a hexadecimal string
 *   byte[] bytes = Hex.fromString("a1a2a3a4a5a6a7a8a9aaabacadaeaf");
 *
 *   // Dump a Java class file to the standard output stream
 *   Hex.dump(Files.readAllBytes(Paths.get("MyApp.class")), System.out);
 *
 *   // Dump a file to the standard output stream, skipping blocks of content comprising all zeros
 *   try (InputStream in = new FileInputStream("mydata.bin")) {
 *       Hex.dumpAsStream(in)
 *           .filter(s ->
 *               !s.contains("00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00"))
 *           .forEachOrdered(System.out::println);
 *   }
 *
 *   // Write the hexadecimal representation of a file to the standard output stream in 32-byte chunks
 *   Hex.stream(Files.readAllBytes(Paths.get("mydata.bin")), 32)
 *       .forEachOrdered(System.out::println);
 * }
* * @since 10 */ public final class Hex { private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); private static final int CHUNK_SIZE = 16; private Hex() {} /** * Returns a hexadecimal string representation of the contents of a * binary buffer. *

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

* {@code 0123456789abcdef} *
* * @param bytes a binary buffer * @return a hexadecimal string representation of the binary buffer. * The string length is twice the buffer 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 the contents of a * range within a binary buffer. *

* The binary value is converted to a string comprising pairs of * hexadecimal digits using 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 binary buffer * @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 binary buffer. * 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); StringBuilder hexString = new StringBuilder((toIndex - fromIndex) * 2); for (int i = fromIndex; i < toIndex; i++) { hexString.append(HEX_DIGITS[(bytes[i] >> 4) & 0xF]); hexString.append(HEX_DIGITS[(bytes[i] & 0xF)]); } return hexString.toString(); } /** * Returns a stream of hexadecimal string representations of the contents * of a binary buffer. *

* The binary values are converted to strings comprising pairs of * hexadecimal digits using only the following ASCII characters: *

* {@code 0123456789abcdef} *
* * @param bytes a binary buffer * @param chunkSize the number of bytes per hexadecimal string * (defaults to 16 if omitted) * @return a stream of hexadecimal strings representating the binary buffer. * Each string length is twice the {@code chunkSize} * (but the final string may be shorter). * @throws NullPointerException if {@code bytes} is {@code null} */ public static Stream stream(byte[] bytes, int... chunkSize) { Objects.requireNonNull(bytes, "bytes"); return stream(bytes, 0, bytes.length, chunkSize); } /** * Returns a stream of hexadecimal string representations of the contents * of a range within a binary buffer. *

* The binary values are converted to strings comprising pairs of * hexadecimal digits using 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.) * * TBD * @param bytes a binary buffer * @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 hexadecimal string * (defaults to 16 if omitted) * @return a stream of hexadecimal strings representating the binary buffer. * Each string length is twice the {@code chunkSize} * (but the final string may be shorter). * @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 Stream stream(byte[] bytes, int fromIndex, int toIndex, int... chunkSize) { Objects.requireNonNull(bytes, "bytes"); Arrays.rangeCheck(bytes.length, fromIndex, toIndex); int range = toIndex - fromIndex; int len = chunkSize.length > 0 ? chunkSize[0] : CHUNK_SIZE; if (len > range) { len = range; } return IntStream.range(0, roundUp(range, len)) .mapToObj(i -> toString(bytes, fromIndex, toIndex)); } // /** // * Returns a stream of hexadecimal strings from the contents of an input // * stream. // * // * TBD // */ // public static Stream stream(InputStream in, int... chunkSize) { // int chunkSize = chunkSize.length > 0 ? chunkSize[0] : CHUNK_SIZE; // } /** * Returns a binary buffer coresponding to the sequence of hexadecimal * digits. *

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

* {@code 0123456789abcdef} *
* * @param hexString an even numbered sequence of hexadecimal digits * @return a binary buffer * @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"); int len = hexString.length(); if (len % 2 != 0) { throw new IllegalArgumentException( "contains an odd number of digits: " + hexString); } byte[] bytes = new byte[len / 2]; for (int i = 0; i < len; i += 2) { int high = hexToBinary(hexString.charAt(i)); int low = hexToBinary(hexString.charAt(i + 1)); if (high == -1 || low == -1) { throw new IllegalArgumentException( "contains an illegal hexadecimal character: " + hexString); } bytes[i / 2] = (byte) (high * 16 + low); } return bytes; } /** * Returns a binary buffer coresponding to a range within the sequence of * hexadecimal digits. *

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

* {@code 0123456789abcdef} *
* * @param hexString an even numbered sequence of hexadecimal digits * @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 binary buffer * @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} * @throws IllegalArgumentException if {@code fromIndex > toIndex} * @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"); return fromString(hexString, 0, hexString.length()); } /** * Generates a hexadecimal dump of the contents of a binary buffer, * as a stream of hexadecimal strings. *

* It outputs the same format as {@link #dump(byte[],OutputStream)}, * without the line separator characters. * When the input is not a multiple of 16-bytes then the final chunk * is shorter than 16-bytes. * * @param bytes a binary buffer * @return a 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); } /** * Generates a hexadecimal dump of the contents of a range within a binary * buffer, as a stream of hexadecimal strings. *

* It outputs the same format as {@link #dump(byte[],OutputStream)}, * without the line separator characters. * When the input is not a multiple of 16-bytes then the final chunk * is shorter than 16-bytes. * 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 binary buffer * @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 stream of hexadecimal strings * @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 Stream dumpAsStream(byte[] bytes, int fromIndex, int toIndex) { Objects.requireNonNull(bytes, "bytes"); Arrays.rangeCheck(bytes.length, fromIndex, toIndex); return IntStream.range(0, roundUp(toIndex - fromIndex, CHUNK_SIZE)) .mapToObj(i -> chunk(i, bytes, fromIndex, toIndex)); } /** * Generates a hexadecimal dump of the contents of an input stream, * as a stream of hexadecimal strings. *

* It outputs the same format as {@link #dump(byte[],OutputStream)}, * without the line separator characters. * When the input is not a multiple of 16-bytes then the final chunk * is shorter than 16-bytes. *

* On return, the input stream will be at end of 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 stream of hexadecimal strings * @throws NullPointerException if {@code in} is {@code null} */ public static Stream dumpAsStream(InputStream in) { Objects.requireNonNull(in, "in"); Iterator iterator = new Iterator<>() { String nextChunk = null; int counter = 0; @Override public boolean hasNext() { if (nextChunk != null) { return true; } else { try { nextChunk = readChunk(in, counter); return (nextChunk != null); } catch (IOException e) { throw new UncheckedIOException(e); } } } @Override public String next() { if (nextChunk != null || hasNext()) { String chunk = nextChunk; nextChunk = null; counter++; return chunk; } else { throw new NoSuchElementException(); } } }; return StreamSupport.stream( Spliterators.spliteratorUnknownSize( iterator, Spliterator.ORDERED | Spliterator.NONNULL), false); } /** * Generates a hexadecimal dump of the contents of a binary buffer and * writes it to the output stream. *

* This is useful when analyzing binary data. * The general output format is as follows: *

     * xxxxxxxx  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  |................|
     * 
* where '{@code xxxxxxxx}' is the offset into the buffer in 16-byte chunks, * followed by ASCII coded hexadecimal bytes, followed by the ASCII * representation of the bytes, or {@code '.'} if it is non-printable. * A non-printable character is one outside the ASCII range * {@code ' '} through {@code '~'} * ({@code '\u005Cu0020'} through {@code '\u005Cu007E'}). * Output lines are separated by the platform-specific line separator. * When the input is not a multiple of 16-bytes then the final line is * shorter than normal. *

* 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 binary buffer * @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, OutputStream out) throws IOException { Objects.requireNonNull(bytes, "bytes"); dump(bytes, 0, bytes.length, out); } /** * Generates a hexadecimal dump of the contents of a range within a * binary buffer and writes it to the output stream. * It outputs the same format as {@link #dump(byte[],OutputStream)}. *

* 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 binary buffer * @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, OutputStream out) throws IOException { dumpAsStream(bytes, fromIndex, toIndex) .forEachOrdered(getPrintStream(out)::println); } /** * Generates a hexadecimal dump of the contents of an input stream and * writes it to the output stream. * It outputs the same format as {@link #dump(byte[],OutputStream)}. *

* 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, OutputStream out) throws IOException { dumpAsStream(in) .forEachOrdered(getPrintStream(out)::println); } //VR: TBD: check for (total + chunk - 1) > Integer.MAX_VALUE ?? private static int roundUp(int total, int chunk) { return (total + chunk - 1) / chunk; } private static String readChunk(InputStream inStream, int counter) throws IOException { byte[] buffer = new byte[CHUNK_SIZE]; int n = inStream.readNBytes(buffer, 0, buffer.length); if (n == 0) { return null; } return chunk(counter, buffer, 0, n); } private static String chunk(int counter, byte[] bytes) { int fromIndex = counter * CHUNK_SIZE; int toIndex = fromIndex + CHUNK_SIZE; if (toIndex > bytes.length) { toIndex = bytes.length; } return chunk(counter, bytes, fromIndex, toIndex); } private static String chunk(int counter, byte[] bytes, int fromIndex, int toIndex) { StringBuilder hex = new StringBuilder(CHUNK_SIZE * 3 + 1); StringBuilder ascii = new StringBuilder(CHUNK_SIZE); boolean hasDivider = false; for (int j = fromIndex; j < toIndex; j++) { // Hex digits hex.append(HEX_DIGITS[(bytes[j] >> 4) & 0xF]); hex.append(HEX_DIGITS[(bytes[j] & 0xF)]); if (j == fromIndex + (CHUNK_SIZE / 2 - 1)) { hex.append(" "); hasDivider = true; } else { hex.append(" "); } // Printable ASCII if (bytes[j] < ' ' || bytes[j] > '~') { ascii.append('.'); } else { ascii.append((char) bytes[j]); } } // Pad the final chunk, if shorter int chunkLength = toIndex - fromIndex; if (chunkLength < CHUNK_SIZE) { int padding = CHUNK_SIZE - chunkLength; for (int k = 0; k < padding; k++) { hex.append(" "); ascii.append(' '); } if (!hasDivider) { hex.append(' '); } } return String.format("%08x %s |%s|", counter * CHUNK_SIZE, hex.toString(), ascii.toString()); } 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; } private static int hexToBinary(char ch) { if ('0' <= ch && ch <= '9') { return ch - '0'; } if ('A' <= ch && ch <= 'F') { return ch - 'A' + 10; } if ('a' <= ch && ch <= 'f') { return ch - 'a' + 10; } return -1; } }