diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 19b9d67a8..71bf6021d 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -64,3 +64,6 @@ PJ Fanning (@pjfanning) * Contributed #752: (cbor) Use `VarHandle` for multi-byte primitive writes in `CBORGenerator` (3.3.0) +* Contributed #757: (smile) Use `VarHandle` for multi-byte primitive reads in + `SmileParser` + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 16ad1bb7b..64aa59d6a 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -23,6 +23,8 @@ implementations) (contributed by @pjfanning) #752: (cbor) Use `VarHandle` for multi-byte primitive writes in `CBORGenerator` (contributed by @pjfanning) +#757: (smile) Use `VarHandle` for multi-byte primitive reads in `SmileParser` + (contributed by @pjfanning) #759: (smile) Use more efficient `String` construction wrt "Compact Strings" for "long" text values (fix by @cowtowncoder, w/ Claude code) diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/SmileParser.java b/smile/src/main/java/tools/jackson/dataformat/smile/SmileParser.java index e3b62c645..ba34ce41c 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/SmileParser.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileParser.java @@ -979,18 +979,7 @@ private final int _nextNameOptimized(PropertyNameMatcher matcher, int len) throw } // First: maybe we already have this name decoded? if (len < 5) { - int inPtr = _inputPtr; - final byte[] inBuf = _inputBuffer; - int q = inBuf[inPtr] & 0xFF; - if (len > 1) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (len > 2) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (len > 3) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - } - } - } + int q = _decodePartialQuad(_inputBuffer, _inputPtr, len); _quad1 = q; return matcher.matchByQuad(q); } @@ -999,45 +988,21 @@ private final int _nextNameOptimized(PropertyNameMatcher matcher, int len) throw int inPtr = _inputPtr; // First quadbyte is easy - int q1 = (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); + int q1 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 9) { - int q2 = (inBuf[inPtr++] & 0xFF); - int left = len - 5; - if (left > 0) { - q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 1) { - q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 2) { - q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF); - } - } - } + int q2 = _decodePartialQuad(inBuf, inPtr, len - 4); _quad1 = q1; _quad2 = q2; return matcher.matchByQuad(q1, q2); } - int q2 = (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); + int q2 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 13) { - int q3 = (inBuf[inPtr++] & 0xFF); - int left = len - 9; - if (left > 0) { - q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 1) { - q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 2) { - q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF); - } - } - } + int q3 = _decodePartialQuad(inBuf, inPtr, len - 8); _quad1 = q1; _quad2 = q2; _quad3 = q3; @@ -1070,22 +1035,12 @@ private final int _nextNameFromSymbolsLong(PropertyNameMatcher matcher, final byte[] inBuf = _inputBuffer; do { - int q = (inBuf[inPtr++] & 0xFF); - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodeQuad(inBuf, inPtr); + inPtr += 4; } while ((len -= 4) > 3); // and then leftovers if (len > 0) { - int q = inBuf[inPtr] & 0xFF; - if (len > 1) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (len > 2) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - } - } - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodePartialQuad(inBuf, inPtr, len); } return matcher.matchByQuad(_quadBuffer, offset); } @@ -2126,18 +2081,7 @@ private final String _findDecodedFromSymbols(final int len) throws JacksonExcept { // First: maybe we already have this name decoded? if (len < 5) { - int inPtr = _inputPtr; - final byte[] inBuf = _inputBuffer; - int q = _padQuadForNulls(inBuf[inPtr]); - if (len > 1) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (len > 2) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (len > 3) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - } - } - } + int q = _decodePartialQuadForNulls(_inputBuffer, _inputPtr, len); _quad1 = q; return _symbols.findName(q); } @@ -2146,45 +2090,21 @@ private final String _findDecodedFromSymbols(final int len) throws JacksonExcept int inPtr = _inputPtr; // First quadbyte is easy - int q1 = (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); - q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF); + int q1 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 9) { - int q2 = _padQuadForNulls(inBuf[inPtr++]); - int left = len - 5; - if (left > 0) { - q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 1) { - q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 2) { - q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF); - } - } - } + int q2 = _decodePartialQuadForNulls(inBuf, inPtr, len - 4); _quad1 = q1; _quad2 = q2; return _symbols.findName(q1, q2); } - int q2 = (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); - q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF); + int q2 = _decodeQuad(inBuf, inPtr); + inPtr += 4; if (len < 13) { - int q3 = _padQuadForNulls(inBuf[inPtr++]); - int left = len - 9; - if (left > 0) { - q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 1) { - q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF); - if (left > 2) { - q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF); - } - } - } + int q3 = _decodePartialQuadForNulls(inBuf, inPtr, len - 8); _quad1 = q1; _quad2 = q2; _quad3 = q3; @@ -2215,22 +2135,12 @@ private final String _findDecodedFixed12(int len, int q1, int q2) throws Jackson final byte[] inBuf = _inputBuffer; do { - int q = (inBuf[inPtr++] & 0xFF); - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - q = (q << 8) | inBuf[inPtr++] & 0xFF; - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodeQuad(inBuf, inPtr); + inPtr += 4; } while ((len -= 4) > 3); // and then leftovers if (len > 0) { - int q = _padQuadForNulls(inBuf[inPtr]); - if (len > 1) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (len > 2) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - } - } - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodePartialQuadForNulls(inBuf, inPtr, len); } return _symbols.findName(_quadBuffer, offset); } @@ -2268,8 +2178,18 @@ private final static int _padLastQuad(int q, int bytes) { return (bytes == 4) ? q : (q | (-1 << (bytes << 3))); } - private final static int _padQuadForNulls(int firstByte) { - return (firstByte & 0xFF) | 0xFFFFFF00; + /** + * Variant of {@link #_decodePartialQuad} that pads the unused high bytes with + * 1s rather than 0s, which is what {@link ByteQuadsCanonicalizer} expects of a + * partial quad: without it a name ending in NULL bytes would collide with the + * shorter name that precedes those NULLs. + * + * @param len Number of bytes to decode; must be between 1 and 4 + * + * @since 3.3 + */ + private final static int _decodePartialQuadForNulls(byte[] buffer, int offset, int len) { + return _padLastQuad(_decodePartialQuad(buffer, offset, len), len); } /* diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/SmileParserBase.java b/smile/src/main/java/tools/jackson/dataformat/smile/SmileParserBase.java index 5ebc80275..1061211f3 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/SmileParserBase.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileParserBase.java @@ -30,6 +30,27 @@ public abstract class SmileParserBase extends ParserMinimalBase protected final static JacksonFeatureSet SMILE_READ_CAPABILITIES = DEFAULT_READ_CAPABILITIES.with(StreamReadCapability.EXACT_FLOATS); + /** + * Whether {@code VarHandle}-based array access is usable on this runtime; + * probed once at class load. See {@link #_decodeQuad}. + * + * @since 3.3 + */ + private final static boolean _VARHANDLE_AVAILABLE = _checkVarHandleAvailable(); + + private static boolean _checkVarHandleAvailable() { + // NOTE: this call is what first loads `SmileVarHandleUtil`, and that class + // names `VarHandle` in its field/method signatures. On a runtime without + // `java.lang.invoke.VarHandle` (some Android builds) loading it raises + // `NoClassDefFoundError` -- an Error, not an Exception -- so `Throwable` + // is what has to be caught here. + try { + return SmileVarHandleUtil.isAvailable(); + } catch (Throwable t) { + return false; + } + } + /* /********************************************************************** /* Config @@ -720,6 +741,65 @@ protected void _reportMismatchedEndMarker(int actCh, char expCh) throws StreamRe (char) actCh, expCh, ctxt.typeDesc(), ctxt.startLocation(_sourceReference()))); } + /** + * Helper method for decoding 4 bytes of an Object property name into the + * "quad" (big-endian {@code int}) form used by {@link ByteQuadsCanonicalizer}. + *

