Skip to content

Commit 873d0be

Browse files
committed
Merge branch '2.x' into 3.2
2 parents 11e920c + dff0fb5 commit 873d0be

7 files changed

Lines changed: 151 additions & 4 deletions

File tree

release-notes/CREDITS-2.x

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,3 +520,13 @@ Mike Pedersen (@mpdncrwd)
520520
* Reported #1581: `NonBlockingByteBufferParser`: Unexpected Illegal surrogate
521521
character when parsing field names
522522
(2.21.3)
523+
524+
Patrick Strawderman (@kilink)
525+
* Requested #1622: `UTF8JsonGenerator.writeBinary()` should allocate buffer
526+
based on supplied length
527+
(2.23.0)
528+
529+
@seonwooj0810
530+
* Contributed #1622: `UTF8JsonGenerator.writeBinary()` should allocate buffer
531+
based on supplied length
532+
(2.23.0)

release-notes/VERSION-2.x

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ a pure JSON library.
1616

1717
2.23.0 (not yet released)
1818

19-
No changes since 2.22
19+
#1622: `UTF8JsonGenerator.writeBinary()` should allocate buffer
20+
based on supplied length
21+
(requested by @kilink)
22+
(contributed by @seonwooj0810)
2023

2124
2.22.0 (03-Jun-2026)
2225

src/main/java/tools/jackson/core/json/JsonGeneratorBase.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ public abstract class JsonGeneratorBase extends GeneratorBase
1919
/**********************************************************************
2020
*/
2121

