1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package java.util;
  27 
  28 import java.io.*;
  29 import java.util.*;
  30 import java.util.stream.*;
  31 
  32 /**
  33  * Converts hexadecimal (base 16) string representations to and from
  34  * binary data. It can also generate the classic Unix hexdump(1) format.
  35  * <p>
  36  * <b>Example usage:</b>
  37  * <pre>{@code
  38  *   // Display the hexadecimal representation of the local Internet Protocol (IP) address
  39  *   System.out.println(Hex.toString(InetAddress.getLocalHost().getAddress()));
  40  *
  41  *   // Initialize a 16-byte array from a hexadecimal string
  42  *   byte[] bytes = Hex.fromString("a1a2a3a4a5a6a7a8a9aaabacadaeaf");
  43  *
  44  *   // Dump a Java class file to the standard output stream
  45  *   Hex.dump(Files.readAllBytes(Paths.get("MyApp.class")), System.out);
  46  *
  47  *   // Dump a file to the standard output stream, skipping blocks of content comprising all zeros
  48  *   try (InputStream in = new FileInputStream("mydata.bin")) {
  49  *       Hex.dumpAsStream(in)
  50  *           .filter(s ->
  51  *               !s.contains("00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00"))
  52  *           .forEachOrdered(System.out::println);
  53  *   }
  54  *
  55  *   // Write the hexadecimal representation of a file to the standard output stream in 32-byte chunks
  56  *   Hex.stream(Files.readAllBytes(Paths.get("mydata.bin")), 32)
  57  *       .forEachOrdered(System.out::println);
  58  * }</pre>
  59  *
  60  * @since 10
  61  */
  62 public final class Hex {
  63 
  64     private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
  65     private static final int CHUNK_SIZE = 16;
  66 
  67     private Hex() {}
  68 
  69     /**
  70      * Returns a hexadecimal string representation of the contents of a
  71      * binary buffer.
  72      * <p>
  73      * The binary value is converted to a string comprising pairs of
  74      * hexadecimal digits using only the following ASCII characters:
  75      * <blockquote>
  76      *  {@code 0123456789abcdef}
  77      * </blockquote>
  78      *
  79      * @param bytes a binary buffer
  80      * @return a hexadecimal string representation of the binary buffer.
  81      *         The string length is twice the buffer length.
  82      * @throws NullPointerException if {@code bytes} is {@code null}
  83      */
  84     public static String toString(byte[] bytes) {
  85         Objects.requireNonNull(bytes, "bytes");
  86         return toString(bytes, 0, bytes.length);
  87     }
  88 
  89     /**
  90      * Returns a hexadecimal string representation of the contents of a
  91      * range within a binary buffer.
  92      * <p>
  93      * The binary value is converted to a string comprising pairs of
  94      * hexadecimal digits using only the following ASCII characters:
  95      * <blockquote>
  96      *  {@code 0123456789abcdef}
  97      * </blockquote>
  98      * The range to be converted extends from index {@code fromIndex},
  99      * inclusive, to index {@code toIndex}, exclusive.
 100      * (If {@code fromIndex==toIndex}, the range to be converted is empty.)
 101      *
 102      * @param bytes a binary buffer
 103      * @param fromIndex the index of the first byte (inclusive) to be converted
 104      * @param toIndex the index of the last byte (exclusive) to be converted
 105      * @return a hexadecimal string representation of the binary buffer.
 106      *         The string length is twice the number of bytes converted.
 107      * @throws NullPointerException if {@code bytes} is {@code null}
 108      * @throws IllegalArgumentException if {@code fromIndex > toIndex}
 109      * @throws ArrayIndexOutOfBoundsException
 110      *     if {@code fromIndex < 0} or {@code toIndex > bytes.length}
 111      */
 112     public static String toString(byte[] bytes, int fromIndex, int toIndex) {
 113         Objects.requireNonNull(bytes, "bytes");
 114         Arrays.rangeCheck(bytes.length, fromIndex, toIndex);
 115 
 116         StringBuilder hexString = new StringBuilder((toIndex - fromIndex) * 2);
 117         for (int i = fromIndex; i < toIndex; i++) {
 118             hexString.append(HEX_DIGITS[(bytes[i] >> 4) & 0xF]);
 119             hexString.append(HEX_DIGITS[(bytes[i] & 0xF)]);
 120         }
 121         return hexString.toString();
 122     }
 123 
 124     /**
 125      * Returns a stream of hexadecimal string representations of the contents
 126      * of a binary buffer.
 127      * <p>
 128      * The binary values are converted to strings comprising pairs of
 129      * hexadecimal digits using only the following ASCII characters:
 130      * <blockquote>
 131      *  {@code 0123456789abcdef}
 132      * </blockquote>
 133      *
 134      * @param bytes a binary buffer
 135      * @param chunkSize the number of bytes per hexadecimal string
 136      *         (defaults to 16 if omitted)
 137      * @return a stream of hexadecimal strings representating the binary buffer.
 138      *         Each string length is twice the {@code chunkSize}
 139      *         (but the final string may be shorter).
 140      * @throws NullPointerException if {@code bytes} is {@code null}
 141      */
 142     public static Stream<String> stream(byte[] bytes, int... chunkSize) {
 143         Objects.requireNonNull(bytes, "bytes");
 144         return stream(bytes, 0, bytes.length, chunkSize);
 145     }
 146 
 147     /**
 148      * Returns a stream of hexadecimal string representations of the contents
 149      * of a range within a binary buffer.
 150      * <p>
 151      * The binary values are converted to strings comprising pairs of
 152      * hexadecimal digits using only the following ASCII characters:
 153      * <blockquote>
 154      *  {@code 0123456789abcdef}
 155      * </blockquote>
 156      * The range to be converted extends from index {@code fromIndex},
 157      * inclusive, to index {@code toIndex}, exclusive.
 158      * (If {@code fromIndex==toIndex}, the range to be converted is empty.)
 159      *
 160      * TBD
 161      * @param bytes a binary buffer
 162      * @param fromIndex the index of the first byte (inclusive) to be converted
 163      * @param toIndex the index of the last byte (exclusive) to be converted
 164      * @param chunkSize the number of bytes per hexadecimal string
 165      *         (defaults to 16 if omitted)
 166      * @return a stream of hexadecimal strings representating the binary buffer.
 167      *         Each string length is twice the {@code chunkSize}
 168      *         (but the final string may be shorter).
 169      * @throws NullPointerException if {@code bytes} is {@code null}
 170      * @throws IllegalArgumentException if {@code fromIndex > toIndex}
 171      * @throws ArrayIndexOutOfBoundsException
 172      *     if {@code fromIndex < 0} or {@code toIndex > bytes.length}
 173      */
 174     public static Stream<String> stream(byte[] bytes, int fromIndex,
 175             int toIndex, int... chunkSize) {
 176         Objects.requireNonNull(bytes, "bytes");
 177         Arrays.rangeCheck(bytes.length, fromIndex, toIndex);
 178         int range = toIndex - fromIndex;
 179         int len = chunkSize.length > 0 ? chunkSize[0] : CHUNK_SIZE;
 180         if (len > range) {
 181             len = range;
 182         }
 183 
 184         return IntStream.range(0, roundUp(range, len))
 185             .mapToObj(i -> toString(bytes, fromIndex, toIndex));
 186     }
 187 
 188 //    /**
 189 //     * Returns a stream of hexadecimal strings from the contents of an input
 190 //     * stream.
 191 //     *
 192 //     * TBD
 193 //     */
 194 //    public static Stream<String> stream(InputStream in, int... chunkSize) {
 195 //        int chunkSize = chunkSize.length > 0 ? chunkSize[0] : CHUNK_SIZE;
 196 //    }
 197 
 198     /**
 199      * Returns a binary buffer coresponding to the sequence of hexadecimal
 200      * digits.
 201      * <p>
 202      * The binary value is generated from pairs of hexadecimal digits that use
 203      * only the following ASCII characters:
 204      * <blockquote>
 205      *  {@code 0123456789abcdef}
 206      * </blockquote>
 207      *
 208      * @param hexString an even numbered sequence of hexadecimal digits
 209      * @return a binary buffer
 210      * @throws IllegalArgumentException if {@code hexString} has an odd number
 211      *         of digits or contains an illegal hexadecimal character
 212      * @throws NullPointerException if {@code hexString} is {@code null}
 213      */
 214     public static byte[] fromString(CharSequence hexString) {
 215         Objects.requireNonNull(hexString, "hexString");
 216         int len = hexString.length();
 217         if (len % 2 != 0) {
 218             throw new IllegalArgumentException(
 219                 "contains an odd number of digits: " + hexString);
 220         }
 221         byte[] bytes = new byte[len / 2];
 222 
 223         for (int i = 0; i < len; i += 2) {
 224             int high = hexToBinary(hexString.charAt(i));
 225             int low = hexToBinary(hexString.charAt(i + 1));
 226             if (high == -1 || low == -1) {
 227                 throw new IllegalArgumentException(
 228                    "contains an illegal hexadecimal character: " + hexString);
 229             }
 230 
 231             bytes[i / 2] = (byte) (high * 16 + low);
 232         }
 233         return bytes;
 234     }
 235 
 236     /**
 237      * Returns a binary buffer coresponding to a range within the sequence of
 238      * hexadecimal digits.
 239      * <p>
 240      * The binary value is generated from pairs of hexadecimal digits that use
 241      * only the following ASCII characters:
 242      * <blockquote>
 243      *  {@code 0123456789abcdef}
 244      * </blockquote>
 245      *
 246      * @param hexString an even numbered sequence of hexadecimal digits
 247      * @param fromIndex the index of the first digit (inclusive) to be converted
 248      * @param toIndex the index of the last digit (exclusive) to be converted
 249      * @return a binary buffer
 250      * @throws IllegalArgumentException if {@code hexString} has an odd number
 251      *         of digits or contains an illegal hexadecimal character
 252      * @throws NullPointerException if {@code hexString} is {@code null}
 253      * @throws IllegalArgumentException if {@code fromIndex > toIndex}
 254      * @throws ArrayIndexOutOfBoundsException
 255      *     if {@code fromIndex < 0} or {@code toIndex > hexString.length()}
 256      */
 257     public static byte[] fromString(CharSequence hexString, int fromIndex,
 258             int toIndex) {
 259         Objects.requireNonNull(hexString, "hexString");
 260         return fromString(hexString, 0, hexString.length());
 261     }
 262 
 263     /**
 264      * Generates a hexadecimal dump of the contents of a binary buffer,
 265      * as a stream of hexadecimal strings.
 266      * <p>
 267      * It outputs the same format as {@link #dump(byte[],OutputStream)},
 268      * without the line separator characters.
 269      * When the input is not a multiple of 16-bytes then the final chunk
 270      * is shorter than 16-bytes.
 271      *
 272      * @param bytes a binary buffer
 273      * @return a stream of hexadecimal strings
 274      * @throws NullPointerException if {@code bytes} is {@code null}
 275      */
 276     public static Stream<String> dumpAsStream(byte[] bytes) {
 277         Objects.requireNonNull(bytes, "bytes");
 278         return dumpAsStream(bytes, 0, bytes.length);
 279     }
 280 
 281     /**
 282      * Generates a hexadecimal dump of the contents of a range within a binary
 283      * buffer, as a stream of hexadecimal strings.
 284      * <p>
 285      * It outputs the same format as {@link #dump(byte[],OutputStream)},
 286      * without the line separator characters.
 287      * When the input is not a multiple of 16-bytes then the final chunk
 288      * is shorter than 16-bytes.
 289      * The range to be converted extends from index {@code fromIndex},
 290      * inclusive, to index {@code toIndex}, exclusive.
 291      * (If {@code fromIndex==toIndex}, the range to be converted is empty.)
 292      *
 293      * @param bytes a binary buffer
 294      * @param fromIndex the index of the first byte (inclusive) to be converted
 295      * @param toIndex the index of the last byte (exclusive) to be converted
 296      * @return a stream of hexadecimal strings
 297      * @throws NullPointerException if {@code bytes} is {@code null}
 298      * @throws IllegalArgumentException if {@code fromIndex > toIndex}
 299      * @throws ArrayIndexOutOfBoundsException
 300      *     if {@code fromIndex < 0} or {@code toIndex > bytes.length}
 301      */
 302     public static Stream<String> dumpAsStream(byte[] bytes, int fromIndex,
 303             int toIndex) {
 304         Objects.requireNonNull(bytes, "bytes");
 305         Arrays.rangeCheck(bytes.length, fromIndex, toIndex);
 306 
 307         return IntStream.range(0, roundUp(toIndex - fromIndex, CHUNK_SIZE))
 308             .mapToObj(i -> chunk(i, bytes, fromIndex, toIndex));
 309     }
 310 
 311     /**
 312      * Generates a hexadecimal dump of the contents of an input stream,
 313      * as a stream of hexadecimal strings.
 314      * <p>
 315      * It outputs the same format as {@link #dump(byte[],OutputStream)},
 316      * without the line separator characters.
 317      * When the input is not a multiple of 16-bytes then the final chunk
 318      * is shorter than 16-bytes.
 319      * <p>
 320      * On return, the input stream will be at end of stream.
 321      * This method does not close the input stream and may block indefinitely
 322      * reading from it. The behavior for the case where it is
 323      * <i>asynchronously closed</i>, or the thread interrupted,
 324      * is highly input stream specific, and therefore not specified.
 325      * <p>
 326      * If an I/O error occurs reading from the input stream then it may not be
 327      * at end of stream and may be in an inconsistent state. It is strongly
 328      * recommended that the input stream be promptly closed if an I/O error
 329      * occurs.
 330      *
 331      * @param  in the input stream, non-null
 332      * @return a stream of hexadecimal strings
 333      * @throws NullPointerException if {@code in} is {@code null}
 334      */
 335     public static Stream<String> dumpAsStream(InputStream in) {
 336         Objects.requireNonNull(in, "in");
 337 
 338         Iterator<String> iterator = new Iterator<>() {
 339             String nextChunk = null;
 340             int counter = 0;
 341 
 342             @Override
 343             public boolean hasNext() {
 344                 if (nextChunk != null) {
 345                     return true;
 346                 } else {
 347                     try {
 348                         nextChunk = readChunk(in, counter);
 349                         return (nextChunk != null);
 350                     } catch (IOException e) {
 351                         throw new UncheckedIOException(e);
 352                     }
 353                 }
 354             }
 355 
 356             @Override
 357             public String next() {
 358                 if (nextChunk != null || hasNext()) {
 359                     String chunk = nextChunk;
 360                     nextChunk = null;
 361                     counter++;
 362                     return chunk;
 363                 } else {
 364                     throw new NoSuchElementException();
 365                 }
 366             }
 367         };
 368 
 369         return StreamSupport.stream(
 370             Spliterators.spliteratorUnknownSize(
 371                 iterator, Spliterator.ORDERED | Spliterator.NONNULL),
 372             false);
 373     }
 374 
 375     /**
 376      * Generates a hexadecimal dump of the contents of a binary buffer and
 377      * writes it to the output stream.
 378      * <p>
 379      * This is useful when analyzing binary data.
 380      * The general output format is as follows:
 381      * <pre>
 382      * xxxxxxxx  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  |................|
 383      * </pre>
 384      * where '{@code xxxxxxxx}' is the offset into the buffer in 16-byte chunks,
 385      * followed by ASCII coded hexadecimal bytes, followed by the ASCII
 386      * representation of the bytes, or {@code '.'} if it is non-printable.
 387      * A non-printable character is one outside the ASCII range
 388      * {@code ' '} through {@code '~'}
 389      * ({@code '\u005Cu0020'} through {@code '\u005Cu007E'}).
 390      * Output lines are separated by the platform-specific line separator.
 391      * When the input is not a multiple of 16-bytes then the final line is
 392      * shorter than normal.
 393      * <p>
 394      * This method does not close the output stream and may block indefinitely
 395      * writing to it. The behavior for the case where it is
 396      * <i>asynchronously closed</i>, or the thread interrupted,
 397      * is highly output stream specific, and therefore not specified.
 398      * <p>
 399      * If an I/O error occurs writing to the output stream, then it may be
 400      * in an inconsistent state. It is strongly recommended that the output
 401      * stream be promptly closed if an I/O error occurs.
 402      *
 403      * @param bytes the binary buffer
 404      * @param out the output stream, non-null
 405      * @throws IOException if an I/O error occurs when writing
 406      * @throws NullPointerException if {@code bytes} or {@code out} is
 407      *     {@code null}
 408      */
 409     public static void dump(byte[] bytes, OutputStream out) throws IOException {
 410         Objects.requireNonNull(bytes, "bytes");
 411         dump(bytes, 0, bytes.length, out);
 412     }
 413 
 414     /**
 415      * Generates a hexadecimal dump of the contents of a range within a
 416      * binary buffer and writes it to the output stream.
 417      * It outputs the same format as {@link #dump(byte[],OutputStream)}. 
 418      * <p>
 419      * The range to be converted extends from index {@code fromIndex},
 420      * inclusive, to index {@code toIndex}, exclusive.
 421      * (If {@code fromIndex==toIndex}, the range to be converted is empty.)
 422      * <p>
 423      * This method does not close the output stream and may block indefinitely
 424      * writing to it. The behavior for the case where it is
 425      * <i>asynchronously closed</i>, or the thread interrupted,
 426      * is highly output stream specific, and therefore not specified.
 427      * <p>
 428      * If an I/O error occurs writing to the output stream, then it may be
 429      * in an inconsistent state. It is strongly recommended that the output
 430      * stream be promptly closed if an I/O error occurs.
 431      *
 432      * @param bytes the binary buffer
 433      * @param fromIndex the index of the first byte (inclusive) to be converted
 434      * @param toIndex the index of the last byte (exclusive) to be converted
 435      * @param out the output stream, non-null
 436      * @throws IOException if an I/O error occurs when writing
 437      * @throws NullPointerException if {@code bytes} or {@code out} is
 438      *     {@code null}
 439      * @throws IllegalArgumentException if {@code fromIndex > toIndex}
 440      * @throws ArrayIndexOutOfBoundsException
 441      *     if {@code fromIndex < 0} or {@code toIndex > bytes.length}
 442      */
 443     public static void dump(byte[] bytes, int fromIndex, int toIndex,
 444             OutputStream out) throws IOException {
 445         dumpAsStream(bytes, fromIndex, toIndex)
 446             .forEachOrdered(getPrintStream(out)::println);
 447     }
 448 
 449     /**
 450      * Generates a hexadecimal dump of the contents of an input stream and
 451      * writes it to the output stream.
 452      * It outputs the same format as {@link #dump(byte[],OutputStream)}.
 453      * <p>
 454      * Reads all bytes from the input stream.
 455      * On return, the input stream will be at end of stream. This method does
 456      * not close either stream and may block indefinitely reading from the
 457      * input stream, or writing to the output stream. The behavior for the case
 458      * where the input and/or output stream is <i>asynchronously closed</i>,
 459      * or the thread interrupted, is highly input stream and output stream
 460      * specific, and therefore not specified.
 461      * <p>
 462      * If an I/O error occurs reading from the input stream or writing to the
 463      * output stream, then it may do so after some bytes have been read or
 464      * written. Consequently the input stream may not be at end of stream and
 465      * one, or both, streams may be in an inconsistent state. It is strongly
 466      * recommended that both streams be promptly closed if an I/O error occurs.
 467      *
 468      * @param  in the input stream, non-null
 469      * @param  out the output stream, non-null
 470      * @throws IOException if an I/O error occurs when reading or writing
 471      * @throws NullPointerException if {@code in} or {@code out} is {@code null}
 472      */
 473     public static void dump(InputStream in, OutputStream out)
 474             throws IOException {
 475         dumpAsStream(in)
 476             .forEachOrdered(getPrintStream(out)::println);
 477     }
 478 
 479 //VR: TBD: check for (total + chunk - 1) > Integer.MAX_VALUE ??
 480     private static int roundUp(int total, int chunk) {
 481         return (total + chunk - 1) / chunk;
 482     }
 483 
 484     private static String readChunk(InputStream inStream, int counter)
 485             throws IOException {
 486         byte[] buffer = new byte[CHUNK_SIZE];
 487 
 488         int n = inStream.readNBytes(buffer, 0, buffer.length);
 489         if (n == 0) {
 490             return null;
 491         }
 492         return chunk(counter, buffer, 0, n);
 493     }
 494 
 495     private static String chunk(int counter, byte[] bytes) {
 496         int fromIndex = counter * CHUNK_SIZE;
 497         int toIndex = fromIndex + CHUNK_SIZE;
 498         if (toIndex > bytes.length) {
 499             toIndex = bytes.length;
 500         }
 501         return chunk(counter, bytes, fromIndex, toIndex);
 502     }
 503 
 504     private static String chunk(int counter, byte[] bytes, int fromIndex,
 505             int toIndex) {
 506         StringBuilder hex = new StringBuilder(CHUNK_SIZE * 3 + 1);
 507         StringBuilder ascii = new StringBuilder(CHUNK_SIZE);
 508         boolean hasDivider = false;
 509 
 510         for (int j = fromIndex; j < toIndex; j++) {
 511             // Hex digits
 512             hex.append(HEX_DIGITS[(bytes[j] >> 4) & 0xF]);
 513             hex.append(HEX_DIGITS[(bytes[j] & 0xF)]);
 514             if (j == fromIndex + (CHUNK_SIZE / 2 - 1)) {
 515                 hex.append("  ");
 516                 hasDivider = true;
 517             } else {
 518                 hex.append(" ");
 519             }
 520 
 521             // Printable ASCII
 522             if (bytes[j] < ' ' || bytes[j] > '~') {
 523                 ascii.append('.');
 524             } else {
 525                 ascii.append((char) bytes[j]);
 526             }
 527         }
 528 
 529         // Pad the final chunk, if shorter
 530         int chunkLength = toIndex - fromIndex;
 531         if (chunkLength < CHUNK_SIZE) {
 532             int padding = CHUNK_SIZE - chunkLength;
 533             for (int k = 0; k < padding; k++) {
 534                 hex.append("   ");
 535                 ascii.append(' ');
 536             }
 537             if (!hasDivider) {
 538                 hex.append(' ');
 539             }
 540         }
 541 
 542         return String.format("%08x  %s |%s|",
 543             counter * CHUNK_SIZE, hex.toString(), ascii.toString());
 544     }
 545 
 546     private static PrintStream getPrintStream(OutputStream out)
 547             throws IOException {
 548         Objects.requireNonNull(out, "out");
 549         PrintStream ps = null;
 550         if (out instanceof PrintStream) {
 551             ps = (PrintStream) out;
 552         } else {
 553             ps = new PrintStream(out, true); // auto flush
 554         }
 555         return ps;
 556     }
 557 
 558     private static int hexToBinary(char ch) {
 559         if ('0' <= ch && ch <= '9') {
 560             return ch - '0';
 561         }
 562         if ('A' <= ch && ch <= 'F') {
 563             return ch - 'A' + 10;
 564         }
 565         if ('a' <= ch && ch <= 'f') {
 566             return ch - 'a' + 10;
 567         }
 568         return -1;
 569     }
 570 }