BinaryWriteBufferSize1622Test.java

package tools.jackson.core.unittest.base64;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;

import org.junit.jupiter.api.Test;

import tools.jackson.core.*;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.unittest.JacksonCoreTestBase;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

// [core#1622]: When a binary content length is known, the encoding/read buffer
// should be sized from that hint (capped) so large content needs far fewer
// InputStream reads than the small default buffer would require.
class BinaryWriteBufferSize1622Test
    extends JacksonCoreTestBase
{
    // Cap mirrored from JsonGeneratorImpl.MAX_BASE64_ENCODE_BUFFER_LENGTH
    private final static int MAX_BUFFER = 64 * 1024;

    private final JsonFactory JSON_F = new JsonFactory();

    private final Base64Variant VARIANT = Base64Variants.MIME;

    /**
     * {@link ByteArrayInputStream} that records the largest {@code len} ever
     * requested via {@link #read(byte[], int, int)}, which equals the size of
     * the read buffer the generator allocated.
     */
    static class ReadSizeRecordingInputStream extends ByteArrayInputStream {
        int maxRequestedRead = 0;

        ReadSizeRecordingInputStream(byte[] buf) {
            super(buf);
        }

        @Override
        public synchronized int read(byte[] b, int off, int len) {
            if (len > maxRequestedRead) {
                maxRequestedRead = len;
            }
            return super.read(b, off, len);
        }
    }

    @Test
    void sizeHintAppliedByteBacked() throws Exception {
        // 50_000 is below the 64kB cap, so the read buffer should be sized to it
        _testSizeHint(true, 50_000, 50_000);
    }

    @Test
    void sizeHintAppliedCharBacked() throws Exception {
        _testSizeHint(false, 50_000, 50_000);
    }

    @Test
    void sizeHintCappedByteBacked() throws Exception {
        // 200_000 exceeds the cap, so the read buffer should be limited to it
        _testSizeHint(true, 200_000, MAX_BUFFER);
    }

    @Test
    void sizeHintCappedCharBacked() throws Exception {
        _testSizeHint(false, 200_000, MAX_BUFFER);
    }

    private void _testSizeHint(boolean useBytes, int dataLength, int expectedMaxRead)
        throws Exception
    {
        byte[] input = new byte[dataLength];
        for (int i = 0; i < input.length; ++i) {
            input[i] = (byte) (i * 31 + 7);
        }
        ReadSizeRecordingInputStream in = new ReadSizeRecordingInputStream(input);

        byte[] rawJson;
        if (useBytes) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            try (JsonGenerator g = JSON_F.createGenerator(ObjectWriteContext.empty(), out, JsonEncoding.UTF8)) {
                g.writeBinary(VARIANT, in, dataLength);
            }
            rawJson = out.toByteArray();
        } else {
            StringWriter sw = new StringWriter();
            try (JsonGenerator g = JSON_F.createGenerator(ObjectWriteContext.empty(), sw)) {
                g.writeBinary(VARIANT, in, dataLength);
            }
            rawJson = sw.toString().getBytes("UTF-8");
        }

        // The generator should have requested reads as large as the (capped) hint,
        // which is much bigger than the small default buffer used before the fix.
        assertEquals(expectedMaxRead, in.maxRequestedRead,
                "read buffer should be sized from the length hint (capped)");
        assertTrue(in.maxRequestedRead <= MAX_BUFFER,
                "read buffer must never exceed the cap");

        // ...and the produced base64 must still decode back to the original bytes.
        try (JsonParser p = JSON_F.createParser(ObjectReadContext.empty(), rawJson)) {
            assertEquals(JsonToken.VALUE_STRING, p.nextToken());
            byte[] decoded = p.getBinaryValue(VARIANT);
            assertArrayEquals(input, decoded);
        }
    }
}