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