diff --git a/bson/src/main/org/bson/ByteBuf.java b/bson/src/main/org/bson/ByteBuf.java
index 089bb67885..bc37102f0b 100644
--- a/bson/src/main/org/bson/ByteBuf.java
+++ b/bson/src/main/org/bson/ByteBuf.java
@@ -184,6 +184,26 @@ public interface ByteBuf {
*/
byte[] array();
+ /**
+ *
States whether this buffer is backed by an accessible byte array.
+ *
+ * If this method returns {@code true} then the {@link #array()} and {@link #arrayOffset()} methods may safely be invoked.
+ *
+ * @return {@code true} if, and only if, this buffer is backed by an array and is not read-only
+ * @since 5.5
+ */
+ boolean hasArray();
+
+ /**
+ * Returns the offset of the first byte within the backing byte array of
+ * this buffer.
+ *
+ * @throws java.nio.ReadOnlyBufferException If this buffer is backed by an array but is read-only
+ * @throws UnsupportedOperationException if this buffer is not backed by an accessible array
+ * @since 5.5
+ */
+ int arrayOffset();
+
/**
* Returns this buffer's limit.
*
diff --git a/bson/src/main/org/bson/ByteBufNIO.java b/bson/src/main/org/bson/ByteBufNIO.java
index ffb6584ac6..ba71625d76 100644
--- a/bson/src/main/org/bson/ByteBufNIO.java
+++ b/bson/src/main/org/bson/ByteBufNIO.java
@@ -132,6 +132,16 @@ public byte[] array() {
return buf.array();
}
+ @Override
+ public boolean hasArray() {
+ return buf.hasArray();
+ }
+
+ @Override
+ public int arrayOffset() {
+ return buf.arrayOffset();
+ }
+
@Override
public int limit() {
return buf.limit();
diff --git a/bson/src/main/org/bson/io/OutputBuffer.java b/bson/src/main/org/bson/io/OutputBuffer.java
index 7c1a64b2f8..e0a1a31401 100644
--- a/bson/src/main/org/bson/io/OutputBuffer.java
+++ b/bson/src/main/org/bson/io/OutputBuffer.java
@@ -197,7 +197,7 @@ public void writeLong(final long value) {
writeInt64(value);
}
- private int writeCharacters(final String str, final boolean checkForNullCharacters) {
+ protected int writeCharacters(final String str, final boolean checkForNullCharacters) {
int len = str.length();
int total = 0;
diff --git a/driver-core/src/main/com/mongodb/internal/connection/ByteBufferBsonOutput.java b/driver-core/src/main/com/mongodb/internal/connection/ByteBufferBsonOutput.java
index d53ffe7c68..a4d4bd1be5 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/ByteBufferBsonOutput.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/ByteBufferBsonOutput.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.connection;
+import org.bson.BsonSerializationException;
import org.bson.ByteBuf;
import org.bson.io.OutputBuffer;
@@ -25,8 +26,10 @@
import java.util.ArrayList;
import java.util.List;
+import static com.mongodb.assertions.Assertions.assertFalse;
import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.assertions.Assertions.notNull;
+import static java.lang.String.format;
/**
* This class is not part of the public API and may be removed or changed at any time
@@ -178,11 +181,17 @@ private ByteBuf getCurrentByteBuffer() {
return getByteBufferAtIndex(curBufferIndex);
}
+ private ByteBuf getNextByteBuffer() {
+ assertFalse(bufferList.get(curBufferIndex).hasRemaining());
+ return getByteBufferAtIndex(++curBufferIndex);
+ }
+
private ByteBuf getByteBufferAtIndex(final int index) {
if (bufferList.size() < index + 1) {
- bufferList.add(bufferProvider.getBuffer(index >= (MAX_SHIFT - INITIAL_SHIFT)
- ? MAX_BUFFER_SIZE
- : Math.min(INITIAL_BUFFER_SIZE << index, MAX_BUFFER_SIZE)));
+ ByteBuf buffer = bufferProvider.getBuffer(index >= (MAX_SHIFT - INITIAL_SHIFT)
+ ? MAX_BUFFER_SIZE
+ : Math.min(INITIAL_BUFFER_SIZE << index, MAX_BUFFER_SIZE));
+ bufferList.add(buffer);
}
return bufferList.get(index);
}
@@ -225,6 +234,16 @@ public List getByteBuffers() {
return buffers;
}
+ public List getDuplicateByteBuffers() {
+ ensureOpen();
+
+ List buffers = new ArrayList<>(bufferList.size());
+ for (final ByteBuf cur : bufferList) {
+ buffers.add(cur.duplicate().order(ByteOrder.LITTLE_ENDIAN));
+ }
+ return buffers;
+ }
+
@Override
public int pipe(final OutputStream out) throws IOException {
@@ -233,14 +252,18 @@ public int pipe(final OutputStream out) throws IOException {
byte[] tmp = new byte[INITIAL_BUFFER_SIZE];
int total = 0;
- for (final ByteBuf cur : getByteBuffers()) {
- ByteBuf dup = cur.duplicate();
- while (dup.hasRemaining()) {
- int numBytesToCopy = Math.min(dup.remaining(), tmp.length);
- dup.get(tmp, 0, numBytesToCopy);
- out.write(tmp, 0, numBytesToCopy);
+ List byteBuffers = getByteBuffers();
+ try {
+ for (final ByteBuf cur : byteBuffers) {
+ while (cur.hasRemaining()) {
+ int numBytesToCopy = Math.min(cur.remaining(), tmp.length);
+ cur.get(tmp, 0, numBytesToCopy);
+ out.write(tmp, 0, numBytesToCopy);
+ }
+ total += cur.limit();
}
- total += dup.limit();
+ } finally {
+ byteBuffers.forEach(ByteBuf::release);
}
return total;
}
@@ -360,4 +383,165 @@ private static final class BufferPositionPair {
this.position = position;
}
}
+
+ protected int writeCharacters(final String str, final boolean checkNullTermination) {
+ int stringLength = str.length();
+ int sp = 0;
+ int prevPos = position;
+
+ ByteBuf curBuffer = getCurrentByteBuffer();
+ int curBufferPos = curBuffer.position();
+ int curBufferLimit = curBuffer.limit();
+ int remaining = curBufferLimit - curBufferPos;
+
+ if (curBuffer.hasArray()) {
+ byte[] dst = curBuffer.array();
+ int arrayOffset = curBuffer.arrayOffset();
+ if (remaining >= str.length() + 1) {
+ // Write ASCII characters directly to the array until we hit a non-ASCII character.
+ sp = writeOnArrayAscii(str, dst, arrayOffset + curBufferPos, checkNullTermination);
+ curBufferPos += sp;
+ // If the whole string was written as ASCII, append the null terminator.
+ if (sp == stringLength) {
+ dst[arrayOffset + curBufferPos++] = 0;
+ position += sp + 1;
+ curBuffer.position(curBufferPos);
+ return sp + 1;
+ }
+ // Otherwise, update the position to reflect the partial write.
+ position += sp;
+ curBuffer.position(curBufferPos);
+ }
+ }
+
+ // We get here, when the buffer is not backed by an array, or when the string contains at least one non-ASCII characters.
+ return writeOnBuffers(str,
+ checkNullTermination,
+ sp,
+ stringLength,
+ curBufferLimit,
+ curBufferPos,
+ curBuffer,
+ prevPos);
+ }
+
+ private int writeOnBuffers(final String str,
+ final boolean checkNullTermination,
+ final int stringPointer,
+ final int stringLength,
+ final int bufferLimit,
+ final int bufferPos,
+ final ByteBuf buffer,
+ final int prevPos) {
+ int remaining;
+ int sp = stringPointer;
+ int curBufferPos = bufferPos;
+ int curBufferLimit = bufferLimit;
+ ByteBuf curBuffer = buffer;
+ while (sp < stringLength) {
+ remaining = curBufferLimit - curBufferPos;
+ int c = str.charAt(sp);
+
+ if (checkNullTermination && c == 0x0) {
+ throw new BsonSerializationException(
+ format("BSON cstring '%s' is not valid because it contains a null character " + "at index %d", str, sp));
+ }
+
+ if (c < 0x80) {
+ if (remaining == 0) {
+ curBuffer = getNextByteBuffer();
+ curBufferPos = 0;
+ curBufferLimit = curBuffer.limit();
+ }
+ curBuffer.put((byte) c);
+ curBufferPos++;
+ position++;
+ } else if (c < 0x800) {
+ if (remaining < 2) {
+ // Not enough space: use write() to handle buffer boundary
+ write((byte) (0xc0 + (c >> 6)));
+ write((byte) (0x80 + (c & 0x3f)));
+
+ curBuffer = getCurrentByteBuffer();
+ curBufferPos = curBuffer.position();
+ curBufferLimit = curBuffer.limit();
+ } else {
+ curBuffer.put((byte) (0xc0 + (c >> 6)));
+ curBuffer.put((byte) (0x80 + (c & 0x3f)));
+ curBufferPos += 2;
+ position += 2;
+ }
+ } else {
+ // Handle multibyte characters (may involve surrogate pairs).
+ c = Character.codePointAt(str, sp);
+ /*
+ Malformed surrogate pairs are encoded as-is (3 byte code unit) without substituting any code point.
+ This known deviation from the spec and current functionality remains for backward compatibility.
+ Ticket: JAVA-5575
+ */
+ if (c < 0x10000) {
+ if (remaining < 3) {
+ write((byte) (0xe0 + (c >> 12)));
+ write((byte) (0x80 + ((c >> 6) & 0x3f)));
+ write((byte) (0x80 + (c & 0x3f)));
+
+ curBuffer = getCurrentByteBuffer();
+ curBufferPos = curBuffer.position();
+ curBufferLimit = curBuffer.limit();
+ } else {
+ curBuffer.put((byte) (0xe0 + (c >> 12)));
+ curBuffer.put((byte) (0x80 + ((c >> 6) & 0x3f)));
+ curBuffer.put((byte) (0x80 + (c & 0x3f)));
+ curBufferPos += 3;
+ position += 3;
+ }
+ } else {
+ if (remaining < 4) {
+ write((byte) (0xf0 + (c >> 18)));
+ write((byte) (0x80 + ((c >> 12) & 0x3f)));
+ write((byte) (0x80 + ((c >> 6) & 0x3f)));
+ write((byte) (0x80 + (c & 0x3f)));
+
+ curBuffer = getCurrentByteBuffer();
+ curBufferPos = curBuffer.position();
+ curBufferLimit = curBuffer.limit();
+ } else {
+ curBuffer.put((byte) (0xf0 + (c >> 18)));
+ curBuffer.put((byte) (0x80 + ((c >> 12) & 0x3f)));
+ curBuffer.put((byte) (0x80 + ((c >> 6) & 0x3f)));
+ curBuffer.put((byte) (0x80 + (c & 0x3f)));
+ curBufferPos += 4;
+ position += 4;
+ }
+ }
+ }
+ sp += Character.charCount(c);
+ }
+
+ getCurrentByteBuffer().put((byte) 0);
+ position++;
+ return position - prevPos;
+ }
+
+ private static int writeOnArrayAscii(final String str,
+ final byte[] dst,
+ final int arrayPosition,
+ final boolean checkNullTermination) {
+ int pos = arrayPosition;
+ int sp = 0;
+ // Fast common path: This tight loop is JIT-friendly (simple, no calls, few branches),
+ // It might be unrolled for performance.
+ for (; sp < str.length(); sp++, pos++) {
+ char c = str.charAt(sp);
+ if (checkNullTermination && c == 0) {
+ throw new BsonSerializationException(
+ format("BSON cstring '%s' is not valid because it contains a null character " + "at index %d", str, sp));
+ }
+ if (c >= 0x80) {
+ break;
+ }
+ dst[pos] = (byte) c;
+ }
+ return sp;
+ }
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/CompositeByteBuf.java b/driver-core/src/main/com/mongodb/internal/connection/CompositeByteBuf.java
index 4754575336..e7e0186e12 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/CompositeByteBuf.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/CompositeByteBuf.java
@@ -213,6 +213,16 @@ public byte[] array() {
throw new UnsupportedOperationException("Not implemented yet!");
}
+ @Override
+ public boolean hasArray() {
+ return false;
+ }
+
+ @Override
+ public int arrayOffset() {
+ throw new UnsupportedOperationException("Not implemented yet!");
+ }
+
@Override
public ByteBuf limit(final int newLimit) {
if (newLimit < 0 || newLimit > capacity()) {
diff --git a/driver-core/src/main/com/mongodb/internal/connection/netty/NettyByteBuf.java b/driver-core/src/main/com/mongodb/internal/connection/netty/NettyByteBuf.java
index cb6ba58741..cbe50aaada 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/netty/NettyByteBuf.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/netty/NettyByteBuf.java
@@ -124,6 +124,16 @@ public byte[] array() {
return proxied.array();
}
+ @Override
+ public boolean hasArray() {
+ return proxied.hasArray();
+ }
+
+ @Override
+ public int arrayOffset() {
+ return proxied.arrayOffset();
+ }
+
@Override
public int limit() {
if (isWriting) {
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufSpecification.groovy
index 0e0755f65b..d052d6b23f 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufSpecification.groovy
@@ -249,11 +249,7 @@ class ByteBufSpecification extends Specification {
@Override
ByteBuf getBuffer(final int size) {
io.netty.buffer.ByteBuf buffer = allocator.directBuffer(size, size)
- try {
- new NettyByteBuf(buffer.retain())
- } finally {
- buffer.release();
- }
+ new NettyByteBuf(buffer)
}
}
}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonOutputTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonOutputTest.java
index 560e317736..bd05546111 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonOutputTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonOutputTest.java
@@ -16,16 +16,20 @@
package com.mongodb.internal.connection;
+import com.google.common.primitives.Ints;
+import com.mongodb.internal.connection.netty.NettyByteBuf;
+import io.netty.buffer.PooledByteBufAllocator;
import org.bson.BsonSerializationException;
import org.bson.ByteBuf;
import org.bson.ByteBufNIO;
+import org.bson.io.OutputBuffer;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
@@ -34,22 +38,83 @@
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
+import java.util.stream.Stream;
-import static com.mongodb.assertions.Assertions.fail;
import static com.mongodb.internal.connection.ByteBufferBsonOutput.INITIAL_BUFFER_SIZE;
import static com.mongodb.internal.connection.ByteBufferBsonOutput.MAX_BUFFER_SIZE;
+import static java.lang.Character.MAX_CODE_POINT;
+import static java.lang.Character.MAX_HIGH_SURROGATE;
+import static java.lang.Character.MAX_LOW_SURROGATE;
+import static java.lang.Character.MIN_HIGH_SURROGATE;
+import static java.lang.Character.MIN_LOW_SURROGATE;
+import static java.lang.Integer.reverseBytes;
+import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Arrays.copyOfRange;
+import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
+import static java.util.stream.IntStream.range;
+import static java.util.stream.IntStream.rangeClosed;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
final class ByteBufferBsonOutputTest {
+
+ private static final List ALL_CODE_POINTS_EXCLUDING_SURROGATES = Stream.concat(
+ range(1, MIN_HIGH_SURROGATE).boxed(),
+ rangeClosed(MAX_LOW_SURROGATE + 1, MAX_CODE_POINT).boxed())
+ .collect(toList());
+ private static final List ALL_SURROGATE_CODE_POINTS = Stream.concat(
+ range(MIN_LOW_SURROGATE, MAX_LOW_SURROGATE).boxed(),
+ range(MIN_HIGH_SURROGATE, MAX_HIGH_SURROGATE).boxed()).collect(toList());
+ public static final List ALL_UTF_16_CODE_POINTS_FORMED_BY_SURROGATE_PAIRS = rangeClosed(0x10000, MAX_CODE_POINT)
+ .boxed()
+ .collect(toList());
+
+ static Stream bufferProviders() {
+ return Stream.of(
+ size -> new NettyByteBuf(PooledByteBufAllocator.DEFAULT.directBuffer(size)),
+ size -> new NettyByteBuf(PooledByteBufAllocator.DEFAULT.heapBuffer(size)),
+ new PowerOfTwoBufferPool(),
+ size -> new ByteBufNIO(ByteBuffer.wrap(new byte[size + 5], 2, size).slice()), //different array offsets
+ size -> new ByteBufNIO(ByteBuffer.wrap(new byte[size + 4], 3, size).slice()), //different array offsets
+ size -> new ByteBufNIO(ByteBuffer.allocate(size)) {
+ @Override
+ public boolean hasArray() {
+ return false;
+ }
+
+ @Override
+ public byte[] array() {
+ return Assertions.fail("array() is called, when hasArray() returns false");
+ }
+
+ @Override
+ public int arrayOffset() {
+ return Assertions.fail("arrayOffset() is called, when hasArray() returns false");
+ }
+ }
+ );
+ }
+
+ public static Stream bufferProvidersWithBranches() {
+ List arguments = new ArrayList<>();
+ List collect = bufferProviders().collect(toList());
+ for (BufferProvider bufferProvider : collect) {
+ arguments.add(Arguments.of(true, bufferProvider));
+ arguments.add(Arguments.of(false, bufferProvider));
+ }
+ return arguments.stream();
+ }
+
+
@DisplayName("constructor should throw if buffer provider is null")
@Test
@SuppressWarnings("try")
@@ -87,7 +152,7 @@ void positionAndSizeShouldBe0AfterConstructor(final String branchState) {
break;
}
default: {
- throw fail(branchState);
+ throw com.mongodb.assertions.Assertions.fail(branchState);
}
}
assertEquals(0, out.getPosition());
@@ -97,9 +162,9 @@ void positionAndSizeShouldBe0AfterConstructor(final String branchState) {
@DisplayName("should write a byte")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteByte(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteByte(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte v = 11;
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -161,9 +226,9 @@ void shouldThrowExceptionWhenWriteByteAtInvalidPosition(final boolean useBranch)
@DisplayName("should write a bytes")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteBytes(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteBytes(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = {1, 2, 3, 4};
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -180,9 +245,9 @@ void shouldWriteBytes(final boolean useBranch) {
@DisplayName("should write bytes from offset until length")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteBytesFromOffsetUntilLength(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteBytesFromOffsetUntilLength(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = {0, 1, 2, 3, 4, 5};
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -199,9 +264,9 @@ void shouldWriteBytesFromOffsetUntilLength(final boolean useBranch) {
@DisplayName("should write a little endian Int32")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteLittleEndianInt32(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteLittleEndianInt32(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
int v = 0x1020304;
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -218,9 +283,9 @@ void shouldWriteLittleEndianInt32(final boolean useBranch) {
@DisplayName("should write a little endian Int64")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteLittleEndianInt64(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteLittleEndianInt64(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
long v = 0x102030405060708L;
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -237,9 +302,9 @@ void shouldWriteLittleEndianInt64(final boolean useBranch) {
@DisplayName("should write a double")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteDouble(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteDouble(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
double v = Double.longBitsToDouble(0x102030405060708L);
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -256,9 +321,9 @@ void shouldWriteDouble(final boolean useBranch) {
@DisplayName("should write an ObjectId")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteObjectId(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteObjectId(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] objectIdAsByteArray = {12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
ObjectId v = new ObjectId(objectIdAsByteArray);
if (useBranch) {
@@ -276,9 +341,9 @@ void shouldWriteObjectId(final boolean useBranch) {
@DisplayName("should write an empty string")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteEmptyString(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteEmptyString(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -295,9 +360,9 @@ void shouldWriteEmptyString(final boolean useBranch) {
@DisplayName("should write an ASCII string")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteAsciiString(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteAsciiString(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "Java";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -314,9 +379,9 @@ void shouldWriteAsciiString(final boolean useBranch) {
@DisplayName("should write a UTF-8 string")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteUtf8String(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteUtf8String(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "\u0900";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -333,9 +398,9 @@ void shouldWriteUtf8String(final boolean useBranch) {
@DisplayName("should write an empty CString")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteEmptyCString(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteEmptyCString(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -352,9 +417,9 @@ void shouldWriteEmptyCString(final boolean useBranch) {
@DisplayName("should write an ASCII CString")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteAsciiCString(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteAsciiCString(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "Java";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -371,9 +436,9 @@ void shouldWriteAsciiCString(final boolean useBranch) {
@DisplayName("should write a UTF-8 CString")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteUtf8CString(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteUtf8CString(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "\u0900";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -390,9 +455,9 @@ void shouldWriteUtf8CString(final boolean useBranch) {
@DisplayName("should get byte buffers as little endian")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldGetByteBuffersAsLittleEndian(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldGetByteBuffersAsLittleEndian(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = {1, 0, 0, 0};
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -407,9 +472,9 @@ void shouldGetByteBuffersAsLittleEndian(final boolean useBranch) {
@DisplayName("null character in CString should throw SerializationException")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void nullCharacterInCStringShouldThrowSerializationException(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void nullCharacterInCStringShouldThrowSerializationException(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "hell\u0000world";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -423,9 +488,9 @@ void nullCharacterInCStringShouldThrowSerializationException(final boolean useBr
@DisplayName("null character in String should not throw SerializationException")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void nullCharacterInStringShouldNotThrowSerializationException(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void nullCharacterInStringShouldNotThrowSerializationException(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
String v = "h\u0000i";
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -438,11 +503,25 @@ void nullCharacterInStringShouldNotThrowSerializationException(final boolean use
}
}
+
+ public static Stream writeInt32AtPositionShouldThrowWithInvalidPosition() {
+ return bufferProvidersWithBranches().flatMap(arguments -> {
+ Object[] args = arguments.get();
+ boolean useBranch = (boolean) args[0];
+ BufferProvider bufferProvider = (BufferProvider) args[1];
+ return Stream.of(
+ Arguments.of(useBranch, -1, bufferProvider),
+ Arguments.of(useBranch, 1, bufferProvider)
+ );
+ });
+ }
+
@DisplayName("write Int32 at position should throw with invalid position")
@ParameterizedTest
- @CsvSource({"false, -1", "false, 1", "true, -1", "true, 1"})
- void writeInt32AtPositionShouldThrowWithInvalidPosition(final boolean useBranch, final int position) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource
+ void writeInt32AtPositionShouldThrowWithInvalidPosition(final boolean useBranch, final int position,
+ final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = {1, 2, 3, 4};
int v2 = 0x1020304;
if (useBranch) {
@@ -459,9 +538,9 @@ void writeInt32AtPositionShouldThrowWithInvalidPosition(final boolean useBranch,
@DisplayName("should write Int32 at position")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldWriteInt32AtPosition(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldWriteInt32AtPosition(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
Consumer lastAssertions = effectiveOut -> {
assertArrayEquals(new byte[] {4, 3, 2, 1}, copyOfRange(effectiveOut.toByteArray(), 1023, 1027), "the position is not in the first buffer");
assertEquals(1032, effectiveOut.getPosition());
@@ -492,9 +571,22 @@ void shouldWriteInt32AtPosition(final boolean useBranch) {
}
}
+ public static Stream truncateShouldThrowWithInvalidPosition() {
+ return bufferProvidersWithBranches().flatMap(arguments -> {
+ Object[] args = arguments.get();
+ boolean useBranch = (boolean) args[0];
+ BufferProvider bufferProvider = (BufferProvider) args[1];
+ return Stream.of(
+ Arguments.of(useBranch, -1, bufferProvider),
+ Arguments.of(useBranch, 5, bufferProvider)
+ );
+ }
+ );
+ }
+
@DisplayName("truncate should throw with invalid position")
@ParameterizedTest
- @CsvSource({"false, -1", "false, 5", "true, -1", "true, 5"})
+ @MethodSource
void truncateShouldThrowWithInvalidPosition(final boolean useBranch, final int position) {
try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
byte[] v = {1, 2, 3, 4};
@@ -512,9 +604,9 @@ void truncateShouldThrowWithInvalidPosition(final boolean useBranch, final int p
@DisplayName("should truncate to position")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldTruncateToPosition(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldTruncateToPosition(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = {1, 2, 3, 4};
byte[] v2 = new byte[1024];
if (useBranch) {
@@ -536,15 +628,23 @@ void shouldTruncateToPosition(final boolean useBranch) {
@DisplayName("should grow to maximum allowed size of byte buffer")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldGrowToMaximumAllowedSizeOfByteBuffer(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldGrowToMaximumAllowedSizeOfByteBuffer(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = new byte[0x2000000];
ThreadLocalRandom.current().nextBytes(v);
- Consumer assertByteBuffers = effectiveOut -> assertEquals(
- asList(1 << 10, 1 << 11, 1 << 12, 1 << 13, 1 << 14, 1 << 15, 1 << 16, 1 << 17, 1 << 18, 1 << 19, 1 << 20,
- 1 << 21, 1 << 22, 1 << 23, 1 << 24, 1 << 24),
- effectiveOut.getByteBuffers().stream().map(ByteBuf::capacity).collect(toList()));
+ Consumer assertByteBuffers = effectiveOut -> {
+ List byteBuffers = new ArrayList<>();
+ try {
+ byteBuffers = effectiveOut.getByteBuffers();
+ assertEquals(
+ asList(1 << 10, 1 << 11, 1 << 12, 1 << 13, 1 << 14, 1 << 15, 1 << 16, 1 << 17, 1 << 18, 1 << 19, 1 << 20,
+ 1 << 21, 1 << 22, 1 << 23, 1 << 24, 1 << 24),
+ byteBuffers.stream().map(ByteBuf::capacity).collect(toList()));
+ } finally {
+ byteBuffers.forEach(ByteBuf::release);
+ }
+ };
Consumer assertions = effectiveOut -> {
effectiveOut.writeBytes(v);
assertEquals(v.length, effectiveOut.size());
@@ -570,9 +670,9 @@ void shouldGrowToMaximumAllowedSizeOfByteBuffer(final boolean useBranch) {
@DisplayName("should pipe")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void shouldPipe(final boolean useBranch) throws IOException {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProvidersWithBranches")
+ void shouldPipe(final boolean useBranch, final BufferProvider bufferProvider) throws IOException {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = new byte[1027];
BiConsumer assertions = (effectiveOut, baos) -> {
assertArrayEquals(v, baos.toByteArray());
@@ -606,10 +706,10 @@ void shouldPipe(final boolean useBranch) throws IOException {
@DisplayName("should close")
@ParameterizedTest
- @ValueSource(booleans = {false, true})
+ @MethodSource("bufferProvidersWithBranches")
@SuppressWarnings("try")
- void shouldClose(final boolean useBranch) {
- try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ void shouldClose(final boolean useBranch, final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput out = new ByteBufferBsonOutput(bufferProvider)) {
byte[] v = new byte[1027];
if (useBranch) {
try (ByteBufferBsonOutput.Branch branch = out.branch()) {
@@ -673,10 +773,11 @@ void shouldHandleMixedBranchingAndTruncating(final int reps) throws CharacterCod
}
}
- @Test
+ @ParameterizedTest
@DisplayName("should throw exception when calling writeInt32 at absolute position where integer would not fit")
- void shouldThrowExceptionWhenIntegerDoesNotFitWriteInt32() {
- try (ByteBufferBsonOutput output = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProviders")
+ void shouldThrowExceptionWhenIntegerDoesNotFitWriteInt32(final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput output = new ByteBufferBsonOutput(bufferProvider)) {
// Write 10 bytes (position becomes 10)
for (int i = 0; i < 10; i++) {
output.writeByte(0);
@@ -689,18 +790,19 @@ void shouldThrowExceptionWhenIntegerDoesNotFitWriteInt32() {
}
}
- @Test
+ @ParameterizedTest
@DisplayName("should throw exception when calling writeInt32 with negative absolute position")
- void shouldThrowExceptionWhenAbsolutePositionIsNegative() {
- try (ByteBufferBsonOutput output = new ByteBufferBsonOutput(new SimpleBufferProvider())) {
+ @MethodSource("bufferProviders")
+ void shouldThrowExceptionWhenAbsolutePositionIsNegative(final BufferProvider bufferProvider) {
+ try (ByteBufferBsonOutput output = new ByteBufferBsonOutput(bufferProvider)) {
Assertions.assertThrows(IllegalArgumentException.class, () ->
output.writeInt32(-1, 5678)
);
}
}
- static java.util.stream.Stream shouldWriteInt32AbsoluteValueWithinSpanningBuffers() {
- return java.util.stream.Stream.of(
+ static Stream shouldWriteInt32AbsoluteValueWithinSpanningBuffers() {
+ return bufferProviders().flatMap(bufferProvider -> Stream.of(
Arguments.of(
0, // absolute position
0x09080706, // int value
@@ -711,22 +813,28 @@ static java.util.stream.Stream shouldWriteInt32AbsoluteValueWithinSpa
asList(
// expected BsonByteBufferOutput data
new byte[]{0x06, 0x07, 0x08, 0x09},
- new byte[]{4, 5, 6, 7})),
+ new byte[]{4, 5, 6, 7}),
+ bufferProvider // buffer to write data to
+ ),
Arguments.of(1, 0x09080706,
asList(new byte[]{0, 1, 2, 3}, new byte[]{4, 5, 6, 7}),
- asList(new byte[]{0, 0x06, 0x07, 0x08}, new byte[]{0x09, 5, 6, 7})),
+ asList(new byte[]{0, 0x06, 0x07, 0x08}, new byte[]{0x09, 5, 6, 7}),
+ bufferProvider),
Arguments.of(2, 0x09080706,
asList(new byte[]{0, 1, 2, 3}, new byte[]{4, 5, 6, 7}),
- asList(new byte[]{0, 1, 0x06, 0x07}, new byte[]{0x08, 0x09, 6, 7})
+ asList(new byte[]{0, 1, 0x06, 0x07}, new byte[]{0x08, 0x09, 6, 7}),
+ bufferProvider
),
Arguments.of(3, 0x09080706,
asList(new byte[]{0, 1, 2, 3}, new byte[]{4, 5, 6, 7}),
- asList(new byte[]{0, 1, 2, 0x06}, new byte[]{0x07, 0x08, 0x09, 7})
+ asList(new byte[]{0, 1, 2, 0x06}, new byte[]{0x07, 0x08, 0x09, 7}),
+ bufferProvider
),
Arguments.of(4, 0x09080706,
asList(new byte[]{0, 1, 2, 3}, new byte[]{4, 5, 6, 7}),
- asList(new byte[]{0, 1, 2, 3}, new byte[]{0x06, 0x07, 0x08, 0x09})
- ));
+ asList(new byte[]{0, 1, 2, 3}, new byte[]{0x06, 0x07, 0x08, 0x09}),
+ bufferProvider
+ )));
}
@ParameterizedTest
@@ -735,10 +843,11 @@ void shouldWriteInt32AbsoluteValueWithinSpanningBuffers(
final int absolutePosition,
final int intValue,
final List initialData,
- final List expectedBuffers) {
+ final List expectedBuffers,
+ final BufferProvider bufferProvider) {
- try (ByteBufferBsonOutput output =
- new ByteBufferBsonOutput(size -> new ByteBufNIO(ByteBuffer.allocate(4)))) {
+ List buffers = new ArrayList<>();
+ try (ByteBufferBsonOutput output = new ByteBufferBsonOutput(size -> bufferProvider.getBuffer(Integer.BYTES))) {
//given
initialData.forEach(output::writeBytes);
@@ -747,17 +856,16 @@ void shouldWriteInt32AbsoluteValueWithinSpanningBuffers(
output.writeInt32(absolutePosition, intValue);
//then
- List buffers = output.getByteBuffers();
+ buffers = output.getByteBuffers();
assertEquals(expectedBuffers.size(), buffers.size(), "Number of buffers mismatch");
- for (int i = 0; i < expectedBuffers.size(); i++) {
- assertArrayEquals(expectedBuffers.get(i), buffers.get(i).array(),
- "Buffer " + i + " contents mismatch");
- }
+ assertBufferContents(expectedBuffers, buffers);
+ } finally {
+ buffers.forEach(ByteBuf::release);
}
}
- static java.util.stream.Stream int32SpanningBuffersData() {
- return java.util.stream.Stream.of(
+ static Stream int32SpanningBuffersData() {
+ return bufferProviders().flatMap(bufferProvider -> Stream.of(
// Test case 1: No initial data; entire int written into one buffer.
Arguments.of(0x09080706,
asList(
@@ -767,28 +875,33 @@ static java.util.stream.Stream int32SpanningBuffersData() {
// expected BsonByteBufferOutput data
new byte[]{0x06, 0x07, 0x08, 0x09}),
4, // expected overall position after write (0 + 4)
- 4 // expected last buffer position (buffer fully written)
+ 4, // expected last buffer position (buffer fully written)
+ bufferProvider //buffer to write data to
),
Arguments.of(0x09080706,
asList(new byte[]{0}),
- asList(new byte[]{0, 0x06, 0x07, 0x08}, new byte[]{0x09, 0, 0, 0}), 5, 1
+ asList(new byte[]{0, 0x06, 0x07, 0x08}, new byte[]{0x09, 0, 0, 0}), 5, 1,
+ bufferProvider
),
Arguments.of(0x09080706,
asList(new byte[]{0, 1}),
- asList(new byte[]{0, 1, 0x06, 0x07}, new byte[]{0x08, 0x09, 0, 0}), 6, 2
+ asList(new byte[]{0, 1, 0x06, 0x07}, new byte[]{0x08, 0x09, 0, 0}), 6, 2,
+ bufferProvider
),
Arguments.of(0x09080706,
asList(new byte[]{0, 1, 2}),
- asList(new byte[]{0, 1, 2, 0x06}, new byte[]{0x07, 0x08, 0x09, 0}), 7, 3
+ asList(new byte[]{0, 1, 2, 0x06}, new byte[]{0x07, 0x08, 0x09, 0}), 7, 3,
+ bufferProvider
),
Arguments.of(0x09080706,
asList(new byte[]{0, 1, 2, 3}),
- asList(new byte[]{0, 1, 2, 3}, new byte[]{0x06, 0x07, 0x08, 0x09}), 8, 4
- ));
+ asList(new byte[]{0, 1, 2, 3}, new byte[]{0x06, 0x07, 0x08, 0x09}), 8, 4,
+ bufferProvider
+ )));
}
- static java.util.stream.Stream int64SpanningBuffersData() {
- return java.util.stream.Stream.of(
+ static Stream int64SpanningBuffersData() {
+ return bufferProviders().flatMap(bufferProvider -> Stream.of(
// Test case 1: No initial data; entire long written into one buffer.
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(
@@ -799,48 +912,56 @@ static java.util.stream.Stream int64SpanningBuffersData() {
new byte[]{0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A}
),
8, // expected overall position after write (0 + 8)
- 8 // expected last buffer position (buffer fully written)
+ 8, // expected last buffer position (buffer fully written)
+ bufferProvider //buffer to write data to
),
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0}),
asList(new byte[]{0, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B}, new byte[]{0x0A, 0, 0, 0, 0, 0, 0, 0}),
- 9, 1
+ 9, 1,
+ bufferProvider
),
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0, 1}),
asList(new byte[]{0, 1, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C}, new byte[]{0x0B, 0x0A, 0, 0, 0, 0, 0, 0}),
- 10, 2
+ 10, 2,
+ bufferProvider
),
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0, 1, 2}),
asList(new byte[]{0, 1, 2, 0x11, 0x10, 0x0F, 0x0E, 0x0D}, new byte[]{0x0C, 0x0B, 0x0A, 0, 0, 0, 0, 0}),
- 11, 3
+ 11, 3,
+ bufferProvider
),
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0, 1, 2, 3}),
asList(new byte[]{0, 1, 2, 3, 0x11, 0x10, 0x0F, 0x0E}, new byte[]{0x0D, 0x0C, 0x0B, 0x0A, 0, 0, 0, 0}),
- 12, 4
+ 12, 4,
+ bufferProvider
),
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0, 1, 2, 3, 4}),
asList(new byte[]{0, 1, 2, 3, 4, 0x11, 0x10, 0x0F}, new byte[]{0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0, 0, 0}),
- 13, 5
+ 13, 5,
+ bufferProvider
),
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0, 1, 2, 3, 4, 5}),
asList(new byte[]{0, 1, 2, 3, 4, 5, 0x11, 0x10}, new byte[]{0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0, 0}),
- 14, 6
+ 14, 6,
+ bufferProvider
), Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0, 1, 2, 3, 4, 5, 6}),
asList(new byte[]{0, 1, 2, 3, 4, 5, 6, 0x11}, new byte[]{0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0}),
- 15, 7
+ 15, 7,
+ bufferProvider
),
Arguments.of(0x0A0B0C0D0E0F1011L,
asList(new byte[]{0, 1, 2, 3, 4, 5, 6, 7}),
asList(new byte[]{0, 1, 2, 3, 4, 5, 6, 7}, new byte[]{0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A}),
- 16, 8
- )
- );
+ 16, 8,
+ bufferProvider
+ )));
}
@ParameterizedTest
@@ -850,10 +971,11 @@ void shouldWriteInt32WithinSpanningBuffers(
final List initialData,
final List expectedBuffers,
final int expectedOutputPosition,
- final int expectedLastBufferPosition) {
+ final int expectedLastBufferPosition,
+ final BufferProvider bufferProvider) {
try (ByteBufferBsonOutput output =
- new ByteBufferBsonOutput(size -> new ByteBufNIO(ByteBuffer.allocate(4)))) {
+ new ByteBufferBsonOutput(size -> bufferProvider.getBuffer(Integer.BYTES))) {
//given
initialData.forEach(output::writeBytes);
@@ -865,10 +987,7 @@ void shouldWriteInt32WithinSpanningBuffers(
//getByteBuffers returns ByteBuffers with limit() set to position, position set to 0.
List buffers = output.getByteBuffers();
assertEquals(expectedBuffers.size(), buffers.size(), "Number of buffers mismatch");
- for (int i = 0; i < expectedBuffers.size(); i++) {
- assertArrayEquals(expectedBuffers.get(i), buffers.get(i).array(),
- "Buffer " + i + " contents mismatch");
- }
+ assertBufferContents(expectedBuffers, buffers);
assertEquals(expectedLastBufferPosition, buffers.get(buffers.size() - 1).limit());
assertEquals(expectedOutputPosition, output.getPosition());
@@ -882,10 +1001,11 @@ void shouldWriteInt64WithinSpanningBuffers(
final List initialData,
final List expectedBuffers,
final int expectedOutputPosition,
- final int expectedLastBufferPosition) {
+ final int expectedLastBufferPosition,
+ final BufferProvider bufferProvider) {
try (ByteBufferBsonOutput output =
- new ByteBufferBsonOutput(size -> new ByteBufNIO(ByteBuffer.allocate(8)))) {
+ new ByteBufferBsonOutput(size -> bufferProvider.getBuffer(Long.BYTES))) {
//given
initialData.forEach(output::writeBytes);
@@ -897,10 +1017,7 @@ void shouldWriteInt64WithinSpanningBuffers(
//getByteBuffers returns ByteBuffers with limit() set to position, position set to 0.
List buffers = output.getByteBuffers();
assertEquals(expectedBuffers.size(), buffers.size(), "Number of buffers mismatch");
- for (int i = 0; i < expectedBuffers.size(); i++) {
- assertArrayEquals(expectedBuffers.get(i), buffers.get(i).array(),
- "Buffer " + i + " contents mismatch");
- }
+ assertBufferContents(expectedBuffers, buffers);
assertEquals(expectedLastBufferPosition, buffers.get(buffers.size() - 1).limit());
assertEquals(expectedOutputPosition, output.getPosition());
@@ -914,10 +1031,11 @@ void shouldWriteDoubleWithinSpanningBuffers(
final List initialData,
final List expectedBuffers,
final int expectedOutputPosition,
- final int expectedLastBufferPosition) {
+ final int expectedLastBufferPosition,
+ final BufferProvider bufferProvider) {
try (ByteBufferBsonOutput output =
- new ByteBufferBsonOutput(size -> new ByteBufNIO(ByteBuffer.allocate(8)))) {
+ new ByteBufferBsonOutput(size -> bufferProvider.getBuffer(Long.BYTES))) {
//given
initialData.forEach(output::writeBytes);
@@ -929,13 +1047,553 @@ void shouldWriteDoubleWithinSpanningBuffers(
//getByteBuffers returns ByteBuffers with limit() set to position, position set to 0.
List buffers = output.getByteBuffers();
assertEquals(expectedBuffers.size(), buffers.size(), "Number of buffers mismatch");
- for (int i = 0; i < expectedBuffers.size(); i++) {
- assertArrayEquals(expectedBuffers.get(i), buffers.get(i).array(),
- "Buffer " + i + " contents mismatch");
- }
+ assertBufferContents(expectedBuffers, buffers);
assertEquals(expectedLastBufferPosition, buffers.get(buffers.size() - 1).limit());
assertEquals(expectedOutputPosition, output.getPosition());
}
}
+
+ private static void assertBufferContents(final List expectedBuffersContent,
+ final List actualByteBuffers) {
+ for (int i = 0; i < expectedBuffersContent.size(); i++) {
+ ByteBuf byteBuf = actualByteBuffers.get(i);
+ byte[] expectedBufferBytes = expectedBuffersContent.get(i);
+ byte[] actualBufferBytes =
+ new byte[byteBuf.capacity()]; //capacity is used because we want to compare internal ByteBuffer arrays.
+ byteBuf.get(actualBufferBytes, 0, byteBuf.limit());
+
+ assertEquals(expectedBufferBytes.length, byteBuf.capacity());
+ assertArrayEquals(expectedBufferBytes, actualBufferBytes,
+ "Buffer " + i + " contents mismatch");
+ }
+ }
+
+ /*
+ Tests that all Unicode code points are correctly encoded in UTF-8 when:
+ - The buffer has just enough capacity for the UTF-8 string plus a null terminator.
+ - The encoded string may span multiple buffers.
+
+ To test edge conditions, the test writes a UTF-8 CString/String at various starting offsets. This simulates scenarios where data
+ doesn't start at index 0, forcing the string to span multiple buffers.
+
+ For example, assume the encoded string requires N bytes and null terminator:
+ 1. startingOffset == 0:
+ [ S S S ... S NULL ]
+
+ 2. startingOffset == 2:
+ ("X" represents dummy bytes written before the string.)
+ Buffer 1: [ X X | S S S ... ] (Buffer 1 runs out of space, the remaining bytes (including the NULL) are written in Buffer 2.)
+ Buffer 2: [ S NULL ...]
+
+ 3. startingOffset == bufferAllocationSize:
+ Buffer 1: [ X X X ... X ]
+ Buffer 2: [ S S S ... S NULL ]
+ */
+ @Nested
+ @DisplayName("UTF-8 String and CString Buffer Boundary Tests")
+ class Utf8StringTests {
+
+ @DisplayName("should write UTF-8 CString across buffers")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteCStringAcrossBuffersUTF8(final BufferProvider bufferProvider) throws IOException {
+ for (Integer codePoint : ALL_CODE_POINTS_EXCLUDING_SURROGATES) {
+ String stringToEncode = new String(Character.toChars(codePoint)) + "a";
+ byte[] expectedStringEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+ int bufferAllocationSize = expectedStringEncoding.length + "\u0000".length();
+ testWriteCStringAcrossBuffers(bufferProvider, codePoint, bufferAllocationSize, stringToEncode, expectedStringEncoding);
+ }
+ }
+
+ @DisplayName("should write UTF-8 CString across buffers with a branch")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteCStringAcrossBuffersUTF8WithBranch(final BufferProvider bufferProvider) throws IOException {
+ for (Integer codePoint : ALL_CODE_POINTS_EXCLUDING_SURROGATES) {
+ String stringToEncode = new String(Character.toChars(codePoint)) + "a";
+ int bufferAllocationSize = stringToEncode.getBytes(StandardCharsets.UTF_8).length + "\u0000".length();
+ byte[] expectedEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+
+ testWriteCStringAcrossBufferWithBranch(bufferProvider, codePoint, bufferAllocationSize, stringToEncode, expectedEncoding);
+ }
+ }
+
+ @DisplayName("should write UTF-8 String across buffers")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteStringAcrossBuffersUTF8(final BufferProvider bufferProvider) throws IOException {
+ for (Integer codePoint : ALL_CODE_POINTS_EXCLUDING_SURROGATES) {
+ // given
+ String stringToEncode = new String(Character.toChars(codePoint)) + "a";
+ //4 bytes for the length prefix, bytes for encoded String, and 1 byte for the null terminator
+ int bufferAllocationSize = Integer.BYTES + stringToEncode.getBytes(StandardCharsets.UTF_8).length + "\u0000".length();
+ byte[] expectedEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+ testWriteStringAcrossBuffers(bufferProvider,
+ codePoint,
+ bufferAllocationSize,
+ stringToEncode,
+ expectedEncoding);
+ }
+ }
+
+ @DisplayName("should write UTF-8 String across buffers with branch")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteStringAcrossBuffersUTF8WithBranch(final BufferProvider bufferProvider) throws IOException {
+ for (Integer codePoint : ALL_CODE_POINTS_EXCLUDING_SURROGATES) {
+ String stringToEncode = new String(Character.toChars(codePoint)) + "a";
+ //4 bytes for the length prefix, bytes for encoded String, and 1 byte for the null terminator
+ int bufferAllocationSize = Integer.BYTES + stringToEncode.getBytes(StandardCharsets.UTF_8).length + "\u0000".length();
+ byte[] expectedEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+ testWriteStringAcrossBuffersWithBranch(
+ bufferProvider,
+ bufferAllocationSize,
+ stringToEncode,
+ codePoint,
+ expectedEncoding);
+ }
+ }
+
+ /*
+ Tests that malformed surrogate pairs are encoded as-is without substituting any code point.
+ This known bug and corresponding test remain for backward compatibility.
+ Ticket: JAVA-5575
+ */
+ @DisplayName("should write malformed surrogate CString across buffers")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteCStringWithMalformedSurrogates(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_SURROGATE_CODE_POINTS) {
+ byte[] expectedEncoding = new byte[]{
+ (byte) (0xE0 | ((surrogateCodePoint >> 12) & 0x0F)),
+ (byte) (0x80 | ((surrogateCodePoint >> 6) & 0x3F)),
+ (byte) (0x80 | (surrogateCodePoint & 0x3F))
+ };
+ String str = new String(Character.toChars(surrogateCodePoint));
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteCStringAcrossBuffers(
+ bufferProvider,
+ surrogateCodePoint,
+ bufferAllocationSize,
+ str,
+ expectedEncoding);
+ }
+ }
+
+ /*
+ Tests that malformed surrogate pairs are encoded as-is without substituting any code point.
+ This known bug and corresponding test remain for backward compatibility.
+ Ticket: JAVA-5575
+ */
+ @DisplayName("should write malformed surrogate CString across buffers with branch")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteCStringWithMalformedSurrogatesWithBranch(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_SURROGATE_CODE_POINTS) {
+ byte[] expectedEncoding = new byte[]{
+ (byte) (0xE0 | ((surrogateCodePoint >> 12) & 0x0F)),
+ (byte) (0x80 | ((surrogateCodePoint >> 6) & 0x3F)),
+ (byte) (0x80 | (surrogateCodePoint & 0x3F))
+ };
+ String str = new String(Character.toChars(surrogateCodePoint));
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteCStringAcrossBufferWithBranch(
+ bufferProvider,
+ surrogateCodePoint,
+ bufferAllocationSize,
+ str,
+ expectedEncoding);
+ }
+ }
+
+ @DisplayName("should write surrogate CString across buffers")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteCStringWithSurrogatePairs(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_UTF_16_CODE_POINTS_FORMED_BY_SURROGATE_PAIRS) {
+ String stringToEncode = new String(toSurrogatePair(surrogateCodePoint));
+ byte[] expectedEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteCStringAcrossBuffers(
+ bufferProvider,
+ surrogateCodePoint,
+ bufferAllocationSize,
+ stringToEncode,
+ expectedEncoding);
+ }
+ }
+
+ @DisplayName("should write surrogate CString across buffers with branch")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteCStringWithSurrogatePairsWithBranch(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_UTF_16_CODE_POINTS_FORMED_BY_SURROGATE_PAIRS) {
+ String stringToEncode = new String(toSurrogatePair(surrogateCodePoint));
+ byte[] expectedEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteCStringAcrossBufferWithBranch(
+ bufferProvider,
+ surrogateCodePoint,
+ bufferAllocationSize,
+ stringToEncode,
+ expectedEncoding);
+ }
+ }
+
+ @DisplayName("should write surrogate String across buffers")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteStringWithSurrogatePairs(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_UTF_16_CODE_POINTS_FORMED_BY_SURROGATE_PAIRS) {
+ String stringToEncode = new String(toSurrogatePair(surrogateCodePoint));
+ byte[] expectedEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteStringAcrossBuffers(
+ bufferProvider,
+ surrogateCodePoint,
+ bufferAllocationSize,
+ stringToEncode,
+ expectedEncoding);
+ }
+ }
+
+ @DisplayName("should write surrogate String across buffers with branch")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteStringWithSurrogatePairsWithBranch(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_UTF_16_CODE_POINTS_FORMED_BY_SURROGATE_PAIRS) {
+ String stringToEncode = new String(toSurrogatePair(surrogateCodePoint));
+ byte[] expectedEncoding = stringToEncode.getBytes(StandardCharsets.UTF_8);
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteStringAcrossBuffersWithBranch(
+ bufferProvider,
+ bufferAllocationSize,
+ stringToEncode,
+ surrogateCodePoint,
+ expectedEncoding);
+ }
+ }
+
+ /*
+ Tests that malformed surrogate pairs are encoded as-is without substituting any code point.
+ This known bug and corresponding test remain for backward compatibility.
+ Ticket: JAVA-5575
+ */
+ @DisplayName("should write malformed surrogate String across buffers")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteStringWithMalformedSurrogates(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_SURROGATE_CODE_POINTS) {
+ byte[] expectedEncoding = new byte[]{
+ (byte) (0xE0 | ((surrogateCodePoint >> 12) & 0x0F)),
+ (byte) (0x80 | ((surrogateCodePoint >> 6) & 0x3F)),
+ (byte) (0x80 | (surrogateCodePoint & 0x3F))
+ };
+ String stringToEncode = new String(Character.toChars(surrogateCodePoint));
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteStringAcrossBuffers(
+ bufferProvider,
+ surrogateCodePoint,
+ bufferAllocationSize,
+ stringToEncode,
+ expectedEncoding);
+ }
+ }
+
+ /*
+ Tests that malformed surrogate pairs are encoded as-is without substituting any code point.
+ This known bug and corresponding test remain for backward compatibility.
+ Ticket: JAVA-5575
+ */
+ @DisplayName("should write malformed surrogate String across buffers with branch")
+ @ParameterizedTest
+ @MethodSource("com.mongodb.internal.connection.ByteBufferBsonOutputTest#bufferProviders")
+ void shouldWriteStringWithMalformedSurrogatesWithBranch(final BufferProvider bufferProvider) throws IOException {
+ for (Integer surrogateCodePoint : ALL_SURROGATE_CODE_POINTS) {
+ byte[] expectedEncoding = new byte[]{
+ (byte) (0xE0 | ((surrogateCodePoint >> 12) & 0x0F)),
+ (byte) (0x80 | ((surrogateCodePoint >> 6) & 0x3F)),
+ (byte) (0x80 | (surrogateCodePoint & 0x3F))
+ };
+ String stringToEncode = new String(Character.toChars(surrogateCodePoint));
+ int bufferAllocationSize = expectedEncoding.length + "\u0000".length();
+
+ testWriteStringAcrossBuffersWithBranch(
+ bufferProvider,
+ bufferAllocationSize,
+ stringToEncode,
+ surrogateCodePoint,
+ expectedEncoding);
+ }
+ }
+
+ private void testWriteCStringAcrossBuffers(final BufferProvider bufferProvider,
+ final Integer surrogateCodePoint,
+ final int bufferAllocationSize,
+ final String str,
+ final byte[] expectedEncoding) throws IOException {
+ for (int startingOffset = 0; startingOffset <= bufferAllocationSize; startingOffset++) {
+ //given
+ List actualByteBuffers = emptyList();
+
+ try (ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(
+ size -> bufferProvider.getBuffer(bufferAllocationSize))) {
+ // Write an initial startingOffset of empty bytes to shift the start position
+ bsonOutput.write(new byte[startingOffset]);
+
+ // when
+ bsonOutput.writeCString(str);
+
+ // then
+ actualByteBuffers = bsonOutput.getDuplicateByteBuffers();
+ byte[] actualFlattenedByteBuffersBytes = getBytes(bsonOutput);
+ assertEncodedResult(surrogateCodePoint,
+ startingOffset,
+ expectedEncoding,
+ bufferAllocationSize,
+ actualByteBuffers,
+ actualFlattenedByteBuffersBytes);
+ } finally {
+ actualByteBuffers.forEach(ByteBuf::release);
+ }
+ }
+ }
+
+ private void testWriteStringAcrossBuffers(final BufferProvider bufferProvider,
+ final Integer codePoint,
+ final int bufferAllocationSize,
+ final String stringToEncode,
+ final byte[] expectedEncoding) throws IOException {
+ for (int startingOffset = 0; startingOffset <= bufferAllocationSize; startingOffset++) {
+ //given
+ List actualByteBuffers = emptyList();
+
+ try (ByteBufferBsonOutput actualBsonOutput = new ByteBufferBsonOutput(
+ size -> bufferProvider.getBuffer(bufferAllocationSize))) {
+ // Write an initial startingOffset of empty bytes to shift the start position
+ actualBsonOutput.write(new byte[startingOffset]);
+
+ // when
+ actualBsonOutput.writeString(stringToEncode);
+
+ // then
+ actualByteBuffers = actualBsonOutput.getDuplicateByteBuffers();
+ byte[] actualFlattenedByteBuffersBytes = getBytes(actualBsonOutput);
+
+ assertEncodedStringSize(codePoint,
+ expectedEncoding,
+ actualFlattenedByteBuffersBytes,
+ startingOffset);
+ assertEncodedResult(codePoint,
+ startingOffset + Integer.BYTES, // +4 bytes for the length prefix
+ expectedEncoding,
+ bufferAllocationSize,
+ actualByteBuffers,
+ actualFlattenedByteBuffersBytes);
+ } finally {
+ actualByteBuffers.forEach(ByteBuf::release);
+ }
+ }
+ }
+
+ private void testWriteStringAcrossBuffersWithBranch(final BufferProvider bufferProvider,
+ final int bufferAllocationSize,
+ final String stringToEncode,
+ final Integer codePoint,
+ final byte[] expectedEncoding) throws IOException {
+ for (int startingOffset = 0; startingOffset <= bufferAllocationSize; startingOffset++) {
+ //given
+ List actualByteBuffers = emptyList();
+ List actualBranchByteBuffers = emptyList();
+
+ try (ByteBufferBsonOutput actualBsonOutput = new ByteBufferBsonOutput(
+ size -> bufferProvider.getBuffer(bufferAllocationSize))) {
+
+ try (ByteBufferBsonOutput.Branch branchOutput = actualBsonOutput.branch()) {
+ // Write an initial startingOffset of empty bytes to shift the start position
+ branchOutput.write(new byte[startingOffset]);
+
+ // when
+ branchOutput.writeString(stringToEncode);
+
+ // then
+ actualBranchByteBuffers = branchOutput.getDuplicateByteBuffers();
+ byte[] actualFlattenedByteBuffersBytes = getBytes(branchOutput);
+ assertEncodedStringSize(
+ codePoint,
+ expectedEncoding,
+ actualFlattenedByteBuffersBytes,
+ startingOffset);
+ assertEncodedResult(codePoint,
+ startingOffset + Integer.BYTES, // +4 bytes for the length prefix
+ expectedEncoding,
+ bufferAllocationSize,
+ actualBranchByteBuffers,
+ actualFlattenedByteBuffersBytes);
+ }
+
+ // then
+ actualByteBuffers = actualBsonOutput.getDuplicateByteBuffers();
+ byte[] actualFlattenedByteBuffersBytes = getBytes(actualBsonOutput);
+ assertEncodedStringSize(
+ codePoint,
+ expectedEncoding,
+ actualFlattenedByteBuffersBytes,
+ startingOffset);
+ assertEncodedResult(codePoint,
+ startingOffset + Integer.BYTES, // +4 bytes for the length prefix
+ expectedEncoding,
+ bufferAllocationSize,
+ actualByteBuffers,
+ actualFlattenedByteBuffersBytes);
+
+ } finally {
+ actualByteBuffers.forEach(ByteBuf::release);
+ actualBranchByteBuffers.forEach(ByteBuf::release);
+ }
+ }
+ }
+
+ // Verify that the resulting byte array (excluding the starting offset and null terminator)
+ // matches the expected UTF-8 encoded length of the test string.
+ private void assertEncodedStringSize(final Integer codePoint,
+ final byte[] expectedStringEncoding,
+ final byte[] actualFlattenedByteBuffersBytes,
+ final int startingOffset) {
+ int littleEndianLength = reverseBytes(expectedStringEncoding.length + "\u0000".length());
+ byte[] expectedEncodedStringSize = Ints.toByteArray(littleEndianLength);
+ byte[] actualEncodedStringSize = copyOfRange(
+ actualFlattenedByteBuffersBytes,
+ startingOffset,
+ startingOffset + Integer.BYTES);
+
+ assertArrayEquals(
+ expectedEncodedStringSize,
+ actualEncodedStringSize,
+ () -> format("Encoded String size before the test String does not match expected size. "
+ + "Failed with code point: %s, startingOffset: %s",
+ codePoint,
+ startingOffset));
+ }
+
+ private void testWriteCStringAcrossBufferWithBranch(final BufferProvider bufferProvider,
+ final Integer codePoint,
+ final int bufferAllocationSize,
+ final String str, final byte[] expectedEncoding) throws IOException {
+ for (int startingOffset = 0; startingOffset <= bufferAllocationSize; startingOffset++) {
+ List actualBranchByteBuffers = emptyList();
+ List actualByteBuffers = emptyList();
+
+ try (ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(
+ size -> bufferProvider.getBuffer(bufferAllocationSize))) {
+
+ try (ByteBufferBsonOutput.Branch branchOutput = bsonOutput.branch()) {
+ // Write an initial startingOffset of empty bytes to shift the start position
+ branchOutput.write(new byte[startingOffset]);
+
+ // when
+ branchOutput.writeCString(str);
+
+ // then
+ actualBranchByteBuffers = branchOutput.getDuplicateByteBuffers();
+ byte[] actualFlattenedByteBuffersBytes = getBytes(branchOutput);
+ assertEncodedResult(codePoint,
+ startingOffset,
+ expectedEncoding,
+ bufferAllocationSize,
+ actualBranchByteBuffers,
+ actualFlattenedByteBuffersBytes);
+ }
+
+ // then
+ actualByteBuffers = bsonOutput.getDuplicateByteBuffers();
+ byte[] actualFlattenedByteBuffersBytes = getBytes(bsonOutput);
+ assertEncodedResult(codePoint,
+ startingOffset,
+ expectedEncoding,
+ bufferAllocationSize,
+ actualByteBuffers,
+ actualFlattenedByteBuffersBytes);
+ } finally {
+ actualByteBuffers.forEach(ByteBuf::release);
+ actualBranchByteBuffers.forEach(ByteBuf::release);
+ }
+ }
+ }
+
+ private void assertEncodedResult(final int codePoint,
+ final int startingOffset,
+ final byte[] expectedEncoding,
+ final int expectedBufferAllocationSize,
+ final List actualByteBuffers,
+ final byte[] actualFlattenedByteBuffersBytes) {
+ int expectedCodeUnitCount = expectedEncoding.length;
+ int byteCount = startingOffset + expectedCodeUnitCount + 1;
+ int expectedBufferCount = (byteCount + expectedBufferAllocationSize - 1) / expectedBufferAllocationSize;
+ int expectedLastBufferPosition = (byteCount % expectedBufferAllocationSize) == 0 ? expectedBufferAllocationSize
+ : byteCount % expectedBufferAllocationSize;
+
+ assertEquals(
+ expectedBufferCount,
+ actualByteBuffers.size(),
+ () -> format("expectedBufferCount failed with code point: %s, offset: %s",
+ codePoint,
+ startingOffset));
+ assertEquals(
+ expectedLastBufferPosition,
+ actualByteBuffers.get(actualByteBuffers.size() - 1).position(),
+ () -> format("expectedLastBufferPosition failed with code point: %s, offset: %s",
+ codePoint,
+ startingOffset));
+
+ for (ByteBuf byteBuf : actualByteBuffers.subList(0, actualByteBuffers.size() - 1)) {
+ assertEquals(
+ byteBuf.position(),
+ byteBuf.limit(),
+ () -> format("All non-final buffers are not full. Code point: %s, offset: %s",
+ codePoint,
+ startingOffset));
+ }
+
+ // Verify that the final byte array (excluding the initial offset and null terminator)
+ // matches the expected UTF-8 encoding of the test string
+ assertArrayEquals(
+ expectedEncoding,
+ Arrays.copyOfRange(actualFlattenedByteBuffersBytes, startingOffset, actualFlattenedByteBuffersBytes.length - 1),
+ () -> format("Expected UTF-8 encoding of the test string does not match actual encoding. Code point: %s, offset: %s",
+ codePoint,
+ startingOffset));
+ assertEquals(
+ 0,
+ actualFlattenedByteBuffersBytes[actualFlattenedByteBuffersBytes.length - 1],
+ () -> format("String does not end with null terminator. Code point: %s, offset: %s",
+ codePoint,
+ startingOffset));
+ }
+
+ public char[] toSurrogatePair(final int codePoint) {
+ if (!Character.isValidCodePoint(codePoint) || codePoint < 0x10000) {
+ throw new IllegalArgumentException("Invalid code point: " + codePoint);
+ }
+ char[] result = new char[2];
+ result[0] = Character.highSurrogate(codePoint);
+ result[1] = Character.lowSurrogate(codePoint);
+ return result;
+ }
+
+ }
+
+ private static byte[] getBytes(final OutputBuffer basicOutputBuffer) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(basicOutputBuffer.getSize());
+ basicOutputBuffer.pipe(baos);
+ return baos.toByteArray();
+ }
}