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]