From fcf8752f87aebbd5fa729e042a1bfa48882972da Mon Sep 17 00:00:00 2001 From: Fredrik Kjellberg Date: Wed, 18 Jan 2023 20:58:28 +0100 Subject: [IO-784] Add support for Appendable to HexDump util (#418) * Add support for Appendable to HexDump util * Added since annotations and some minor code cleanup * Remove flush call * Add test to verify that OutputStream is not closed by the dump method * Use ThrowOnCloseOutputStream to make sure that the output stream is not closed --- src/main/java/org/apache/commons/io/HexDump.java | 103 +++++++++++++++++---- .../java/org/apache/commons/io/HexDumpTest.java | 70 +++++++++++++- 2 files changed, 155 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/main/java/org/apache/commons/io/HexDump.java b/src/main/java/org/apache/commons/io/HexDump.java index 5482b851..0360a494 100644 --- a/src/main/java/org/apache/commons/io/HexDump.java +++ b/src/main/java/org/apache/commons/io/HexDump.java @@ -18,9 +18,12 @@ package org.apache.commons.io; import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.Objects; +import org.apache.commons.io.output.CloseShieldOutputStream; + /** * Dumps data in hexadecimal format. *

@@ -53,7 +56,28 @@ public class HexDump { }; /** - * Dumps an array of bytes to an OutputStream. The output is formatted + * Dumps an array of bytes to an Appendable. The output is formatted + * for human inspection, with a hexadecimal offset followed by the + * hexadecimal values of the next 16 bytes of data and the printable ASCII + * characters (if any) that those bytes represent printed per each line + * of output. + * + * @param data the byte array to be dumped + * @param appendable the Appendable to which the data is to be written + * + * @throws IOException is thrown if anything goes wrong writing + * the data to appendable + * @throws NullPointerException if the output appendable is null + * + * @since 2.12.0 + */ + public static void dump(final byte[] data, final Appendable appendable) + throws IOException { + dump(data, 0, appendable, 0, data.length); + } + + /** + * Dumps an array of bytes to an Appendable. The output is formatted * for human inspection, with a hexadecimal offset followed by the * hexadecimal values of the next 16 bytes of data and the printable ASCII * characters (if any) that those bytes represent printed per each line @@ -66,27 +90,26 @@ public class HexDump { * at the beginning of each line indicates where in that larger entity * the first byte on that line is located. *

- *

- * All bytes between the given index (inclusive) and the end of the - * data array are dumped. - *

* * @param data the byte array to be dumped * @param offset offset of the byte array within a larger entity - * @param stream the OutputStream to which the data is to be - * written + * @param appendable the Appendable to which the data is to be written * @param index initial index into the byte array + * @param length number of bytes to dump from the array * * @throws IOException is thrown if anything goes wrong writing - * the data to stream - * @throws ArrayIndexOutOfBoundsException if the index is + * the data to appendable + * @throws ArrayIndexOutOfBoundsException if the index or length is * outside the data array's bounds - * @throws NullPointerException if the output stream is null + * @throws NullPointerException if the output appendable is null + * + * @since 2.12.0 */ public static void dump(final byte[] data, final long offset, - final OutputStream stream, final int index) + final Appendable appendable, final int index, + final int length) throws IOException, ArrayIndexOutOfBoundsException { - Objects.requireNonNull(stream, "stream"); + Objects.requireNonNull(appendable, "appendable"); if (index < 0 || index >= data.length) { throw new ArrayIndexOutOfBoundsException( "illegal index: " + index + " into array of length " @@ -95,8 +118,15 @@ public class HexDump { long display_offset = offset + index; final StringBuilder buffer = new StringBuilder(74); - for (int j = index; j < data.length; j += 16) { - int chars_read = data.length - j; + // TODO Use Objects.checkFromIndexSize(index, length, data.length) when upgrading to JDK9 + if (length < 0 || (index + length) > data.length) { + throw new ArrayIndexOutOfBoundsException(String.format("Range [%s, % 16) { chars_read = 16; @@ -118,14 +148,53 @@ public class HexDump { } } buffer.append(System.lineSeparator()); - // make explicit the dependency on the default encoding - stream.write(buffer.toString().getBytes(Charset.defaultCharset())); - stream.flush(); + appendable.append(buffer); buffer.setLength(0); display_offset += chars_read; } } + /** + * Dumps an array of bytes to an OutputStream. The output is formatted + * for human inspection, with a hexadecimal offset followed by the + * hexadecimal values of the next 16 bytes of data and the printable ASCII + * characters (if any) that those bytes represent printed per each line + * of output. + *

+ * The offset argument specifies the start offset of the data array + * within a larger entity like a file or an incoming stream. For example, + * if the data array contains the third kibibyte of a file, then the + * offset argument should be set to 2048. The offset value printed + * at the beginning of each line indicates where in that larger entity + * the first byte on that line is located. + *

+ *

+ * All bytes between the given index (inclusive) and the end of the + * data array are dumped. + *

+ * + * @param data the byte array to be dumped + * @param offset offset of the byte array within a larger entity + * @param stream the OutputStream to which the data is to be + * written + * @param index initial index into the byte array + * + * @throws IOException is thrown if anything goes wrong writing + * the data to stream + * @throws ArrayIndexOutOfBoundsException if the index is + * outside the data array's bounds + * @throws NullPointerException if the output stream is null + */ + public static void dump(final byte[] data, final long offset, + final OutputStream stream, final int index) + throws IOException, ArrayIndexOutOfBoundsException { + Objects.requireNonNull(stream, "stream"); + + try (OutputStreamWriter out = new OutputStreamWriter(CloseShieldOutputStream.wrap(stream), Charset.defaultCharset())) { + dump(data, offset, out, index, data.length - index); + } + } + /** * Dumps a byte value into a StringBuilder. * diff --git a/src/test/java/org/apache/commons/io/HexDumpTest.java b/src/test/java/org/apache/commons/io/HexDumpTest.java index 6e8e0046..a7da7f15 100644 --- a/src/test/java/org/apache/commons/io/HexDumpTest.java +++ b/src/test/java/org/apache/commons/io/HexDumpTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.commons.io.test.ThrowOnCloseOutputStream; import org.junit.jupiter.api.Test; @@ -31,7 +32,71 @@ import org.junit.jupiter.api.Test; public class HexDumpTest { @Test - public void testDump() throws IOException { + public void testDumpAppendable() throws IOException { + final byte[] testArray = new byte[256]; + + for (int j = 0; j < 256; j++) { + testArray[j] = (byte) j; + } + + // verify proper behavior dumping the entire array + StringBuilder out = new StringBuilder(); + HexDump.dump(testArray, out); + assertEquals( + "00000000 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ................" + System.lineSeparator() + + "00000010 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F ................" + System.lineSeparator() + + "00000020 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F !\"#$%&'()*+,-./" + System.lineSeparator() + + "00000030 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 0123456789:;<=>?" + System.lineSeparator() + + "00000040 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F @ABCDEFGHIJKLMNO" + System.lineSeparator() + + "00000050 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F PQRSTUVWXYZ[\\]^_" + System.lineSeparator() + + "00000060 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F `abcdefghijklmno" + System.lineSeparator() + + "00000070 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F pqrstuvwxyz{|}~." + System.lineSeparator() + + "00000080 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F ................" + System.lineSeparator() + + "00000090 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F ................" + System.lineSeparator() + + "000000A0 A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF ................" + System.lineSeparator() + + "000000B0 B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF ................" + System.lineSeparator() + + "000000C0 C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF ................" + System.lineSeparator() + + "000000D0 D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF ................" + System.lineSeparator() + + "000000E0 E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF ................" + System.lineSeparator() + + "000000F0 F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................" + System.lineSeparator(), + out.toString()); + + // verify proper behavior with non-zero offset, non-zero index and length shorter than array size + out = new StringBuilder(); + HexDump.dump(testArray, 0x10000000, out, 0x28, 32); + assertEquals( + "10000028 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 ()*+,-./01234567" + System.lineSeparator() + + "10000038 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 89:;<=>?@ABCDEFG" + System.lineSeparator(), + out.toString()); + + // verify proper behavior with non-zero index and length shorter than array size + out = new StringBuilder(); + HexDump.dump(testArray, 0, out, 0x40, 24); + assertEquals( + "00000040 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F @ABCDEFGHIJKLMNO" + System.lineSeparator() + + "00000050 50 51 52 53 54 55 56 57 PQRSTUVW" + System.lineSeparator(), + out.toString()); + + // verify proper behavior with negative index + assertThrows(ArrayIndexOutOfBoundsException.class, () -> HexDump.dump(testArray, 0x10000000, new StringBuilder(), -1, testArray.length)); + + // verify proper behavior with index that is too large + assertThrows(ArrayIndexOutOfBoundsException.class, () -> HexDump.dump(testArray, 0x10000000, new StringBuilder(), testArray.length, testArray.length)); + + // verify proper behavior with length that is negative + assertThrows(ArrayIndexOutOfBoundsException.class, () -> HexDump.dump(testArray, 0, new StringBuilder(), 0, -1)); + + // verify proper behavior with length that is too large + final Exception exception = assertThrows(ArrayIndexOutOfBoundsException.class, () -> HexDump.dump(testArray, 0, new StringBuilder(), 1, + testArray.length)); + assertEquals("Range [1, 1 + 256) out of bounds for length 256", exception.getMessage()); + + // verify proper behavior with null appendable + assertThrows(NullPointerException.class, () -> HexDump.dump(testArray, 0x10000000, null, 0, testArray.length)); + } + + @Test + public void testDumpOutputStream() throws IOException { final byte[] testArray = new byte[256]; for (int j = 0; j < 256; j++) { @@ -189,6 +254,9 @@ public class HexDumpTest { // verify proper behavior with null stream assertThrows(NullPointerException.class, () -> HexDump.dump(testArray, 0x10000000, null, 0)); + + // verify output stream is not closed by the dump method + HexDump.dump(testArray, 0, new ThrowOnCloseOutputStream(new ByteArrayOutputStream()), 0); } private char toAscii(final int c) { -- cgit v1.2.3