diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/Smile7BitBinaryCodec.java b/smile/src/main/java/tools/jackson/dataformat/smile/Smile7BitBinaryCodec.java new file mode 100644 index 000000000..60fb08029 --- /dev/null +++ b/smile/src/main/java/tools/jackson/dataformat/smile/Smile7BitBinaryCodec.java @@ -0,0 +1,103 @@ +package tools.jackson.dataformat.smile; + +/** + * Helper for Smile's "7-bit safe" binary encoding, which packs 7 payload bytes + * into 8 output bytes of 7 significant bits each (see + * {@link SmileWriteFeature#ENCODE_BINARY_AS_7BIT}, enabled by default). + *
+ * Both directions are done with SWAR bit manipulation over a single 8-byte load + * and a single 8-byte store, replacing the byte-at-a-time shifting the callers + * would otherwise do. The load and store go through {@link SmileVarHandleUtil}; + * where that is unusable this deliberately does NOT fall back to + * {@link tools.jackson.core.util.ByteArrayUtil}: composing the 8-byte load and + * store out of shifts costs more per chunk than the per-byte loop it would be + * replacing (measured on JDK 17, 0.79x on encode and 0.72x on decode, i.e. a + * slowdown), so both methods simply report failure and the caller runs its own + * loop, which stays the faster path there. + *
+ * Note that the SWAR forms mask each input byte to 7 bits. For well-formed Smile + * content, where the high bit is always clear, that is a no-op. For corrupt + * content the two paths can differ -- but so does the value they decode to, + * which is meaningless either way. + * + * @since 3.3 + */ +final class Smile7BitBinaryCodec +{ + /** + * Whether {@code VarHandle}-based array access is usable on this runtime; + * probed once at class load, using the same pattern as + * {@code SmileParserBase._decodeQuad()}. + */ + 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; + } + } + + private Smile7BitBinaryCodec() { } + + /** + * Decodes 8 encoded bytes at {@code inPtr} into the 7 payload bytes they + * represent, writing them at {@code outPtr}. + *
+ * The store writes a full 8 bytes: the 7 wanted ones plus a trailing zero. + * That byte is either overwritten by the next chunk or lies past the end of + * the decoded content, but it still has to be inside {@code out} -- hence + * the bounds check, which also rejects a final chunk that ends flush with + * the output array. + * + * @return {@code true} if the 7 bytes were written; {@code false} if the + * caller must decode this chunk itself + */ + static boolean decodeChunk(byte[] in, int inPtr, byte[] out, int outPtr) + { + if (!VARHANDLE_AVAILABLE + || ((inPtr + 8) > in.length) || ((outPtr + 8) > out.length)) { + return false; + } + long v = SmileVarHandleUtil.getLongBE(in, inPtr); + // Compact 8 x 7 bits down to 56, doubling the field width each round + v = ((v & 0x7F007F007F007F00L) >>> 1) | (v & 0x007F007F007F007FL); + v = ((v & 0x3FFF00003FFF0000L) >>> 2) | (v & 0x00003FFF00003FFFL); + v = ((v & 0x0FFFFFFF00000000L) >>> 4) | (v & 0x0FFFFFFFL); + // Left-align the 56 bits so they land in the first 7 bytes written + SmileVarHandleUtil.setLongBE(out, outPtr, v << 8); + return true; + } + + /** + * Encodes the 7 payload bytes at {@code inPtr} as the 8 bytes of 7 + * significant bits they become, writing them at {@code outPtr}. + *
+ * The load reads a full 8 bytes and discards the last, so -- as in + * {@link #decodeChunk} -- the extra byte has to be inside {@code in}. + * + * @return {@code true} if the 8 bytes were written; {@code false} if the + * caller must encode this chunk itself + */ + static boolean encodeChunk(byte[] in, int inPtr, byte[] out, int outPtr) + { + if (!VARHANDLE_AVAILABLE + || ((inPtr + 8) > in.length) || ((outPtr + 8) > out.length)) { + return false; + } + // Drop the 8th byte: we want the 56 bits of the 7 payload bytes + long v = SmileVarHandleUtil.getLongBE(in, inPtr) >>> 8; + // Spread 56 bits back out into 8 x 7, halving the field width each round + v = ((v & (0x0FFFFFFFL << 28)) << 4) | (v & 0x0FFFFFFFL); + v = ((v & 0x0FFFC0000FFFC000L) << 2) | (v & 0x00003FFF00003FFFL); + v = ((v & 0x3F803F803F803F80L) << 1) | (v & 0x007F007F007F007FL); + SmileVarHandleUtil.setLongBE(out, outPtr, v); + return true; + } +} diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java b/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java index fce5d2b4e..a52398141 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileGenerator.java @@ -2297,21 +2297,26 @@ protected void _write7BitBinaryWithLength(byte[] data, int offset, int len) thro if ((_outputTail + 8) >= _outputEnd) { _flushBuffer(); } - int i = data[offset++]; // 1st byte - _outputBuffer[_outputTail++] = (byte) ((i >> 1) & 0x7F); - i = (i << 8) | (data[offset++] & 0xFF); // 2nd - _outputBuffer[_outputTail++] = (byte) ((i >> 2) & 0x7F); - i = (i << 8) | (data[offset++] & 0xFF); // 3rd - _outputBuffer[_outputTail++] = (byte) ((i >> 3) & 0x7F); - i = (i << 8) | (data[offset++] & 0xFF); // 4th - _outputBuffer[_outputTail++] = (byte) ((i >> 4) & 0x7F); - i = (i << 8) | (data[offset++] & 0xFF); // 5th - _outputBuffer[_outputTail++] = (byte) ((i >> 5) & 0x7F); - i = (i << 8) | (data[offset++] & 0xFF); // 6th - _outputBuffer[_outputTail++] = (byte) ((i >> 6) & 0x7F); - i = (i << 8) | (data[offset++] & 0xFF); // 7th - _outputBuffer[_outputTail++] = (byte) ((i >> 7) & 0x7F); - _outputBuffer[_outputTail++] = (byte) (i & 0x7F); + if (Smile7BitBinaryCodec.encodeChunk(data, offset, _outputBuffer, _outputTail)) { + offset += 7; + _outputTail += 8; + } else { + int i = data[offset++]; // 1st byte + _outputBuffer[_outputTail++] = (byte) ((i >> 1) & 0x7F); + i = (i << 8) | (data[offset++] & 0xFF); // 2nd + _outputBuffer[_outputTail++] = (byte) ((i >> 2) & 0x7F); + i = (i << 8) | (data[offset++] & 0xFF); // 3rd + _outputBuffer[_outputTail++] = (byte) ((i >> 3) & 0x7F); + i = (i << 8) | (data[offset++] & 0xFF); // 4th + _outputBuffer[_outputTail++] = (byte) ((i >> 4) & 0x7F); + i = (i << 8) | (data[offset++] & 0xFF); // 5th + _outputBuffer[_outputTail++] = (byte) ((i >> 5) & 0x7F); + i = (i << 8) | (data[offset++] & 0xFF); // 6th + _outputBuffer[_outputTail++] = (byte) ((i >> 6) & 0x7F); + i = (i << 8) | (data[offset++] & 0xFF); // 7th + _outputBuffer[_outputTail++] = (byte) ((i >> 7) & 0x7F); + _outputBuffer[_outputTail++] = (byte) (i & 0x7F); + } len -= 7; } // and then partial piece, if any @@ -2378,21 +2383,26 @@ protected int _write7BitBinaryWithLength(InputStream in, int bytesLeft, byte[] b if ((_outputTail + 8) >= _outputEnd) { _flushBuffer(); } - int i = buffer[inputPtr++]; // 1st byte - _outputBuffer[_outputTail++] = (byte) ((i >> 1) & 0x7F); - i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 2nd - _outputBuffer[_outputTail++] = (byte) ((i >> 2) & 0x7F); - i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 3rd - _outputBuffer[_outputTail++] = (byte) ((i >> 3) & 0x7F); - i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 4th - _outputBuffer[_outputTail++] = (byte) ((i >> 4) & 0x7F); - i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 5th - _outputBuffer[_outputTail++] = (byte) ((i >> 5) & 0x7F); - i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 6th - _outputBuffer[_outputTail++] = (byte) ((i >> 6) & 0x7F); - i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 7th - _outputBuffer[_outputTail++] = (byte) ((i >> 7) & 0x7F); - _outputBuffer[_outputTail++] = (byte) (i & 0x7F); + if (Smile7BitBinaryCodec.encodeChunk(buffer, inputPtr, _outputBuffer, _outputTail)) { + inputPtr += 7; + _outputTail += 8; + } else { + int i = buffer[inputPtr++]; // 1st byte + _outputBuffer[_outputTail++] = (byte) ((i >> 1) & 0x7F); + i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 2nd + _outputBuffer[_outputTail++] = (byte) ((i >> 2) & 0x7F); + i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 3rd + _outputBuffer[_outputTail++] = (byte) ((i >> 3) & 0x7F); + i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 4th + _outputBuffer[_outputTail++] = (byte) ((i >> 4) & 0x7F); + i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 5th + _outputBuffer[_outputTail++] = (byte) ((i >> 5) & 0x7F); + i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 6th + _outputBuffer[_outputTail++] = (byte) ((i >> 6) & 0x7F); + i = (i << 8) | (buffer[inputPtr++] & 0xFF); // 7th + _outputBuffer[_outputTail++] = (byte) ((i >> 7) & 0x7F); + _outputBuffer[_outputTail++] = (byte) (i & 0x7F); + } bytesLeft -= 7; } 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 f46f41496..518a8dead 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/SmileParser.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileParser.java @@ -1561,24 +1561,29 @@ private void _readBinaryEncoded(OutputStream out, int length, byte[] buffer) thr if ((_inputEnd - _inputPtr) < 8) { _loadToHaveAtLeast(8); } - int i1 = (_inputBuffer[_inputPtr++] << 25) - + (_inputBuffer[_inputPtr++] << 18) - + (_inputBuffer[_inputPtr++] << 11) - + (_inputBuffer[_inputPtr++] << 4); - int x = _inputBuffer[_inputPtr++]; - i1 += x >> 3; - int i2 = ((x & 0x7) << 21) - + (_inputBuffer[_inputPtr++] << 14) - + (_inputBuffer[_inputPtr++] << 7) - + _inputBuffer[_inputPtr++]; - // Ok: got our 7 bytes, just need to split, copy - buffer[outPtr++] = (byte)(i1 >> 24); - buffer[outPtr++] = (byte)(i1 >> 16); - buffer[outPtr++] = (byte)(i1 >> 8); - buffer[outPtr++] = (byte)i1; - buffer[outPtr++] = (byte)(i2 >> 16); - buffer[outPtr++] = (byte)(i2 >> 8); - buffer[outPtr++] = (byte)i2; + if (Smile7BitBinaryCodec.decodeChunk(_inputBuffer, _inputPtr, buffer, outPtr)) { + _inputPtr += 8; + outPtr += 7; + } else { + int i1 = (_inputBuffer[_inputPtr++] << 25) + + (_inputBuffer[_inputPtr++] << 18) + + (_inputBuffer[_inputPtr++] << 11) + + (_inputBuffer[_inputPtr++] << 4); + int x = _inputBuffer[_inputPtr++]; + i1 += x >> 3; + int i2 = ((x & 0x7) << 21) + + (_inputBuffer[_inputPtr++] << 14) + + (_inputBuffer[_inputPtr++] << 7) + + _inputBuffer[_inputPtr++]; + // Ok: got our 7 bytes, just need to split, copy + buffer[outPtr++] = (byte)(i1 >> 24); + buffer[outPtr++] = (byte)(i1 >> 16); + buffer[outPtr++] = (byte)(i1 >> 8); + buffer[outPtr++] = (byte)i1; + buffer[outPtr++] = (byte)(i2 >> 16); + buffer[outPtr++] = (byte)(i2 >> 8); + buffer[outPtr++] = (byte)i2; + } length -= 7; // ensure there's always room for at least 7 bytes more after looping: if (outPtr > lastSafeOut) { @@ -2973,24 +2978,29 @@ private final byte[] _read7BitBinaryWithLength() throws JacksonException _reportIncompleteBinaryRead7Bit(byteLen, ptr); } } - int i1 = (_inputBuffer[_inputPtr++] << 25) - + (_inputBuffer[_inputPtr++] << 18) - + (_inputBuffer[_inputPtr++] << 11) - + (_inputBuffer[_inputPtr++] << 4); - int x = _inputBuffer[_inputPtr++]; - i1 += x >> 3; - int i2 = ((x & 0x7) << 21) - + (_inputBuffer[_inputPtr++] << 14) - + (_inputBuffer[_inputPtr++] << 7) - + _inputBuffer[_inputPtr++]; - // Ok: got our 7 bytes, just need to split, copy - result[ptr++] = (byte)(i1 >> 24); - result[ptr++] = (byte)(i1 >> 16); - result[ptr++] = (byte)(i1 >> 8); - result[ptr++] = (byte)i1; - result[ptr++] = (byte)(i2 >> 16); - result[ptr++] = (byte)(i2 >> 8); - result[ptr++] = (byte)i2; + if (Smile7BitBinaryCodec.decodeChunk(_inputBuffer, _inputPtr, result, ptr)) { + _inputPtr += 8; + ptr += 7; + } else { + int i1 = (_inputBuffer[_inputPtr++] << 25) + + (_inputBuffer[_inputPtr++] << 18) + + (_inputBuffer[_inputPtr++] << 11) + + (_inputBuffer[_inputPtr++] << 4); + int x = _inputBuffer[_inputPtr++]; + i1 += x >> 3; + int i2 = ((x & 0x7) << 21) + + (_inputBuffer[_inputPtr++] << 14) + + (_inputBuffer[_inputPtr++] << 7) + + _inputBuffer[_inputPtr++]; + // Ok: got our 7 bytes, just need to split, copy + result[ptr++] = (byte)(i1 >> 24); + result[ptr++] = (byte)(i1 >> 16); + result[ptr++] = (byte)(i1 >> 8); + result[ptr++] = (byte)i1; + result[ptr++] = (byte)(i2 >> 16); + result[ptr++] = (byte)(i2 >> 8); + result[ptr++] = (byte)i2; + } } // and then leftovers: n+1 bytes to decode n bytes int toDecode = (result.length - ptr); @@ -3032,25 +3042,30 @@ protected byte[] _finishBinary7BitLong(final int expLen) throws JacksonException _reportIncompleteBinaryRead7Bit(expLen, bb.size() + bufPtr); } } - int i1 = (_inputBuffer[_inputPtr++] << 25) - + (_inputBuffer[_inputPtr++] << 18) - + (_inputBuffer[_inputPtr++] << 11) - + (_inputBuffer[_inputPtr++] << 4); - int x = _inputBuffer[_inputPtr++]; - i1 += x >> 3; - int i2 = ((x & 0x7) << 21) - + (_inputBuffer[_inputPtr++] << 14) - + (_inputBuffer[_inputPtr++] << 7) - + _inputBuffer[_inputPtr++]; - // Ok: got our 7 bytes, just need to split, copy - // NOTE: lgtm cannot deduce the checks but a single bounds check IS enough here - buffer[bufPtr++] = (byte)(i1 >> 24); - buffer[bufPtr++] = (byte)(i1 >> 16); // lgtm [java/index-out-of-bounds] - buffer[bufPtr++] = (byte)(i1 >> 8); // lgtm [java/index-out-of-bounds] - buffer[bufPtr++] = (byte)i1; // lgtm [java/index-out-of-bounds] - buffer[bufPtr++] = (byte)(i2 >> 16); // lgtm [java/index-out-of-bounds] - buffer[bufPtr++] = (byte)(i2 >> 8); // lgtm [java/index-out-of-bounds] - buffer[bufPtr++] = (byte)i2; // lgtm [java/index-out-of-bounds] + if (Smile7BitBinaryCodec.decodeChunk(_inputBuffer, _inputPtr, buffer, bufPtr)) { + _inputPtr += 8; + bufPtr += 7; + } else { + int i1 = (_inputBuffer[_inputPtr++] << 25) + + (_inputBuffer[_inputPtr++] << 18) + + (_inputBuffer[_inputPtr++] << 11) + + (_inputBuffer[_inputPtr++] << 4); + int x = _inputBuffer[_inputPtr++]; + i1 += x >> 3; + int i2 = ((x & 0x7) << 21) + + (_inputBuffer[_inputPtr++] << 14) + + (_inputBuffer[_inputPtr++] << 7) + + _inputBuffer[_inputPtr++]; + // Ok: got our 7 bytes, just need to split, copy + // NOTE: lgtm cannot deduce the checks but a single bounds check IS enough here + buffer[bufPtr++] = (byte)(i1 >> 24); + buffer[bufPtr++] = (byte)(i1 >> 16); // lgtm [java/index-out-of-bounds] + buffer[bufPtr++] = (byte)(i1 >> 8); // lgtm [java/index-out-of-bounds] + buffer[bufPtr++] = (byte)i1; // lgtm [java/index-out-of-bounds] + buffer[bufPtr++] = (byte)(i2 >> 16); // lgtm [java/index-out-of-bounds] + buffer[bufPtr++] = (byte)(i2 >> 8); // lgtm [java/index-out-of-bounds] + buffer[bufPtr++] = (byte)i2; // lgtm [java/index-out-of-bounds] + } if (bufPtr >= buffer.length) { bb.write(buffer, 0, bufPtr); bufPtr = 0; diff --git a/smile/src/main/java/tools/jackson/dataformat/smile/SmileVarHandleUtil.java b/smile/src/main/java/tools/jackson/dataformat/smile/SmileVarHandleUtil.java index 511e802c0..ad939bd60 100644 --- a/smile/src/main/java/tools/jackson/dataformat/smile/SmileVarHandleUtil.java +++ b/smile/src/main/java/tools/jackson/dataformat/smile/SmileVarHandleUtil.java @@ -5,8 +5,8 @@ import java.nio.ByteOrder; /** - * Utility class that provides {@link VarHandle}-based access for reading - * multi-byte primitives out of byte arrays. + * Utility class that provides {@link VarHandle}-based access for reading and + * writing multi-byte primitives on 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} @@ -16,7 +16,8 @@ *
+ * Same constraints as {@link #getIntBE}; caller MUST have verified that + * {@code offset+8} is within bounds of given array. + */ + static long getLongBE(byte[] buffer, int offset) { + return (long) LONG_BE.get(buffer, offset); + } + + /** + * Writes given {@code long} as 8 big-endian bytes starting at given offset. + *
+ * Same constraints as {@link #getIntBE}; caller MUST have verified that + * {@code offset+8} is within bounds of given array. + */ + static void setLongBE(byte[] buffer, int offset, long value) { + LONG_BE.set(buffer, offset, value); + } } diff --git a/smile/src/test/java/tools/jackson/dataformat/smile/gen/Binary7BitRoundtripTest.java b/smile/src/test/java/tools/jackson/dataformat/smile/gen/Binary7BitRoundtripTest.java new file mode 100644 index 000000000..b5aae2a50 --- /dev/null +++ b/smile/src/test/java/tools/jackson/dataformat/smile/gen/Binary7BitRoundtripTest.java @@ -0,0 +1,145 @@ +package tools.jackson.dataformat.smile.gen; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; + +import tools.jackson.dataformat.smile.BaseTestForSmile; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Round-trip coverage for Smile's "7-bit safe" binary encoding, which packs 7 + * payload bytes into 8 encoded bytes. Exercises every combination of full and + * partial trailing chunk against each of the four code paths (byte[] vs + * InputStream on write, in-memory vs streaming on read), since those paths take + * a SWAR fast path for whole chunks and fall back to byte-at-a-time for chunks + * that would over-read or over-write the buffer. + */ +public class Binary7BitRoundtripTest extends BaseTestForSmile +{ + // Every length across the first few chunk boundaries, then sizes that push + // past the parser's internal buffers and the "long binary" threshold + private final static int[] LENGTHS = _lengths(); + + private static int[] _lengths() { + int[] big = { 500, 999, 1000, 1001, 2000, 4096, 7 * 128, (7 * 128) + 1, + 50_000, 260_000 }; + int[] all = new int[64 + big.length]; + for (int i = 0; i < 64; ++i) { + all[i] = i; + } + System.arraycopy(big, 0, all, 64, big.length); + return all; + } + + @Test + public void testFromByteArrayReadInMemory() throws Exception { + for (int len : LENGTHS) { + byte[] data = _data(len); + assertArrayEquals(data, _readInMemory(_writeFromArray(data)), "length "+len); + } + } + + @Test + public void testFromByteArrayReadStreaming() throws Exception { + for (int len : LENGTHS) { + byte[] data = _data(len); + assertArrayEquals(data, _readStreaming(_writeFromArray(data)), "length "+len); + } + } + + @Test + public void testFromInputStreamReadInMemory() throws Exception { + for (int len : LENGTHS) { + byte[] data = _data(len); + assertArrayEquals(data, _readInMemory(_writeFromStream(data)), "length "+len); + } + } + + @Test + public void testFromInputStreamReadStreaming() throws Exception { + for (int len : LENGTHS) { + byte[] data = _data(len); + assertArrayEquals(data, _readStreaming(_writeFromStream(data)), "length "+len); + } + } + + /** + * The encoded form itself must not change: the SWAR and byte-shifting paths + * have to agree byte for byte, and both have to agree with what previous + * versions wrote. + */ + @Test + public void testEncodedFormIsStable() throws Exception { + // 7 payload bytes with bits set across every 7-bit group boundary + byte[] data = new byte[] { (byte) 0xFF, 0x00, (byte) 0xAA, 0x55, + (byte) 0x80, 0x7F, (byte) 0xC3 }; + byte[] encoded = _writeFromArray(data); + // trailing 8 bytes are the single encoded chunk + byte[] chunk = new byte[8]; + System.arraycopy(encoded, encoded.length-8, chunk, 0, 8); + + // Reference: accumulate all 56 bits, then split into 7-bit groups + long bits = 0L; + for (byte b : data) { + bits = (bits << 8) | (b & 0xFF); + } + for (int i = 0; i < 8; ++i) { + assertEquals((int) ((bits >>> (7 * (7-i))) & 0x7F), chunk[i] & 0xFF, + "encoded byte #"+i); + } + assertArrayEquals(data, _readInMemory(encoded)); + } + + private byte[] _data(int len) { + // Deterministic, and full-range so the high bit of every payload byte + // gets exercised (encoded bytes must still stay 7-bit) + byte[] data = new byte[len]; + new Random(len).nextBytes(data); + return data; + } + + private byte[] _writeFromArray(byte[] data) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (JsonGenerator g = _smileGenerator(out, true)) { + g.writeBinary(data); + } + return out.toByteArray(); + } + + private byte[] _writeFromStream(byte[] data) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (JsonGenerator g = _smileGenerator(out, true)) { + g.writeBinary(new ByteArrayInputStream(data), data.length); + } + return out.toByteArray(); + } + + private byte[] _readInMemory(byte[] doc) throws Exception { + try (JsonParser p = _smileParser(doc)) { + assertToken(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken()); + byte[] result = p.getBinaryValue(); + assertEquals(null, p.nextToken()); + return result; + } + } + + private byte[] _readStreaming(byte[] doc) throws Exception { + try (JsonParser p = _smileParser(doc)) { + assertToken(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int count = p.readBinaryValue(out); + assertEquals(out.size(), count); + assertEquals(null, p.nextToken()); + return out.toByteArray(); + } + } +}