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  import static org.junit.jupiter.api.Assertions.assertTrue;
26  
27  import java.io.ByteArrayOutputStream;
28  import java.io.OutputStream;
29  
30  import org.apache.commons.codec.CodecPolicy;
31  import org.junit.jupiter.api.Test;
32  
33  /**
34   * Tests {@link Base64OutputStream}.
35   */
36  class Base64OutputStreamTest {
37  
38      private static final byte[] CR_LF = {(byte) '\r', (byte) '\n'};
39  
40      private static final byte[] LF = {(byte) '\n'};
41  
42      private static final String STRING_FIXTURE = "Hello World";
43  
44      private void testBase64EmptyOutputStream(final int chunkSize) throws Exception {
45          final byte[] emptyEncoded = {};
46          final byte[] emptyDecoded = {};
47          testByteByByte(emptyEncoded, emptyDecoded, chunkSize, CR_LF);
48          testByChunk(emptyEncoded, emptyDecoded, chunkSize, CR_LF);
49      }
50  
51      /**
52       * Test the Base64OutputStream implementation against empty input.
53       *
54       * @throws Exception
55       *             for some failure scenarios.
56       */
57      @Test
58      void testBase64EmptyOutputStreamMimeChunkSize() throws Exception {
59          testBase64EmptyOutputStream(BaseNCodec.MIME_CHUNK_SIZE);
60      }
61  
62      /**
63       * Test the Base64OutputStream implementation against empty input.
64       *
65       * @throws Exception
66       *             for some failure scenarios.
67       */
68      @Test
69      void testBase64EmptyOutputStreamPemChunkSize() throws Exception {
70          testBase64EmptyOutputStream(BaseNCodec.PEM_CHUNK_SIZE);
71      }
72  
73      /**
74       * Test the Base64OutputStream implementation
75       *
76       * @throws Exception
77       *             for some failure scenarios.
78       */
79      @Test
80      void testBase64OutputStreamByChunk() throws Exception {
81          // Hello World test.
82          byte[] encoded = StringUtils.getBytesUtf8("SGVsbG8gV29ybGQ=\r\n");
83          byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
84          testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CR_LF);
85  
86          // Single Byte test.
87          encoded = StringUtils.getBytesUtf8("AA==\r\n");
88          decoded = new byte[]{(byte) 0};
89          testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CR_LF);
90  
91          // OpenSSL interop test.
92          encoded = StringUtils.getBytesUtf8(Base64TestData.ENCODED_64_CHARS_PER_LINE);
93          decoded = BaseNTestData.DECODED;
94          testByChunk(encoded, decoded, BaseNCodec.PEM_CHUNK_SIZE, LF);
95  
96          // Single Line test.
97          final String singleLine = Base64TestData.ENCODED_64_CHARS_PER_LINE.replace("\n", "");
98          encoded = StringUtils.getBytesUtf8(singleLine);
99          decoded = BaseNTestData.DECODED;
100         testByChunk(encoded, decoded, 0, LF);
101 
102         // test random data of sizes 0 through 150
103         final BaseNCodec codec = new Base64(0, null, false);
104         for (int i = 0; i <= 150; i++) {
105             final byte[][] randomData = BaseNTestData.randomData(codec, i);
106             encoded = randomData[1];
107             decoded = randomData[0];
108             testByChunk(encoded, decoded, 0, LF);
109         }
110     }
111 
112     /**
113      * Test the Base64OutputStream implementation
114      *
115      * @throws Exception
116      *             for some failure scenarios.
117      */
118     @Test
119     void testBase64OutputStreamByteByByte() throws Exception {
120         // Hello World test.
121         byte[] encoded = StringUtils.getBytesUtf8("SGVsbG8gV29ybGQ=\r\n");
122         byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
123         testByteByByte(encoded, decoded, 76, CR_LF);
124 
125         // Single Byte test.
126         encoded = StringUtils.getBytesUtf8("AA==\r\n");
127         decoded = new byte[]{(byte) 0};
128         testByteByByte(encoded, decoded, 76, CR_LF);
129 
130         // OpenSSL interop test.
131         encoded = StringUtils.getBytesUtf8(Base64TestData.ENCODED_64_CHARS_PER_LINE);
132         decoded = BaseNTestData.DECODED;
133         testByteByByte(encoded, decoded, 64, LF);
134 
135         // Single Line test.
136         final String singleLine = Base64TestData.ENCODED_64_CHARS_PER_LINE.replace("\n", "");
137         encoded = StringUtils.getBytesUtf8(singleLine);
138         decoded = BaseNTestData.DECODED;
139         testByteByByte(encoded, decoded, 0, LF);
140 
141         // test random data of sizes 0 through 150
142         final BaseNCodec codec = new Base64(0, null, false);
143         for (int i = 0; i <= 150; i++) {
144             final byte[][] randomData = BaseNTestData.randomData(codec, i);
145             encoded = randomData[1];
146             decoded = randomData[0];
147             testByteByByte(encoded, decoded, 0, LF);
148         }
149     }
150 
151     @Test
152     void testBuilder() {
153         assertNotNull(Base64OutputStream.builder().getBaseNCodec());
154     }
155 
156     /**
157      * Test method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]-->
158      * encoded 3. decoded ---[WRAP-WRAP-WRAP-etc...] --> decoded
159      * <p/>
160      * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base64OutputStream wraps itself in encode and decode
161      * mode over and over again.
162      *
163      * @param encoded
164      *            base64 encoded data
165      * @param decoded
166      *            the data from above, but decoded
167      * @param chunkSize
168      *            chunk size (line-length) of the base64 encoded data.
169      * @param separator
170      *            Line separator in the base64 encoded data.
171      * @throws Exception
172      *             Usually signifies a bug in the Base64 commons-codec implementation.
173      */
174     private void testByChunk(final byte[] encoded, final byte[] decoded, final int chunkSize, final byte[] separator) throws Exception {
175 
176         // Start with encode.
177         ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
178         try (OutputStream out = new Base64OutputStream(byteOut, true, chunkSize, separator)) {
179             out.write(decoded);
180         }
181         byte[] output = byteOut.toByteArray();
182         assertArrayEquals(encoded, output, "Streaming chunked base64 encode");
183 
184         // Now let's try to decode.
185         byteOut = new ByteArrayOutputStream();
186         try (OutputStream out = new Base64OutputStream(byteOut, false)) {
187             out.write(encoded);
188         }
189         output = byteOut.toByteArray();
190         assertArrayEquals(decoded, output, "Streaming chunked base64 decode");
191 
192         // I always wanted to do this! (wrap encoder with decoder etc.).
193         byteOut = new ByteArrayOutputStream();
194         OutputStream out = byteOut;
195         for (int i = 0; i < 10; i++) {
196             out = new Base64OutputStream(out, false);
197             out = new Base64OutputStream(out, true, chunkSize, separator);
198         }
199         out.write(decoded);
200         out.close();
201         output = byteOut.toByteArray();
202 
203         assertArrayEquals(decoded, output, "Streaming chunked base64 wrap-wrap-wrap!");
204     }
205 
206     /**
207      * Test method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]-->
208      * encoded 3. decoded ---[WRAP-WRAP-WRAP-etc...] --> decoded
209      * <p/>
210      * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base64OutputStream wraps itself in encode and decode
211      * mode over and over again.
212      *
213      * @param encoded
214      *            base64 encoded data
215      * @param decoded
216      *            the data from above, but decoded
217      * @param chunkSize
218      *            chunk size (line-length) of the base64 encoded data.
219      * @param separator
220      *            Line separator in the base64 encoded data.
221      * @throws Exception
222      *             Usually signifies a bug in the Base64 commons-codec implementation.
223      */
224     private void testByteByByte(final byte[] encoded, final byte[] decoded, final int chunkSize, final byte[] separator) throws Exception {
225 
226         // Start with encode.
227         ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
228         try (OutputStream out = new Base64OutputStream(byteOut, true, chunkSize, separator)) {
229             for (final byte element : decoded) {
230                 out.write(element);
231             }
232         }
233         byte[] output = byteOut.toByteArray();
234         assertArrayEquals(encoded, output, "Streaming byte-by-byte base64 encode");
235 
236         // Now let's try to decode.
237         byteOut = new ByteArrayOutputStream();
238         try (OutputStream out = new Base64OutputStream(byteOut, false)) {
239             for (final byte element : encoded) {
240                 out.write(element);
241             }
242         }
243         output = byteOut.toByteArray();
244         assertArrayEquals(decoded, output, "Streaming byte-by-byte base64 decode");
245 
246         // Now let's try to decode with tonnes of flushes.
247         byteOut = new ByteArrayOutputStream();
248         try (OutputStream out = new Base64OutputStream(byteOut, false)) {
249             for (final byte element : encoded) {
250                 out.write(element);
251                 out.flush();
252             }
253         }
254         output = byteOut.toByteArray();
255         assertArrayEquals(decoded, output, "Streaming byte-by-byte flush() base64 decode");
256 
257         // I always wanted to do this! (wrap encoder with decoder etc.).
258         byteOut = new ByteArrayOutputStream();
259         OutputStream out = byteOut;
260         for (int i = 0; i < 10; i++) {
261             out = new Base64OutputStream(out, false);
262             out = new Base64OutputStream(out, true, chunkSize, separator);
263         }
264         for (final byte element : decoded) {
265             out.write(element);
266         }
267         out.close();
268         output = byteOut.toByteArray();
269 
270         assertArrayEquals(decoded, output, "Streaming byte-by-byte base64 wrap-wrap-wrap!");
271     }
272 
273     /**
274      * Test the Base64OutputStream implementation against the special NPE inducing input
275      * identified in the CODEC-98 bug.
276      *
277      * @throws Exception for some failure scenarios.
278      */
279     @Test
280     void testCodec98NPE() throws Exception {
281         final byte[] codec98 = StringUtils.getBytesUtf8(Base64TestData.CODEC_98_NPE);
282         final byte[] codec98_1024 = new byte[1024];
283         System.arraycopy(codec98, 0, codec98_1024, 0, codec98.length);
284         final ByteArrayOutputStream data = new ByteArrayOutputStream(1024);
285         try (Base64OutputStream stream = new Base64OutputStream(data, false)) {
286             stream.write(codec98_1024, 0, 1024);
287         }
288 
289         final byte[] decodedBytes = data.toByteArray();
290         final String decoded = StringUtils.newStringUtf8(decodedBytes);
291         assertEquals(Base64TestData.CODEC_98_NPE_DECODED, decoded, "codec-98 NPE Base64OutputStream");
292     }
293 
294     /**
295      * Test strict decoding.
296      *
297      * @throws Exception
298      *             for some failure scenarios.
299      */
300     @Test
301     void testStrictDecoding() throws Exception {
302         for (final String impossibleStr : Base64Test.BASE64_IMPOSSIBLE_CASES) {
303             final byte[] impossibleEncoded = StringUtils.getBytesUtf8(impossibleStr);
304             ByteArrayOutputStream bout = new ByteArrayOutputStream();
305             try (Base64OutputStream out = new Base64OutputStream(bout, false)) {
306                 // Default is lenient decoding; it should not throw
307                 assertFalse(out.isStrictDecoding());
308                 out.write(impossibleEncoded);
309             }
310             assertTrue(bout.size() > 0);
311             // Strict decoding should throw
312             bout = new ByteArrayOutputStream();
313             try (Base64OutputStream out = new Base64OutputStream(bout, false, 0, null, CodecPolicy.STRICT)) {
314                 // May throw on write or on close depending on the position of the
315                 // impossible last character in the output block size
316                 assertThrows(IllegalArgumentException.class, () -> {
317                     out.write(impossibleEncoded);
318                     out.close();
319                 });
320             }
321             try (Base64OutputStream out = Base64OutputStream.builder()
322                     .setOutputStream(bout).setEncode(false)
323                     .setBaseNCodec(Base64.builder().setLineLength(0).setLineSeparator(null).setDecodingPolicy(CodecPolicy.STRICT).get())
324                     .get()) {
325                 assertTrue(out.isStrictDecoding());
326                 assertThrows(IllegalArgumentException.class, () -> {
327                     out.write(impossibleEncoded);
328                     out.close();
329                 });
330             }
331             try (Base64OutputStream out = Base64OutputStream.builder()
332                     .setOutputStream(bout).setEncode(false)
333                     .setBaseNCodec(Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).get())
334                     .get()) {
335                 // May throw on write or on close depending on the position of the
336                 // impossible last character in the output block size
337                 assertThrows(IllegalArgumentException.class, () -> {
338                     out.write(impossibleEncoded);
339                     out.close();
340                 });
341             }
342         }
343     }
344 
345     /**
346      * Tests Base64OutputStream.write for expected IndexOutOfBoundsException conditions.
347      *
348      * @throws Exception
349      *             for some failure scenarios.
350      */
351     @Test
352     void testWriteOutOfBounds() throws Exception {
353         final byte[] buf = new byte[1024];
354         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
355         try (Base64OutputStream out = new Base64OutputStream(bout)) {
356             assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, -1, 1), "Base64OutputStream.write(buf, -1, 1)");
357             assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 1, -1), "Base64OutputStream.write(buf, 1, -1)");
358             assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, buf.length + 1, 0), "Base64OutputStream.write(buf, buf.length + 1, 0)");
359             assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, buf.length - 1, 2), "Base64OutputStream.write(buf, buf.length - 1, 2)");
360         }
361     }
362 
363     /**
364      * Tests Base64OutputStream.write(null).
365      *
366      * @throws Exception
367      *             for some failure scenarios.
368      */
369     @Test
370     void testWriteToNullCoverage() throws Exception {
371         final ByteArrayOutputStream bout = new ByteArrayOutputStream();
372         try (Base64OutputStream out = new Base64OutputStream(bout)) {
373             assertThrows(NullPointerException.class, () -> out.write(null, 0, 0));
374         }
375     }
376 }