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