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
018package org.apache.commons.codec.binary;
019
020import static org.junit.Assert.assertArrayEquals;
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertFalse;
023import static org.junit.Assert.assertNotNull;
024import static org.junit.Assert.assertTrue;
025import static org.junit.Assert.fail;
026
027import java.io.BufferedReader;
028import java.io.ByteArrayInputStream;
029import java.io.ByteArrayOutputStream;
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.InputStreamReader;
033import java.util.Arrays;
034
035import org.junit.Test;
036
037/**
038 * @version $Id $
039 * @since 1.4
040 */
041public 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 separator
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[] separator) throws Exception {
300
301        // Start with encode.
302        InputStream in;
303        in = new Base64InputStream(new ByteArrayInputStream(decoded), true, chunkSize, separator);
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, separator);
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 separator
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[] separator) throws Exception {
353
354        // Start with encode.
355        InputStream in;
356        in = new Base64InputStream(new ByteArrayInputStream(decoded), true, chunkSize, separator);
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, separator);
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}