--- /dev/null Mon Jul 24 10:17:39 2017 +++ new/src/java.base/share/classes/java/util/Hex.java Mon Jul 24 10:17:38 2017 @@ -0,0 +1,570 @@ +/* + * 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; + } +}