22+
/**
23+
* Maximum size, in bytes, of the recyclable base64 encoding buffer to
24+
* allocate when a binary content length hint is available (see
25+
* {@code writeBinary(Base64Variant, InputStream, int)}). Allocating a
26+
* larger buffer for big content reduces the number of
27+
* {@link java.io.InputStream} reads required, but the size is capped to
28+
* limit retention of large {@code ThreadLocal}-recycled buffers.
29+
*
30+
* @since 2.23
31+
*/
32+
protected final static int MAX_BASE64_ENCODE_BUFFER_LENGTH = 64 * 1024;
33+
2234
/**
2335
* This is the default set of escape codes, over 7-bit ASCII range
2436
* (first 128 character codes), used for single-byte UTF-8 characters.

src/main/java/tools/jackson/core/json/UTF8JsonGenerator.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,11 @@ public int writeBinary(Base64Variant b64variant,
941941
_flushBuffer();
942942
}
943943
_outputBuffer[_outputTail++] = _quoteChar;
944-
byte[] encodingBuffer = _ioContext.allocBase64Buffer();
944+
// [core#1622]: when length is known, size the read buffer accordingly
945+
// (capped) so large content needs fewer InputStream reads
946+
byte[] encodingBuffer = (dataLength > 0)
947+
? _ioContext.allocBase64Buffer(Math.min(dataLength, MAX_BASE64_ENCODE_BUFFER_LENGTH))
948+
: _ioContext.allocBase64Buffer();
945949
int bytes;
946950
try {
947951
if (dataLength < 0) { // length unknown

src/main/java/tools/jackson/core/json/WriterBasedJsonGenerator.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,11 @@ public int writeBinary(Base64Variant b64variant,
738738
_flushBuffer();
739739
}
740740
_outputBuffer[_outputTail++] = _quoteChar;
741-
byte[] encodingBuffer = _ioContext.allocBase64Buffer();
741+
// [core#1622]: when length is known, size the read buffer accordingly
742+
// (capped) so large content needs fewer InputStream reads
743+
byte[] encodingBuffer = (dataLength > 0)
744+
? _ioContext.allocBase64Buffer(Math.min(dataLength, MAX_BASE64_ENCODE_BUFFER_LENGTH))
745+
: _ioContext.allocBase64Buffer();
742746
int bytes;
743747
try {
744748
if (dataLength < 0) { // length unknown

src/main/java/tools/jackson/core/util/BufferRecycler.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,11 @@ public interface Gettable {
8989
public final static int CHAR_NAME_COPY_BUFFER = 3;
9090

9191
// Buffer lengths
92+
// 22-Jun-2026, [core#1622]: bumped default base64 codec buffer (index
93+
// BYTE_BASE64_CODEC_BUFFER) from 2000 to 4000 to reduce InputStream
94+
// reads when encoding binary content of unknown/large length.
9295

93-
private final static int[] BYTE_BUFFER_LENGTHS = new int[] { 8000, 8000, 2000, 2000 };
96+
private final static int[] BYTE_BUFFER_LENGTHS = new int[] { 8000, 8000, 2000, 4000 };
9497
private final static int[] CHAR_BUFFER_LENGTHS = new int[] { 4000, 4000, 200, 200 };
9598

9699
// Note: changed from simple array in 2.10:
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package tools.jackson.core.unittest.base64;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.ByteArrayOutputStream;
5+
import java.io.StringWriter;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import tools.jackson.core.*;
10+
import tools.jackson.core.json.JsonFactory;
11+
import tools.jackson.core.unittest.JacksonCoreTestBase;
12+
13+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
14+
import static org.junit.jupiter.api.Assertions.assertEquals;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
17+
// [core#1622]: When a binary content length is known, the encoding/read buffer
18+
// should be sized from that hint (capped) so large content needs far fewer
19+
// InputStream reads than the small default buffer would require.
20+
class BinaryWriteBufferSize1622Test
21+
extends JacksonCoreTestBase
22+
{
23+
// Cap mirrored from JsonGeneratorImpl.MAX_BASE64_ENCODE_BUFFER_LENGTH
24+
private final static int MAX_BUFFER = 64 * 1024;
25+
26+
private final JsonFactory JSON_F = new JsonFactory();
27+
28+
private final Base64Variant VARIANT = Base64Variants.MIME;
29+
30+
/**
31+
* {@link ByteArrayInputStream} that records the largest {@code len} ever
32+
* requested via {@link #read(byte[], int, int)}, which equals the size of
33+
* the read buffer the generator allocated.
34+
*/
35+
static class ReadSizeRecordingInputStream extends ByteArrayInputStream {
36+
int maxRequestedRead = 0;
37+
38+
ReadSizeRecordingInputStream(byte[] buf) {
39+
super(buf);
40+
}
41+
42+
@Override
43+
public synchronized int read(byte[] b, int off, int len) {
44+
if (len > maxRequestedRead) {
45+
maxRequestedRead = len;
46+
}
47+
return super.read(b, off, len);
48+
}
49+
}
50+
51+
@Test
52+
void sizeHintAppliedByteBacked() throws Exception {
53+
// 50_000 is below the 64kB cap, so the read buffer should be sized to it
54+
_testSizeHint(true, 50_000, 50_000);
55+
}
56+
57+
@Test
58+
void sizeHintAppliedCharBacked() throws Exception {
59+
_testSizeHint(false, 50_000, 50_000);
60+
}
61+
62+
@Test
63+
void sizeHintCappedByteBacked() throws Exception {
64+
// 200_000 exceeds the cap, so the read buffer should be limited to it
65+
_testSizeHint(true, 200_000, MAX_BUFFER);
66+
}
67+
68+
@Test
69+
void sizeHintCappedCharBacked() throws Exception {
70+
_testSizeHint(false, 200_000, MAX_BUFFER);
71+
}
72+
73+
private void _testSizeHint(boolean useBytes, int dataLength, int expectedMaxRead)
74+
throws Exception
75+
{
76+
byte[] input = new byte[dataLength];
77+
for (int i = 0; i < input.length; ++i) {
78+
input[i] = (byte) (i * 31 + 7);
79+
}
80+
ReadSizeRecordingInputStream in = new ReadSizeRecordingInputStream(input);
81+
82+
byte[] rawJson;
83+
if (useBytes) {
84+
ByteArrayOutputStream out = new ByteArrayOutputStream();
85+
try (JsonGenerator g = JSON_F.createGenerator(ObjectWriteContext.empty(), out, JsonEncoding.UTF8)) {
86+
g.writeBinary(VARIANT, in, dataLength);
87+
}
88+
rawJson = out.toByteArray();
89+
} else {
90+
StringWriter sw = new StringWriter();
91+
try (JsonGenerator g = JSON_F.createGenerator(ObjectWriteContext.empty(), sw)) {
92+
g.writeBinary(VARIANT, in, dataLength);
93+
}
94+
rawJson = sw.toString().getBytes("UTF-8");
95+
}
96+
97+
// The generator should have requested reads as large as the (capped) hint,
98+
// which is much bigger than the small default buffer used before the fix.
99+
assertEquals(expectedMaxRead, in.maxRequestedRead,
100+
"read buffer should be sized from the length hint (capped)");
101+
assertTrue(in.maxRequestedRead <= MAX_BUFFER,
102+
"read buffer must never exceed the cap");
103+
104+
// ...and the produced base64 must still decode back to the original bytes.
105+
try (JsonParser p = JSON_F.createParser(ObjectReadContext.empty(), rawJson)) {
106+
assertEquals(JsonToken.VALUE_STRING, p.nextToken());
107+
byte[] decoded = p.getBinaryValue(VARIANT);
108+
assertArrayEquals(input, decoded);
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)