Base64BinaryParsingTest.java

package com.fasterxml.jackson.core.base64;

import java.io.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.core.*;

import static org.junit.jupiter.api.Assertions.*;

class Base64BinaryParsingTest
        extends JUnit5TestBase
{
    private final JsonFactory JSON_F = newStreamFactory();

    @Test
    void base64UsingInputStream() throws Exception
    {
        _testBase64Text(MODE_INPUT_STREAM);
        _testBase64Text(MODE_INPUT_STREAM_THROTTLED);
        _testBase64Text(MODE_DATA_INPUT);
    }

    @Test
    void base64UsingReader() throws Exception
    {
        _testBase64Text(MODE_READER);
    }

    @Test
    void streaming() throws IOException
    {
        _testStreaming(MODE_INPUT_STREAM);
        _testStreaming(MODE_INPUT_STREAM_THROTTLED);
        _testStreaming(MODE_DATA_INPUT);
        _testStreaming(MODE_READER);
    }

    @Test
    void simple() throws IOException
    {
        for (int mode : ALL_MODES) {
            // [core#414]: Allow leading/trailign white-space, ensure it is accepted
            _testSimple(mode, false, false);
            _testSimple(mode, true, false);
            _testSimple(mode, false, true);
            _testSimple(mode, true, true);
        }
    }

    @Test
    void inArray() throws IOException
    {
        for (int mode : ALL_MODES) {
            _testInArray(mode);
        }
    }

    @Test
    void withEscaped() throws IOException {
        for (int mode : ALL_MODES) {
            _testEscaped(mode);
        }
    }

    @Test
    void withEscapedPadding() throws IOException {
        for (int mode : ALL_MODES) {
            _testEscapedPadding(mode);
        }
    }

    @Test
    void invalidTokenForBase64() throws IOException
    {
        for (int mode : ALL_MODES) {

            // First: illegal padding
            JsonParser p = createParser(JSON_F, mode, "[ ]");
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            try {
                p.getBinaryValue();
                fail("Should not pass");
            } catch (JsonParseException e) {
                verifyException(e, "current token");
                verifyException(e, "can not access as binary");
            }
            p.close();
        }
    }

    @Test
    void invalidChar() throws IOException
    {
        for (int mode : ALL_MODES) {

            // First: illegal padding
            JsonParser p = createParser(JSON_F, mode, q("a==="));
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            try {
                p.getBinaryValue(Base64Variants.MIME);
                fail("Should not pass");
            } catch (JsonParseException e) {
                verifyException(e, "padding only legal");
            }
            p.close();

            // second: invalid space within
            p = createParser(JSON_F, mode, q("ab de"));
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            try {
                p.getBinaryValue(Base64Variants.MIME);
                fail("Should not pass");
            } catch (JsonParseException e) {
                verifyException(e, "illegal white space");
            }
            p.close();

            // third: something else
            p = createParser(JSON_F, mode, q("ab#?"));
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            try {
                p.getBinaryValue(Base64Variants.MIME);
                fail("Should not pass");
            } catch (JsonParseException e) {
                verifyException(e, "illegal character '#'");
            }
            p.close();
        }
    }

    @Test
    void okMissingPadding() throws IOException {
        final byte[] DOC1 = new byte[] { (byte) 0xAD };
        _testOkMissingPadding(DOC1, MODE_INPUT_STREAM);
        _testOkMissingPadding(DOC1, MODE_INPUT_STREAM_THROTTLED);
        _testOkMissingPadding(DOC1, MODE_READER);
        _testOkMissingPadding(DOC1, MODE_DATA_INPUT);

        final byte[] DOC2 = new byte[] { (byte) 0xAC, (byte) 0xDC };
        _testOkMissingPadding(DOC2, MODE_INPUT_STREAM);
        _testOkMissingPadding(DOC2, MODE_INPUT_STREAM_THROTTLED);
        _testOkMissingPadding(DOC2, MODE_READER);
        _testOkMissingPadding(DOC2, MODE_DATA_INPUT);
    }

    private void _testOkMissingPadding(byte[] input, int mode) throws IOException
    {
        final Base64Variant b64 = Base64Variants.MODIFIED_FOR_URL;
        final String encoded = b64.encode(input, false);
        JsonParser p = createParser(JSON_F, mode, q(encoded));
        // 1 byte -> 2 encoded chars; 2 bytes -> 3 encoded chars
        assertEquals(input.length+1, encoded.length());
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        byte[] actual = p.getBinaryValue(b64);
        assertArrayEquals(input, actual);
        p.close();
    }

    @Test
    void failDueToMissingPadding() throws IOException {
        final String DOC1 = q("fQ"); // 1 bytes, no padding
        _testFailDueToMissingPadding(DOC1, MODE_INPUT_STREAM);
        _testFailDueToMissingPadding(DOC1, MODE_INPUT_STREAM_THROTTLED);
        _testFailDueToMissingPadding(DOC1, MODE_READER);
        _testFailDueToMissingPadding(DOC1, MODE_DATA_INPUT);

        final String DOC2 = q("A/A"); // 2 bytes, no padding
        _testFailDueToMissingPadding(DOC2, MODE_INPUT_STREAM);
        _testFailDueToMissingPadding(DOC2, MODE_INPUT_STREAM_THROTTLED);
        _testFailDueToMissingPadding(DOC2, MODE_READER);
        _testFailDueToMissingPadding(DOC2, MODE_DATA_INPUT);
    }

    private void _testFailDueToMissingPadding(String doc, int mode) throws IOException {
        final String EXP_EXCEPTION_MATCH = "Unexpected end of base64-encoded String: base64 variant 'MIME' expects padding";

        // First, without getting text value first:
        JsonParser p = createParser(JSON_F, mode, doc);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        try {
            /*byte[] b =*/ p.getBinaryValue(Base64Variants.MIME);
            fail("Should not pass");
        } catch (JsonParseException e) {
            verifyException(e, EXP_EXCEPTION_MATCH);
        }
        p.close();

        // second, access String first
        p = createParser(JSON_F, mode, doc);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        /*String str =*/ p.getText();
        try {
            /*byte[] b =*/ p.getBinaryValue(Base64Variants.MIME);
            fail("Should not pass");
        } catch (JsonParseException e) {
            verifyException(e, EXP_EXCEPTION_MATCH);
        }
        p.close();
    }

    /*
    /**********************************************************
    /* Test helper methods
    /**********************************************************
     */

    @SuppressWarnings("resource")
    void _testBase64Text(int mode) throws Exception
    {
        // let's actually iterate over sets of encoding modes, lengths

        final int[] LENS = { 1, 2, 3, 4, 7, 9, 32, 33, 34, 35 };
        final Base64Variant[] VARIANTS = {
                Base64Variants.MIME,
                Base64Variants.MIME_NO_LINEFEEDS,
                Base64Variants.MODIFIED_FOR_URL,
                Base64Variants.PEM
        };

        final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        StringWriter chars = null;
        for (int len : LENS) {
            byte[] input = new byte[len];
            for (int i = 0; i < input.length; ++i) {
                input[i] = (byte) i;
            }
            for (Base64Variant variant : VARIANTS) {
                JsonGenerator g;

                if (mode == MODE_READER) {
                    chars = new StringWriter();
                    g = JSON_F.createGenerator(chars);
                } else {
                    bytes.reset();
                    g = JSON_F.createGenerator(bytes, JsonEncoding.UTF8);
                }
                g.writeBinary(variant, input, 0, input.length);
                g.close();
                JsonParser p;
                if (mode == MODE_READER) {
                    p = JSON_F.createParser(chars.toString());
                } else {
                    p = createParser(JSON_F, mode, bytes.toByteArray());
                }
                assertToken(JsonToken.VALUE_STRING, p.nextToken());

                // minor twist: for even-length values, force access as String first:
                if ((len & 1) == 0) {
                    assertNotNull(p.getText());
                }

                byte[] data = null;
                try {
                    data = p.getBinaryValue(variant);
                } catch (Exception e) {
                    IOException ioException = new IOException("Failed (variant "+variant+", data length "+len+"): "+e.getMessage());
                    ioException.initCause(e);
                    throw ioException;
                }
                assertNotNull(data);
                assertArrayEquals(data, input);
                if (mode != MODE_DATA_INPUT) { // no look-ahead for DataInput
                    assertNull(p.nextToken());
                }
                p.close();
            }
        }
    }

    private byte[] _generateData(int size)
    {
        byte[] result = new byte[size];
        for (int i = 0; i < size; ++i) {
            result[i] = (byte) (i % 255);
        }
        return result;
    }

    private void _testStreaming(int mode) throws IOException
    {
        final int[] SIZES = new int[] {
            1, 2, 3, 4, 5, 6,
            7, 8, 12,
            100, 350, 1900, 6000, 19000, 65000,
            139000
        };

        final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        StringWriter chars = null;

        for (int size : SIZES) {
            byte[] data = _generateData(size);
            JsonGenerator g;
            if (mode == MODE_READER) {
                chars = new StringWriter();
                g = JSON_F.createGenerator(chars);
            } else {
                bytes.reset();
                g = JSON_F.createGenerator(bytes, JsonEncoding.UTF8);
            }

            g.writeStartObject();
            g.writeFieldName("b");
            g.writeBinary(data);
            g.writeEndObject();
            g.close();

            // and verify
            JsonParser p;
            if (mode == MODE_READER) {
                p = JSON_F.createParser(chars.toString());
            } else {
                p = createParser(JSON_F, mode, bytes.toByteArray());
            }
            assertToken(JsonToken.START_OBJECT, p.nextToken());

            assertToken(JsonToken.FIELD_NAME, p.nextToken());
            assertEquals("b", p.currentName());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            ByteArrayOutputStream result = new ByteArrayOutputStream(size);
            int gotten = p.readBinaryValue(result);
            assertEquals(size, gotten);
            assertArrayEquals(data, result.toByteArray());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
            if (mode != MODE_DATA_INPUT) { // no look-ahead for DataInput
                assertNull(p.nextToken());
            }
            p.close();
        }
    }

    private void _testSimple(int mode, boolean leadingWS, boolean trailingWS) throws IOException
    {
        // The usual sample input string, from Thomas Hobbes's "Leviathan"
        // (via Wikipedia)
        final String RESULT = "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.";
        final byte[] RESULT_BYTES = RESULT.getBytes("US-ASCII");

        // And here's what should produce it...
        String INPUT_STR =
 "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz"
+"IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg"
+"dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu"
+"dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo"
+"ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4="
            ;
        if (leadingWS) {
            INPUT_STR = "   "+INPUT_STR;
        }
        if (leadingWS) {
            INPUT_STR = INPUT_STR+"   ";
        }

        final String DOC = "\""+INPUT_STR+"\"";
        JsonParser p = createParser(JSON_F, mode, DOC);

        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        byte[] data = p.getBinaryValue();
        assertNotNull(data);
        assertArrayEquals(RESULT_BYTES, data);
        p.close();
    }

    private void _testInArray(int mode) throws IOException
    {
        final int entryCount = 7;

        StringWriter sw = new StringWriter();
        JsonGenerator jg = JSON_F.createGenerator(sw);
        jg.writeStartArray();

        byte[][] entries = new byte[entryCount][];
        for (int i = 0; i < entryCount; ++i) {
            byte[] b = new byte[200 + i * 100];
            for (int x = 0; x < b.length; ++x) {
                b[x] = (byte) (i + x);
            }
            entries[i] = b;
            jg.writeBinary(b);
        }

        jg.writeEndArray();
        jg.close();

        JsonParser p = createParser(JSON_F, mode, sw.toString());

        assertToken(JsonToken.START_ARRAY, p.nextToken());

        for (int i = 0; i < entryCount; ++i) {
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            byte[] b = p.getBinaryValue();
            assertArrayEquals(entries[i], b);
        }
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        p.close();
    }

    private void _testEscaped(int mode) throws IOException
    {
        // Input: "Test!" -> "VGVzdCE="

        // First, try with embedded linefeed half-way through:

        String DOC = q("VGVz\\ndCE="); // note: must double-quote to get linefeed
        JsonParser p = createParser(JSON_F, mode, DOC);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        byte[] b = p.getBinaryValue();
        assertEquals("Test!", new String(b, "US-ASCII"));
        if (mode != MODE_DATA_INPUT) {
            assertNull(p.nextToken());
        }
        p.close();

        // and then with escaped chars
//            DOC = quote("V\\u0047V\\u007AdCE="); // note: must escape backslash...
        DOC = q("V\\u0047V\\u007AdCE="); // note: must escape backslash...
        p = createParser(JSON_F, mode, DOC);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        b = p.getBinaryValue();
        assertEquals("Test!", new String(b, "US-ASCII"));
        if (mode != MODE_DATA_INPUT) {
            assertNull(p.nextToken());
        }
        p.close();
    }

    private void _testEscapedPadding(int mode) throws IOException
    {
        // Input: "Test!" -> "VGVzdCE="
        final String DOC = q("VGVzdCE\\u003d");

        // 06-Sep-2018, tatu: actually one more, test escaping of padding
        JsonParser p = createParser(JSON_F, mode, DOC);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        assertEquals("Test!", new String(p.getBinaryValue(), "US-ASCII"));
        if (mode != MODE_DATA_INPUT) {
            assertNull(p.nextToken());
        }
        p.close();

        // also, try out alternate access method
        p = createParser(JSON_F, mode, DOC);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        assertEquals("Test!", new String(_readBinary(p), "US-ASCII"));
        if (mode != MODE_DATA_INPUT) {
            assertNull(p.nextToken());
        }
        p.close();

        // and then different padding; "X" -> "WA=="
        final String DOC2 = q("WA\\u003D\\u003D");
        p = createParser(JSON_F, mode, DOC2);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        assertEquals("X", new String(p.getBinaryValue(), "US-ASCII"));
        if (mode != MODE_DATA_INPUT) {
            assertNull(p.nextToken());
        }
        p.close();

        p = createParser(JSON_F, mode, DOC2);
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        assertEquals("X", new String(_readBinary(p), "US-ASCII"));
        if (mode != MODE_DATA_INPUT) {
            assertNull(p.nextToken());
        }
        p.close();
    }

    private byte[] _readBinary(JsonParser p) throws IOException
    {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        p.readBinaryValue(bytes);
        return bytes.toByteArray();
    }
}