+ * Uses a single unaligned load via {@code VarHandle} where supported, + * falling back to byte shifting otherwise. Since {@link #_VARHANDLE_AVAILABLE} + * is a {@code static final} the branch folds away at JIT time; and on the + * fallback path {@code SmileVarHandleUtil} is never resolved, which is what + * keeps this working on runtimes that lack {@code VarHandle} entirely. + *

+ * Exists on the base class so that subclasses in the {@code async} package + * can share the same implementation; caller MUST have verified that 4 bytes + * are readable at given offset. + * + * @since 3.3 + */ + protected final static int _decodeQuad(byte[] buffer, int offset) { + if (_VARHANDLE_AVAILABLE) { + return SmileVarHandleUtil.getIntBE(buffer, offset); + } + return ((buffer[offset] & 0xFF) << 24) + | ((buffer[offset+1] & 0xFF) << 16) + | ((buffer[offset+2] & 0xFF) << 8) + | (buffer[offset+3] & 0xFF); + } + + /** + * Helper method for decoding the trailing 1 - 4 bytes of an Object property + * name into a right-aligned, zero-padded "quad": that is, produces the same + * value as accumulating {@code len} bytes with 8-bit shifts would. + *

+ * Reads all 4 bytes with a single load where it can, which may read up to 3 + * bytes past {@code offset+len}. Those bytes are shifted out and cannot + * affect the result, but the read still has to stay within the array: hence + * the length check, which also selects the byte-shifting path for a name + * that ends within 3 bytes of the end of the buffer. + *

+ * Caller MUST have verified that {@code len} bytes are readable at given + * offset, and that {@code len} is between 1 and 4. + * + * @since 3.3 + */ + protected final static int _decodePartialQuad(byte[] buffer, int offset, int len) { + if ((offset + 4) <= buffer.length) { + return _decodeQuad(buffer, offset) >>> ((4 - len) << 3); + } + int q = buffer[offset] & 0xFF; + if (len > 1) { + q = (q << 8) | (buffer[offset+1] & 0xFF); + if (len > 2) { + q = (q << 8) | (buffer[offset+2] & 0xFF); + if (len > 3) { + q = (q << 8) | (buffer[offset+3] & 0xFF); + } + } + } + return q; + } + /** * Helper method used to encapsulate logic of including (or not) of * "source reference" when constructing {@link TokenStreamLocation} instances. diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/SmileVarHandleUtil.java b/smile/src/main/java/tools/jackson/dataformat/smile/SmileVarHandleUtil.java new file mode 100644 index 000000000..511e802c0 --- /dev/null +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileVarHandleUtil.java @@ -0,0 +1,65 @@ +package tools.jackson.dataformat.smile; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; + +/** + * Utility class that provides {@link VarHandle}-based access for reading + * multi-byte primitives out of byte arrays. + *

+ * IMPORTANT: this class references {@link VarHandle} in field and method + * signatures, so on a runtime that does not provide {@code java.lang.invoke.VarHandle} + * at all it will fail to link, before any code here gets a chance to run. + * Callers MUST therefore both: + *

    + *
  1. load this class from within a {@code try}/{@code catch (Throwable)} block, + * so that {@link LinkageError} is caught, and
  2. + *
  3. keep the byte-shifting fallback in a class that does not reference + * {@link VarHandle}, so the fallback path never resolves this class.
  4. + *
+ * {@code SmileParserBase._decodeQuad()} does both; see it for the pattern. + * + * @since 3.3 + */ +final class SmileVarHandleUtil +{ + /** + * VarHandle for reading 4 big-endian bytes as an {@code int}. + * {@code null} if {@code byteArrayViewVarHandle()} is unsupported. + */ + private static final VarHandle INT_BE; + + static { + VarHandle intBe = null; + try { + intBe = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.BIG_ENDIAN); + } catch (Throwable t) { + // Byte-array views not supported: caller falls back to byte shifting + } + INT_BE = intBe; + } + + private SmileVarHandleUtil() { } + + /** + * @return {@code true} if {@link #getIntBE} may be called; if {@code false}, + * caller MUST use its own byte-shifting fallback + */ + static boolean isAvailable() { + return INT_BE != null; + } + + /** + * Reads 4 bytes starting at given offset as a big-endian {@code int}. + *

+ * Only to be called if {@link #isAvailable()} returned {@code true}: the + * handle is dereferenced unconditionally, and the fallback lives in the + * caller, not here. + * Caller MUST also have verified that {@code offset+4} is within bounds of + * given array. + */ + static int getIntBE(byte[] buffer, int offset) { + return (int) INT_BE.get(buffer, offset); + } +} 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 001cce336..ceb55b9b7 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 @@ -702,22 +702,11 @@ private final JsonToken _finishLongFieldName(int outPtr) throws JacksonException int quadCount = 0; for (final int inEnd = (outPtr & ~3); in < inEnd; in += 4) { - int q = (copyBuffer[in] << 24) - | ((copyBuffer[in+1] & 0xFF) << 16) - | ((copyBuffer[in+2] & 0xFF) << 8) - | (copyBuffer[in+3] & 0xFF); - quads[quadCount++] = q; + quads[quadCount++] = _decodeQuad(copyBuffer, in); } // and possibly more... ? - if (in < outPtr) { // at least 1 - int q = copyBuffer[in++] & 0xFF; - if (in < outPtr) { // at least 2 - q = (q << 8) | (copyBuffer[in++] & 0xFF); - if (in < outPtr) { // 3 (can't be more) - q = (q << 8) | (copyBuffer[in++] & 0xFF); - } - } - quads[quadCount++] = q; + if (in < outPtr) { // 1 - 3 bytes left over + quads[quadCount++] = _decodePartialQuad(copyBuffer, in, outPtr - in); } String name = _symbols.findName(quads, quadCount); diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingParserBase.java b/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingParserBase.java index 5a56367d8..eb52e3e5a 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingParserBase.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/async/NonBlockingParserBase.java @@ -447,38 +447,14 @@ protected final String _findDecodedFromSymbols(byte[] inBuf, int inPtr, int len) { // First: maybe we already have this name decoded? if (len < 5) { - int q = inBuf[inPtr] & 0xFF; - if (--len > 0) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (--len > 0) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (--len > 0) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - } - } - } + int q = _decodePartialQuad(inBuf, inPtr, len); _quad1 = q; return _symbols.findName(q); } if (len < 9) { // First quadbyte is easy - int q1 = (inBuf[inPtr] & 0xFF) << 8; - q1 += (inBuf[++inPtr] & 0xFF); - q1 <<= 8; - q1 += (inBuf[++inPtr] & 0xFF); - q1 <<= 8; - q1 += (inBuf[++inPtr] & 0xFF); - int q2 = (inBuf[++inPtr] & 0xFF); - len -= 5; - if (len > 0) { - q2 = (q2 << 8) + (inBuf[++inPtr] & 0xFF); - if (--len > 0) { - q2 = (q2 << 8) + (inBuf[++inPtr] & 0xFF); - if (--len > 0) { - q2 = (q2 << 8) + (inBuf[++inPtr] & 0xFF); - } - } - } + int q1 = _decodeQuad(inBuf, inPtr); + int q2 = _decodePartialQuad(inBuf, inPtr+4, len - 4); _quad1 = q1; _quad2 = q2; return _symbols.findName(q1, q2); @@ -499,24 +475,12 @@ private final String _findDecodedLonger(byte[] inBuf, int inPtr, int len) throws // then decode, full quads first int offset = 0; do { - int q = (inBuf[inPtr++] & 0xFF) << 8; - q |= inBuf[inPtr++] & 0xFF; - q <<= 8; - q |= inBuf[inPtr++] & 0xFF; - q <<= 8; - q |= inBuf[inPtr++] & 0xFF; - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodeQuad(inBuf, inPtr); + inPtr += 4; } while ((len -= 4) > 3); // and then leftovers if (len > 0) { - int q = inBuf[inPtr] & 0xFF; - if (--len > 0) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - if (--len > 0) { - q = (q << 8) + (inBuf[++inPtr] & 0xFF); - } - } - _quadBuffer[offset++] = q; + _quadBuffer[offset++] = _decodePartialQuad(inBuf, inPtr, len); } return _symbols.findName(_quadBuffer, offset); } diff --git a/smile/src/test/java/tools/jackson/dataformat/smile/parse/NameQuadDecodingTest.java b/smile/src/test/java/tools/jackson/dataformat/smile/parse/NameQuadDecodingTest.java new file mode 100644 index 000000000..64e8a28b2 --- /dev/null +++ b/smile/src/test/java/tools/jackson/dataformat/smile/parse/NameQuadDecodingTest.java @@ -0,0 +1,190 @@ +package tools.jackson.dataformat.smile.parse; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.async.ByteArrayFeeder; + +import tools.jackson.dataformat.smile.BaseTestForSmile; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for the "quad" decoding shared by the blocking and non-blocking Smile + * parsers, which reads Object property names 4 bytes at a time for symbol table + * lookup. + *

+ * Trailing partial quads have two decoding paths -- a single wide load, taken + * only when 4 bytes are readable at the offset, and byte-at-a-time shifting + * otherwise -- so this covers every name length across the branch points (4, 8 + * and 12 bytes) and asserts that all three parser flavors agree. Parsing from a + * {@code byte[]} exercises the shifting path for the last name in a document + * (which ends within 3 bytes of the end of the array), while parsing the same + * bytes from an {@link java.io.InputStream} exercises the wide load. + */ +public class NameQuadDecodingTest extends BaseTestForSmile +{ + private final static int MAX_NAME_LEN = 40; + + private final static char UNICODE_2BYTES = (char) 167; // law symbol + private final static char UNICODE_3BYTES = (char) 0x4567; + + @Test + public void testAsciiNamesOfEveryLength() throws Exception + { + for (String name : _asciiNames()) { + _verifyName(name); + } + } + + @Test + public void testUnicodeNamesOfEveryLength() throws Exception + { + for (String name : _unicodeNames()) { + _verifyName(name); + } + } + + // Partial quads are padded with 1s, not 0s, so that a name with trailing + // NULLs does not collide with the shorter name that precedes them + @Test + public void testNamesWithNulls() throws Exception + { + for (int len = 1; len <= MAX_NAME_LEN; ++len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; ++i) { + sb.append('a'); + } + // "a", "a\0", "a\0\0", "a\0\0\0" and so on must all stay distinct + for (int nulls = 1; nulls <= 3; ++nulls) { + _verifyName(sb.toString() + String.valueOf(new char[nulls])); + } + } + } + + // Same names within a single document, so that symbol table interning is + // exercised across all lengths and both decoding paths + @Test + public void testAllNamesInSingleDocument() throws Exception + { + List names = new ArrayList<>(); + names.addAll(_asciiNames()); + names.addAll(_unicodeNames()); + + Map input = new LinkedHashMap<>(); + for (int i = 0; i < names.size(); ++i) { + input.put(names.get(i), i); + } + byte[] doc = smileMapper().writeValueAsBytes(input); + + for (String read : new String[] { "bytes", "stream", "async" }) { + List actual = _readNames(doc, read); + assertEquals(names, actual, "Mismatch when reading via "+read); + } + } + + /* + /********************************************************************** + /* Helper methods + /********************************************************************** + */ + + private List _asciiNames() + { + List names = new ArrayList<>(); + for (int len = 1; len <= MAX_NAME_LEN; ++len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; ++i) { + sb.append((char) ('a' + (i % 26))); + } + names.add(sb.toString()); + } + return names; + } + + // Multi-byte characters, so that byte length and character count differ + private List _unicodeNames() + { + List names = new ArrayList<>(); + for (int len = 1; len <= MAX_NAME_LEN; ++len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; ++i) { + sb.append((i % 2 == 0) ? UNICODE_2BYTES : UNICODE_3BYTES); + } + names.add(sb.toString()); + } + return names; + } + + private void _verifyName(String name) throws Exception + { + Map input = new LinkedHashMap<>(); + input.put(name, 13); + byte[] doc = smileMapper().writeValueAsBytes(input); + + for (String read : new String[] { "bytes", "stream", "async" }) { + List actual = _readNames(doc, read); + assertEquals(1, actual.size(), "Mismatch when reading via "+read); + assertEquals(name, actual.get(0), "Mismatch when reading via "+read); + } + } + + private List _readNames(byte[] doc, String read) throws Exception + { + if (read.equals("async")) { + return _readNamesAsync(doc); + } + List names = new ArrayList<>(); + try (JsonParser p = read.equals("bytes") + ? _smileParser(doc) + : _smileParser(new ByteArrayInputStream(doc))) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + while (p.nextToken() == JsonToken.PROPERTY_NAME) { + names.add(p.currentName()); + p.nextToken(); // and past the value + } + assertToken(JsonToken.END_OBJECT, p.currentToken()); + assertNull(p.nextToken()); + } + return names; + } + + // Fed one byte at a time, so names are also split across input boundaries + private List _readNamesAsync(byte[] doc) throws Exception + { + List names = new ArrayList<>(); + try (JsonParser p = smileMapper().reader().createNonBlockingByteArrayParser()) { + final ByteArrayFeeder feeder = (ByteArrayFeeder) p.nonBlockingInputFeeder(); + int offset = 0; + JsonToken t; + + while (true) { + while ((t = p.nextToken()) == JsonToken.NOT_AVAILABLE) { + if (offset < doc.length) { + assertTrue(feeder.needMoreInput()); + feeder.feedInput(doc, offset, offset+1); + ++offset; + } else { + feeder.endOfInput(); + } + } + if (t == null) { + break; + } + if (t == JsonToken.PROPERTY_NAME) { + names.add(p.currentName()); + } + } + } + return names; + } +}