diff --git a/src/java.base/share/classes/java/lang/Integer.java b/src/java.base/share/classes/java/lang/Integer.java index 815ae10e540..2e33818963d 100644 --- a/src/java.base/share/classes/java/lang/Integer.java +++ b/src/java.base/share/classes/java/lang/Integer.java @@ -264,10 +264,18 @@ public final class Integer extends Number *
* {@code Integer.toHexString(n).toUpperCase()} *
+ *

+ * @apiNote + * The {@link java.util.HexFormat} class provides formatting and parsing + * of byte arrays and primitives to return a string or adding to an {@link Appendable}. + * {@code HexFormat} formats and parses uppercase or lowercase hexadecimal characters, + * with leading zeros and for byte arrays includes for each byte + * a delimiter, prefix, and suffix. * * @param i an integer to be converted to a string. * @return the string representation of the unsigned integer value * represented by the argument in hexadecimal (base 16). + * @see java.util.HexFormat * @see #parseUnsignedInt(String, int) * @see #toUnsignedString(int, int) * @since 1.0.2 diff --git a/src/java.base/share/classes/java/lang/Long.java b/src/java.base/share/classes/java/lang/Long.java index 21323e19de6..89650321018 100644 --- a/src/java.base/share/classes/java/lang/Long.java +++ b/src/java.base/share/classes/java/lang/Long.java @@ -299,11 +299,19 @@ public final class Long extends Number *

* {@code Long.toHexString(n).toUpperCase()} *
+ *

+ * @apiNote + * The {@link java.util.HexFormat} class provides formatting and parsing + * of byte arrays and primitives to return a string or adding to an {@link Appendable}. + * {@code HexFormat} formats and parses uppercase or lowercase hexadecimal characters, + * with leading zeros and for byte arrays includes for each byte + * a delimiter, prefix, and suffix. * * @param i a {@code long} to be converted to a string. * @return the string representation of the unsigned {@code long} * value represented by the argument in hexadecimal * (base 16). + * @see java.util.HexFormat * @see #parseUnsignedLong(String, int) * @see #toUnsignedString(long, int) * @since 1.0.2 diff --git a/src/java.base/share/classes/java/lang/module/Resolver.java b/src/java.base/share/classes/java/lang/module/Resolver.java index d926cdb27e0..8cbb35cb78e 100644 --- a/src/java.base/share/classes/java/lang/module/Resolver.java +++ b/src/java.base/share/classes/java/lang/module/Resolver.java @@ -35,6 +35,7 @@ import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.HexFormat; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -475,9 +476,10 @@ final class Resolver { if (actualHash == null) findFail("Unable to compute the hash of module %s", dn); if (!Arrays.equals(recordedHash, actualHash)) { + HexFormat hex = HexFormat.of(); findFail("Hash of %s (%s) differs to expected hash (%s)" + - " recorded in %s", dn, toHexString(actualHash), - toHexString(recordedHash), descriptor.name()); + " recorded in %s", dn, hex.formatHex(actualHash), + hex.formatHex(recordedHash), descriptor.name()); } } } @@ -485,15 +487,6 @@ final class Resolver { } } - private static String toHexString(byte[] ba) { - StringBuilder sb = new StringBuilder(ba.length * 2); - for (byte b: ba) { - sb.append(String.format("%02x", b & 0xff)); - } - return sb.toString(); - } - - /** * Computes the readability graph for the modules in the given Configuration. * diff --git a/src/java.base/share/classes/java/util/HexFormat.java b/src/java.base/share/classes/java/util/HexFormat.java new file mode 100644 index 00000000000..5f504f985db --- /dev/null +++ b/src/java.base/share/classes/java/util/HexFormat.java @@ -0,0 +1,1108 @@ +/* + * Copyright (c) 2020, 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 jdk.internal.access.JavaLangAccess; +import jdk.internal.access.SharedSecrets; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; + +/** + * {@code HexFormat} converts between bytes and chars and hex-encoded strings which may include + * additional formatting markup such as prefixes, suffixes, and delimiters. + *

