Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -293,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
Expand Down Expand Up @@ -1589,28 +1590,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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,81 @@ public void testShortAsciiStrings() throws IOException
_testStrings(input, data, 1, 1);
}

@Test
public void testShortAsciiStringAccessors() 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);

// 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) {
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());
}

// [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
{
Expand Down Expand Up @@ -203,4 +278,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();
}
}
Loading