From d124184dc5f5bb121efa6981b497e81be030b3b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 14:14:47 +0000 Subject: [PATCH] Make ByteString.copyToBuffer(buffer, offset) public, add scaladoc and test coverage Agent-Logs-Url: https://github.com/pjfanning/incubator-pekko/sessions/09848a76-7cf9-49ad-b3a6-e466857fa140 Co-authored-by: pjfanning <11783444+pjfanning@users.noreply.github.com> --- .../apache/pekko/util/ByteStringSpec.scala | 61 +++++++++++++++++++ .../org/apache/pekko/util/ByteString.scala | 21 ++++--- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala b/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala index f4075ec5a5..c596dcad6e 100644 --- a/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala +++ b/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala @@ -2604,6 +2604,67 @@ class ByteStringSpec extends AnyWordSpec with Matchers with Checkers { ByteString.empty.copyToBuffer(ByteBuffer.allocate(10)) should ===(0) } + "copyToBuffer(buffer, offset) copies from the given offset for different ByteString types" in { + import java.nio.ByteBuffer + + // ByteString1C — copy from offset 2 + val bs1c = ByteString1C(Array[Byte](1, 2, 3, 4, 5)) + val buf1 = ByteBuffer.allocate(5) + bs1c.copyToBuffer(buf1, 2) should ===(3) + buf1.flip() + val result1 = new Array[Byte](3) + buf1.get(result1) + result1.toSeq should ===(Seq[Byte](3, 4, 5)) + + // ByteString1C — offset beyond length copies nothing + val buf2 = ByteBuffer.allocate(5) + bs1c.copyToBuffer(buf2, 10) should ===(0) + + // ByteString1C — offset 0 copies all + val buf3 = ByteBuffer.allocate(5) + bs1c.copyToBuffer(buf3, 0) should ===(5) + + // ByteString1 — copy from offset 1 (internal startIndex + user offset) + val bs1 = ByteString1(Array[Byte](0, 10, 20, 30, 40, 50), 1, 4) // logical bytes [10, 20, 30, 40] + val buf4 = ByteBuffer.allocate(4) + bs1.copyToBuffer(buf4, 1) should ===(3) + buf4.flip() + val result4 = new Array[Byte](3) + buf4.get(result4) + result4.toSeq should ===(Seq[Byte](20, 30, 40)) + + // ByteString1 — offset 0 copies all logical bytes + val buf5 = ByteBuffer.allocate(4) + bs1.copyToBuffer(buf5, 0) should ===(4) + buf5.flip() + buf5.get() should ===(10.toByte) + + // ByteStrings — copy from offset that skips first segment entirely + val bss = ByteStrings(ByteString1.fromString("abc"), ByteString1.fromString("def")) + val buf6 = ByteBuffer.allocate(10) + bss.copyToBuffer(buf6, 3) should ===(3) + buf6.flip() + val result6 = new Array[Byte](3) + buf6.get(result6) + result6.toSeq should ===(Seq[Byte]('d', 'e', 'f')) + + // ByteStrings — copy from offset mid-first-segment + val buf7 = ByteBuffer.allocate(10) + bss.copyToBuffer(buf7, 1) should ===(5) + buf7.flip() + val result7 = new Array[Byte](5) + buf7.get(result7) + result7.toSeq should ===(Seq[Byte]('b', 'c', 'd', 'e', 'f')) + + // ByteStrings — offset 0 copies all + val buf8 = ByteBuffer.allocate(6) + bss.copyToBuffer(buf8, 0) should ===(6) + + // ByteString.empty — any offset copies nothing + ByteString.empty.copyToBuffer(ByteBuffer.allocate(10), 0) should ===(0) + ByteString.empty.copyToBuffer(ByteBuffer.allocate(10), 5) should ===(0) + } + "copying chunks to an array" in { val iterator = (ByteString("123") ++ ByteString("456")).iterator val array = Array.ofDim[Byte](6) diff --git a/actor/src/main/scala/org/apache/pekko/util/ByteString.scala b/actor/src/main/scala/org/apache/pekko/util/ByteString.scala index 42aa48d941..0d4984a2fa 100644 --- a/actor/src/main/scala/org/apache/pekko/util/ByteString.scala +++ b/actor/src/main/scala/org/apache/pekko/util/ByteString.scala @@ -433,8 +433,7 @@ object ByteString { override def copyToBuffer(buffer: ByteBuffer): Int = writeToBuffer(buffer, offset = 0) - /** INTERNAL API: Specialized for internal use, copying from an offset without slicing. */ - private[pekko] override def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = + override def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = writeToBuffer(buffer, offset) /** INTERNAL API: Specialized for internal use, writing multiple ByteString1C into the same ByteBuffer. */ @@ -576,8 +575,7 @@ object ByteString { override def copyToBuffer(buffer: ByteBuffer): Int = writeToBuffer(buffer, offset = 0) - /** INTERNAL API: Specialized for internal use, copying from an offset without slicing. */ - private[pekko] override def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = + override def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = writeToBuffer(buffer, offset) /** INTERNAL API: Specialized for internal use, writing multiple ByteString1C into the same ByteBuffer. */ @@ -993,8 +991,7 @@ object ByteString { override def copyToBuffer(buffer: ByteBuffer): Int = copyToBuffer(buffer, offset = 0) - /** INTERNAL API: Specialized for internal use, copying from an offset without slicing. */ - private[pekko] override def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = { + override def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = { var remainingOffset = offset var written = 0 var i = 0 @@ -1899,8 +1896,16 @@ sealed abstract class ByteString */ def copyToBuffer(@nowarn("msg=never used") buffer: ByteBuffer): Int - /** INTERNAL API: Copy bytes to a ByteBuffer from a ByteString offset without allocating a slice. */ - private[pekko] def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = + /** + * Copy as many bytes as possible to a ByteBuffer, starting from a given offset within this ByteString + * and the buffer's current position. This method will not overflow the buffer. + * + * @param buffer a ByteBuffer to copy bytes to + * @param offset the offset within this ByteString to start copying from + * @return the number of bytes actually copied + * @since 2.0.0 + */ + def copyToBuffer(buffer: ByteBuffer, offset: Int): Int = if (offset <= 0) copyToBuffer(buffer) else drop(offset).copyToBuffer(buffer)