This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new a257a9fca1 Fix copyToBuffer: guard negative return in 
ByteString1C.writeToBuffer (#2897)
a257a9fca1 is described below

commit a257a9fca1e4398098d2e15cdd3701de07bfa1e6
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Apr 24 13:24:35 2026 +0200

    Fix copyToBuffer: guard negative return in ByteString1C.writeToBuffer 
(#2897)
    
    * Fix copyToBuffer: guard negative return in ByteString1C.writeToBuffer; 
early-exit in ByteStrings.copyToBuffer; add comprehensive tests
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/273deee1-93de-4334-a624-88dd1868db37
    
    Co-authored-by: pjfanning <[email protected]>
    
    * Replace tailrec+index in ByteStrings.copyToBuffer with iterator-based 
while loop
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/9f4b72b7-3024-4576-98bb-e1c3a56747ed
    
    Co-authored-by: pjfanning <[email protected]>
    
    ---------
    
    Co-authored-by: copilot-swe-agent[bot] 
<[email protected]>
    Co-authored-by: pjfanning <[email protected]>
---
 .../org/apache/pekko/util/ByteStringSpec.scala     | 65 ++++++++++++++++++++++
 .../scala/org/apache/pekko/util/ByteString.scala   | 13 +++--
 2 files changed, 72 insertions(+), 6 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 1956c556a4..13e4fd40b5 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
@@ -2501,6 +2501,71 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
         }
       }
 
+      "copyToBuffer returns the number of bytes written and respects buffer 
capacity" in {
+        import java.nio.ByteBuffer
+        // ByteString1C — full copy
+        val bs1c = ByteString1C(Array[Byte](1, 2, 3, 4, 5))
+        val buf1 = ByteBuffer.allocate(5)
+        bs1c.copyToBuffer(buf1) should ===(5)
+        buf1.flip()
+        buf1.get() should ===(1.toByte)
+
+        // ByteString1C — partial copy when buffer is smaller
+        val buf2 = ByteBuffer.allocate(3)
+        bs1c.copyToBuffer(buf2) should ===(3)
+        buf2.flip()
+        buf2.get() should ===(1.toByte)
+        buf2.get() should ===(2.toByte)
+        buf2.get() should ===(3.toByte)
+
+        // ByteString1C — empty buffer, 0 bytes copied
+        val buf3 = ByteBuffer.allocate(0)
+        bs1c.copyToBuffer(buf3) should ===(0)
+
+        // ByteString1 with internal offset — full copy
+        val bs1 = ByteString1(Array[Byte](0, 10, 20, 30, 40, 50), 1, 4) // 
[10, 20, 30, 40]
+        val buf4 = ByteBuffer.allocate(4)
+        bs1.copyToBuffer(buf4) should ===(4)
+        buf4.flip()
+        buf4.get() should ===(10.toByte)
+
+        // ByteString1 with internal offset — partial copy
+        val buf5 = ByteBuffer.allocate(2)
+        bs1.copyToBuffer(buf5) should ===(2)
+        buf5.flip()
+        buf5.get() should ===(10.toByte)
+        buf5.get() should ===(20.toByte)
+
+        // ByteStrings — full copy, all segments visited
+        val bss = ByteStrings(ByteString1.fromString("abc"), 
ByteString1.fromString("def"))
+        val buf6 = ByteBuffer.allocate(6)
+        bss.copyToBuffer(buf6) should ===(6)
+        buf6.flip()
+        val result6 = new Array[Byte](6)
+        buf6.get(result6)
+        result6.toSeq should ===(Seq[Byte]('a', 'b', 'c', 'd', 'e', 'f'))
+
+        // ByteStrings — partial copy stops mid-segment-boundary
+        val buf7 = ByteBuffer.allocate(4)
+        bss.copyToBuffer(buf7) should ===(4)
+        buf7.flip()
+        val result7 = new Array[Byte](4)
+        buf7.get(result7)
+        result7.toSeq should ===(Seq[Byte]('a', 'b', 'c', 'd'))
+
+        // ByteStrings — partial copy that exactly fills after first segment
+        val bss2 = ByteStrings(ByteString1(Array[Byte](1, 2, 3)), 
ByteString1(Array[Byte](4, 5, 6)))
+        val buf8 = ByteBuffer.allocate(3) // exactly the first segment
+        bss2.copyToBuffer(buf8) should ===(3)
+        buf8.flip()
+        val result8 = new Array[Byte](3)
+        buf8.get(result8)
+        result8.toSeq should ===(Seq[Byte](1, 2, 3))
+
+        // Empty ByteString — 0 bytes copied
+        ByteString.empty.copyToBuffer(ByteBuffer.allocate(10)) 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 43735db440..0526d2855d 100644
--- a/actor/src/main/scala/org/apache/pekko/util/ByteString.scala
+++ b/actor/src/main/scala/org/apache/pekko/util/ByteString.scala
@@ -415,7 +415,7 @@ object ByteString {
 
     /** INTERNAL API: Specialized for internal use, writing multiple 
ByteString1C into the same ByteBuffer. */
     private[pekko] def writeToBuffer(buffer: ByteBuffer, offset: Int): Int = {
-      val copyLength = Math.min(buffer.remaining, length - offset)
+      val copyLength = Math.max(0, Math.min(buffer.remaining, length - offset))
       if (copyLength > 0) {
         buffer.put(bytes, offset, copyLength)
       }
@@ -945,11 +945,12 @@ object ByteString {
     def isCompact: Boolean = if (bytestrings.length == 1) 
bytestrings.head.isCompact else false
 
     override def copyToBuffer(buffer: ByteBuffer): Int = {
-      @tailrec def copyItToTheBuffer(buffer: ByteBuffer, i: Int, written: 
Int): Int =
-        if (i < bytestrings.length) copyItToTheBuffer(buffer, i + 1, written + 
bytestrings(i).writeToBuffer(buffer))
-        else written
-
-      copyItToTheBuffer(buffer, 0, 0)
+      val it = bytestrings.iterator
+      var written = 0
+      while (it.hasNext && buffer.hasRemaining) {
+        written += it.next().writeToBuffer(buffer)
+      }
+      written
     }
 
     def compact: CompactByteString = {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to