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