001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.commons.codec.binary;
019    
020    import static org.junit.Assert.assertArrayEquals;
021    import static org.junit.Assert.assertEquals;
022    import static org.junit.Assert.assertFalse;
023    import static org.junit.Assert.assertNotNull;
024    import static org.junit.Assert.assertTrue;
025    import static org.junit.Assert.fail;
026    
027    import java.io.BufferedReader;
028    import java.io.ByteArrayInputStream;
029    import java.io.ByteArrayOutputStream;
030    import java.io.IOException;
031    import java.io.InputStream;
032    import java.io.InputStreamReader;
033    import java.util.Arrays;
034    
035    import org.junit.Test;
036    
037    /**
038     * @version $Id $
039     * @since 1.4
040     */
041    public class Base64InputStreamTest {
042    
043        /**
044         * Decodes to {0, 0, 0, 255, 255, 255}
045         */
046        private static final String ENCODED_B64 = "AAAA////";
047    
048        private final static byte[] CRLF = { (byte) '\r', (byte) '\n' };
049    
050        private final static byte[] LF = { (byte) '\n' };
051    
052        private static final String STRING_FIXTURE = "Hello World";
053    
054        /**
055         * Tests the problem reported in CODEC-130. Missing / wrong implementation of skip.
056         */
057        @Test
058        public void testCodec130() throws IOException {
059            final ByteArrayOutputStream bos = new ByteArrayOutputStream();
060            final Base64OutputStream base64os = new Base64OutputStream(bos);
061    
062            base64os.write(StringUtils.getBytesUtf8(STRING_FIXTURE));
063            base64os.close();
064    
065            final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
066            final Base64InputStream ins = new Base64InputStream(bis);
067    
068            // we skip the first character read from the reader
069            ins.skip(1);
070            final byte[] decodedBytes = Base64TestData.streamToBytes(ins, new byte[64]);
071            final String str = StringUtils.newStringUtf8(decodedBytes);
072    
073            assertEquals(STRING_FIXTURE.substring(1), str);
074        }
075    
076        /**
077         * Tests the bug reported in CODEC-105. Bad interactions with InputStream when reading one byte at a time.
078         */
079        @Test
080        public void testCodec105() throws IOException {
081            final Base64InputStream in = new Base64InputStream(new Codec105ErrorInputStream(), true, 0, null);
082            try {
083                for (int i = 0; i < 5; i++) {
084                    in.read();
085                }
086            } finally {
087                in.close();
088            }
089        }
090    
091        /**
092         * Test for the CODEC-101 bug: InputStream.read(byte[]) should never return 0 because Java's builtin InputStreamReader hates that.
093         *
094         * @throws Exception
095         *             for some failure scenarios.
096         */
097        @Test
098        public void testCodec101() throws Exception {
099            final byte[] codec101 = StringUtils.getBytesUtf8(Base64TestData.CODEC_101_MULTIPLE_OF_3);
100            final ByteArrayInputStream bais = new ByteArrayInputStream(codec101);
101            final Base64InputStream in = new Base64InputStream(bais);
102            final byte[] result = new byte[8192];
103            int c = in.read(result);
104            assertTrue("Codec101: First read successful [c=" + c + "]", c > 0);
105    
106            c = in.read(result);
107            assertTrue("Codec101: Second read should report end-of-stream [c=" + c + "]", c < 0);
108            in.close();
109        }
110    
111        /**
112         * Another test for the CODEC-101 bug: In commons-codec-1.4 this test shows InputStreamReader explicitly hating an
113         * InputStream.read(byte[]) return of 0:
114         *
115         * java.io.IOException: Underlying input stream returned zero bytes at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:268) at
116         * sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158) at
117         * java.io.InputStreamReader.read(InputStreamReader.java:167) at java.io.BufferedReader.fill(BufferedReader.java:136) at
118         * java.io.BufferedReader.readLine(BufferedReader.java:299) at java.io.BufferedReader.readLine(BufferedReader.java:362) at
119         * org.apache.commons.codec.binary.Base64InputStreamTest.testInputStreamReader(Base64InputStreamTest.java:75)
120         *
121         * But in commons-codec-1.5 it's fixed. :-)
122         *
123         * @throws Exception
124         *             for some failure scenarios.
125         */
126        @Test
127        public void testInputStreamReader() throws Exception {
128            final byte[] codec101 = StringUtils.getBytesUtf8(Base64TestData.CODEC_101_MULTIPLE_OF_3);
129            final ByteArrayInputStream bais = new ByteArrayInputStream(codec101);
130            final Base64InputStream in = new Base64InputStream(bais);
131            final InputStreamReader isr = new InputStreamReader(in);
132            final BufferedReader br = new BufferedReader(isr);
133            final String line = br.readLine();
134            assertNotNull("Codec101:  InputStreamReader works!", line);
135            br.close();
136        }
137    
138        /**
139         * Test the Base64InputStream implementation against the special NPE inducing input identified in the CODEC-98 bug.
140         *
141         * @throws Exception
142         *             for some failure scenarios.
143         */
144        @Test
145        public void testCodec98NPE() throws Exception {
146            final byte[] codec98 = StringUtils.getBytesUtf8(Base64TestData.CODEC_98_NPE);
147            final ByteArrayInputStream data = new ByteArrayInputStream(codec98);
148            final Base64InputStream stream = new Base64InputStream(data);
149    
150            // This line causes an NPE in commons-codec-1.4.jar:
151            final byte[] decodedBytes = Base64TestData.streamToBytes(stream, new byte[1024]);
152    
153            final String decoded = StringUtils.newStringUtf8(decodedBytes);
154            assertEquals("codec-98 NPE Base64InputStream", Base64TestData.CODEC_98_NPE_DECODED, decoded);
155        }
156    
157        /**
158         * Tests skipping past the end of a stream.
159         *
160         * @throws Throwable
161         */
162        @Test
163        public void testAvailable() throws Throwable {
164            final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
165            final Base64InputStream b64stream = new Base64InputStream(ins);
166            assertEquals(1, b64stream.available());
167            assertEquals(6, b64stream.skip(10));
168            // End of stream reached
169            assertEquals(0, b64stream.available());
170            assertEquals(-1, b64stream.read());
171            assertEquals(-1, b64stream.read());
172            assertEquals(0, b64stream.available());
173            b64stream.close();
174        }
175    
176        /**
177         * Tests the Base64InputStream implementation against empty input.
178         *
179         * @throws Exception
180         *             for some failure scenarios.
181         */
182        @Test
183        public void testBase64EmptyInputStreamMimeChuckSize() throws Exception {
184            testBase64EmptyInputStream(BaseNCodec.MIME_CHUNK_SIZE);
185        }
186    
187        /**
188         * Tests the Base64InputStream implementation against empty input.
189         *
190         * @throws Exception
191         *             for some failure scenarios.
192         */
193        @Test
194        public void testBase64EmptyInputStreamPemChuckSize() throws Exception {
195            testBase64EmptyInputStream(BaseNCodec.PEM_CHUNK_SIZE);
196        }
197    
198        private void testBase64EmptyInputStream(final int chuckSize) throws Exception {
199            final byte[] emptyEncoded = new byte[0];
200            final byte[] emptyDecoded = new byte[0];
201            testByteByByte(emptyEncoded, emptyDecoded, chuckSize, CRLF);
202            testByChunk(emptyEncoded, emptyDecoded, chuckSize, CRLF);
203        }
204    
205        /**
206         * Tests the Base64InputStream implementation.
207         *
208         * @throws Exception
209         *             for some failure scenarios.
210         */
211        @Test
212        public void testBase64InputStreamByChunk() throws Exception {
213            // Hello World test.
214            byte[] encoded = StringUtils.getBytesUtf8("SGVsbG8gV29ybGQ=\r\n");
215            byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
216            testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
217    
218            // Single Byte test.
219            encoded = StringUtils.getBytesUtf8("AA==\r\n");
220            decoded = new byte[] { (byte) 0 };
221            testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
222    
223            // OpenSSL interop test.
224            encoded = StringUtils.getBytesUtf8(Base64TestData.ENCODED_64_CHARS_PER_LINE);
225            decoded = Base64TestData.DECODED;
226            testByChunk(encoded, decoded, BaseNCodec.PEM_CHUNK_SIZE, LF);
227    
228            // Single Line test.
229            final String singleLine = Base64TestData.ENCODED_64_CHARS_PER_LINE.replaceAll("\n", "");
230            encoded = StringUtils.getBytesUtf8(singleLine);
231            decoded = Base64TestData.DECODED;
232            testByChunk(encoded, decoded, 0, LF);
233    
234            // test random data of sizes 0 thru 150
235            for (int i = 0; i <= 150; i++) {
236                final byte[][] randomData = Base64TestData.randomData(i, false);
237                encoded = randomData[1];
238                decoded = randomData[0];
239                testByChunk(encoded, decoded, 0, LF);
240            }
241        }
242    
243        /**
244         * Tests the Base64InputStream implementation.
245         *
246         * @throws Exception
247         *             for some failure scenarios.
248         */
249        @Test
250        public void testBase64InputStreamByteByByte() throws Exception {
251            // Hello World test.
252            byte[] encoded = StringUtils.getBytesUtf8("SGVsbG8gV29ybGQ=\r\n");
253            byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
254            testByteByByte(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
255    
256            // Single Byte test.
257            encoded = StringUtils.getBytesUtf8("AA==\r\n");
258            decoded = new byte[] { (byte) 0 };
259            testByteByByte(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
260    
261            // OpenSSL interop test.
262            encoded = StringUtils.getBytesUtf8(Base64TestData.ENCODED_64_CHARS_PER_LINE);
263            decoded = Base64TestData.DECODED;
264            testByteByByte(encoded, decoded, BaseNCodec.PEM_CHUNK_SIZE, LF);
265    
266            // Single Line test.
267            final String singleLine = Base64TestData.ENCODED_64_CHARS_PER_LINE.replaceAll("\n", "");
268            encoded = StringUtils.getBytesUtf8(singleLine);
269            decoded = Base64TestData.DECODED;
270            testByteByByte(encoded, decoded, 0, LF);
271    
272            // test random data of sizes 0 thru 150
273            for (int i = 0; i <= 150; i++) {
274                final byte[][] randomData = Base64TestData.randomData(i, false);
275                encoded = randomData[1];
276                decoded = randomData[0];
277                testByteByByte(encoded, decoded, 0, LF);
278            }
279        }
280    
281        /**
282         * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
283         * ---[WRAP-WRAP-WRAP-etc...] --> decoded
284         * <p/>
285         * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base64InputStream wraps itself in encode and decode mode over and over
286         * again.
287         *
288         * @param encoded
289         *            base64 encoded data
290         * @param decoded
291         *            the data from above, but decoded
292         * @param chunkSize
293         *            chunk size (line-length) of the base64 encoded data.
294         * @param seperator
295         *            Line separator in the base64 encoded data.
296         * @throws Exception
297         *             Usually signifies a bug in the Base64 commons-codec implementation.
298         */
299        private void testByChunk(final byte[] encoded, final byte[] decoded, final int chunkSize, final byte[] seperator) throws Exception {
300    
301            // Start with encode.
302            InputStream in;
303            in = new Base64InputStream(new ByteArrayInputStream(decoded), true, chunkSize, seperator);
304            byte[] output = Base64TestData.streamToBytes(in);
305    
306            assertEquals("EOF", -1, in.read());
307            assertEquals("Still EOF", -1, in.read());
308            assertTrue("Streaming base64 encode", Arrays.equals(output, encoded));
309    
310            in.close();
311    
312            // Now let's try decode.
313            in = new Base64InputStream(new ByteArrayInputStream(encoded));
314            output = Base64TestData.streamToBytes(in);
315    
316            assertEquals("EOF", -1, in.read());
317            assertEquals("Still EOF", -1, in.read());
318            assertTrue("Streaming base64 decode", Arrays.equals(output, decoded));
319    
320            // I always wanted to do this! (wrap encoder with decoder etc etc).
321            in = new ByteArrayInputStream(decoded);
322            for (int i = 0; i < 10; i++) {
323                in = new Base64InputStream(in, true, chunkSize, seperator);
324                in = new Base64InputStream(in, false);
325            }
326            output = Base64TestData.streamToBytes(in);
327    
328            assertEquals("EOF", -1, in.read());
329            assertEquals("Still EOF", -1, in.read());
330            assertTrue("Streaming base64 wrap-wrap-wrap!", Arrays.equals(output, decoded));
331            in.close();
332        }
333    
334        /**
335         * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
336         * ---[WRAP-WRAP-WRAP-etc...] --> decoded
337         * <p/>
338         * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base64InputStream wraps itself in encode and decode mode over and over
339         * again.
340         *
341         * @param encoded
342         *            base64 encoded data
343         * @param decoded
344         *            the data from above, but decoded
345         * @param chunkSize
346         *            chunk size (line-length) of the base64 encoded data.
347         * @param seperator
348         *            Line separator in the base64 encoded data.
349         * @throws Exception
350         *             Usually signifies a bug in the Base64 commons-codec implementation.
351         */
352        private void testByteByByte(final byte[] encoded, final byte[] decoded, final int chunkSize, final byte[] seperator) throws Exception {
353    
354            // Start with encode.
355            InputStream in;
356            in = new Base64InputStream(new ByteArrayInputStream(decoded), true, chunkSize, seperator);
357            byte[] output = new byte[encoded.length];
358            for (int i = 0; i < output.length; i++) {
359                output[i] = (byte) in.read();
360            }
361    
362            assertEquals("EOF", -1, in.read());
363            assertEquals("Still EOF", -1, in.read());
364            assertTrue("Streaming base64 encode", Arrays.equals(output, encoded));
365    
366            in.close();
367            // Now let's try decode.
368            in = new Base64InputStream(new ByteArrayInputStream(encoded));
369            output = new byte[decoded.length];
370            for (int i = 0; i < output.length; i++) {
371                output[i] = (byte) in.read();
372            }
373    
374            assertEquals("EOF", -1, in.read());
375            assertEquals("Still EOF", -1, in.read());
376            assertTrue("Streaming base64 decode", Arrays.equals(output, decoded));
377    
378            in.close();
379    
380            // I always wanted to do this! (wrap encoder with decoder etc etc).
381            in = new ByteArrayInputStream(decoded);
382            for (int i = 0; i < 10; i++) {
383                in = new Base64InputStream(in, true, chunkSize, seperator);
384                in = new Base64InputStream(in, false);
385            }
386            output = new byte[decoded.length];
387            for (int i = 0; i < output.length; i++) {
388                output[i] = (byte) in.read();
389            }
390    
391            assertEquals("EOF", -1, in.read());
392            assertEquals("Still EOF", -1, in.read());
393            assertTrue("Streaming base64 wrap-wrap-wrap!", Arrays.equals(output, decoded));
394            in.close();
395        }
396    
397        /**
398         * Tests markSupported.
399         *
400         * @throws Exception
401         */
402        @Test
403        public void testMarkSupported() throws Exception {
404            final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
405            final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
406            final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
407            // Always returns false for now.
408            assertFalse("Base64InputStream.markSupported() is false", in.markSupported());
409            in.close();
410        }
411    
412        /**
413         * Tests read returning 0
414         *
415         * @throws Exception
416         */
417        @Test
418        public void testRead0() throws Exception {
419            final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
420            final byte[] buf = new byte[1024];
421            int bytesRead = 0;
422            final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
423            final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
424            bytesRead = in.read(buf, 0, 0);
425            assertEquals("Base64InputStream.read(buf, 0, 0) returns 0", 0, bytesRead);
426            in.close();
427        }
428    
429        /**
430         * Tests read with null.
431         *
432         * @throws Exception
433         *             for some failure scenarios.
434         */
435        @Test
436        public void testReadNull() throws Exception {
437            final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
438            final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
439            final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
440            try {
441                in.read(null, 0, 0);
442                fail("Base64InputStream.read(null, 0, 0) to throw a NullPointerException");
443            } catch (final NullPointerException e) {
444                // Expected
445            }
446            in.close();
447        }
448    
449        /**
450         * Tests read throwing IndexOutOfBoundsException
451         *
452         * @throws Exception
453         */
454        @Test
455        public void testReadOutOfBounds() throws Exception {
456            final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
457            final byte[] buf = new byte[1024];
458            final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
459            final Base64InputStream in = new Base64InputStream(bin, true, 4, new byte[] { 0, 0, 0 });
460    
461            try {
462                in.read(buf, -1, 0);
463                fail("Expected Base64InputStream.read(buf, -1, 0) to throw IndexOutOfBoundsException");
464            } catch (final IndexOutOfBoundsException e) {
465                // Expected
466            }
467    
468            try {
469                in.read(buf, 0, -1);
470                fail("Expected Base64InputStream.read(buf, 0, -1) to throw IndexOutOfBoundsException");
471            } catch (final IndexOutOfBoundsException e) {
472                // Expected
473            }
474    
475            try {
476                in.read(buf, buf.length + 1, 0);
477                fail("Base64InputStream.read(buf, buf.length + 1, 0) throws IndexOutOfBoundsException");
478            } catch (final IndexOutOfBoundsException e) {
479                // Expected
480            }
481    
482            try {
483                in.read(buf, buf.length - 1, 2);
484                fail("Base64InputStream.read(buf, buf.length - 1, 2) throws IndexOutOfBoundsException");
485            } catch (final IndexOutOfBoundsException e) {
486                // Expected
487            }
488            in.close();
489        }
490    
491        /**
492         * Tests skipping number of characters larger than the internal buffer.
493         *
494         * @throws Throwable
495         */
496        @Test
497        public void testSkipBig() throws Throwable {
498            final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
499            final Base64InputStream b64stream = new Base64InputStream(ins);
500            assertEquals(6, b64stream.skip(Integer.MAX_VALUE));
501            // End of stream reached
502            assertEquals(-1, b64stream.read());
503            assertEquals(-1, b64stream.read());
504            b64stream.close();
505        }
506    
507        /**
508         * Tests skipping as a noop
509         *
510         * @throws Throwable
511         */
512        @Test
513        public void testSkipNone() throws Throwable {
514            final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
515            final Base64InputStream b64stream = new Base64InputStream(ins);
516            final byte[] actualBytes = new byte[6];
517            assertEquals(0, b64stream.skip(0));
518            b64stream.read(actualBytes, 0, actualBytes.length);
519            assertArrayEquals(actualBytes, new byte[] { 0, 0, 0, (byte) 255, (byte) 255, (byte) 255 });
520            // End of stream reached
521            assertEquals(-1, b64stream.read());
522            b64stream.close();
523        }
524    
525        /**
526         * Tests skipping past the end of a stream.
527         *
528         * @throws Throwable
529         */
530        @Test
531        public void testSkipPastEnd() throws Throwable {
532            final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
533            final Base64InputStream b64stream = new Base64InputStream(ins);
534            // due to CODEC-130, skip now skips correctly decoded characters rather than encoded
535            assertEquals(6, b64stream.skip(10));
536            // End of stream reached
537            assertEquals(-1, b64stream.read());
538            assertEquals(-1, b64stream.read());
539            b64stream.close();
540        }
541    
542        /**
543         * Tests skipping to the end of a stream.
544         *
545         * @throws Throwable
546         */
547        @Test
548        public void testSkipToEnd() throws Throwable {
549            final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
550            final Base64InputStream b64stream = new Base64InputStream(ins);
551            // due to CODEC-130, skip now skips correctly decoded characters rather than encoded
552            assertEquals(6, b64stream.skip(6));
553            // End of stream reached
554            assertEquals(-1, b64stream.read());
555            assertEquals(-1, b64stream.read());
556            b64stream.close();
557        }
558    
559        /**
560         * Tests if negative arguments to skip are handled correctly.
561         *
562         * @throws Throwable
563         */
564        @Test(expected=IllegalArgumentException.class)
565        public void testSkipWrongArgument() throws Throwable {
566            final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B64));
567            final Base64InputStream b64stream = new Base64InputStream(ins);
568            b64stream.skip(-10);
569            b64stream.close();
570        }
571    }