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