+ * There are two factories of {@code HexFormat} with preset parameters {@link #of()} and + * {@link #ofDelimiter(String) ofDelimiter(delimiter)}. For other parameter combinations + * the {@code withXXX} methods return copies of {@code HexFormat} modified + * {@link #withPrefix(String)}, {@link #withSuffix(String)}, {@link #withDelimiter(String)} + * or choice of {@link #withUpperCase()} or {@link #withLowerCase()} parameters. + *

+ * For primitive to hexadecimal string conversions the {@code toHexDigits} + * methods include {@link #toHexDigits(byte)}, {@link #toHexDigits(int)}, and + * {@link #toHexDigits(long)}, etc. The default is to use lowercase characters {@code "0-9","a-f"}. + * For conversions producing uppercase hexadecimal the characters are {@code "0-9","A-F"}. + * Only the {@link HexFormat#isUpperCase() HexFormat.isUpperCase()} parameter is + * considered; the delimiter, prefix and suffix are not used. + * + *

+ * For hexadecimal string to primitive conversions the {@code fromHexDigits} + * methods include {@link #fromHexDigits(CharSequence) fromHexDigits(string)}, + * {@link #fromHexDigitsToLong(CharSequence) fromHexDigitsToLong(string)}, and + * {@link #fromHexDigit(int) fromHexDigit(int)} converts a single character or codepoint. + * For conversions from hexadecimal characters the digits and uppercase and lowercase + * characters in {@code "0-9", "a-f", and "A-F"} are converted to corresponding values + * {@code 0-15}. The delimiter, prefix, suffix, and uppercase parameters are not used. + * + *

+ * For byte array to formatted hexadecimal string conversions + * the {@code formatHex} methods include {@link #formatHex(byte[]) formatHex(byte[])} + * and {@link #formatHex(Appendable, byte[]) formatHex(Appendable, byte[])}. + * The formatted output is a string or is appended to an {@link Appendable} such as + * {@link StringBuilder} or {@link java.io.PrintStream}. + * Each byte value is formatted as the prefix, two hexadecimal characters from the + * uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * For conversions producing uppercase hexadecimal strings use {@link #withUpperCase()}. + * + *

+ * For formatted hexadecimal string to byte array conversions the + * {@code parseHex} methods include {@link #parseHex(CharSequence) parseHex(CharSequence)} and + * {@link #parseHex(char[], int, int) parseHex(char[], offset, length)}. + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * + * @apiNote + * For example, an individual byte is converted to a string of hexadecimal digits using + * {@link HexFormat#toHexDigits(int) toHexDigits(int)} and converted from a string to a + * primitive value using {@link HexFormat#fromHexDigits(CharSequence) fromHexDigits(string)}. + *

{@code
+ *     HexFormat hex = HexFormat.of();
+ *     byte b = 127;
+ *     String byteStr = hex.toHexDigits(b);
+ *
+ *     byte byteVal = (byte)hex.fromHexDigits(byteStr);
+ *     assert(byteStr.equals("7f"));
+ *     assert(b == byteVal);
+ *
+ *     // The hexadecimal digits are: "7f"
+ * }
+ *

+ * For a comma ({@code ", "}) separated format with a prefix ({@code "#"}) + * using lowercase hex digits the {@code HexFormat} is: + *

{@code
+ *     HexFormat commaFormat = HexFormat.ofDelimiter(", ").withPrefix("#");
+ *     byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127};
+ *     String str = commaFormat.formatHex(bytes);
+ *
+ *     byte[] parsed = commaFormat.parseHex(str);
+ *     assert(Arrays.equals(bytes, parsed));
+ *
+ *     // The formatted string is: "#00, #01, #02, #03, #7c, #7d, #7e, #7f"
+ * }
+ *

+ * For a fingerprint of byte values that uses the delimiter colon ({@code ":"}) + * and uppercase characters the {@code HexFormat} is: + *

{@code
+ *     HexFormat formatFingerprint = HexFormat.ofDelimiter(":").withUpperCase();
+ *     byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127};
+ *     String str = formatFingerprint.formatHex(bytes);
+ *     byte[] parsed = formatFingerprint.parseHex(str);
+ *     assert(Arrays.equals(bytes, parsed));
+ *
+ *     // The formatted string is: "00:01:02:03:7C:7D:7E:7F"
+ * }
+ * + *

+ * This is a value-based + * class; use of identity-sensitive operations (including reference equality + * ({@code ==}), identity hash code, or synchronization) on instances of + * {@code HexFormat} may have unpredictable results and should be avoided. + * The {@code equals} method should be used for comparisons. + *

+ * This class is immutable and thread-safe. + *

+ * Unless otherwise noted, passing a null argument to any method will cause a + * {@link java.lang.NullPointerException NullPointerException} to be thrown. + * + * @since 16 + */ + + +public final class HexFormat { + + // Access to create strings from a byte array. + private static final JavaLangAccess jla = SharedSecrets.getJavaLangAccess(); + + private static final byte[] UPPERCASE_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + }; + private static final byte[] LOWERCASE_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', + }; + // Analysis has shown that generating the whole array allows the JIT to generate + // better code compared to a slimmed down array, such as one cutting off after 'f' + private static final byte[] DIGITS = new byte[] { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, + -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, + 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; + /** + * Format each byte of an array as a pair of hexadecimal digits. + * The hexadecimal characters are from lowercase alpha digits. + */ + private static final HexFormat HEX_FORMAT = + new HexFormat("", "", "", LOWERCASE_DIGITS); + + private static final byte[] EMPTY_BYTES = new byte[0]; + + private final String delimiter; + private final String prefix; + private final String suffix; + private final byte[] digits; + + /** + * Returns a HexFormat with a delimiter, prefix, suffix, and array of digits. + * + * @param delimiter a delimiter, non-null + * @param prefix a prefix, non-null + * @param suffix a suffix, non-null + * @param digits byte array of digits indexed by low nibble, non-null + * @throws NullPointerException if any argument is null + */ + private HexFormat(String delimiter, String prefix, String suffix, byte[] digits) { + this.delimiter = Objects.requireNonNull(delimiter, "delimiter"); + this.prefix = Objects.requireNonNull(prefix, "prefix"); + this.suffix = Objects.requireNonNull(suffix, "suffix"); + this.digits = Objects.requireNonNull(digits, "digits"); + } + + /** + * Returns a hexadecimal formatter with no delimiter and lowercase characters. + * The delimiter, prefix, and suffix are empty. + * The methods {@link #withDelimiter(String) withDelimiter}, + * {@link #withUpperCase() withUpperCase}, {@link #withLowerCase() withLowerCase}, + * {@link #withPrefix(String) withPrefix}, and {@link #withSuffix(String) withSuffix} + * return copies of formatters with new parameters. + * + * @return a hexadecimal formatter with no delimiter and lowercase characters + */ + public static HexFormat of() { + return HEX_FORMAT; + } + + /** + * Returns a hexadecimal formatter with the delimiter and lowercase characters. + * The prefix and suffix are empty. + * The methods {@link #withDelimiter(String) withDelimiter}, + * {@link #withUpperCase() withUpperCase}, {@link #withLowerCase() withLowerCase}, + * {@link #withPrefix(String) withPrefix}, and {@link #withSuffix(String) withSuffix} + * return copies of formatters with new parameters. + * + * @param delimiter a delimiter, non-null, may be empty + * @return a {@link HexFormat} with the delimiter and lowercase characters + */ + public static HexFormat ofDelimiter(String delimiter) { + return new HexFormat(delimiter, "", "", LOWERCASE_DIGITS); + } + + /** + * Returns a copy of this {@code HexFormat} with the delimiter. + * @param delimiter the delimiter, non-null, may be empty + * @return a copy of this {@code HexFormat} with the delimiter + */ + public HexFormat withDelimiter(String delimiter) { + return new HexFormat(delimiter, this.prefix, this.suffix, this.digits); + } + + /** + * Returns a copy of this {@code HexFormat} with the prefix. + * + * @param prefix a prefix, non-null, may be empty + * @return a copy of this {@code HexFormat} with the prefix + */ + public HexFormat withPrefix(String prefix) { + return new HexFormat(this.delimiter, prefix, this.suffix, this.digits); + } + + /** + * Returns a copy of this {@code HexFormat} with the suffix. + * + * @param suffix a suffix, non-null, may be empty + * @return a copy of this {@code HexFormat} with the suffix + */ + public HexFormat withSuffix(String suffix) { + return new HexFormat(this.delimiter, this.prefix, suffix, this.digits); + } + + /** + * Returns a copy of this {@code HexFormat} to use uppercase hexadecimal characters. + * The uppercase hexadecimal characters are {@code "0-9", "A-F"}. + * + * @return a copy of this {@code HexFormat} with uppercase hexadecimal characters + */ + public HexFormat withUpperCase() { + return new HexFormat(this.delimiter, this.prefix, this.suffix, UPPERCASE_DIGITS); + } + + /** + * Returns a copy of this {@code HexFormat} to use lowercase hexadecimal characters. + * The lowercase hexadecimal characters are {@code "0-9", "a-f"}. + * + * @return a copy of this {@code HexFormat} with lowercase hexadecimal characters + */ + public HexFormat withLowerCase() { + return new HexFormat(this.delimiter, this.prefix, this.suffix, LOWERCASE_DIGITS); + } + + /** + * Returns the delimiter between hexadecimal values in formatted hexadecimal strings. + * + * @return the delimiter, non-null, may be empty {@code ""} + */ + public String delimiter() { + return delimiter; + } + + /** + * Returns the prefix used for each hexadecimal value in formatted hexadecimal strings. + * + * @return the prefix, non-null, may be empty {@code ""} + */ + public String prefix() { + return prefix; + } + + /** + * Returns the suffix used for each hexadecimal value in formatted hexadecimal strings. + * + * @return the suffix, non-null, may be empty {@code ""} + */ + public String suffix() { + return suffix; + } + + /** + * Returns {@code true} if the hexadecimal digits are uppercase, + * otherwise {@code false}. + * + * @return {@code true} if the hexadecimal digits are uppercase, + * otherwise {@code false} + */ + public boolean isUpperCase() { + return Arrays.equals(digits, UPPERCASE_DIGITS); + } + + /** + * Returns a hexadecimal string formatted from a byte array. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * + * The behavior is equivalent to + * {@link #formatHex(byte[], int, int) formatHex(bytes, 0, bytes.length))}. + * + * @param bytes a non-null array of bytes + * @return a string hexadecimal formatting of the byte array + */ + public String formatHex(byte[] bytes) { + return formatHex(bytes, 0, bytes.length); + } + + /** + * Returns a hexadecimal string formatted from a byte array range. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * + * @param bytes a non-null array of bytes + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive + * @return a string hexadecimal formatting each byte of the array range + * @throws IndexOutOfBoundsException if the array range is out of bounds + */ + public String formatHex(byte[] bytes, int fromIndex, int toIndex) { + Objects.requireNonNull(bytes,"bytes"); + Objects.checkFromToIndex(fromIndex, toIndex, bytes.length); + if (toIndex - fromIndex == 0) { + return ""; + } + // Format efficiently if possible + String s = formatOptDelimiter(bytes, fromIndex, toIndex); + if (s == null) { + long stride = prefix.length() + 2L + suffix.length() + delimiter.length(); + int capacity = checkMaxArraySize((toIndex - fromIndex) * stride - delimiter.length()); + StringBuilder sb = new StringBuilder(capacity); + formatHex(sb, bytes, fromIndex, toIndex); + s = sb.toString(); + } + return s; + } + + /** + * Appends formatted hexadecimal strings from a byte array to the {@link Appendable}. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * The formatted hexadecimal strings are appended in zero or more calls to the {@link Appendable} methods. + * + * @param The type of {@code Appendable} + * @param out an {@code Appendable}, non-null + * @param bytes a byte array + * @return the {@code Appendable} + * @throws UncheckedIOException if an I/O exception occurs appending to the output + */ + public A formatHex(A out, byte[] bytes) { + return formatHex(out, bytes, 0, bytes.length); + } + + /** + * Appends formatted hexadecimal strings from a byte array range to the {@link Appendable}. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * The formatted hexadecimal strings are appended in zero or more calls to the {@link Appendable} methods. + * + * @param The type of {@code Appendable} + * @param out an {@code Appendable}, non-null + * @param bytes a byte array, non-null + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return the {@code Appendable} + * @throws IndexOutOfBoundsException if the array range is out of bounds + * @throws UncheckedIOException if an I/O exception occurs appending to the output + */ + public A formatHex(A out, byte[] bytes, int fromIndex, int toIndex) { + Objects.requireNonNull(out, "out"); + Objects.requireNonNull(bytes, "bytes"); + Objects.checkFromToIndex(fromIndex, toIndex, bytes.length); + + int length = toIndex - fromIndex; + if (length > 0) { + try { + String between = suffix + delimiter + prefix; + out.append(prefix); + toHexDigits(out, bytes[fromIndex]); + if (between.isEmpty()) { + for (int i = 1; i < length; i++) { + toHexDigits(out, bytes[fromIndex + i]); + } + } else { + for (int i = 1; i < length; i++) { + out.append(between); + toHexDigits(out, bytes[fromIndex + i]); + } + } + out.append(suffix); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe.getMessage(), ioe); + } + } + return out; + } + + /** + * Returns a string formatting of the range of bytes optimized + * for a single allocation. + * Prefix and suffix must be empty and the delimiter + * must be empty or a single byte character, otherwise null is returned. + * + * @param bytes the bytes, non-null + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return a String formatted or null for non-single byte delimiter + * or non-empty prefix or suffix + */ + private String formatOptDelimiter(byte[] bytes, int fromIndex, int toIndex) { + byte[] rep; + if (!prefix.isEmpty() || !suffix.isEmpty()) { + return null; + } + int length = toIndex - fromIndex; + if (delimiter.isEmpty()) { + // Allocate the byte array and fill in the hex pairs for each byte + rep = new byte[checkMaxArraySize(length * 2L)]; + for (int i = 0; i < length; i++) { + rep[i * 2] = (byte)toHighHexDigit(bytes[fromIndex + i]); + rep[i * 2 + 1] = (byte)toLowHexDigit(bytes[fromIndex + i]); + } + } else if (delimiter.length() == 1 && delimiter.charAt(0) < 256) { + // Allocate the byte array and fill in the characters for the first byte + // Then insert the delimiter and hexadecimal characters for each of the remaining bytes + char sep = delimiter.charAt(0); + rep = new byte[checkMaxArraySize(length * 3L - 1L)]; + rep[0] = (byte) toHighHexDigit(bytes[fromIndex]); + rep[1] = (byte) toLowHexDigit(bytes[fromIndex]); + for (int i = 1; i < length; i++) { + rep[i * 3 - 1] = (byte) sep; + rep[i * 3 ] = (byte) toHighHexDigit(bytes[fromIndex + i]); + rep[i * 3 + 1] = (byte) toLowHexDigit(bytes[fromIndex + i]); + } + } else { + // Delimiter formatting not to a single byte + return null; + } + try { + // Return a new string using the bytes without making a copy + return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Checked that the requested size for the result string is less than the max array size. + * + * @param length the requested size of a byte array. + * @return the length + * @throws OutOfMemoryError if the size is larger than Integer.MAX_VALUE + */ + private static int checkMaxArraySize(long length) { + if (length > Integer.MAX_VALUE) + throw new OutOfMemoryError("String size " + length + + " exceeds maximum " + Integer.MAX_VALUE); + return (int)length; + } + + /** + * Returns a byte array containing hexadecimal values parsed from the string. + * + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * The delimiters, prefixes, and suffixes strings must be present; they may be empty strings. + * A valid string consists only of the above format. + * + * @param string a string containing the byte values with prefix, hexadecimal digits, suffix, + * and delimiters + * @return a byte array with the values parsed from the string + * @throws IllegalArgumentException if the prefix or suffix is not present for each byte value, + * the byte values are not hexadecimal characters, or if the delimiter is not present + * after all but the last byte value + */ + public byte[] parseHex(CharSequence string) { + return parseHex(string, 0, string.length()); + } + + /** + * Returns a byte array containing hexadecimal values parsed from a range of the string. + * + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * The delimiters, prefixes, and suffixes strings must be present; they may be empty strings. + * A valid string consists only of the above format. + * + * @param string a string range containing hexadecimal digits, + * delimiters, prefix, and suffix. + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return a byte array with the values parsed from the string range + * @throws IllegalArgumentException if the prefix or suffix is not present for each byte value, + * the byte values are not hexadecimal characters, or if the delimiter is not present + * after all but the last byte value + * @throws IndexOutOfBoundsException if the string range is out of bounds + */ + public byte[] parseHex(CharSequence string, int fromIndex, int toIndex) { + Objects.requireNonNull(string, "string"); + Objects.checkFromToIndex(fromIndex, toIndex, string.length()); + + if (fromIndex != 0 || toIndex != string.length()) { + string = string.subSequence(fromIndex, toIndex); + } + + if (string.length() == 0) + return EMPTY_BYTES; + if (delimiter.isEmpty() && prefix.isEmpty() && suffix.isEmpty()) + return parseNoDelimiter(string); + + // avoid overflow for max length prefix or suffix + long valueChars = prefix.length() + 2L + suffix.length(); + long stride = valueChars + delimiter.length(); + if (string.length() < valueChars || (string.length() - valueChars) % stride != 0) + throw new IllegalArgumentException("extra or missing delimiters " + + "or values consisting of prefix, two hexadecimal digits, and suffix"); + + checkLiteral(string, 0, prefix); + checkLiteral(string, string.length() - suffix.length(), suffix); + String between = suffix + delimiter + prefix; + final int len = (int)((string.length() - valueChars) / stride + 1L); + byte[] bytes = new byte[len]; + int i, offset; + for (i = 0, offset = prefix.length(); i < len - 1; i++, offset += 2 + between.length()) { + int v = fromHexDigits(string, offset); + if (v < 0) + throw new IllegalArgumentException("input contains non-hexadecimal characters"); + bytes[i] = (byte) v; + checkLiteral(string, offset + 2, between); + } + int v = fromHexDigits(string, offset); + if (v < 0) + throw new IllegalArgumentException("input contains non-hexadecimal characters"); + bytes[i] = (byte) v; + + return bytes; + } + + /** + * Returns a byte array containing hexadecimal values parsed from + * a range of the character array. + * + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * The delimiters, prefixes, and suffixes strings must be present; they may be empty strings. + * A valid character array range consists only of the above format. + * + * @param chars a character array range containing an even number of hexadecimal digits, + * delimiters, prefix, and suffix. + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return a byte array with the values parsed from the character array range + * @throws IllegalArgumentException if the prefix or suffix is not present for each byte value, + * the byte values are not hexadecimal characters, or if the delimiter is not present + * after all but the last byte value + * @throws IndexOutOfBoundsException if the character array range is out of bounds + */ + public byte[] parseHex(char[] chars, int fromIndex, int toIndex) { + Objects.requireNonNull(chars, "chars"); + Objects.checkFromToIndex(fromIndex, toIndex, chars.length); + CharBuffer cb = CharBuffer.wrap(chars, fromIndex, toIndex - fromIndex); + return parseHex(cb); + } + + /** + * Compare the literal and throw an exception if it does not match. + * Pre-condition: {@code index + literal.length() <= string.length()}. + * + * @param string a CharSequence + * @param index the index of the literal in the CharSequence + * @param literal the expected literal + * @throws IllegalArgumentException if the literal is not present + */ + private static void checkLiteral(CharSequence string, int index, String literal) { + assert index <= string.length() - literal.length() : "pre-checked invariant error"; + if (literal.isEmpty() || + (literal.length() == 1 && literal.charAt(0) == string.charAt(index))) { + return; + } + for (int i = 0; i < literal.length(); i++) { + if (string.charAt(index + i) != literal.charAt(i)) { + throw new IllegalArgumentException(escapeNL("found: \"" + + string.subSequence(index, index + literal.length()) + + "\", expected: \"" + literal + "\", index: " + index + + " ch: " + (int)string.charAt(index + i))); + } + } + } + + /** + * Expands new line characters to escaped newlines for display. + * + * @param string a string + * @return a string with newline characters escaped + */ + private static String escapeNL(String string) { + return string.replace("\n", "\\n") + .replace("\r", "\\r"); + } + + /** + * Returns the hexadecimal character for the low 4 bits of the value considering it to be a byte. + * If the parameter {@link #isUpperCase()} is {@code true} the + * character returned for values {@code 10-15} is uppercase {@code "A-F"}, + * otherwise the character returned is lowercase {@code "a-f"}. + * The values in the range {@code 0-9} are returned as {@code "0-9"}. + * + * @param value a value, only the low 4 bits {@code 0-3} of the value are used + * @return the hexadecimal character for the low 4 bits {@code 0-3} of the value + */ + public char toLowHexDigit(int value) { + return (char)digits[value & 0xf]; + } + + /** + * Returns the hexadecimal character for the high 4 bits of the value considering it to be a byte. + * If the parameter {@link #isUpperCase()} is {@code true} the + * character returned for values {@code 10-15} is uppercase {@code "A-F"}, + * otherwise the character returned is lowercase {@code "a-f"}. + * The values in the range {@code 0-9} are returned as {@code "0-9"}. + * + * @param value a value, only bits {@code 4-7} of the value are used + * @return the hexadecimal character for the bits {@code 4-7} of the value + */ + public char toHighHexDigit(int value) { + return (char)digits[(value >> 4) & 0xf]; + } + + /** + * Appends two hexadecimal characters for the byte value to the {@link Appendable}. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The hexadecimal characters are appended in one or more calls to the + * {@link Appendable} methods. The delimiter, prefix and suffix are not used. + * + * @param The type of {@code Appendable} + * @param out an {@code Appendable}, non-null + * @param value a byte value + * @return the {@code Appendable} + * @throws UncheckedIOException if an I/O exception occurs appending to the output + */ + public A toHexDigits(A out, byte value) { + Objects.requireNonNull(out, "out"); + try { + out.append(toHighHexDigit(value)); + out.append(toLowHexDigit(value)); + return out; + } catch (IOException ioe) { + throw new UncheckedIOException(ioe.getMessage(), ioe); + } + } + + /** + * Returns the two hexadecimal characters for the {@code byte} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a byte value + * @return the two hexadecimal characters for the byte value + */ + public String toHexDigits(byte value) { + byte[] rep = new byte[2]; + rep[0] = (byte)toHighHexDigit(value); + rep[1] = (byte)toLowHexDigit(value); + try { + return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns the four hexadecimal characters for the {@code char} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code char} value + * @return the four hexadecimal characters for the {@code char} value + */ + public String toHexDigits(char value) { + return toHexDigits((short)value); + } + + /** + * Returns the four hexadecimal characters for the {@code short} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code short} value + * @return the four hexadecimal characters for the {@code short} value + */ + public String toHexDigits(short value) { + byte[] rep = new byte[4]; + rep[0] = (byte)toHighHexDigit((byte)(value >> 8)); + rep[1] = (byte)toLowHexDigit((byte)(value >> 8)); + rep[2] = (byte)toHighHexDigit((byte)value); + rep[3] = (byte)toLowHexDigit((byte)value); + + try { + return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns the eight hexadecimal characters for the {@code int} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value an {@code int} value + * @return the eight hexadecimal characters for the {@code int} value + * @see Integer#toHexString + */ + public String toHexDigits(int value) { + byte[] rep = new byte[8]; + rep[0] = (byte)toHighHexDigit((byte)(value >> 24)); + rep[1] = (byte)toLowHexDigit((byte)(value >> 24)); + rep[2] = (byte)toHighHexDigit((byte)(value >> 16)); + rep[3] = (byte)toLowHexDigit((byte)(value >> 16)); + rep[4] = (byte)toHighHexDigit((byte)(value >> 8)); + rep[5] = (byte)toLowHexDigit((byte)(value >> 8)); + rep[6] = (byte)toHighHexDigit((byte)value); + rep[7] = (byte)toLowHexDigit((byte)value); + + try { + return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns the sixteen hexadecimal characters for the {@code long} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code long} value + * @return the sixteen hexadecimal characters for the {@code long} value + * @see Long#toHexString + */ + public String toHexDigits(long value) { + byte[] rep = new byte[16]; + rep[0] = (byte)toHighHexDigit((byte)(value >>> 56)); + rep[1] = (byte)toLowHexDigit((byte)(value >>> 56)); + rep[2] = (byte)toHighHexDigit((byte)(value >>> 48)); + rep[3] = (byte)toLowHexDigit((byte)(value >>> 48)); + rep[4] = (byte)toHighHexDigit((byte)(value >>> 40)); + rep[5] = (byte)toLowHexDigit((byte)(value >>> 40)); + rep[6] = (byte)toHighHexDigit((byte)(value >>> 32)); + rep[7] = (byte)toLowHexDigit((byte)(value >>> 32)); + rep[8] = (byte)toHighHexDigit((byte)(value >>> 24)); + rep[9] = (byte)toLowHexDigit((byte)(value >>> 24)); + rep[10] = (byte)toHighHexDigit((byte)(value >>> 16)); + rep[11] = (byte)toLowHexDigit((byte)(value >>> 16)); + rep[12] = (byte)toHighHexDigit((byte)(value >>> 8)); + rep[13] = (byte)toLowHexDigit((byte)(value >>> 8)); + rep[14] = (byte)toHighHexDigit((byte)value); + rep[15] = (byte)toLowHexDigit((byte)value); + + try { + return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns up to sixteen hexadecimal characters for the {@code long} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code long} value + * @param digits the number of hexadecimal digits to return, 0 to 16 + * @return the hexadecimal characters for the {@code long} value + * @throws IllegalArgumentException if {@code digits} is negative or greater than 16 + */ + public String toHexDigits(long value, int digits) { + if (digits < 0 || digits > 16) + throw new IllegalArgumentException("number of digits: " + digits); + if (digits == 0) + return ""; + byte[] rep = new byte[digits]; + for (int i = rep.length - 1; i >= 0; i--) { + rep[i] = (byte)toLowHexDigit((byte)(value)); + value = value >>> 4; + } + try { + return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns a byte array containing the parsed hex digits. + * A valid string consists only of an even number of hex digits. + * + * @param string a string containing an even number of only hex digits + * @return a byte array + * @throws IllegalArgumentException if the string length is not valid or + * the string contains non-hexadecimal characters + */ + private byte[] parseNoDelimiter(CharSequence string) { + if ((string.length() & 1) != 0) + throw new IllegalArgumentException("string length not even: " + + string.length()); + + byte[] bytes = new byte[string.length() / 2]; + int illegal = 0; // Accumulate logical-or of all bytes + for (int i = 0; i < bytes.length; i++) { + int v = fromHexDigits(string, i * 2); + bytes[i] = (byte) v; + illegal |= v; + } + // check if any character was an illegal character + if (illegal < 0) + throw new IllegalArgumentException("input contains non-hexadecimal characters"); + + return bytes; + } + + /** + * Check the number of requested digits against a limit. + * + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @param limit the maximum allowed + * @return the length of the range + */ + private static int checkDigitCount(int fromIndex, int toIndex, int limit) { + int length = toIndex - fromIndex; + if (length > limit) + throw new IllegalArgumentException("string length greater than " + + limit + ": " + length); + return length; + } + + /** + * Returns {@code true} if the character is a valid hexadecimal character or codepoint. + * The valid hexadecimal characters are: + *

+ * @param ch a codepoint + * @return {@code true} if the character is valid a hexadecimal character, + * otherwise {@code false} + */ + public boolean isHexDigit(int ch) { + return ((ch >>> 8) == 0 && DIGITS[ch] >= 0); + } + + /** + * Returns the value for the hexadecimal character or codepoint. + * The value is: + * + * The delimiter, prefix, suffix, and uppercase parameters are not used. + * + * @param ch a character or codepoint + * @return the value {@code 0-15} + * @throws NumberFormatException if the codepoint is not a hexadecimal character + */ + public int fromHexDigit(int ch) { + int value; + if ((ch >>> 8) == 0 && (value = DIGITS[ch]) >= 0) { + return value; + } + throw new NumberFormatException("not a hexadecimal digit: \"" + (char) ch + "\" = " + ch); + } + + /** + * Returns a value parsed from two hexadecimal characters in a string. + * The characters in the range from {@code index} to {@code index + 1}, + * inclusive, must be valid hex digits according to {@link #fromHexDigit(int)}. + * The delimiter, prefix, suffix, and uppercase parameters are not used. + * + * @param string a CharSequence containing the characters + * @param index the index of the first character of the range + * @return the value parsed from the string range + * @throws NumberFormatException if any of the characters in the range + * is not a hexadecimal character + * @throws IndexOutOfBoundsException if the range is out of bounds + * for the {@code CharSequence} + */ + private int fromHexDigits(CharSequence string, int index) { + Objects.requireNonNull(string, "string"); + int high = fromHexDigit(string.charAt(index)); + int low = fromHexDigit(string.charAt(index + 1)); + return (high << 4) | low; + } + + /** + * Returns the {@code int} value parsed from a string of up to eight hexadecimal characters. + * The hexadecimal characters are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 32 bits and is returned as an {@code int}. + * The delimiter, prefix, suffix, and uppercase parameters are not used. + * + * @apiNote + * {@link Integer#parseInt(String, int) Integer.parseInt(s, 16)} and + * {@link Integer#parseUnsignedInt(String, int) Integer.parseUnsignedInt(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters "0-9, "A-F", and "a-f". + * Signed hexadecimal strings can be parsed with {@link Integer#parseInt(String, int)}. + * + * @param string a CharSequence containing up to eight hexadecimal characters + * @return the value parsed from the string + * @throws IllegalArgumentException if the string length is greater than eight (8) or + * if any of the characters is not a hexadecimal character + */ + public int fromHexDigits(CharSequence string) { + Objects.requireNonNull(string, "string"); + int length = checkDigitCount(0, string.length(), 8); + int value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(i)); + } + return value; + } + + /** + * Returns the {@code int} value parsed from a string range of up to eight hexadecimal + * characters. + * The characters in the range {@code fromIndex} to {@code toIndex}, exclusive, + * are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 32 bits and is returned as an {@code int}. + * The delimiter, prefix, suffix, and uppercase parameters are not used. + * + * @apiNote + * {@link Integer#parseInt(String, int) Integer.parseInt(s, 16)} and + * {@link Integer#parseUnsignedInt(String, int) Integer.parseUnsignedInt(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters "0-9, "A-F", and "a-f". + * Signed hexadecimal strings can be parsed with {@link Integer#parseInt(String, int)}. + * + * @param string a CharSequence containing the characters + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return the value parsed from the string range + * @throws IndexOutOfBoundsException if the range is out of bounds + * for the {@code CharSequence} + * @throws IllegalArgumentException if length of the range is greater than eight (8) or + * if any of the characters is not a hexadecimal character + */ + public int fromHexDigits(CharSequence string, int fromIndex, int toIndex) { + Objects.requireNonNull(string, "string"); + Objects.checkFromToIndex(fromIndex, toIndex, string.length()); + int length = checkDigitCount(fromIndex, toIndex, 8); + int value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(fromIndex + i)); + } + return value; + } + + /** + * Returns the long value parsed from a string of up to sixteen hexadecimal characters. + * The hexadecimal characters are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 64 bits and is returned as a {@code long}. + * The delimiter, prefix, suffix, and uppercase parameters are not used. + * + * @apiNote + * {@link Long#parseLong(String, int) Long.parseLong(s, 16)} and + * {@link Long#parseUnsignedLong(String, int) Long.parseUnsignedLong(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters "0-9, "A-F", and "a-f". + * Signed hexadecimal strings can be parsed with {@link Long#parseLong(String, int)}. + * + * @param string a CharSequence containing up to sixteen hexadecimal characters + * @return the value parsed from the string + * @throws IllegalArgumentException if the string length is greater than sixteen (16) or + * if any of the characters is not a hexadecimal character + */ + public long fromHexDigitsToLong(CharSequence string) { + Objects.requireNonNull(string, "string"); + int length = checkDigitCount(0, string.length(), 16); + long value = 0L; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(i)); + } + return value; + } + + /** + * Returns the long value parsed from a string range of up to sixteen hexadecimal + * characters. + * The characters in the range {@code fromIndex} to {@code toIndex}, exclusive, + * are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 64 bits and is returned as a {@code long}. + * The delimiter, prefix, suffix, and uppercase parameters are not used. + * + * @apiNote + * {@link Long#parseLong(String, int) Long.parseLong(s, 16)} and + * {@link Long#parseUnsignedLong(String, int) Long.parseUnsignedLong(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters "0-9, "A-F", and "a-f". + * Signed hexadecimal strings can be parsed with {@link Long#parseLong(String, int)}. + * + * @param string a CharSequence containing the characters + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return the value parsed from the string range + * @throws IndexOutOfBoundsException if the range is out of bounds + * for the {@code CharSequence} + * @throws IllegalArgumentException if the length of the range is greater than sixteen (16) or + * if any of the characters is not a hexadecimal character + */ + public long fromHexDigitsToLong(CharSequence string, int fromIndex, int toIndex) { + Objects.requireNonNull(string, "string"); + Objects.checkFromToIndex(fromIndex, toIndex, string.length()); + int length = checkDigitCount(fromIndex, toIndex, 16); + long value = 0L; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(fromIndex + i)); + } + return value; + } + + /** + * Returns {@code true} if the other object is a {@code HexFormat} + * with the same parameters. + * + * @param o an object, may be null + * @return {@code true} if the other object is a {@code HexFormat} and the parameters + * uppercase, delimiter, prefix, and suffix are equal; + * otherwise {@code false} + */ + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + HexFormat otherHex = (HexFormat) o; + return delimiter.equals(otherHex.delimiter) && + prefix.equals(otherHex.prefix) && + suffix.equals(otherHex.suffix) && + Arrays.equals(digits, otherHex.digits); + } + + /** + * Returns a hashcode for this {@code HexFormat}. + * + * @return a hashcode for this {@code HexFormat} + */ + @Override + public int hashCode() { + int result = Objects.hash(delimiter, prefix, suffix); + result = 31 * result + Boolean.hashCode(Arrays.equals(digits, UPPERCASE_DIGITS)); + return result; + } + + /** + * Returns a description of the formatter parameters for uppercase, + * delimiter, prefix, and suffix. + * + * @return a description of this {@code HexFormat} + */ + @Override + public String toString() { + return escapeNL("uppercase: " + Arrays.equals(digits, UPPERCASE_DIGITS) + + ", delimiter: \"" + delimiter + + "\", prefix: \"" + prefix + + "\", suffix: \"" + suffix + "\""); + } +} diff --git a/src/java.base/share/classes/java/util/Properties.java b/src/java.base/share/classes/java/util/Properties.java index e86dc3ab460..a80acdc532c 100644 --- a/src/java.base/share/classes/java/util/Properties.java +++ b/src/java.base/share/classes/java/util/Properties.java @@ -701,7 +701,7 @@ public class Properties extends Hashtable { bufLen = Integer.MAX_VALUE; } StringBuilder outBuffer = new StringBuilder(bufLen); - + HexFormat hex = HexFormat.of().withUpperCase(); for(int x=0; x { break; default: if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode ) { - outBuffer.append('\\'); - outBuffer.append('u'); - outBuffer.append(toHex((aChar >> 12) & 0xF)); - outBuffer.append(toHex((aChar >> 8) & 0xF)); - outBuffer.append(toHex((aChar >> 4) & 0xF)); - outBuffer.append(toHex( aChar & 0xF)); + outBuffer.append("\\u"); + outBuffer.append(hex.toHexDigits(aChar)); } else { outBuffer.append(aChar); } @@ -752,24 +748,19 @@ public class Properties extends Hashtable { private static void writeComments(BufferedWriter bw, String comments) throws IOException { + HexFormat hex = HexFormat.of().withUpperCase(); bw.write("#"); int len = comments.length(); int current = 0; int last = 0; - char[] uu = new char[6]; - uu[0] = '\\'; - uu[1] = 'u'; while (current < len) { char c = comments.charAt(current); if (c > '\u00ff' || c == '\n' || c == '\r') { if (last != current) bw.write(comments.substring(last, current)); if (c > '\u00ff') { - uu[2] = toHex((c >> 12) & 0xf); - uu[3] = toHex((c >> 8) & 0xf); - uu[4] = toHex((c >> 4) & 0xf); - uu[5] = toHex( c & 0xf); - bw.write(new String(uu)); + bw.write("\\u"); + bw.write(hex.toHexDigits(c)); } else { bw.newLine(); if (c == '\r' && @@ -1271,19 +1262,6 @@ public class Properties extends Hashtable { } } - /** - * Convert a nibble to a hex character - * @param nibble the nibble to convert. - */ - private static char toHex(int nibble) { - return hexDigit[(nibble & 0xF)]; - } - - /** A table of hex digits */ - private static final char[] hexDigit = { - '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' - }; - // // Hashtable methods overridden and delegated to a ConcurrentHashMap instance diff --git a/src/java.base/share/classes/javax/net/ssl/SNIServerName.java b/src/java.base/share/classes/javax/net/ssl/SNIServerName.java index 9df92d75db8..d0ec849f71b 100644 --- a/src/java.base/share/classes/javax/net/ssl/SNIServerName.java +++ b/src/java.base/share/classes/javax/net/ssl/SNIServerName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2020, 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 @@ -26,6 +26,7 @@ package javax.net.ssl; import java.util.Arrays; +import java.util.HexFormat; /** * Instances of this class represent a server name in a Server Name @@ -52,9 +53,6 @@ public abstract class SNIServerName { // the encoded value of the server name private final byte[] encoded; - // the hex digitals - private static final char[] HEXES = "0123456789ABCDEF".toCharArray(); - /** * Creates an {@code SNIServerName} using the specified name type and * encoded value. @@ -192,22 +190,7 @@ public abstract class SNIServerName { if (bytes.length == 0) { return "(empty)"; } - - StringBuilder sb = new StringBuilder(bytes.length * 3 - 1); - boolean isInitial = true; - for (byte b : bytes) { - if (isInitial) { - isInitial = false; - } else { - sb.append(':'); - } - - int k = b & 0xFF; - sb.append(HEXES[k >>> 4]); - sb.append(HEXES[k & 0xF]); - } - - return sb.toString(); + return HexFormat.ofDelimiter(":").withUpperCase().formatHex(bytes); } } diff --git a/src/java.base/share/classes/sun/net/www/ParseUtil.java b/src/java.base/share/classes/sun/net/www/ParseUtil.java index c73c52e15d8..56d83f1e1f0 100644 --- a/src/java.base/share/classes/sun/net/www/ParseUtil.java +++ b/src/java.base/share/classes/sun/net/www/ParseUtil.java @@ -37,6 +37,7 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; +import java.util.HexFormat; import sun.nio.cs.UTF_8; @@ -47,6 +48,8 @@ import sun.nio.cs.UTF_8; public final class ParseUtil { + private static final HexFormat HEX_UPPERCASE = HexFormat.of().withUpperCase(); + private ParseUtil() {} /** @@ -515,15 +518,9 @@ public final class ParseUtil { } } - private static final char[] hexDigits = { - '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' - }; - private static void appendEscape(StringBuilder sb, byte b) { sb.append('%'); - sb.append(hexDigits[(b >> 4) & 0x0f]); - sb.append(hexDigits[(b >> 0) & 0x0f]); + HEX_UPPERCASE.toHexDigits(sb, b); } // Tell whether the given character is permitted by the given mask pair diff --git a/src/java.base/unix/classes/sun/nio/fs/UnixUriUtils.java b/src/java.base/unix/classes/sun/nio/fs/UnixUriUtils.java index c25f837a6cc..133e123b636 100644 --- a/src/java.base/unix/classes/sun/nio/fs/UnixUriUtils.java +++ b/src/java.base/unix/classes/sun/nio/fs/UnixUriUtils.java @@ -30,6 +30,7 @@ import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.HexFormat; /** * Unix specific Path <--> URI conversion @@ -102,14 +103,14 @@ class UnixUriUtils { byte[] path = up.toAbsolutePath().asByteArray(); StringBuilder sb = new StringBuilder("file:///"); assert path[0] == '/'; + HexFormat hex = HexFormat.of().withUpperCase(); for (int i=1; i> 4) & 0x0f]); - sb.append(hexDigits[(c) & 0x0f]); + hex.toHexDigits(sb, (byte)c); } } @@ -242,9 +243,4 @@ class UnixUriUtils { // All valid path characters private static final long L_PATH = L_PCHAR | lowMask(";/"); private static final long H_PATH = H_PCHAR | highMask(";/"); - - private static final char[] hexDigits = { - '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' - }; } diff --git a/src/java.naming/share/classes/com/sun/jndi/ldap/Filter.java b/src/java.naming/share/classes/com/sun/jndi/ldap/Filter.java index 6a3b6db0446..c4cd4db38b4 100644 --- a/src/java.naming/share/classes/com/sun/jndi/ldap/Filter.java +++ b/src/java.naming/share/classes/com/sun/jndi/ldap/Filter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2020, 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 @@ -189,24 +189,6 @@ final class Filter { } - /** - * convert character 'c' that represents a hexadecimal digit to an integer. - * if 'c' is not a hexadecimal digit [0-9A-Fa-f], -1 is returned. - * otherwise the converted value is returned. - */ - private static int hexchar2int( byte c ) { - if ( c >= '0' && c <= '9' ) { - return( c - '0' ); - } - if ( c >= 'A' && c <= 'F' ) { - return( c - 'A' + 10 ); - } - if ( c >= 'a' && c <= 'f' ) { - return( c - 'a' + 10 ); - } - return( -1 ); - } - // called by the LdapClient.compare method static byte[] unescapeFilterValue(byte[] orig, int start, int end) throws NamingException { @@ -225,7 +207,7 @@ final class Filter { ch = orig[i]; if (escape) { // Try LDAP V3 escape (\xx) - if ((ival = hexchar2int(ch)) < 0) { + if ((ival = Character.digit(ch, 16)) < 0) { /** * If there is no hex char following a '\' when diff --git a/src/java.naming/share/classes/com/sun/jndi/toolkit/dir/SearchFilter.java b/src/java.naming/share/classes/com/sun/jndi/toolkit/dir/SearchFilter.java index bc56d4a6174..26e01a70c01 100644 --- a/src/java.naming/share/classes/com/sun/jndi/toolkit/dir/SearchFilter.java +++ b/src/java.naming/share/classes/com/sun/jndi/toolkit/dir/SearchFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2020, 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 @@ -27,6 +27,7 @@ package com.sun.jndi.toolkit.dir; import javax.naming.*; import javax.naming.directory.*; import java.util.Enumeration; +import java.util.HexFormat; import java.util.StringTokenizer; import java.util.Vector; import java.util.Locale; @@ -466,26 +467,6 @@ public class SearchFilter implements AttrFilter { return answer; } - // Writes the hex representation of a byte to a StringBuffer. - private static void hexDigit(StringBuffer buf, byte x) { - char c; - - c = (char) ((x >> 4) & 0xf); - if (c > 9) - c = (char) ((c-10) + 'A'); - else - c = (char)(c + '0'); - - buf.append(c); - c = (char) (x & 0xf); - if (c > 9) - c = (char)((c-10) + 'A'); - else - c = (char)(c + '0'); - buf.append(c); - } - - /** * Returns the string representation of an object (such as an attr value). * If obj is a byte array, encode each item as \xx, where xx is hex encoding @@ -506,13 +487,9 @@ public class SearchFilter implements AttrFilter { if (obj instanceof byte[]) { // binary data must be encoded as \hh where hh is a hex char + HexFormat hex = HexFormat.of().withUpperCase().withPrefix("\\"); byte[] bytes = (byte[])obj; - StringBuffer b1 = new StringBuffer(bytes.length*3); - for (int i = 0; i < bytes.length; i++) { - b1.append('\\'); - hexDigit(b1, bytes[i]); - } - return b1.toString(); + return hex.formatHex(bytes); } if (!(obj instanceof String)) { str = obj.toString(); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index fb16496edfe..0a54a462d9d 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -59,6 +59,7 @@ import java.text.Normalizer; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HexFormat; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -1085,17 +1086,6 @@ public final class Utils { // -- toAsciiString-like support to encode path and query URI segments - private static final char[] hexDigits = { - '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' - }; - - private static void appendEscape(StringBuilder sb, byte b) { - sb.append('%'); - sb.append(hexDigits[(b >> 4) & 0x0f]); - sb.append(hexDigits[(b >> 0) & 0x0f]); - } - // Encodes all characters >= \u0080 into escaped, normalized UTF-8 octets, // assuming that s is otherwise legal // @@ -1123,13 +1113,16 @@ public final class Utils { assert false : x; } + HexFormat format = HexFormat.of().withUpperCase(); StringBuilder sb = new StringBuilder(); while (bb.hasRemaining()) { int b = bb.get() & 0xff; - if (b >= 0x80) - appendEscape(sb, (byte)b); - else - sb.append((char)b); + if (b >= 0x80) { + sb.append('%'); + format.toHexDigits(sb, (byte)b); + } else { + sb.append((char) b); + } } return sb.toString(); } diff --git a/test/jdk/java/lang/StringBuffer/Supplementary.java b/test/jdk/java/lang/StringBuffer/Supplementary.java index 55b7cc7fdfb..0cc56cc3778 100644 --- a/test/jdk/java/lang/StringBuffer/Supplementary.java +++ b/test/jdk/java/lang/StringBuffer/Supplementary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2020, 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 @@ -28,6 +28,8 @@ * @summary Unit tests for supplementary character support (JSR-204) */ +import java.util.HexFormat; + public class Supplementary { public static void main(String[] args) { @@ -401,15 +403,13 @@ public class Supplementary { } private static String toHexString(String s) { - StringBuffer sb = new StringBuffer(); + HexFormat format = HexFormat.of(); + StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); sb.append(" 0x"); - if (c < 0x10) sb.append('0'); - if (c < 0x100) sb.append('0'); - if (c < 0x1000) sb.append('0'); - sb.append(Integer.toHexString(c)); + sb.append(format.toHexDigits(c)); } sb.append(' '); return sb.toString(); diff --git a/test/jdk/java/util/HexFormat/HexFormatTest.java b/test/jdk/java/util/HexFormat/HexFormatTest.java new file mode 100644 index 00000000000..0d10a34c8c0 --- /dev/null +++ b/test/jdk/java/util/HexFormat/HexFormatTest.java @@ -0,0 +1,756 @@ +/* + * Copyright (c) 2020, 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. + * + * 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. + */ + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Locale; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +/* + * @test + * @summary Check HexFormat formatting and parsing + * @run testng/othervm -Xmx4G HexFormatTest + */ + +@Test +public class HexFormatTest { + static final Class NPE = NullPointerException.class; + + @DataProvider(name = "HexFormattersParsers") + Object[][] hexFormattersParsers() { + return new Object[][]{ + {"", "", "", true, + HexFormat.of().withUpperCase()}, + {", ", "#", "L", false, + HexFormat.ofDelimiter(", ").withPrefix("#").withSuffix("L")}, + {"", "", "", false, + HexFormat.of().withPrefix("").withSuffix("")}, + {".", "", "", false, + HexFormat.ofDelimiter(".").withPrefix("").withSuffix("")}, + {", ", "0x", "", true, + HexFormat.ofDelimiter(", ").withUpperCase().withPrefix("0x")}, + {"\u0202", "\u0203", "\u0204", false, + HexFormat.ofDelimiter("\u0202").withPrefix("\u0203").withSuffix("\u0204")}, + {"\u0202", "", "", false, + HexFormat.ofDelimiter("\u0202")}, + + }; + } + + @DataProvider(name = "HexStringsThrowing") + Object[][] HexStringsThrowing() { + return new Object[][]{ + {"0", ":", "", ""}, // wrong string length + {"01:", ":", "", ""}, // wrong string length + {"01:0", ":", "", ""}, // wrong string length + {"0", ",", "", ""}, // wrong length and separator + {"01:", ",", "", ""}, // wrong length and separator + {"01:0", ",", "", ""}, // wrong length and separator + {"01:00", ",", "", ""}, // wrong separator + {"00]", ",", "[", "]"}, // missing prefix + {"[00", ",", "[", "]"}, // missing suffix + {"]", ",", "[", "]"}, // missing prefix + {"[", ",", "[", "]"}, // missing suffix + {"00", ",", "abc", ""}, // Prefix longer than string + {"01", ",", "", "def"}, // Suffix longer than string + {"abc00,", ",", "abc", ""}, // Prefix and delim but not another value + {"01def,", ",", "", "def"}, // Suffix and delim but not another value + }; + } + + @DataProvider(name = "BadBytesThrowing") + Object[][] badBytesThrowing() { + return new Object[][]{ + {new byte[1], 0, 2}, // bad toIndex + {new byte[1], 1, 2}, // bad fromIndex + toIndex + {new byte[1], -1, 2}, // bad fromIndex + {new byte[1], -1, 1}, // bad fromIndex + {new byte[1], 0, -1}, // bad toIndex + {new byte[1], 1, -1}, // bad toIndex + }; + } + + @DataProvider(name = "BadParseHexThrowing") + Object[][] badParseHexThrowing() { + return new Object[][]{ + {"a", 0, 2, IndexOutOfBoundsException.class}, // bad toIndex + {"b", 1, 2, IndexOutOfBoundsException.class}, // bad toIndex + {"a", -1, 2, IndexOutOfBoundsException.class}, // bad fromIndex + {"b", -1, 1, IndexOutOfBoundsException.class}, // bad fromIndex + {"a", 0, -1, IndexOutOfBoundsException.class}, // bad toIndex + {"b", 1, -1, IndexOutOfBoundsException.class}, // bad fromIndex + toIndex + {"76543210", 0, 7, IllegalArgumentException.class}, // odd number of digits + {"zz00", 0, 4, IllegalArgumentException.class}, // non-hex digits + {"00zz", 0, 4, IllegalArgumentException.class}, // non-hex digits + }; + } + + @DataProvider(name = "BadFromHexDigitsThrowing") + Object[][] badHexDigitsThrowing() { + return new Object[][]{ + {"a", 0, 2, IndexOutOfBoundsException.class}, // bad toIndex + {"b", 1, 2, IndexOutOfBoundsException.class}, // bad fromIndex + toIndex + {"a", -1, 2, IndexOutOfBoundsException.class}, // bad toIndex + {"b", -1, 1, IndexOutOfBoundsException.class}, // bad fromIndex + toIndex + {"a", 0, -1, IndexOutOfBoundsException.class}, // bad toIndex + {"b", 1, -1, IndexOutOfBoundsException.class}, // bad fromIndex + toIndex + }; + } + + static byte[] genBytes(int origin, int len) { + byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) + bytes[i] = (byte) (origin + i); + return bytes; + } + + @Test + static void testToHex() { + HexFormat hex = HexFormat.of(); + for (int i = 0; i < 32; i++) { + char c = hex.toLowHexDigit((byte)i); + String expected = Integer.toHexString(i & 0xf); + assertEquals(c, expected.charAt(0), "toHex formatting"); + } + } + + @Test + static void testToHexDigits() { + HexFormat hex = HexFormat.of(); + for (int i = 0; i < 256; i++) { + String actual = hex.toHexDigits((byte)i); + int expected = hex.fromHexDigits(actual); + assertEquals(expected, i, "fromHexDigits"); + assertEquals(actual.charAt(0), hex.toHighHexDigit((byte)i), + "first char mismatch"); + assertEquals(actual.charAt(1), hex.toLowHexDigit((byte)i), + "second char mismatch"); + } + } + + @Test + static void testIsHexDigit() { + HexFormat hex = HexFormat.of(); + for (int i = 0; i < 0x3ff; i++) { + boolean actual = hex.isHexDigit(i); + boolean expected = Character.digit(i, 16) >= 0; + assertEquals(actual, expected, "isHexDigit: " + i); + } + } + + @Test + static void testFromHexDigit() { + HexFormat hex = HexFormat.of(); + String chars = "0123456789ABCDEF0123456789abcdef"; + for (int i = 0; i < chars.length(); i++) { + int v = hex.fromHexDigit(chars.charAt(i)); + assertEquals(v, i & 0xf, "fromHex decode"); + } + } + + @Test + static void testFromHexInvalid() { + HexFormat hex = HexFormat.of(); + for (int i = 0; i < 65536; i++) { + char ch = (char)i; + if (ch > 0xff || Character.digit(ch, 16) < 0) { + assertFalse(hex.isHexDigit(ch), "isHexDigit incorrect for '" + ch + "' = " + i); + expectThrows(NumberFormatException.class, + () -> hex.fromHexDigit(ch)); + + } + } + } + + @Test + static void testAppendHexByteWithStringBuilder() { + HexFormat hex = HexFormat.of(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 256; i++) { + sb.setLength(0); + StringBuilder sb1 = hex.toHexDigits(sb, (byte)i); + assertSame(sb1, sb, "toHexDigits returned different StringBuilder"); + assertEquals(sb.length(), 2, "wrong length after append: " + i); + assertEquals(sb.charAt(0), hex.toHighHexDigit((byte)i), "MSB converted wrong"); + assertEquals(sb.charAt(1), hex.toLowHexDigit((byte)i), "LSB converted wrong"); + + assertEquals(hex.fromHexDigits(sb), i, "hex.format(sb, byte) wrong"); + } + } + + @Test + static void testAppendHexByteWithCharBuffer() { + HexFormat hex = HexFormat.of(); + CharBuffer cb = CharBuffer.allocate(256); + for (int i = 1; i <= 128; i++) { + CharBuffer cb1 = hex.toHexDigits(cb, (byte)i); + assertTrue(cb1 == cb); + assertEquals(cb.position(), i * 2); + } + assertEquals(cb.remaining(), 0); + } + + @Test + static void testAppendHexByteWithCharArrayWriter() { + HexFormat hex = HexFormat.of(); + CharArrayWriter caw = new CharArrayWriter(); + for (int i = 1; i <= 128; i++) { + CharArrayWriter caw1 = hex.toHexDigits(caw, (byte)i); + assertTrue(caw1 == caw); + assertEquals(caw.size(), i * 2); + } + } + + @Test + static void testFromHexPairInvalid() { + HexFormat hex = HexFormat.of(); + + // An assortment of invalid characters + String chars = "-0--0-"; + for (int i = 0; i < chars.length(); i += 2) { + final int ndx = i; + Throwable ex = expectThrows(NumberFormatException.class, + () -> hex.fromHexDigits(chars.subSequence(ndx, ndx+2))); + System.out.println(ex); + } + } + + @Test(dataProvider = "HexStringsThrowing") + static void testToBytesThrowing(String value, String sep, String prefix, String suffix) { + HexFormat hex = HexFormat.ofDelimiter(sep).withPrefix(prefix).withSuffix(suffix); + Throwable ex = expectThrows(IllegalArgumentException.class, + () -> { + byte[] v = hex.parseHex(value); + System.out.println("str: " + value + ", actual: " + v + ", bytes: " + + Arrays.toString(v)); + }); + System.out.println("ex: " + ex); + } + + @Test + static void testFactoryNPE() { + assertThrows(NPE, () -> HexFormat.ofDelimiter(null)); + assertThrows(NPE, () -> HexFormat.of().withDelimiter(null)); + assertThrows(NPE, () -> HexFormat.of().withPrefix(null)); + assertThrows(NPE, () -> HexFormat.of().withSuffix(null)); + } + + @Test + static void testFormatHexNPE() { + assertThrows(NPE, () -> HexFormat.of().formatHex(null)); + assertThrows(NPE, () -> HexFormat.of().formatHex(null, 0, 1)); + assertThrows(NPE, () -> HexFormat.of().formatHex(null, null)); + assertThrows(NPE, () -> HexFormat.of().formatHex(null, null, 0, 0)); + StringBuilder sb = new StringBuilder(); + assertThrows(NPE, () -> HexFormat.of().formatHex(sb, null)); + assertThrows(NPE, () -> HexFormat.of().formatHex(sb, null, 0, 1)); + } + + @Test + static void testParseHexNPE() { + assertThrows(NPE, () -> HexFormat.of().parseHex(null)); + assertThrows(NPE, () -> HexFormat.of().parseHex((String)null, 0, 0)); + assertThrows(NPE, () -> HexFormat.of().parseHex((char[])null, 0, 0)); + } + + @Test + static void testFromHexNPE() { + assertThrows(NPE, () -> HexFormat.of().fromHexDigits(null)); + assertThrows(NPE, () -> HexFormat.of().fromHexDigits(null, 0, 0)); + assertThrows(NPE, () -> HexFormat.of().fromHexDigitsToLong(null)); + assertThrows(NPE, () -> HexFormat.of().fromHexDigitsToLong(null, 0, 0)); + } + + @Test + static void testToHexDigitsNPE() { + assertThrows(NPE, () -> HexFormat.of().toHexDigits(null, (byte)0)); + } + + @Test(dataProvider = "BadParseHexThrowing") + static void badParseHex(String string, int offset, int length, + Class exClass) { + assertThrows(exClass, + () -> HexFormat.of().parseHex(string, offset, length)); + char[] chars = string.toCharArray(); + assertThrows(exClass, + () -> HexFormat.of().parseHex(chars, offset, length)); + } + + @Test(dataProvider = "BadFromHexDigitsThrowing") + static void badFromHexDigits(String string, int fromIndex, int toIndex, + Class exClass) { + assertThrows(exClass, + () -> HexFormat.of().fromHexDigits(string, fromIndex, toIndex)); + assertThrows(exClass, + () -> HexFormat.of().fromHexDigitsToLong(string, fromIndex, toIndex)); + } + + // Verify IAE for strings that are too long for the target primitive type + // or the number of requested digits is too large. + @Test + static void wrongNumberDigits() { + assertThrows(IllegalArgumentException.class, + () -> HexFormat.of().fromHexDigits("9876543210")); + assertThrows(IllegalArgumentException.class, + () -> HexFormat.of().fromHexDigits("9876543210", 0, 9)); + assertThrows(IllegalArgumentException.class, + () -> HexFormat.of().fromHexDigitsToLong("98765432109876543210")); + assertThrows(IllegalArgumentException.class, + () -> HexFormat.of().fromHexDigitsToLong("98765432109876543210", 0, 17)); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatter(String delimiter, String prefix, String suffix, + boolean uppercase, + HexFormat hex) { + byte[] expected = genBytes('A', 15); + String res = hex.formatHex(expected); + assertTrue(res.startsWith(prefix), "Prefix not found"); + assertTrue(res.endsWith(suffix), "Suffix not found"); + int expectedLen = expected.length * (2 + prefix.length() + + delimiter.length() + suffix.length()) - delimiter.length(); + assertEquals(res.length(), expectedLen, "String length"); + + if (expected.length > 1) { + // check prefix and suffix is present for each hex pair + for (int i = 0; i < expected.length; i++) { + int valueChars = prefix.length() + 2 + suffix.length(); + int offset = i * (valueChars + delimiter.length()); + String value = res.substring(offset, offset + valueChars); + assertTrue(value.startsWith(prefix), "wrong prefix"); + assertTrue(value.endsWith(suffix), "wrong suffix"); + + // Check case of digits + String cc = value.substring(prefix.length(), prefix.length() + 2); + assertEquals(cc, + (uppercase) ? cc.toUpperCase(Locale.ROOT) : cc.toLowerCase(Locale.ROOT), + "Case mismatch"); + if (i < expected.length - 1 && !delimiter.isEmpty()) { + // Check the delimiter is present for each pair except the last + assertEquals(res.substring(offset + valueChars, + offset + valueChars + delimiter.length()), delimiter); + } + } + } + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatHexString(String unused1, String unused2, String unused3, + boolean unused4, HexFormat hex) { + byte[] expected = genBytes('A', 15); + String s = hex.formatHex(expected); + System.out.println(" formatted: " + s); + + byte[] actual = hex.parseHex(s); + System.out.println(" parsed as: " + Arrays.toString(actual)); + int mismatch = Arrays.mismatch(expected, actual); + assertEquals(actual, expected, "format/parse cycle failed, mismatch: " + mismatch); + } + + @Test(dataProvider="HexFormattersParsers") + static void testParseHexStringRange(String delimiter, String prefix, String suffix, + boolean unused4, HexFormat hex) { + byte[] expected = genBytes('A', 15); + String s = hex.formatHex(expected); + + // Parse values 2, 3, 4 from the generated string + int low = 2; + int high = 5; + int stride = prefix.length() + 2 + suffix.length() + delimiter.length(); + System.out.println(" formatted subrange: " + + s.substring(low * stride, high * stride - delimiter.length())); + byte[] actual = hex.parseHex(s, low * stride, + high * stride - delimiter.length()); + System.out.println(" parsed as: " + Arrays.toString(actual)); + + assertEquals(actual.length, (high - low), "array length"); + int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low); + assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch); + } + + @Test(dataProvider="HexFormattersParsers") + static void testParseHexEmptyString(String delimiter, String prefix, String suffix, + boolean unused4, HexFormat hex) { + byte[] actual = hex.parseHex(""); + assertEquals(actual.length, 0, "empty string parse"); + actual = hex.parseHex("abc", 0, 0); + assertEquals(actual.length, 0, "empty string range parse"); + actual = hex.parseHex(new char[1], 0, 0); + assertEquals(actual.length, 0, "empty char array subrange empty parse"); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatHexRangeString(String unused1, String unused2, String unused3, + boolean unused4, HexFormat hex) { + byte[] expected = genBytes('A', 15); + int low = 1; + int high = expected.length - 2; + String s = hex.formatHex(expected, low, high); + System.out.println(" formatted: " + s); + + byte[] actual = hex.parseHex(s); + System.out.println(" parsed as: " + Arrays.toString(actual)); + int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low); + assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatHexAppendable(String unused1, String unused2, String unused3, + boolean unused4, HexFormat hex) { + byte[] expected = genBytes('A', 15); + StringBuilder sb = new StringBuilder(); + StringBuilder s = hex.formatHex(sb, expected); + assertEquals(s, sb, "formatHex returned unknown StringBuilder"); + System.out.println(" formatted: " + s); + + byte[] actual = hex.parseHex(s.toString()); + System.out.println(" parsed as: " + Arrays.toString(actual)); + int mismatch = Arrays.mismatch(expected, actual); + assertEquals(actual, expected, "format/parse cycle failed, mismatch: " + mismatch); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatHexRangeAppendable(String unused1, String unused2, String unused3, + boolean unused4, HexFormat hex) { + byte[] expected = genBytes('A', 15); + int low = 1; + int high = expected.length - 2; + StringBuilder sb = new StringBuilder(); + StringBuilder s = hex.formatHex(sb, expected, low, high); + assertEquals(s, sb, "formatHex returned unknown StringBuilder"); + System.out.println(" formatted: " + s); + + byte[] actual = hex.parseHex(s.toString()); + System.out.println(" parsed as: " + Arrays.toString(actual)); + byte[] sub = Arrays.copyOfRange(expected, low, high); + System.out.println("actual: " + Arrays.toString(actual)); + System.out.println("sub : " + Arrays.toString(sub)); + int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low); + + assertEquals(actual, sub, "format/parse cycle failed, mismatch: " + mismatch); + assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatHexCharArray(String unused1, String unused2, String unused3, + boolean unused4, HexFormat hex) { + byte[] expected = genBytes('A', 15); + String s = hex.formatHex(expected); + System.out.println(" formatted: " + s); + + char[] chars = s.toCharArray(); + byte[] actual = hex.parseHex(chars, 0, chars.length); + System.out.println(" parsed as: " + Arrays.toString(actual)); + int mismatch = Arrays.mismatch(expected, actual); + assertEquals(actual, expected, "format/parse cycle failed, mismatch: " + mismatch); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatHexCharArrayIndexed(String delimiter, String prefix, String suffix, + boolean unused4, HexFormat hex) { + byte[] expected = genBytes('A', 15); + String s = hex.formatHex(expected); + System.out.println(" formatted: " + s); + + + // Parse values 2, 3, 4 from the generated string + int low = 2; + int high = 5; + int stride = prefix.length() + 2 + suffix.length() + delimiter.length(); + System.out.println(" formatted subrange: " + + s.substring(low * stride, high * stride - delimiter.length())); + char[] chars = s.toCharArray(); + byte[] actual = hex.parseHex(chars, low * stride, + high * stride - delimiter.length()); + System.out.println(" parsed as: " + Arrays.toString(actual)); + + assertEquals(actual.length, (high - low), "array length"); + int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low); + assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatterToString(String delimiter, String prefix, String suffix, + boolean uppercase, + HexFormat hex) { + String actual = String.format( + "uppercase: %s, delimiter: \"%s\", prefix: \"%s\", suffix: \"%s\"", + uppercase, escapeNL(delimiter), escapeNL(prefix), escapeNL(suffix)); + System.out.println(" hex: " + actual); + assertEquals(actual, hex.toString(), "Formatter toString mismatch"); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatterParameterMethods(String delimiter, String prefix, String suffix, + boolean uppercase, + HexFormat hex) { + assertEquals(hex.delimiter(), delimiter); + assertEquals(hex.prefix(), prefix); + assertEquals(hex.suffix(), suffix); + assertEquals(hex.isUpperCase(), uppercase); + } + + @Test(dataProvider="HexFormattersParsers") + static void testFormatterTestEquals(String delimiter, String prefix, String suffix, + boolean uppercase, + HexFormat expected) { + HexFormat actual = HexFormat.of() + .withDelimiter(delimiter) + .withPrefix(prefix) + .withSuffix(suffix); + actual = uppercase ? actual.withUpperCase() : actual.withLowerCase(); + + assertEquals(actual.delimiter(), delimiter, "delimiter"); + assertEquals(actual.prefix(), prefix, "prefix"); + assertEquals(actual.suffix(), suffix, "suffix"); + assertEquals(actual.isUpperCase(), uppercase, "uppercase"); + assertTrue(actual.equals(expected), "equals method"); + assertEquals(actual.hashCode(), expected.hashCode(), "hashCode"); + + assertTrue(actual.equals(actual)); // equals self + assertFalse(actual.equals(null)); // never equals null + } + + @Test(dataProvider="HexFormattersParsers") + static void testZeroLength(String delimiter, String prefix, String suffix, boolean uppercase, + HexFormat hex) { + // Test formatting of zero length byte arrays, should produce no output + StringBuilder sb = new StringBuilder(); + assertEquals(hex.formatHex(new byte[0]), "", "Zero length"); + assertEquals(hex.formatHex(new byte[0], 0, 0), "", "Zero length"); + + hex.formatHex(sb, new byte[0]); + assertEquals(sb.length(), 0, "length should not change"); + hex.formatHex(sb, new byte[0], 0, 0); + assertEquals(sb.length(), 0, "length should not change"); + + } + private static String escapeNL(String string) { + return string.replace("\n", "\\n") + .replace("\r", "\\r"); + } + + @Test + static void testfromHexDigitsToInt() { + HexFormat hex = HexFormat.of(); + + String allHex = "76543210"; + final int orig = 0x76543210; + for (int digits = 0; digits <= 8; digits++) { + String s = hex.toHexDigits(orig, digits); + long actual = hex.fromHexDigits(s, 0, digits); + System.out.printf(" digits: %2d, formatted: \"%s\", parsed as: 0x%08x%n", + digits, s, actual); + assertEquals(s, allHex.substring(8 - digits, 8)); + long expected = (digits < 8) ? orig & ~(0xffffffff << (4 * digits)) : orig; + assertEquals(actual, expected); + } + } + + @Test + static void testfromHexDigitsToLong() { + HexFormat hex = HexFormat.of(); + + String allHex = "fedcba9876543210"; + final long orig = 0xfedcba9876543210L; + for (int digits = 0; digits <= 16; digits++) { + String s = hex.toHexDigits(orig, digits); + long actual = hex.fromHexDigitsToLong(s, 0, digits); + System.out.printf(" digits: %2d, formatted: \"%s\", parsed as: 0x%016xL%n", + digits, s, actual); + assertEquals(s, allHex.substring(16 - digits, 16)); + long expected = (digits < 16) ? orig & ~(0xffffffffffffffffL << (4 * digits)) : orig; + assertEquals(actual, expected); + } + } + + @Test + static void testToHexDigitsLong() { + HexFormat hex = HexFormat.of(); + + String allHex = "fedcba9876543210"; + final long expected = 0xfedcba9876543210L; + String s = hex.toHexDigits(expected); + long actual = hex.fromHexDigitsToLong(s); + System.out.printf(" formatted: \"%s\", parsed as: 0x%016xL%n", s, actual); + assertEquals(s, allHex); + assertEquals(actual, expected); + } + + @Test(dataProvider="HexFormattersParsers") + static void testIOException(String delimiter, String prefix, String suffix, boolean uppercase, + HexFormat hex) { + Appendable throwingAppendable = new ThrowingAppendable(); + assertThrows(UncheckedIOException.class, + () -> hex.formatHex(throwingAppendable, new byte[1])); + assertThrows(UncheckedIOException.class, + () -> hex.formatHex(throwingAppendable, new byte[1], 0, 1)); + assertThrows(UncheckedIOException.class, + () -> hex.toHexDigits(throwingAppendable, (byte)1)); + } + + @Test(dataProvider="HexFormattersParsers") + static void testOOME(String delimiter, String prefix, String suffix, boolean uppercase, + HexFormat hex) { + // compute the size of byte array that will exceed the buffer + long valueChars = prefix.length() + 2 + suffix.length(); + long stride = valueChars + delimiter.length(); + long max = Integer.MAX_VALUE & 0xFFFFFFFFL; + long len = max / stride; + long remainder = max - ((len - 1) * stride); + if (remainder > valueChars) { + len++; + remainder -= valueChars; + } + try { + byte[] bytes = new byte[(int) len]; + Throwable ex = expectThrows(OutOfMemoryError.class, + () -> hex.formatHex(bytes)); + System.out.println("ex: " + ex); + } catch (OutOfMemoryError oome) { + System.out.printf("OOME: total mem: %08x, free mem: %08x, max mem: %08x%n", + Runtime.getRuntime().totalMemory(), + Runtime.getRuntime().freeMemory(), + Runtime.getRuntime().maxMemory()); + throw oome; + } + + } + + /** + * Example code from the HexFormat javadoc. + * Showing simple usage of the API using "assert" to express the correct results + * when shown in the javadoc. + * The additional TestNG asserts verify the correctness of the same code. + */ + @Test + private static void samples() { + { + // Primitive formatting and parsing. + HexFormat hex = HexFormat.of(); + + byte b = 127; + String byteStr = hex.toHexDigits(b); + System.out.println(" " + byteStr); + + byte byteVal = (byte)hex.fromHexDigits(byteStr); + assert(byteStr.equals("7f")); + assert(b == byteVal); + assertTrue(byteStr.equals("7f")); + assertTrue(b == byteVal); + + + char c = 'A'; + String charStr = hex.toHexDigits(c); + System.out.println(" " + charStr); + int charVal = hex.fromHexDigits(charStr); + assert(c == charVal); + assertTrue(c == charVal); + + int i = 12345; + String intStr = hex.toHexDigits(i); + System.out.println(" " + intStr); + int intVal = hex.fromHexDigits(intStr); + assert(i == intVal); + assertTrue(i == intVal); + + long l = Long.MAX_VALUE; + String longStr = hex.toHexDigits(l, 16); + long longVal = hex.fromHexDigitsToLong(longStr, 0, 16); + System.out.println(" " + longStr + ", " + longVal); + assert(l == longVal); + assertTrue(l == longVal); + } + + { + // RFC 4752 Fingerprint + HexFormat formatFingerprint = HexFormat.ofDelimiter(":").withUpperCase(); + byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127}; + String str = formatFingerprint.formatHex(bytes); + System.out.println(" Formatted: " + str); + + byte[] parsed = formatFingerprint.parseHex(str); + System.out.println(" Parsed: " + Arrays.toString(parsed)); + assert(Arrays.equals(bytes, parsed)); + assertTrue(Arrays.equals(bytes, parsed)); + } + + { + // Comma separated formatting + HexFormat commaFormat = HexFormat.ofDelimiter(","); + byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127}; + String str = commaFormat.formatHex(bytes); + System.out.println(" Formatted: " + str); + + byte[] parsed = commaFormat.parseHex(str); + System.out.println(" Parsed: " + Arrays.toString(parsed)); + assert(Arrays.equals(bytes, parsed)); + assertTrue(Arrays.equals(bytes, parsed)); + } + { + // Text formatting + HexFormat commaFormat = HexFormat.ofDelimiter(", ").withPrefix("#"); + byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127}; + String str = commaFormat.formatHex(bytes); + System.out.println(" Formatted: " + str); + + byte[] parsed = commaFormat.parseHex(str); + System.out.println(" Parsed: " + Arrays.toString(parsed)); + assert(Arrays.equals(bytes, parsed)); + assertTrue(Arrays.equals(bytes, parsed)); + } + } + + /** + * A test implementation of Appendable that throws IOException on all methods. + */ + static class ThrowingAppendable implements Appendable { + @Override + public Appendable append(CharSequence csq) throws IOException { + throw new IOException(".append(CharSequence) always throws"); + } + + @Override + public Appendable append(CharSequence csq, int start, int end) throws IOException { + throw new IOException(".append(CharSequence, start, end) always throws"); + } + + @Override + public Appendable append(char c) throws IOException { + throw new IOException(".append(char) always throws"); + } + } +} diff --git a/test/jdk/java/util/Locale/ThaiGov.java b/test/jdk/java/util/Locale/ThaiGov.java index 5bef2afde2b..25bf026e5ea 100644 --- a/test/jdk/java/util/Locale/ThaiGov.java +++ b/test/jdk/java/util/Locale/ThaiGov.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2020, 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 @@ -33,36 +33,11 @@ import java.text.*; public class ThaiGov { - char[] hex = {'0', '1', '2', '3', - '4', '5', '6', '7', - '8', '9', 'A', 'B', - 'C', 'D', 'E', 'F'}; - ThaiGov() { System.out.println("ThaiGov locale test..."); } - String toHex(String str) { - StringBuffer buff = new StringBuffer(); - int y=0; - for(int x=0; x < str.length(); ++x) { - buff.append("\\u"); - buff.append(toHex(str.charAt(x))); - } - return buff.toString(); - } - - String toHex(char ch) { - StringBuffer buff = new StringBuffer(); - buff.append(hex[ch>>12]); - buff.append(hex[(ch>>8) & 0x0F]); - buff.append(hex[(ch>>4) & 0x0F]); - buff.append(hex[ch & 0x0F]); - return buff.toString(); - } - - void numberTest() throws RuntimeException { final String strExpected = "\u0E51\u0E52\u002C\u0E53\u0E54\u0E55\u002C\u0E56\u0E57\u0E58\u002E\u0E52\u0E53\u0E54"; final double value = 12345678.234;