From cd52b6b412e675feafce506c8c3ba7323b10e6d9 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Mon, 16 Mar 2026 19:42:41 -0700 Subject: [PATCH 1/8] Optimize RawBsonDocument encode and decode by eliminating intermediate allocations - Add BsonWriter.pipe(byte[], int, int) with BsonBinaryWriter override to write raw BSON bytes directly to the output, avoiding intermediate object allocation on the encode path - Add BsonInput.pipe(BsonOutput, int) to remove the temporary byte[] copy in BsonBinaryWriter.pipeDocument() on both encode and decode paths - Add public getByteBacking(), getByteOffset(), getByteLength() on RawBsonDocument to expose the backing byte array JAVA-6133 --- bson/src/main/org/bson/BsonBinaryWriter.java | 40 ++++++++++++------- bson/src/main/org/bson/BsonWriter.java | 25 ++++++++++++ bson/src/main/org/bson/RawBsonDocument.java | 30 ++++++++++++++ .../org/bson/codecs/RawBsonDocumentCodec.java | 6 +-- bson/src/main/org/bson/io/BsonInput.java | 9 +++++ .../main/org/bson/io/ByteBufferBsonInput.java | 18 +++++++++ 6 files changed, 109 insertions(+), 19 deletions(-) diff --git a/bson/src/main/org/bson/BsonBinaryWriter.java b/bson/src/main/org/bson/BsonBinaryWriter.java index 20e73d97d44..aada1ecc12b 100644 --- a/bson/src/main/org/bson/BsonBinaryWriter.java +++ b/bson/src/main/org/bson/BsonBinaryWriter.java @@ -334,6 +334,17 @@ public void pipe(final BsonReader reader) { pipeDocument(reader, null); } + @Override + public void pipe(final byte[] bytes, final int offset, final int length) { + if (getState() == State.VALUE) { + bsonOutput.writeByte(BsonType.DOCUMENT.getValue()); + writeCurrentName(); + } + int pipedDocumentStartPosition = bsonOutput.getPosition(); + bsonOutput.writeBytes(bytes, offset, length); + completePipeDocument(pipedDocumentStartPosition); + } + @Override public void pipe(final BsonReader reader, final List extraElements) { notNull("reader", reader); @@ -355,9 +366,7 @@ private void pipeDocument(final BsonReader reader, final List extra } int pipedDocumentStartPosition = bsonOutput.getPosition(); bsonOutput.writeInt32(size); - byte[] bytes = new byte[size - 4]; - bsonInput.readBytes(bytes); - bsonOutput.writeBytes(bytes); + bsonInput.pipe(bsonOutput, size - 4); binaryReader.setState(AbstractBsonReader.State.TYPE); @@ -371,17 +380,7 @@ private void pipeDocument(final BsonReader reader, final List extra setContext(getContext().getParentContext()); } - if (getContext() == null) { - setState(State.DONE); - } else { - if (getContext().getContextType() == BsonContextType.JAVASCRIPT_WITH_SCOPE) { - backpatchSize(); // size of the JavaScript with scope value - setContext(getContext().getParentContext()); - } - setState(getNextState()); - } - - validateSize(bsonOutput.getPosition() - pipedDocumentStartPosition); + completePipeDocument(pipedDocumentStartPosition); } else if (extraElements != null) { super.pipe(reader, extraElements); } else { @@ -389,6 +388,19 @@ private void pipeDocument(final BsonReader reader, final List extra } } + private void completePipeDocument(final int pipedDocumentStartPosition) { + if (getContext() == null) { + setState(State.DONE); + } else { + if (getContext().getContextType() == BsonContextType.JAVASCRIPT_WITH_SCOPE) { + backpatchSize(); // size of the JavaScript with scope value + setContext(getContext().getParentContext()); + } + setState(getNextState()); + } + validateSize(bsonOutput.getPosition() - pipedDocumentStartPosition); + } + /** * Sets a maximum size for documents from this point. * diff --git a/bson/src/main/org/bson/BsonWriter.java b/bson/src/main/org/bson/BsonWriter.java index c3da5dc6059..0043ea8ecb3 100644 --- a/bson/src/main/org/bson/BsonWriter.java +++ b/bson/src/main/org/bson/BsonWriter.java @@ -16,9 +16,13 @@ package org.bson; +import org.bson.io.ByteBufferBsonInput; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + /** * An interface for writing a logical BSON document using a push-oriented API. * @@ -357,4 +361,25 @@ public interface BsonWriter { */ void pipe(BsonReader reader); + /** + * Pipes a raw BSON document from the given byte array to this writer. + * + *

The default implementation wraps the bytes in a {@linkplain BsonBinaryReader} + * and calls {@link #pipe(BsonReader)}. Implementations may override this + * to write the raw bytes directly without intermediate object allocation.

+ * + * @param bytes the byte array containing the BSON document + * @param offset the offset into the byte array + * @param length the length of the BSON document + * @since 5.7 + */ + default void pipe(byte[] bytes, int offset, int length) { + try (BsonBinaryReader reader = new BsonBinaryReader( + new ByteBufferBsonInput( + new ByteBufNIO(ByteBuffer.wrap(bytes, offset, length) + .order(ByteOrder.LITTLE_ENDIAN))))) { + pipe(reader); + } + } + } diff --git a/bson/src/main/org/bson/RawBsonDocument.java b/bson/src/main/org/bson/RawBsonDocument.java index eb672bcef8d..4d478234a35 100644 --- a/bson/src/main/org/bson/RawBsonDocument.java +++ b/bson/src/main/org/bson/RawBsonDocument.java @@ -144,6 +144,36 @@ public ByteBuf getByteBuffer() { return new ByteBufNIO(buffer); } + /** + * Returns the byte array backing this document. Changes to the returned array will be reflected in this document. + * + * @return the backing byte array + * @since 5.7 + */ + public byte[] getByteBacking() { + return bytes; + } + + /** + * Returns the offset into the {@linkplain #getByteBacking() backing byte array} where this document starts. + * + * @return the offset + * @since 5.7 + */ + public int getByteOffset() { + return offset; + } + + /** + * Returns the length of this document within the {@linkplain #getByteBacking() backing byte array}. + * + * @return the length + * @since 5.7 + */ + public int getByteLength() { + return length; + } + /** * Decode this into a document. * diff --git a/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java b/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java index 4d81b7f97aa..2b45c58de17 100644 --- a/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java +++ b/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java @@ -16,13 +16,11 @@ package org.bson.codecs; -import org.bson.BsonBinaryReader; import org.bson.BsonBinaryWriter; import org.bson.BsonReader; import org.bson.BsonWriter; import org.bson.RawBsonDocument; import org.bson.io.BasicOutputBuffer; -import org.bson.io.ByteBufferBsonInput; /** * A simple BSONDocumentBuffer codec. It does not attempt to validate the contents of the underlying ByteBuffer. It assumes that it @@ -40,9 +38,7 @@ public RawBsonDocumentCodec() { @Override public void encode(final BsonWriter writer, final RawBsonDocument value, final EncoderContext encoderContext) { - try (BsonBinaryReader reader = new BsonBinaryReader(new ByteBufferBsonInput(value.getByteBuffer()))) { - writer.pipe(reader); - } + writer.pipe(value.getByteBacking(), value.getByteOffset(), value.getByteLength()); } @Override diff --git a/bson/src/main/org/bson/io/BsonInput.java b/bson/src/main/org/bson/io/BsonInput.java index 823355fe3ee..012d9ec4a07 100644 --- a/bson/src/main/org/bson/io/BsonInput.java +++ b/bson/src/main/org/bson/io/BsonInput.java @@ -127,6 +127,15 @@ public interface BsonInput extends Closeable { */ boolean hasRemaining(); + /** + * Pipes the specified number of bytes from {@linkplain BsonInput this} input to the given {@linkplain BsonOutput output}. + * + * @param output the output to pipe to + * @param numBytes the number of bytes to pipe + * @since 5.7 + */ + void pipe(BsonOutput output, int numBytes); + @Override void close(); } diff --git a/bson/src/main/org/bson/io/ByteBufferBsonInput.java b/bson/src/main/org/bson/io/ByteBufferBsonInput.java index 2819bdcb091..1ab5ac9f5b3 100644 --- a/bson/src/main/org/bson/io/ByteBufferBsonInput.java +++ b/bson/src/main/org/bson/io/ByteBufferBsonInput.java @@ -275,6 +275,24 @@ public boolean hasRemaining() { return buffer.hasRemaining(); } + @Override + public void pipe(final BsonOutput output, final int numBytes) { + ensureOpen(); + ensureAvailable(numBytes); + + if (buffer.isBackedByArray()) { + int position = buffer.position(); + int arrayOffset = buffer.arrayOffset(); + output.writeBytes(buffer.array(), arrayOffset + position, numBytes); + buffer.position(position + numBytes); + } else { + // Fallback: use temporary buffer for non-array-backed buffers + byte[] temp = new byte[numBytes]; + buffer.get(temp); + output.writeBytes(temp); + } + } + @Override public void close() { buffer.release(); From fc3726de2d4a09a91fe53a1d669e85a66c3ad438 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 20 May 2026 15:47:10 -0700 Subject: [PATCH 2/8] Refines RawBsonDocument encode optimization - Removes pipe(byte[], int, int) from BsonWriter interface to avoid coupling it to concrete IO classes; dispatches via instanceof BsonBinaryWriter in the codec instead - Renames getByteBacking() to getBackingArray() for clarity - Validates minimum BSON document size before writing raw bytes, consistent with the reader-based pipe path - Adds tests for the raw-byte pipe happy path and invalid-size rejection JAVA-6133 --- bson/src/main/org/bson/BsonBinaryWriter.java | 21 ++++++++++++--- bson/src/main/org/bson/BsonWriter.java | 26 ------------------ bson/src/main/org/bson/RawBsonDocument.java | 12 ++++----- .../org/bson/codecs/RawBsonDocumentCodec.java | 15 ++++++++++- bson/src/main/org/bson/io/BsonInput.java | 2 +- .../unit/org/bson/BsonBinaryWriterTest.java | 27 +++++++++++++++++++ .../bson/RawBsonDocumentSpecification.groovy | 17 ++++++++++++ 7 files changed, 82 insertions(+), 38 deletions(-) diff --git a/bson/src/main/org/bson/BsonBinaryWriter.java b/bson/src/main/org/bson/BsonBinaryWriter.java index aada1ecc12b..e57d061ddc4 100644 --- a/bson/src/main/org/bson/BsonBinaryWriter.java +++ b/bson/src/main/org/bson/BsonBinaryWriter.java @@ -334,8 +334,17 @@ public void pipe(final BsonReader reader) { pipeDocument(reader, null); } - @Override + /** + * Pipes a raw BSON document from the given byte array to this writer, writing the bytes directly to the + * output without intermediate object allocation. + * + * @param bytes the byte array containing the BSON document + * @param offset the offset into the byte array + * @param length the length of the BSON document + * @since 5.8 + */ public void pipe(final byte[] bytes, final int offset, final int length) { + checkMinDocumentSize(length); if (getState() == State.VALUE) { bsonOutput.writeByte(BsonType.DOCUMENT.getValue()); writeCurrentName(); @@ -361,9 +370,7 @@ private void pipeDocument(final BsonReader reader, final List extra } BsonInput bsonInput = binaryReader.getBsonInput(); int size = bsonInput.readInt32(); - if (size < 5) { - throw new BsonSerializationException("Document size must be at least 5"); - } + checkMinDocumentSize(size); int pipedDocumentStartPosition = bsonOutput.getPosition(); bsonOutput.writeInt32(size); bsonInput.pipe(bsonOutput, size - 4); @@ -438,6 +445,12 @@ public void reset() { mark = null; } + private static void checkMinDocumentSize(final int size) { + if (size < 5) { + throw new BsonSerializationException("Document size must be at least 5"); + } + } + private void writeCurrentName() { if (getContext().getContextType() == BsonContextType.ARRAY) { int index = getContext().index++; diff --git a/bson/src/main/org/bson/BsonWriter.java b/bson/src/main/org/bson/BsonWriter.java index 0043ea8ecb3..1d2e5055a3c 100644 --- a/bson/src/main/org/bson/BsonWriter.java +++ b/bson/src/main/org/bson/BsonWriter.java @@ -16,13 +16,9 @@ package org.bson; -import org.bson.io.ByteBufferBsonInput; import org.bson.types.Decimal128; import org.bson.types.ObjectId; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - /** * An interface for writing a logical BSON document using a push-oriented API. * @@ -360,26 +356,4 @@ public interface BsonWriter { * @param reader The source. */ void pipe(BsonReader reader); - - /** - * Pipes a raw BSON document from the given byte array to this writer. - * - *

The default implementation wraps the bytes in a {@linkplain BsonBinaryReader} - * and calls {@link #pipe(BsonReader)}. Implementations may override this - * to write the raw bytes directly without intermediate object allocation.

- * - * @param bytes the byte array containing the BSON document - * @param offset the offset into the byte array - * @param length the length of the BSON document - * @since 5.7 - */ - default void pipe(byte[] bytes, int offset, int length) { - try (BsonBinaryReader reader = new BsonBinaryReader( - new ByteBufferBsonInput( - new ByteBufNIO(ByteBuffer.wrap(bytes, offset, length) - .order(ByteOrder.LITTLE_ENDIAN))))) { - pipe(reader); - } - } - } diff --git a/bson/src/main/org/bson/RawBsonDocument.java b/bson/src/main/org/bson/RawBsonDocument.java index 4d478234a35..6d4c5328810 100644 --- a/bson/src/main/org/bson/RawBsonDocument.java +++ b/bson/src/main/org/bson/RawBsonDocument.java @@ -148,27 +148,27 @@ public ByteBuf getByteBuffer() { * Returns the byte array backing this document. Changes to the returned array will be reflected in this document. * * @return the backing byte array - * @since 5.7 + * @since 5.8 */ - public byte[] getByteBacking() { + public byte[] getBackingArray() { return bytes; } /** - * Returns the offset into the {@linkplain #getByteBacking() backing byte array} where this document starts. + * Returns the offset into the {@linkplain #getBackingArray() backing byte array} where this document starts. * * @return the offset - * @since 5.7 + * @since 5.8 */ public int getByteOffset() { return offset; } /** - * Returns the length of this document within the {@linkplain #getByteBacking() backing byte array}. + * Returns the length of this document within the {@linkplain #getBackingArray() backing byte array}. * * @return the length - * @since 5.7 + * @since 5.8 */ public int getByteLength() { return length; diff --git a/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java b/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java index 2b45c58de17..a0d5947429f 100644 --- a/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java +++ b/bson/src/main/org/bson/codecs/RawBsonDocumentCodec.java @@ -16,11 +16,13 @@ package org.bson.codecs; +import org.bson.BsonBinaryReader; import org.bson.BsonBinaryWriter; import org.bson.BsonReader; import org.bson.BsonWriter; import org.bson.RawBsonDocument; import org.bson.io.BasicOutputBuffer; +import org.bson.io.ByteBufferBsonInput; /** * A simple BSONDocumentBuffer codec. It does not attempt to validate the contents of the underlying ByteBuffer. It assumes that it @@ -38,7 +40,18 @@ public RawBsonDocumentCodec() { @Override public void encode(final BsonWriter writer, final RawBsonDocument value, final EncoderContext encoderContext) { - writer.pipe(value.getByteBacking(), value.getByteOffset(), value.getByteLength()); + if (writer instanceof BsonBinaryWriter) { + // Fast path. The pipe method should ideally exist on BsonWriter, but adding it as + // abstract would be a breaking change, and adding it as a default method would force + // BsonWriter to depend on BsonBinaryReader/ByteBufferBsonInput, violating the + // interface's abstraction. + // TODO JAVA-6211 move pipe(byte[], int, int) to BsonWriter to remove this instanceof. + ((BsonBinaryWriter) writer).pipe(value.getBackingArray(), value.getByteOffset(), value.getByteLength()); + } else { + try (BsonBinaryReader reader = new BsonBinaryReader(new ByteBufferBsonInput(value.getByteBuffer()))) { + writer.pipe(reader); + } + } } @Override diff --git a/bson/src/main/org/bson/io/BsonInput.java b/bson/src/main/org/bson/io/BsonInput.java index 012d9ec4a07..411345d0785 100644 --- a/bson/src/main/org/bson/io/BsonInput.java +++ b/bson/src/main/org/bson/io/BsonInput.java @@ -132,7 +132,7 @@ public interface BsonInput extends Closeable { * * @param output the output to pipe to * @param numBytes the number of bytes to pipe - * @since 5.7 + * @since 5.8 */ void pipe(BsonOutput output, int numBytes); diff --git a/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java b/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java index 0b067fc816f..4f589a42263 100644 --- a/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java +++ b/bson/src/test/unit/org/bson/BsonBinaryWriterTest.java @@ -802,7 +802,34 @@ public void testPipeOfDocumentWithInvalidSize() { // expected } } + } + + @Test + public void testPipeOfRawBytes() { + BasicOutputBuffer sourceBuffer = new BasicOutputBuffer(); + try (BsonBinaryWriter sourceWriter = new BsonBinaryWriter(sourceBuffer)) { + sourceWriter.writeStartDocument(); + sourceWriter.writeBoolean("a", true); + sourceWriter.writeEndDocument(); + } + byte[] documentBytes = sourceBuffer.toByteArray(); + BasicOutputBuffer destBuffer = new BasicOutputBuffer(); + try (BsonBinaryWriter destWriter = new BsonBinaryWriter(destBuffer)) { + destWriter.pipe(documentBytes, 0, documentBytes.length); + } + + assertArrayEquals(documentBytes, destBuffer.toByteArray()); + } + + @Test + public void testPipeOfRawBytesWithInvalidSize() { + byte[] bytes = {4, 0, 0, 0}; // minimum document size is 5 + + BasicOutputBuffer newBuffer = new BasicOutputBuffer(); + try (BsonBinaryWriter newWriter = new BsonBinaryWriter(newBuffer)) { + assertThrows(BsonSerializationException.class, () -> newWriter.pipe(bytes, 0, bytes.length)); + } } // CHECKSTYLE:OFF diff --git a/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy b/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy index a23ec06dedb..4a3748f2971 100644 --- a/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy +++ b/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy @@ -113,6 +113,23 @@ class RawBsonDocumentSpecification extends Specification { rawDocument << createRawDocumentVariants() } + + def 'getBackingArray, getByteOffset and getByteLength should expose the document range'() { + expect: + rawDocument.getByteOffset() == expectedOffset + rawDocument.getByteLength() == expectedLength + Arrays.copyOfRange( + rawDocument.getBackingArray(), + rawDocument.getByteOffset(), + rawDocument.getByteOffset() + rawDocument.getByteLength()) == getBytesFromDocument() + + where: + rawDocument | expectedOffset | expectedLength + createRawDocumenFromDocument() | 0 | 66 + createRawDocumentFromByteArray() | 0 | 66 + createRawDocumentFromByteArrayOffsetLength()| 1 | 66 + } + def 'parse should through if parameter is invalid'() { when: RawBsonDocument.parse(null) From 81ec442d9c3584dfc61f574645b94030ac94cbd3 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Thu, 21 May 2026 21:36:01 -0700 Subject: [PATCH 3/8] Make pipe default method. --- bson/src/main/org/bson/io/BsonInput.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bson/src/main/org/bson/io/BsonInput.java b/bson/src/main/org/bson/io/BsonInput.java index 411345d0785..250cddab0e4 100644 --- a/bson/src/main/org/bson/io/BsonInput.java +++ b/bson/src/main/org/bson/io/BsonInput.java @@ -134,7 +134,11 @@ public interface BsonInput extends Closeable { * @param numBytes the number of bytes to pipe * @since 5.8 */ - void pipe(BsonOutput output, int numBytes); + default void pipe(BsonOutput output, int numBytes) { + byte[] bytes = new byte[numBytes]; + readBytes(bytes); + output.writeBytes(bytes); + } @Override void close(); From a5790af26af134c7eae80fadb65e011ede0f3854 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Thu, 21 May 2026 22:12:18 -0700 Subject: [PATCH 4/8] Add tests. --- .../test/unit/org/bson/io/BsonInputTest.java | 125 ++++++++++++++++++ .../connection/ByteBufferBsonInputTest.java | 54 ++++++++ 2 files changed, 179 insertions(+) create mode 100644 bson/src/test/unit/org/bson/io/BsonInputTest.java diff --git a/bson/src/test/unit/org/bson/io/BsonInputTest.java b/bson/src/test/unit/org/bson/io/BsonInputTest.java new file mode 100644 index 00000000000..e86fc40eb20 --- /dev/null +++ b/bson/src/test/unit/org/bson/io/BsonInputTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.io; + +import org.bson.ByteBufNIO; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import org.bson.BsonSerializationException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BsonInputTest { + + @Test + void defaultPipeShouldCopyBytesFromInputToOutput() { + // given + byte[] inputBytes = {0x4a, 0x61, 0x76, 0x61, 0x21}; + + try (BsonInput bsonInput = new ForwardingBsonInput( + new ByteBufferBsonInput(new ByteBufNIO(ByteBuffer.wrap(inputBytes)))); + BasicOutputBuffer output = new BasicOutputBuffer()) { + // when + bsonInput.pipe(output, inputBytes.length); + + // then + assertEquals(inputBytes.length, bsonInput.getPosition()); + assertEquals(inputBytes.length, output.getPosition()); + assertArrayEquals(inputBytes, output.toByteArray()); + } + } + + @Test + void defaultPipeShouldCopyPartialBytesFromInputToOutput() { + // given + byte[] inputBytes = {0x4a, 0x61, 0x76, 0x61, 0x21}; + + try (BsonInput bsonInput = new ForwardingBsonInput( + new ByteBufferBsonInput(new ByteBufNIO(ByteBuffer.wrap(inputBytes)))); + BasicOutputBuffer output = new BasicOutputBuffer()) { + // when + bsonInput.pipe(output, 3); + + // then + assertEquals(3, bsonInput.getPosition()); + assertEquals(3, output.getPosition()); + assertArrayEquals(new byte[]{0x4a, 0x61, 0x76}, output.toByteArray()); + } + } + + /** + * Delegates all abstract methods but does NOT override pipe, + * so the default implementation is exercised. + */ + private static class ForwardingBsonInput implements BsonInput { + private final ByteBufferBsonInput delegate; + + ForwardingBsonInput(final ByteBufferBsonInput delegate) { + this.delegate = delegate; + } + + @Override + public int getPosition() { return delegate.getPosition(); } + + @Override + public byte readByte() { return delegate.readByte(); } + + @Override + public void readBytes(final byte[] bytes) { delegate.readBytes(bytes); } + + @Override + public void readBytes(final byte[] bytes, final int offset, final int length) { delegate.readBytes(bytes, offset, length); } + + @Override + public long readInt64() { return delegate.readInt64(); } + + @Override + public double readDouble() { return delegate.readDouble(); } + + @Override + public int readInt32() { return delegate.readInt32(); } + + @Override + public String readString() { return delegate.readString(); } + + @Override + public ObjectId readObjectId() { return delegate.readObjectId(); } + + @Override + public String readCString() { return delegate.readCString(); } + + @Override + public void skipCString() { delegate.skipCString(); } + + @Override + public void skip(final int numBytes) { delegate.skip(numBytes); } + + @Override + public BsonInputMark getMark(final int readLimit) { return delegate.getMark(readLimit); } + + @Override + public boolean hasRemaining() { return delegate.hasRemaining(); } + + @Override + public void close() { delegate.close(); } + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java index b988f1cde1a..d1918f5f7bf 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java @@ -22,6 +22,7 @@ import org.bson.BsonSerializationException; import org.bson.ByteBuf; import org.bson.ByteBufNIO; +import org.bson.io.BasicOutputBuffer; import org.bson.io.ByteBufferBsonInput; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; @@ -45,6 +46,7 @@ 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; @@ -710,6 +712,58 @@ void shouldReadSkipCStringWhenMultipleNullTerminatorPresentWithinBuffer(final Bu } + @ParameterizedTest(name = "should pipe bytes to output. BufferProvider={0}") + @MethodSource("bufferProviders") + void shouldPipeBytesToOutput(final BufferProvider bufferProvider) { + // given + byte[] input = {0x4a, 0x61, 0x76, 0x61, 0x21}; + ByteBuf buffer = allocateAndWriteToBuffer(bufferProvider, input); + + try (ByteBufferBsonInput bufferInput = new ByteBufferBsonInput(buffer); + BasicOutputBuffer bufferOutput = new BasicOutputBuffer()) { + // when + bufferInput.pipe(bufferOutput, input.length); + + // then + assertEquals(input.length, bufferInput.getPosition()); + assertEquals(input.length, bufferOutput.getPosition()); + assertArrayEquals(input, bufferOutput.toByteArray()); + } + } + + @ParameterizedTest(name = "should pipe partial bytes to output. BufferProvider={0}") + @MethodSource("bufferProviders") + void shouldPipePartialBytesToOutput(final BufferProvider bufferProvider) { + // given + byte[] input = {0x4a, 0x61, 0x76, 0x61, 0x21}; + ByteBuf buffer = allocateAndWriteToBuffer(bufferProvider, input); + + try (ByteBufferBsonInput bufferInput = new ByteBufferBsonInput(buffer); + BasicOutputBuffer output = new BasicOutputBuffer()) { + // when + bufferInput.pipe(output, 3); + + // then + assertEquals(3, bufferInput.getPosition()); + assertEquals(3, output.getPosition()); + assertArrayEquals(new byte[]{0x4a, 0x61, 0x76}, output.toByteArray()); + } + } + + @ParameterizedTest(name = "should throw when piping more bytes than available. BufferProvider={0}") + @MethodSource("bufferProviders") + void shouldThrowWhenPipingMoreBytesThanAvailable(final BufferProvider bufferProvider) { + // given + byte[] input = {0x4a, 0x61, 0x76}; + ByteBuf buffer = allocateAndWriteToBuffer(bufferProvider, input); + + try (ByteBufferBsonInput bufferInput = new ByteBufferBsonInput(buffer); + BasicOutputBuffer output = new BasicOutputBuffer()) { + // when & then + assertThrows(BsonSerializationException.class, () -> bufferInput.pipe(output, 10)); + } + } + private static ByteBuf allocateAndWriteToBuffer(final BufferProvider bufferProvider, final byte[] input) { ByteBuf buffer = bufferProvider.getBuffer(input.length); buffer.put(input, 0, input.length); From 9f49d2d781c30b38ef8cdb07b62e4359a16193bc Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Fri, 22 May 2026 09:16:37 -0700 Subject: [PATCH 5/8] Remove unused imports. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- bson/src/test/unit/org/bson/io/BsonInputTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/bson/src/test/unit/org/bson/io/BsonInputTest.java b/bson/src/test/unit/org/bson/io/BsonInputTest.java index e86fc40eb20..dd73a62a060 100644 --- a/bson/src/test/unit/org/bson/io/BsonInputTest.java +++ b/bson/src/test/unit/org/bson/io/BsonInputTest.java @@ -22,11 +22,8 @@ import java.nio.ByteBuffer; -import org.bson.BsonSerializationException; - import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; class BsonInputTest { From d5006c0e3947b9046a8b6abd68f26b2138f6bbe3 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Tue, 26 May 2026 23:21:32 -0700 Subject: [PATCH 6/8] Address PR review feedback and improve test coverage - Use String.getBytes(UTF_8) instead of raw hex bytes in pipe tests for readability - Add RawBsonDocumentTest with Named parametrized padding permutations (before, after, both) - Move backing array accessor test from Groovy spec to Java with broader coverage - Fix checkstyle violations in ForwardingBsonInput delegate methods JAVA-6133 --- .../bson/RawBsonDocumentSpecification.groovy | 16 --- .../unit/org/bson/RawBsonDocumentTest.java | 106 ++++++++++++++++++ .../test/unit/org/bson/io/BsonInputTest.java | 67 ++++++++--- .../connection/ByteBufferBsonInputTest.java | 8 +- 4 files changed, 159 insertions(+), 38 deletions(-) create mode 100644 bson/src/test/unit/org/bson/RawBsonDocumentTest.java diff --git a/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy b/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy index 4a3748f2971..7b4841a6577 100644 --- a/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy +++ b/bson/src/test/unit/org/bson/RawBsonDocumentSpecification.groovy @@ -114,22 +114,6 @@ class RawBsonDocumentSpecification extends Specification { } - def 'getBackingArray, getByteOffset and getByteLength should expose the document range'() { - expect: - rawDocument.getByteOffset() == expectedOffset - rawDocument.getByteLength() == expectedLength - Arrays.copyOfRange( - rawDocument.getBackingArray(), - rawDocument.getByteOffset(), - rawDocument.getByteOffset() + rawDocument.getByteLength()) == getBytesFromDocument() - - where: - rawDocument | expectedOffset | expectedLength - createRawDocumenFromDocument() | 0 | 66 - createRawDocumentFromByteArray() | 0 | 66 - createRawDocumentFromByteArrayOffsetLength()| 1 | 66 - } - def 'parse should through if parameter is invalid'() { when: RawBsonDocument.parse(null) diff --git a/bson/src/test/unit/org/bson/RawBsonDocumentTest.java b/bson/src/test/unit/org/bson/RawBsonDocumentTest.java new file mode 100644 index 00000000000..866dc101799 --- /dev/null +++ b/bson/src/test/unit/org/bson/RawBsonDocumentTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson; + +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.EncoderContext; +import org.bson.io.BasicOutputBuffer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RawBsonDocumentTest { + + private static final BsonDocument DOCUMENT = new BsonDocument() + .append("a", new BsonInt32(1)) + .append("b", new BsonInt32(2)) + .append("c", new BsonDocument("x", BsonBoolean.TRUE)) + .append("d", new BsonArray(Arrays.asList( + new BsonDocument("y", BsonBoolean.FALSE), + new BsonArray(Arrays.asList(new BsonInt32(1)))))); + + private static final byte[] DOCUMENT_BYTES = encodeDocument(); + + static Stream backingArrayAccessors() { + int documentLength = DOCUMENT_BYTES.length; + + Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(createFromDocument(), 0, documentLength)); + builder.add(Arguments.of(createFromByteArray(), 0, documentLength)); + + for (int padding = 1; padding <= 2; padding++) { + builder.add(Arguments.of(createPaddedBefore(padding), padding, documentLength)); + builder.add(Arguments.of(createPaddedAfter(padding), 0, documentLength)); + builder.add(Arguments.of(createPaddedBoth(padding), padding, documentLength)); + } + + return builder.build(); + } + + @ParameterizedTest(name = "{0}, expectedOffset={1}, expectedLength={2}") + @MethodSource("backingArrayAccessors") + void shouldExposeBackingArrayOffsetAndLength(final RawBsonDocument rawDocument, + final int expectedOffset, + final int expectedLength) { + assertEquals(expectedOffset, rawDocument.getByteOffset()); + assertEquals(expectedLength, rawDocument.getByteLength()); + assertArrayEquals(DOCUMENT_BYTES, + Arrays.copyOfRange( + rawDocument.getBackingArray(), + rawDocument.getByteOffset(), + rawDocument.getByteOffset() + rawDocument.getByteLength())); + } + + private static Named createFromDocument() { + return Named.of("from document", new RawBsonDocument(DOCUMENT, new BsonDocumentCodec())); + } + + private static Named createFromByteArray() { + return Named.of("from byte array", new RawBsonDocument(DOCUMENT_BYTES)); + } + + private static Named createPaddedBefore(final int padding) { + byte[] padded = new byte[DOCUMENT_BYTES.length + padding]; + System.arraycopy(DOCUMENT_BYTES, 0, padded, padding, DOCUMENT_BYTES.length); + return Named.of("padded before " + padding, new RawBsonDocument(padded, padding, DOCUMENT_BYTES.length)); + } + + private static Named createPaddedAfter(final int padding) { + byte[] padded = new byte[DOCUMENT_BYTES.length + padding]; + System.arraycopy(DOCUMENT_BYTES, 0, padded, 0, DOCUMENT_BYTES.length); + return Named.of("padded after " + padding, new RawBsonDocument(padded, 0, DOCUMENT_BYTES.length)); + } + + private static Named createPaddedBoth(final int padding) { + byte[] padded = new byte[DOCUMENT_BYTES.length + padding * 2]; + System.arraycopy(DOCUMENT_BYTES, 0, padded, padding, DOCUMENT_BYTES.length); + return Named.of("padded both " + padding, new RawBsonDocument(padded, padding, DOCUMENT_BYTES.length)); + } + + private static byte[] encodeDocument() { + BasicOutputBuffer buffer = new BasicOutputBuffer(); + new BsonDocumentCodec().encode(new BsonBinaryWriter(buffer), DOCUMENT, EncoderContext.builder().build()); + return Arrays.copyOf(buffer.getInternalBuffer(), buffer.getPosition()); + } +} diff --git a/bson/src/test/unit/org/bson/io/BsonInputTest.java b/bson/src/test/unit/org/bson/io/BsonInputTest.java index dd73a62a060..f9676f6a8de 100644 --- a/bson/src/test/unit/org/bson/io/BsonInputTest.java +++ b/bson/src/test/unit/org/bson/io/BsonInputTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -30,7 +31,7 @@ class BsonInputTest { @Test void defaultPipeShouldCopyBytesFromInputToOutput() { // given - byte[] inputBytes = {0x4a, 0x61, 0x76, 0x61, 0x21}; + byte[] inputBytes = "Java!".getBytes(StandardCharsets.UTF_8); try (BsonInput bsonInput = new ForwardingBsonInput( new ByteBufferBsonInput(new ByteBufNIO(ByteBuffer.wrap(inputBytes)))); @@ -48,7 +49,7 @@ void defaultPipeShouldCopyBytesFromInputToOutput() { @Test void defaultPipeShouldCopyPartialBytesFromInputToOutput() { // given - byte[] inputBytes = {0x4a, 0x61, 0x76, 0x61, 0x21}; + byte[] inputBytes = "Java!".getBytes(StandardCharsets.UTF_8); try (BsonInput bsonInput = new ForwardingBsonInput( new ByteBufferBsonInput(new ByteBufNIO(ByteBuffer.wrap(inputBytes)))); @@ -59,7 +60,7 @@ void defaultPipeShouldCopyPartialBytesFromInputToOutput() { // then assertEquals(3, bsonInput.getPosition()); assertEquals(3, output.getPosition()); - assertArrayEquals(new byte[]{0x4a, 0x61, 0x76}, output.toByteArray()); + assertArrayEquals("Jav".getBytes(StandardCharsets.UTF_8), output.toByteArray()); } } @@ -75,48 +76,78 @@ private static class ForwardingBsonInput implements BsonInput { } @Override - public int getPosition() { return delegate.getPosition(); } + public int getPosition() { + return delegate.getPosition(); + } @Override - public byte readByte() { return delegate.readByte(); } + public byte readByte() { + return delegate.readByte(); + } @Override - public void readBytes(final byte[] bytes) { delegate.readBytes(bytes); } + public void readBytes(final byte[] bytes) { + delegate.readBytes(bytes); + } @Override - public void readBytes(final byte[] bytes, final int offset, final int length) { delegate.readBytes(bytes, offset, length); } + public void readBytes(final byte[] bytes, final int offset, final int length) { + delegate.readBytes(bytes, offset, length); + } @Override - public long readInt64() { return delegate.readInt64(); } + public long readInt64() { + return delegate.readInt64(); + } @Override - public double readDouble() { return delegate.readDouble(); } + public double readDouble() { + return delegate.readDouble(); + } @Override - public int readInt32() { return delegate.readInt32(); } + public int readInt32() { + return delegate.readInt32(); + } @Override - public String readString() { return delegate.readString(); } + public String readString() { + return delegate.readString(); + } @Override - public ObjectId readObjectId() { return delegate.readObjectId(); } + public ObjectId readObjectId() { + return delegate.readObjectId(); + } @Override - public String readCString() { return delegate.readCString(); } + public String readCString() { + return delegate.readCString(); + } @Override - public void skipCString() { delegate.skipCString(); } + public void skipCString() { + delegate.skipCString(); + } @Override - public void skip(final int numBytes) { delegate.skip(numBytes); } + public void skip(final int numBytes) { + delegate.skip(numBytes); + } @Override - public BsonInputMark getMark(final int readLimit) { return delegate.getMark(readLimit); } + public BsonInputMark getMark(final int readLimit) { + return delegate.getMark(readLimit); + } @Override - public boolean hasRemaining() { return delegate.hasRemaining(); } + public boolean hasRemaining() { + return delegate.hasRemaining(); + } @Override - public void close() { delegate.close(); } + public void close() { + delegate.close(); + } } } diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java index d1918f5f7bf..d5128be8dc9 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/ByteBufferBsonInputTest.java @@ -716,7 +716,7 @@ void shouldReadSkipCStringWhenMultipleNullTerminatorPresentWithinBuffer(final Bu @MethodSource("bufferProviders") void shouldPipeBytesToOutput(final BufferProvider bufferProvider) { // given - byte[] input = {0x4a, 0x61, 0x76, 0x61, 0x21}; + byte[] input = "Java!".getBytes(StandardCharsets.UTF_8); ByteBuf buffer = allocateAndWriteToBuffer(bufferProvider, input); try (ByteBufferBsonInput bufferInput = new ByteBufferBsonInput(buffer); @@ -735,7 +735,7 @@ void shouldPipeBytesToOutput(final BufferProvider bufferProvider) { @MethodSource("bufferProviders") void shouldPipePartialBytesToOutput(final BufferProvider bufferProvider) { // given - byte[] input = {0x4a, 0x61, 0x76, 0x61, 0x21}; + byte[] input = "Java!".getBytes(StandardCharsets.UTF_8); ByteBuf buffer = allocateAndWriteToBuffer(bufferProvider, input); try (ByteBufferBsonInput bufferInput = new ByteBufferBsonInput(buffer); @@ -746,7 +746,7 @@ void shouldPipePartialBytesToOutput(final BufferProvider bufferProvider) { // then assertEquals(3, bufferInput.getPosition()); assertEquals(3, output.getPosition()); - assertArrayEquals(new byte[]{0x4a, 0x61, 0x76}, output.toByteArray()); + assertArrayEquals("Jav".getBytes(StandardCharsets.UTF_8), output.toByteArray()); } } @@ -754,7 +754,7 @@ void shouldPipePartialBytesToOutput(final BufferProvider bufferProvider) { @MethodSource("bufferProviders") void shouldThrowWhenPipingMoreBytesThanAvailable(final BufferProvider bufferProvider) { // given - byte[] input = {0x4a, 0x61, 0x76}; + byte[] input = "Jav".getBytes(StandardCharsets.UTF_8); ByteBuf buffer = allocateAndWriteToBuffer(bufferProvider, input); try (ByteBufferBsonInput bufferInput = new ByteBufferBsonInput(buffer); From 54292e812923bdf72821cd88524773d3dd47f883 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 27 May 2026 00:01:31 -0700 Subject: [PATCH 7/8] Change javadoc. --- bson/src/main/org/bson/BsonBinaryWriter.java | 7 +++---- bson/src/main/org/bson/RawBsonDocument.java | 6 +++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/bson/src/main/org/bson/BsonBinaryWriter.java b/bson/src/main/org/bson/BsonBinaryWriter.java index e57d061ddc4..9641f1d678d 100644 --- a/bson/src/main/org/bson/BsonBinaryWriter.java +++ b/bson/src/main/org/bson/BsonBinaryWriter.java @@ -335,12 +335,11 @@ public void pipe(final BsonReader reader) { } /** - * Pipes a raw BSON document from the given byte array to this writer, writing the bytes directly to the - * output without intermediate object allocation. + * Pipes an encoded BSON document from the given byte array to this writer. * - * @param bytes the byte array containing the BSON document + * @param bytes the byte array containing the encoded BSON document * @param offset the offset into the byte array - * @param length the length of the BSON document + * @param length the length of the encoded BSON document * @since 5.8 */ public void pipe(final byte[] bytes, final int offset, final int length) { diff --git a/bson/src/main/org/bson/RawBsonDocument.java b/bson/src/main/org/bson/RawBsonDocument.java index 6d4c5328810..7b604354b04 100644 --- a/bson/src/main/org/bson/RawBsonDocument.java +++ b/bson/src/main/org/bson/RawBsonDocument.java @@ -145,10 +145,14 @@ public ByteBuf getByteBuffer() { } /** - * Returns the byte array backing this document. Changes to the returned array will be reflected in this document. + * Returns the byte array backing this document. The returned array may be larger than the BSON document itself; + * only the range from {@link #getByteOffset()} to {@code getByteOffset() + }{@link #getByteLength()} contains + * valid document bytes. Changes to the returned array will be reflected in this document. * * @return the backing byte array * @since 5.8 + * @see #getByteOffset() + * @see #getByteLength() */ public byte[] getBackingArray() { return bytes; From 495fe2b8d59dea9215eacd50c6322aefe3faa35e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 27 May 2026 13:07:03 +0100 Subject: [PATCH 8/8] Update bson/src/main/org/bson/RawBsonDocument.java --- bson/src/main/org/bson/RawBsonDocument.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bson/src/main/org/bson/RawBsonDocument.java b/bson/src/main/org/bson/RawBsonDocument.java index 7b604354b04..7a9cbbd3b3c 100644 --- a/bson/src/main/org/bson/RawBsonDocument.java +++ b/bson/src/main/org/bson/RawBsonDocument.java @@ -44,7 +44,7 @@ import static org.bson.assertions.Assertions.notNull; /** - * An immutable BSON document that is represented using only the raw bytes. + * A BSON document that is represented using only the raw bytes. * * @since 3.0 */