From c7216c6fe50f9c9dfd6c0b00d21759b6626da898 Mon Sep 17 00:00:00 2001 From: Dongnyoung Date: Sun, 30 Aug 2026 17:48:51 +0900 Subject: [PATCH 1/2] Optimize async short ASCII string decoding --- .../async/NonBlockingByteArrayParser.java | 26 ++----------- .../smile/async/SimpleStringArrayTest.java | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java b/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java index ceb55b9b7..aea407b55 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java @@ -6,6 +6,7 @@ import java.io.OutputStream; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import tools.jackson.core.JacksonException; @@ -1583,28 +1584,9 @@ private final JsonToken _finish7BitBinaryBody() throws JacksonException private final String _decodeASCIIText(byte[] inBuf, int inPtr, int len) throws JacksonException { // note: caller ensures we have enough bytes available - char[] outBuf = _textBuffer.emptyAndGetCurrentSegment(); - int outPtr = 0; - - // loop unrolling seems to help here: - for (int inEnd = inPtr + len - 3; inPtr < inEnd; ) { - outBuf[outPtr++] = (char) inBuf[inPtr++]; - outBuf[outPtr++] = (char) inBuf[inPtr++]; - outBuf[outPtr++] = (char) inBuf[inPtr++]; - outBuf[outPtr++] = (char) inBuf[inPtr++]; - } - int left = (len & 3); - if (left > 0) { - outBuf[outPtr++] = (char) inBuf[inPtr++]; - if (left > 1) { - outBuf[outPtr++] = (char) inBuf[inPtr++]; - if (left > 2) { - outBuf[outPtr++] = (char) inBuf[inPtr++]; - } - } - } - _textBuffer.setCurrentLength(len); - return _textBuffer.contentsAsString(); + String str = new String(inBuf, inPtr, len, StandardCharsets.US_ASCII); + _textBuffer.resetWithString(str); + return str; } /** diff --git a/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java b/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java index 4fcfb54bd..3580e5320 100644 --- a/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java +++ b/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java @@ -57,6 +57,35 @@ public void testShortAsciiStrings() throws IOException _testStrings(input, data, 1, 1); } + @Test + public void testShortAsciiStringAccessorsWithContiguousInput() throws IOException + { + final int[] lengths = { 1, 2, 3, 4, 31, 32, 33, 63, 64 }; + final String[] input = new String[lengths.length]; + for (int i = 0; i < lengths.length; ++i) { + input[i] = _ascii(lengths[i]); + } + byte[] data = _stringDoc(_smileWriter(true), input); + + AsyncReaderWrapper r = asyncForBytes(_smileReader(true), data.length + 1, data, 0); + assertNull(r.currentToken()); + assertToken(JsonToken.START_ARRAY, r.nextToken()); + for (String value : input) { + assertToken(JsonToken.VALUE_STRING, r.nextToken()); + + assertEquals(value, r.currentText()); + assertEquals(value.length(), r.parser().getStringLength()); + + final char[] ch = r.parser().getStringCharacters(); + final int offset = r.parser().getStringOffset(); + final int len = r.parser().getStringLength(); + assertEquals(value, new String(ch, offset, len)); + } + assertToken(JsonToken.END_ARRAY, r.nextToken()); + assertNull(r.nextToken()); + assertTrue(r.isClosed()); + } + @Test public void testShortUnicodeStrings() throws IOException { @@ -203,4 +232,13 @@ private byte[] _stringDoc(ObjectWriter w, String[] input) throws IOException g.close(); return bytes.toByteArray(); } + + private String _ascii(int len) + { + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; ++i) { + sb.append((char) ('a' + (i % 26))); + } + return sb.toString(); + } } From 3403f501a990ba1e8878abced9e0d3d6dce70442 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 4 Sep 2026 17:29:35 -0700 Subject: [PATCH 2/2] Release notes, fix --- release-notes/VERSION | 4 ++ .../async/NonBlockingByteArrayParser.java | 2 +- .../smile/async/SimpleStringArrayTest.java | 50 ++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/release-notes/VERSION b/release-notes/VERSION index 221f94d53..ab35588bc 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -32,6 +32,10 @@ implementations) (fix by @cowtowncoder, w/ Claude code) #762: (avro) Use `VarHandle` for `float`/`double` reads in Avro parser (contributed by @pjfanning) +#767: (smile) Use more efficient `String` construction wrt "Compact Strings" + for "short" ASCII text values of async parser; also fixes decoding of such + values that are split across input feeds + (fix by @cowtowncoder, w/ Claude code) 3.2.3 (not yet released) diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java b/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java index f0ee2ad83..6ce2d37ed 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingByteArrayParser.java @@ -294,7 +294,7 @@ protected final JsonToken _finishToken() throws JacksonException if (avail >= needed) { // got it all System.arraycopy(_inputBuffer, _inputPtr, _inputCopy, _inputCopyLen, needed); _inputPtr += needed; - String text = (_minorState == MINOR_PROPERTY_NAME_SHORT_ASCII) + String text = (_minorState == MINOR_VALUE_STRING_SHORT_ASCII) ? _decodeASCIIText(_inputCopy, 0, fullLen) : _decodeShortUnicodeText(_inputCopy, 0, fullLen); if (_seenStringValueCount >= 0) { // shared String values enabled diff --git a/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java b/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java index 3580e5320..0f866716e 100644 --- a/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java +++ b/smile/src/test/java/tools/jackson/dataformat/smile/async/SimpleStringArrayTest.java @@ -58,7 +58,7 @@ public void testShortAsciiStrings() throws IOException } @Test - public void testShortAsciiStringAccessorsWithContiguousInput() throws IOException + public void testShortAsciiStringAccessors() throws IOException { final int[] lengths = { 1, 2, 3, 4, 31, 32, 33, 63, 64 }; final String[] input = new String[lengths.length]; @@ -67,7 +67,16 @@ public void testShortAsciiStringAccessorsWithContiguousInput() throws IOExceptio } byte[] data = _stringDoc(_smileWriter(true), input); - AsyncReaderWrapper r = asyncForBytes(_smileReader(true), data.length + 1, data, 0); + // Contiguous input, but also chunked, to cover split-across-feeds decoding + _testShortAsciiStringAccessors(input, data, data.length + 1); + _testShortAsciiStringAccessors(input, data, 3); + _testShortAsciiStringAccessors(input, data, 1); + } + + private void _testShortAsciiStringAccessors(String[] input, byte[] data, int readSize) + throws IOException + { + AsyncReaderWrapper r = asyncForBytes(_smileReader(true), readSize, data, 0); assertNull(r.currentToken()); assertToken(JsonToken.START_ARRAY, r.nextToken()); for (String value : input) { @@ -86,6 +95,43 @@ public void testShortAsciiStringAccessorsWithContiguousInput() throws IOExceptio assertTrue(r.isClosed()); } + // [dataformats-binary#767]: short ASCII value split across feeds must decode + // the same as one fed contiguously (it used to take the Unicode path instead) + @Test + public void testShortAsciiValueChunkIndependence() throws IOException + { + byte[] data = _stringDoc(_smileWriter(true), new String[] { "abcd" }); + // Corrupt one content byte so ASCII and Unicode decoding disagree + int ix = _lastIndexOf(data, (byte) 'b'); + assertTrue(ix > 0, "Should find content byte to corrupt"); + data[ix] = (byte) 0xC5; + + String contiguous = _readSingleString(data, data.length + 1); + assertEquals(contiguous, _readSingleString(data, 3)); + assertEquals(contiguous, _readSingleString(data, 1)); + } + + private String _readSingleString(byte[] data, int readSize) throws IOException + { + AsyncReaderWrapper r = asyncForBytes(_smileReader(true), readSize, data, 0); + assertToken(JsonToken.START_ARRAY, r.nextToken()); + assertToken(JsonToken.VALUE_STRING, r.nextToken()); + String text = r.currentText(); + assertToken(JsonToken.END_ARRAY, r.nextToken()); + r.close(); + return text; + } + + private int _lastIndexOf(byte[] data, byte b) + { + for (int i = data.length; --i >= 0; ) { + if (data[i] == b) { + return i; + } + } + return -1; + } + @Test public void testShortUnicodeStrings() throws IOException {