diff --git a/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java b/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java index b2b9d088bf..863351f82f 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java +++ b/src/main/java/com/fasterxml/jackson/core/json/ByteSourceJsonBootstrapper.java @@ -1,6 +1,8 @@ package com.fasterxml.jackson.core.json; import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.format.InputAccessor; @@ -230,6 +232,13 @@ public Reader constructReader() throws IOException InputStream in = _in; if (in == null) { + int size = _inputEnd - _inputPtr; + if (size >= 0 && size <= 8192) { + // [jackson-core#488] Avoid overhead of heap ByteBuffer allocated by InputStreamReader + // when processing small inputs up to 8KiB. + Charset charset = Charset.forName(enc.getJavaName()); + return new CharBufferReader(charset.decode(ByteBuffer.wrap(_inputBuffer, _inputPtr, _inputEnd))); + } in = new ByteArrayInputStream(_inputBuffer, _inputPtr, _inputEnd); } else { /* Also, if we have any read but unused input (usually true), diff --git a/src/main/java/com/fasterxml/jackson/core/json/CharBufferReader.java b/src/main/java/com/fasterxml/jackson/core/json/CharBufferReader.java new file mode 100644 index 0000000000..5fddd9eee6 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/core/json/CharBufferReader.java @@ -0,0 +1,75 @@ +package com.fasterxml.jackson.core.json; + +import java.io.Reader; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +/** + * An adapter implementation from {@link CharBuffer} to {@link Reader}. + * This is used by {@link ByteSourceJsonBootstrapper#constructReader()} when processing small inputs to + * avoid the 8KiB {@link java.nio.ByteBuffer} allocated by {@link java.io.InputStreamReader}, see [jackson-core#488]. + */ +final class CharBufferReader extends Reader { + private final CharBuffer charBuffer; + + CharBufferReader(CharBuffer buffer) { + this.charBuffer = buffer; + } + + @Override + public int read(char[] chars, int off, int len) { + int remaining = this.charBuffer.remaining(); + if (remaining <= 0) { + return -1; + } + int length = Math.min(len, remaining); + this.charBuffer.get(chars, off, length); + return length; + } + + @Override + public int read() { + if (this.charBuffer.hasRemaining()) { + return this.charBuffer.get(); + } + return -1; + } + + @Override + public long skip(long n) { + if (n < 0L) { + throw new IllegalArgumentException("number of characters to skip cannot be negative"); + } + int skipped = Math.min(this.charBuffer.remaining(), (int) Math.min(n, Integer.MAX_VALUE)); + this.charBuffer.position(this.charBuffer.position() + skipped); + return skipped; + } + + @Override + public boolean ready() { + return true; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void mark(int readAheadLimit) { + if (readAheadLimit < 0L) { + throw new IllegalArgumentException("read ahead limit cannot be negative"); + } + this.charBuffer.mark(); + } + + @Override + public void reset() { + this.charBuffer.reset(); + } + + @Override + public void close() { + this.charBuffer.position(this.charBuffer.limit()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/json/CharBufferReaderTest.java b/src/test/java/com/fasterxml/jackson/core/json/CharBufferReaderTest.java new file mode 100644 index 0000000000..17736ca6e5 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/json/CharBufferReaderTest.java @@ -0,0 +1,108 @@ +package com.fasterxml.jackson.core.json; + +import java.io.IOException; +import java.io.Reader; +import java.nio.CharBuffer; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; + +public class CharBufferReaderTest extends com.fasterxml.jackson.core.BaseTest { + + public void testSingleCharRead() throws IOException { + CharBuffer buffer = CharBuffer.wrap("aB\u00A0\u1AE9\uFFFC"); + Reader charBufferReader = new CharBufferReader(buffer); + try (Reader reader = charBufferReader) { + assertEquals('a', reader.read()); + assertEquals('B', reader.read()); + assertEquals('\u00A0', reader.read()); + assertEquals('\u1AE9', reader.read()); + assertEquals('', reader.read()); + assertEquals(-1, reader.read()); + } + assertEquals(-1, charBufferReader.read()); + } + + public void testBulkRead() throws IOException { + CharBuffer buffer = CharBuffer.wrap("abcdefghijklmnopqrst\u00A0"); + char[] chars = new char[12]; + Reader charBufferReader = new CharBufferReader(buffer); + try (Reader reader = charBufferReader) { + assertEquals(12, reader.read(chars)); + assertArrayEquals("abcdefghijkl".toCharArray(), chars); + assertEquals(9, reader.read(chars)); + assertArrayEquals("mnopqrst\u00A0".toCharArray(), Arrays.copyOf(chars, 9)); + assertEquals(-1, reader.read(chars)); + } + assertEquals(-1, charBufferReader.read(chars)); + } + + public void testSkip() throws IOException { + CharBuffer buffer = CharBuffer.wrap("abcdefghijklmnopqrst\u00A0"); + Reader charBufferReader = new CharBufferReader(buffer); + char[] chars = new char[12]; + try (Reader reader = charBufferReader) { + assertEquals(12, reader.read(chars)); + assertArrayEquals("abcdefghijkl".toCharArray(), chars); + assertEquals(4, reader.skip(4)); + assertEquals(4, reader.read(chars, 3, 4)); + assertArrayEquals("qrst".toCharArray(), Arrays.copyOfRange(chars, 3, 7)); + assertEquals(1, reader.skip(Long.MAX_VALUE)); + assertEquals(0, reader.skip(Integer.MAX_VALUE)); + assertEquals(0, reader.skip(Long.MAX_VALUE)); + assertEquals(0, reader.skip(0)); + assertEquals(0, reader.skip(1)); + } + assertEquals(0, charBufferReader.skip(1)); + } + + public void testInvalidSkip() throws IOException { + try (Reader reader = new CharBufferReader(CharBuffer.wrap("test"))) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> reader.skip(-1)); + assertEquals("number of characters to skip cannot be negative", exception.getMessage()); + } + } + + public void testReady() throws IOException { + try (Reader reader = new CharBufferReader(CharBuffer.wrap("test"))) { + assertEquals(true, reader.ready()); + } + } + + public void testMarkReset() throws IOException { + char[] chars = new char[8]; + try (Reader reader = new CharBufferReader(CharBuffer.wrap("test"))) { + assertEquals(true, reader.markSupported()); + reader.mark(3); + assertEquals(3, reader.read(chars, 0, 3)); + assertArrayEquals("tes".toCharArray(), Arrays.copyOf(chars, 3)); + reader.reset(); + reader.mark(Integer.MAX_VALUE); + assertEquals(4, reader.read(chars)); + assertArrayEquals("test".toCharArray(), Arrays.copyOf(chars, 4)); + reader.reset(); + Arrays.fill(chars, '\0'); + assertEquals(4, reader.read(chars)); + } + } + + public void testInvalidMark() throws IOException { + try (Reader reader = new CharBufferReader(CharBuffer.wrap("test"))) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> reader.mark(-1)); + assertEquals("read ahead limit cannot be negative", exception.getMessage()); + } + } + + public void testClose() throws IOException { + char[] chars = new char[2]; + try (Reader reader = new CharBufferReader(CharBuffer.wrap("test"))) { + assertEquals(2, reader.read(chars)); + assertArrayEquals("te".toCharArray(), chars); + reader.close(); + Arrays.fill(chars, '\0'); + assertEquals(-1, reader.read(chars)); + assertArrayEquals("\0\0".toCharArray(), chars); + } + } +}