View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.binary;
19  
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  
26  import java.io.ByteArrayInputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  
30  import org.apache.commons.io.IOUtils;
31  import org.junit.jupiter.api.Test;
32  
33  /**
34   * Tests {@link Base16InputStream}.
35   */
36  class Base16InputStreamTest {
37  
38      /**
39       * Decodes to {202, 254, 186, 190, 255, 255}
40       */
41      private static final String ENCODED_B16 = "CAFEBABEFFFF";
42  
43      private static final String STRING_FIXTURE = "Hello World";
44  
45      /**
46       * Tests skipping past the end of a stream.
47       *
48       * @throws IOException for some failure scenarios.
49       */
50      @Test
51      void testAvailable() throws IOException {
52          final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16));
53          try (Base16InputStream b16Stream = new Base16InputStream(ins)) {
54              assertEquals(1, b16Stream.available());
55              assertEquals(6, b16Stream.skip(10));
56              // End of stream reached
57              assertEquals(0, b16Stream.available());
58              assertEquals(-1, b16Stream.read());
59              assertEquals(-1, b16Stream.read());
60              assertEquals(0, b16Stream.available());
61          }
62      }
63  
64      /**
65       * Tests the Base16InputStream implementation against empty input.
66       *
67       * @throws IOException for some failure scenarios.
68       */
69      @Test
70      void testBase16EmptyInputStream() throws IOException {
71          final byte[] emptyEncoded = {};
72          final byte[] emptyDecoded = {};
73          testByteByByte(emptyEncoded, emptyDecoded);
74          testByChunk(emptyEncoded, emptyDecoded);
75      }
76  
77      /**
78       * Tests the Base16InputStream implementation.
79       *
80       * @throws IOException for some failure scenarios.
81       */
82      @Test
83      void testBase16InputStreamByChunk() throws IOException {
84          // Hello World test.
85          byte[] encoded = StringUtils.getBytesUtf8("48656C6C6F20576F726C64");
86          byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
87          testByChunk(encoded, decoded);
88          // Single Byte test.
89          encoded = StringUtils.getBytesUtf8("41");
90          decoded = new byte[] { (byte) 0x41 };
91          testByChunk(encoded, decoded);
92          // OpenSSL interop test.
93          encoded = StringUtils.getBytesUtf8(Base16TestData.ENCODED_UTF8_UPPERCASE);
94          decoded = BaseNTestData.DECODED;
95          testByChunk(encoded, decoded);
96          // test random data of sizes 0 through 150
97          final BaseNCodec codec = Base16.builder().setLowerCase(true).get();
98          for (int i = 0; i <= 150; i++) {
99              final byte[][] randomData = BaseNTestData.randomData(codec, i);
100             encoded = randomData[1];
101             decoded = randomData[0];
102             testByChunk(encoded, decoded, true);
103         }
104     }
105 
106     /**
107      * Tests the Base16InputStream implementation.
108      *
109      * @throws IOException for some failure scenarios.
110      */
111     @Test
112     void testBase16InputStreamByteByByte() throws IOException {
113         // Hello World test.
114         byte[] encoded = StringUtils.getBytesUtf8("48656C6C6F20576F726C64");
115         byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
116         testByteByByte(encoded, decoded);
117         // Single Byte test.
118         encoded = StringUtils.getBytesUtf8("41");
119         decoded = new byte[] { (byte) 0x41 };
120         testByteByByte(encoded, decoded);
121         // OpenSSL interop test.
122         encoded = StringUtils.getBytesUtf8(Base16TestData.ENCODED_UTF8_UPPERCASE);
123         decoded = BaseNTestData.DECODED;
124         testByteByByte(encoded, decoded);
125         // test random data of sizes 0 through 150
126         final BaseNCodec codec = Base16.builder().setLowerCase(true).get();
127         for (int i = 0; i <= 150; i++) {
128             final byte[][] randomData = BaseNTestData.randomData(codec, i);
129             encoded = randomData[1];
130             decoded = randomData[0];
131             testByteByByte(encoded, decoded, true);
132         }
133     }
134 
135     @Test
136     void testBuilder() {
137         assertNotNull(Base16InputStream.builder().getBaseNCodec());
138     }
139 
140     /**
141      * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
142      * ---[WRAP-WRAP-WRAP-etc...] --> decoded
143      * <p/>
144      * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16InputStream wraps itself in encode and decode mode over and over again.
145      *
146      * @param encoded Base16 encoded data
147      * @param decoded the data from above, but decoded
148      * @throws IOException Usually signifies a bug in the Base16 commons-codec implementation.
149      */
150     private void testByChunk(final byte[] encoded, final byte[] decoded) throws IOException {
151         testByChunk(encoded, decoded, false);
152     }
153 
154     /**
155      * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
156      * ---[WRAP-WRAP-WRAP-etc...] --> decoded
157      * <p/>
158      * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16InputStream wraps itself in encode and decode mode over and over again.
159      *
160      * @param encoded   Base16 encoded data
161      * @param decoded   the data from above, but decoded
162      * @param lowerCase if {@code true} then use a lower-case Base16 alphabet
163      * @throws IOException Usually signifies a bug in the Base16 commons-codec implementation.
164      */
165     private void testByChunk(final byte[] encoded, final byte[] decoded, final boolean lowerCase) throws IOException {
166         // Start with encode.
167         try (InputStream in = new Base16InputStream(new ByteArrayInputStream(decoded), true, lowerCase)) {
168             final byte[] output = IOUtils.toByteArray(in);
169             assertEquals(-1, in.read(), "EOF");
170             assertEquals(-1, in.read(), "Still EOF");
171             assertArrayEquals(encoded, output, "Streaming Base16 encode");
172         }
173         // Now let's try to decode.
174         try (InputStream in = new Base16InputStream(new ByteArrayInputStream(encoded), false, lowerCase)) {
175             final byte[] output = IOUtils.toByteArray(in);
176             assertEquals(-1, in.read(), "EOF");
177             assertEquals(-1, in.read(), "Still EOF");
178             assertArrayEquals(decoded, output, "Streaming Base16 decode");
179         }
180         // wrap encoder with decoder
181         try (InputStream in = new ByteArrayInputStream(decoded);
182                 InputStream inEncode = new Base16InputStream(in, true, lowerCase);
183                 InputStream inDecode = new Base16InputStream(inEncode, false, lowerCase)) {
184             final byte[] output = IOUtils.toByteArray(inDecode);
185             assertEquals(-1, inDecode.read(), "EOF");
186             assertEquals(-1, inDecode.read(), "Still EOF");
187             assertArrayEquals(decoded, output, "Streaming Base16 wrap-wrap!");
188         }
189     }
190 
191     /**
192      * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
193      * ---[WRAP-WRAP-WRAP-etc...] --> decoded
194      * <p/>
195      * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16InputStream wraps itself in encode and decode mode over and over again.
196      *
197      * @param encoded Base16 encoded data
198      * @param decoded the data from above, but decoded
199      * @throws IOException Usually signifies a bug in the Base16 commons-codec implementation.
200      */
201     private void testByteByByte(final byte[] encoded, final byte[] decoded) throws IOException {
202         testByteByByte(encoded, decoded, false);
203     }
204 
205     /**
206      * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
207      * ---[WRAP-WRAP-WRAP-etc...] --> decoded
208      * <p/>
209      * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16InputStream wraps itself in encode and decode mode over and over again.
210      *
211      * @param encoded   Base16 encoded data
212      * @param decoded   the data from above, but decoded
213      * @param lowerCase if {@code true} then use a lower-case Base16 alphabet
214      * @throws IOException Usually signifies a bug in the Base16 commons-codec implementation.
215      */
216     private void testByteByByte(final byte[] encoded, final byte[] decoded, final boolean lowerCase) throws IOException {
217         // Start with encode.
218         try (InputStream in = new Base16InputStream(new ByteArrayInputStream(decoded), true, lowerCase)) {
219             final byte[] output = new byte[encoded.length];
220             for (int i = 0; i < output.length; i++) {
221                 output[i] = (byte) in.read();
222             }
223             assertEquals(-1, in.read(), "EOF");
224             assertEquals(-1, in.read(), "Still EOF");
225             assertArrayEquals(encoded, output, "Streaming Base16 encode");
226         }
227         try (InputStream in = Base16InputStream.builder()
228                 .setByteArray(decoded)
229                 .setEncode(true).setBaseNCodec(Base16.builder().setLowerCase(lowerCase).get())
230                 .get()) {
231             final byte[] output = new byte[encoded.length];
232             for (int i = 0; i < output.length; i++) {
233                 output[i] = (byte) in.read();
234             }
235             assertEquals(-1, in.read(), "EOF");
236             assertEquals(-1, in.read(), "Still EOF");
237             assertArrayEquals(encoded, output, "Streaming Base16 encode");
238         }
239         // Now let's try to decode.
240         try (InputStream in = new Base16InputStream(new ByteArrayInputStream(encoded), false, lowerCase)) {
241             final byte[] output = new byte[decoded.length];
242             for (int i = 0; i < output.length; i++) {
243                 output[i] = (byte) in.read();
244             }
245             assertEquals(-1, in.read(), "EOF");
246             assertEquals(-1, in.read(), "Still EOF");
247             assertArrayEquals(decoded, output, "Streaming Base16 decode");
248         }
249         // wrap encoder with decoder
250         try (InputStream in = new ByteArrayInputStream(decoded);
251                 InputStream inEncode = new Base16InputStream(in, true, lowerCase);
252                 InputStream inDecode = new Base16InputStream(inEncode, false, lowerCase)) {
253             final byte[] output = new byte[decoded.length];
254             for (int i = 0; i < output.length; i++) {
255                 output[i] = (byte) inDecode.read();
256             }
257             assertEquals(-1, inDecode.read(), "EOF");
258             assertEquals(-1, inDecode.read(), "Still EOF");
259             assertArrayEquals(decoded, output, "Streaming Base16 wrap-wrap!");
260         }
261     }
262 
263     /**
264      * Tests markSupported.
265      *
266      * @throws IOException for some failure scenarios.
267      */
268     @Test
269     void testMarkSupported() throws IOException {
270         final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
271         final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
272         try (Base16InputStream in = new Base16InputStream(bin, true)) {
273             // Always returns false for now.
274             assertFalse(in.markSupported(), "Base16InputStream.markSupported() is false");
275         }
276     }
277 
278     /**
279      * Tests read returning 0
280      *
281      * @throws IOException for some failure scenarios.
282      */
283     @Test
284     void testRead0() throws IOException {
285         final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
286         final byte[] buf = new byte[1024];
287         int bytesRead = 0;
288         final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
289         try (Base16InputStream in = new Base16InputStream(bin, true)) {
290             bytesRead = in.read(buf, 0, 0);
291             assertEquals(0, bytesRead, "Base16InputStream.read(buf, 0, 0) returns 0");
292         }
293     }
294 
295     /**
296      * Tests read with null.
297      *
298      * @throws IOException for some failure scenarios.
299      */
300     @Test
301     void testReadNull() throws IOException {
302         final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
303         final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
304         try (Base16InputStream in = new Base16InputStream(bin, true)) {
305             assertThrows(NullPointerException.class, () -> in.read(null, 0, 0), "Base16InputStream.read(null, 0, 0)");
306         }
307     }
308 
309     /**
310      * Tests read throwing IndexOutOfBoundsException
311      *
312      * @throws IOException for some failure scenarios.
313      */
314     @Test
315     void testReadOutOfBounds() throws IOException {
316         final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
317         final byte[] buf = new byte[1024];
318         final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
319         try (Base16InputStream in = new Base16InputStream(bin, true)) {
320             assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, -1, 0), "Base16InputStream.read(buf, -1, 0)");
321             assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 0, -1), "Base16InputStream.read(buf, 0, -1)");
322             assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, buf.length + 1, 0), "Base16InputStream.read(buf, buf.length + 1, 0)");
323             assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, buf.length - 1, 2), "Base16InputStream.read(buf, buf.length - 1, 2)");
324         }
325     }
326 
327     /**
328      * Tests skipping number of characters larger than the internal buffer.
329      *
330      * @throws IOException for some failure scenarios.
331      */
332     @Test
333     void testSkipBig() throws IOException {
334         final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16));
335         try (Base16InputStream b16Stream = new Base16InputStream(ins)) {
336             assertEquals(6, b16Stream.skip(Integer.MAX_VALUE));
337             // End of stream reached
338             assertEquals(-1, b16Stream.read());
339             assertEquals(-1, b16Stream.read());
340         }
341     }
342 
343     /**
344      * Tests skipping as a noop
345      *
346      * @throws IOException for some failure scenarios.
347      */
348     @Test
349     void testSkipNone() throws IOException {
350         final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16));
351         try (Base16InputStream b16Stream = new Base16InputStream(ins)) {
352             final byte[] actualBytes = new byte[6];
353             assertEquals(0, b16Stream.skip(0));
354             b16Stream.read(actualBytes, 0, actualBytes.length);
355             assertArrayEquals(actualBytes, new byte[] { (byte) 202, (byte) 254, (byte) 186, (byte) 190, (byte) 255, (byte) 255 });
356             // End of stream reached
357             assertEquals(-1, b16Stream.read());
358         }
359     }
360 
361     /**
362      * Tests skipping past the end of a stream.
363      *
364      * @throws IOException for some failure scenarios.
365      */
366     @Test
367     void testSkipPastEnd() throws IOException {
368         final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16));
369         try (Base16InputStream b16Stream = new Base16InputStream(ins)) {
370             // due to CODEC-130, skip now skips correctly decoded characters rather than encoded
371             assertEquals(6, b16Stream.skip(10));
372             // End of stream reached
373             assertEquals(-1, b16Stream.read());
374             assertEquals(-1, b16Stream.read());
375         }
376     }
377 
378     /**
379      * Tests skipping to the end of a stream.
380      *
381      * @throws IOException for some failure scenarios.
382      */
383     @Test
384     void testSkipToEnd() throws IOException {
385         final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16));
386         try (Base16InputStream b16Stream = new Base16InputStream(ins)) {
387             // due to CODEC-130, skip now skips correctly decoded characters rather than encoded
388             assertEquals(6, b16Stream.skip(6));
389             // End of stream reached
390             assertEquals(-1, b16Stream.read());
391             assertEquals(-1, b16Stream.read());
392         }
393     }
394 
395     /**
396      * Tests if negative arguments to skip are handled correctly.
397      *
398      * @throws IOException for some failure scenarios.
399      */
400     @Test
401     void testSkipWrongArgument() throws IOException {
402         final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16));
403         try (Base16InputStream b16Stream = new Base16InputStream(ins)) {
404             assertThrows(IllegalArgumentException.class, () -> b16Stream.skip(-10));
405         }
406         // Same with a builder
407         try (Base16InputStream b16Stream = Base16InputStream.builder().setByteArray(StringUtils.getBytesIso8859_1(ENCODED_B16)).get()) {
408             assertThrows(IllegalArgumentException.class, () -> b16Stream.skip(-10));
409         }
410     }
411 }