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 2f9917b2c5 Add ByteString.endsWith and expand startsWith test coverage 
(#2862)
2f9917b2c5 is described below

commit 2f9917b2c58e49c64dff58f2753c433bc11f0380
Author: PJ Fanning <[email protected]>
AuthorDate: Sat Apr 18 12:48:49 2026 +0200

    Add ByteString.endsWith and expand startsWith test coverage (#2862)
    
    * Add endsWith to ByteString and improve startsWith/endsWith tests
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/ec49ac93-3978-458d-882a-e3a243c1b19f
    
    Co-authored-by: pjfanning <[email protected]>
    
    * overrides
    
    * Update ByteStringSpec.scala
    
    * Fix startsWith[B >: Byte](IterableOnce, Int) consuming iterator before 
loop
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/fe94910a-758a-4038-be9e-32cad79ed452
    
    Co-authored-by: pjfanning <[email protected]>
    
    * add string tests
    
    * Create ByteString_startEnd_Benchmark.scala
    
    * Update ByteStringSpec.scala
    
    * Fix variable name from 'bss' to 'bs' in benchmarks
    
    * Add SWAR-optimised startsWith/endsWith overrides to ByteString1C and 
ByteString1
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/4e1ea18b-b9d9-4c17-8b0a-bc51a3aac377
    
    Co-authored-by: pjfanning <[email protected]>
    
    * Use Arrays.equals for non-SWAR tail bytes in ByteString1C and ByteString1 
startsWith/endsWith
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/d8dfbe8f-158a-4cc1-92a3-0e84575dfa4c
    
    Co-authored-by: pjfanning <[email protected]>
    
    * Update ByteString_startEnd_Benchmark.scala
    
    * Update ByteString_startEnd_Benchmark.scala
    
    ---------
    
    Co-authored-by: copilot-swe-agent[bot] 
<[email protected]>
    Co-authored-by: pjfanning <[email protected]>
---
 .../org/apache/pekko/util/ByteStringSpec.scala     | 267 ++++++++++++++++++++-
 .../scala/org/apache/pekko/util/ByteString.scala   | 101 ++++++++
 .../pekko/util/ByteString_startEnd_Benchmark.scala |  50 ++++
 3 files changed, 412 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 0891437a14..acfbe83280 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
@@ -699,12 +699,6 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       byteStringLong.lastIndexOf('m') should ===(12)
       byteStringLong.lastIndexOf('z') should ===(25)
       byteStringLong.lastIndexOf('a') should ===(0)
-
-      val long1 = ByteString1.fromString("abcdefghijklmnop") // 16 bytes
-      long1.lastIndexOf('a'.toByte) should ===(0)
-      long1.lastIndexOf('p'.toByte) should ===(15)
-      long1.lastIndexOf('h'.toByte, 7) should ===(7)
-      long1.lastIndexOf('h'.toByte, 6) should ===(-1)
     }
     "indexOf from offset" in {
       ByteString.empty.indexOf(5, -1) should ===(-1)
@@ -944,6 +938,15 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       val slicedLong = ByteString1.fromString("xxabcdefghijk").drop(2) // 
"abcdefghijk", 11 bytes
       slicedLong.lastIndexOf('a'.toByte) should ===(0) // first byte, found 
via chunk scan
       slicedLong.lastIndexOf('h'.toByte) should ===(7) // last byte of chunk
+
+      val long1 = ByteString1.fromString("abcdefghijklmnop") // 16 bytes
+      long1.lastIndexOf('a'.toByte) should ===(0)
+      long1.lastIndexOf('p'.toByte) should ===(15)
+      long1.lastIndexOf('h'.toByte, 7) should ===(7)
+      long1.lastIndexOf('h'.toByte, 6) should ===(-1)
+
+      val concat1 = makeMultiByteStringsWithEmptyComponents()
+      concat1.lastIndexOf(16.toByte) should ===(17)
     }
     "indexOf (specialized)" in {
       ByteString.empty.indexOf(5.toByte) should ===(-1)
@@ -981,6 +984,11 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       concat0.indexOf(0xFF.toByte) should ===(0)
       concat0.indexOf(16.toByte) should ===(17)
       concat0.indexOf(0xFE.toByte) should ===(-1)
+
+      val concat1 = makeMultiByteStringsWithEmptyComponents()
+      concat1.indexOf(0xFF.toByte) should ===(0)
+      concat1.indexOf(16.toByte) should ===(17)
+      concat1.indexOf(0xFE.toByte) should ===(-1)
     }
     "indexOf (specialized) from offset" in {
       ByteString.empty.indexOf(5.toByte, -1) should ===(-1)
@@ -1270,6 +1278,10 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       val byteStringWithOffset = ByteString1(
         "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 24)
       byteStringWithOffset.indexOfSlice(slice0) should ===(21)
+
+      val concat0 = makeMultiByteStringsWithEmptyComponents()
+      concat0.indexOfSlice(Array(15.toByte, 16.toByte)) should ===(16)
+      concat0.indexOfSlice(Array(16.toByte, 15.toByte)) should ===(-1)
     }
     "lastIndexOfSlice" in {
       val slice0 = ByteString1.fromString("xyz")
@@ -1360,6 +1372,10 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       concat0.lastIndexOfSlice(Array(16.toByte, 0xFF.toByte)) should ===(17)
       concat0.lastIndexOfSlice(Array(16.toByte, 0xFE.toByte)) should ===(-1)
 
+      val concat1 = makeMultiByteStringsWithEmptyComponents()
+      concat1.lastIndexOfSlice(Array(15.toByte, 16.toByte)) should ===(16)
+      concat1.lastIndexOfSlice(Array(16.toByte, 15.toByte)) should ===(-1)
+
       // Empty source with empty slice -> 0; with non-empty slice -> -1
       ByteString.empty.lastIndexOfSlice(Array.empty[Byte]) should ===(0)
       ByteString.empty.lastIndexOfSlice(Array[Byte]('a')) should ===(-1)
@@ -1383,6 +1399,52 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       ByteStrings(ByteString1.fromString("ab"), ByteString1.fromString("cd"))
         .lastIndexOfSlice(Array[Byte]('b', 'c')) should ===(1)
     }
+    "startsWith" in {
+      val slice0 = ByteString1.fromString("abcdefghijk")
+      val slice1 = ByteString1.fromString("xyz")
+      val slice2 = ByteString1.fromString("zabcdefghijk")
+      val notSlice = ByteString1.fromString("12345")
+      val byteStringLong = ByteString1.fromString("abcdefghijklmnopqrstuvwxyz")
+      val byteStrings = ByteStrings(byteStringLong, byteStringLong)
+      byteStringLong.startsWith(slice0) should ===(true)
+      byteStringLong.startsWith(slice1, 23) should ===(true)
+      byteStringLong.startsWith(notSlice) should ===(false)
+
+      byteStrings.startsWith(slice0) should ===(true)
+      byteStrings.startsWith(slice1, 23) should ===(true)
+      byteStrings.startsWith(slice2, 25) should ===(true)
+      byteStrings.startsWith(notSlice) should ===(false)
+
+      val byteStringWithOffset = ByteString1(
+        "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 20)
+      val slice3 = ByteString1.fromString("cdefghijklmn")
+      byteStringWithOffset.startsWith(slice3) should ===(true)
+
+      // empty bytes array always returns true
+      byteStringLong.startsWith(Array.emptyByteArray) should ===(true)
+      byteStrings.startsWith(Array.emptyByteArray) should ===(true)
+
+      // exact match
+      val fullSliceText = "abcdefghijklmnopqrstuvwxyz"
+      val fullSlice = ByteString1.fromString(fullSliceText)
+      byteStringLong.startsWith(fullSlice) should ===(true)
+      byteStringLong.startsWith(fullSliceText) should ===(true)
+
+      // bytes longer than ByteString returns false
+      val tooLong = ByteString1.fromString("abcdefghijklmnopqrstuvwxyz1")
+      byteStringLong.startsWith(tooLong) should ===(false)
+
+      // ByteString1C
+      val byteString1C = 
ByteString1C("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8))
+      byteString1C.startsWith(slice0) should ===(true)
+      byteString1C.startsWith(notSlice) should ===(false)
+      byteString1C.startsWith(Array.emptyByteArray) should ===(true)
+
+      // empty ByteString
+      ByteString.empty.startsWith(Array.emptyByteArray) should ===(true)
+      ByteString.empty.startsWith(ByteString1.fromString("a")) should 
===(false)
+      ByteString.empty.startsWith("a") should ===(false)
+    }
     "startsWith (specialized)" in {
       val slice0 = "abcdefghijk".getBytes(StandardCharsets.UTF_8)
       val slice1 = "xyz".getBytes(StandardCharsets.UTF_8)
@@ -1403,6 +1465,193 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
         "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 20)
       val slice3 = "cdefghijklmn".getBytes(StandardCharsets.UTF_8)
       byteStringWithOffset.startsWith(slice3) should ===(true)
+
+      // empty bytes array always returns true
+      byteStringLong.startsWith(Array.emptyByteArray) should ===(true)
+      byteStrings.startsWith(Array.emptyByteArray) should ===(true)
+
+      // exact match
+      val fullSlice = 
"abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)
+      byteStringLong.startsWith(fullSlice) should ===(true)
+
+      // bytes longer than ByteString returns false
+      val tooLong = 
"abcdefghijklmnopqrstuvwxyz1".getBytes(StandardCharsets.UTF_8)
+      byteStringLong.startsWith(tooLong) should ===(false)
+
+      // ByteString1C
+      val byteString1C = 
ByteString1C("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8))
+      byteString1C.startsWith(slice0) should ===(true)
+      byteString1C.startsWith(notSlice) should ===(false)
+      byteString1C.startsWith(Array.emptyByteArray) should ===(true)
+
+      // empty ByteString
+      ByteString.empty.startsWith(Array.emptyByteArray) should ===(true)
+      ByteString.empty.startsWith(Array[Byte]('a')) should ===(false)
+
+      val concat0 = makeMultiByteStringsWithEmptyComponents()
+      concat0.startsWith(Array(0xFF.toByte, 0.toByte, 1.toByte)) should 
===(true)
+      concat0.startsWith(Array(0xFF.toByte, 1.toByte)) should ===(false)
+
+      // SWAR-optimised path: needles spanning full 8-byte chunks (ByteString1)
+      // exactly 8 bytes: one SWAR iteration, no tail
+      val exactly8 = "abcdefgh".getBytes(StandardCharsets.UTF_8)
+      byteStringLong.startsWith(exactly8) should ===(true)
+      byteStringLong.startsWith("12345678".getBytes(StandardCharsets.UTF_8)) 
should ===(false)
+      // 16 bytes: two SWAR iterations, no tail
+      val exactly16 = "abcdefghijklmnop".getBytes(StandardCharsets.UTF_8)
+      byteStringLong.startsWith(exactly16) should ===(true)
+      
byteStringLong.startsWith("abcdefghijklmnop".reverse.getBytes(StandardCharsets.UTF_8))
 should ===(false)
+      // 9 bytes: one SWAR iteration + 1-byte tail
+      val nine = "abcdefghi".getBytes(StandardCharsets.UTF_8)
+      byteStringLong.startsWith(nine) should ===(true)
+      // mismatch buried inside the 2nd 8-byte chunk
+      val mismatchInSecondChunk = 
"abcdefghijklmno_".getBytes(StandardCharsets.UTF_8)
+      byteStringLong.startsWith(mismatchInSecondChunk) should ===(false)
+      // ByteString1 with startsWith(Array[Byte], offset) exercising offset != 0
+      val bs1WithOffset = 
ByteString1("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 0, 
26)
+      bs1WithOffset.startsWith("ijklmnop".getBytes(StandardCharsets.UTF_8), 8) 
should ===(true)
+      bs1WithOffset.startsWith("12345678".getBytes(StandardCharsets.UTF_8), 8) 
should ===(false)
+      // ByteString1C SWAR path
+      val bs1c = 
ByteString1C("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8))
+      bs1c.startsWith(exactly8) should ===(true)
+      bs1c.startsWith(exactly16) should ===(true)
+      bs1c.startsWith(nine) should ===(true)
+      bs1c.startsWith("12345678".getBytes(StandardCharsets.UTF_8)) should 
===(false)
+      bs1c.startsWith("abcdefghi".getBytes(StandardCharsets.UTF_8), 0) should 
===(true)
+      bs1c.startsWith("bcdefghi".getBytes(StandardCharsets.UTF_8), 1) should 
===(true)
+      bs1c.startsWith("12345678".getBytes(StandardCharsets.UTF_8), 1) should 
===(false)
+    }
+    "endsWith" in {
+      val suffix0 = ByteString1.fromString("uvwxyz")
+      val suffix1 = ByteString1.fromString("abcdefghijklmnopqrstuvwxyz")
+      val notSuffix = ByteString1.fromString("12345")
+      val byteStringLong = ByteString1.fromString("abcdefghijklmnopqrstuvwxyz")
+      val byteStrings = ByteStrings(byteStringLong, byteStringLong)
+
+      // ByteString1 basic cases
+      byteStringLong.endsWith(suffix0) should ===(true)
+      byteStringLong.endsWith(notSuffix) should ===(false)
+
+      // exact match
+      byteStringLong.endsWith(suffix1) should ===(true)
+
+      // bytes longer than ByteString returns false
+      val tooLong = ByteString1.fromString("0abcdefghijklmnopqrstuvwxyz")
+      byteStringLong.endsWith(tooLong) should ===(false)
+
+      // empty bytes array always returns true
+      byteStringLong.endsWith(Array.emptyByteArray) should ===(true)
+
+      // ByteStrings (multi-segment)
+      byteStrings.endsWith(suffix0) should ===(true)
+      byteStrings.endsWith(notSuffix) should ===(false)
+      byteStrings.endsWith(Array.emptyByteArray) should ===(true)
+
+      // suffix spanning the segment boundary
+      val crossBoundary = 
ByteString1.fromString("xyzabcdefghijklmnopqrstuvwxyz")
+      byteStrings.endsWith(crossBoundary) should ===(true)
+
+      // ByteString1C
+      val byteString1C = 
ByteString1C("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8))
+      byteString1C.endsWith(suffix0) should ===(true)
+      byteString1C.endsWith(notSuffix) should ===(false)
+      byteString1C.endsWith(Array.emptyByteArray) should ===(true)
+
+      // ByteString1 with internal offset
+      val byteStringWithOffset = ByteString1(
+        "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 20)
+      // ByteString1(bytes, 2, 20) represents "cdefghijklmnopqrstuv"
+      val offsetSuffixText = "rstuv"
+      val offsetSuffix = ByteString1.fromString(offsetSuffixText)
+      byteStringWithOffset.endsWith(offsetSuffix) should ===(true)
+      byteStringWithOffset.endsWith(offsetSuffixText) should ===(true)
+      byteStringWithOffset.endsWith(notSuffix) should ===(false)
+
+      // empty ByteString
+      ByteString.empty.endsWith(Array.emptyByteArray) should ===(true)
+      ByteString.empty.endsWith(ByteString1.fromString("a")) should ===(false)
+      ByteString.empty.endsWith("a") should ===(false)
+    }
+    "endsWith (specialized)" in {
+      val suffix0 = "uvwxyz".getBytes(StandardCharsets.UTF_8)
+      val suffix1 = 
"abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)
+      val notSuffix = "12345".getBytes(StandardCharsets.UTF_8)
+      val byteStringLong = ByteString1.fromString("abcdefghijklmnopqrstuvwxyz")
+      val byteStrings = ByteStrings(byteStringLong, byteStringLong)
+
+      // ByteString1 basic cases
+      byteStringLong.endsWith(suffix0) should ===(true)
+      byteStringLong.endsWith(notSuffix) should ===(false)
+
+      // exact match
+      byteStringLong.endsWith(suffix1) should ===(true)
+
+      // bytes longer than ByteString returns false
+      val tooLong = 
"0abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)
+      byteStringLong.endsWith(tooLong) should ===(false)
+
+      // empty bytes array always returns true
+      byteStringLong.endsWith(Array.emptyByteArray) should ===(true)
+
+      // ByteStrings (multi-segment)
+      byteStrings.endsWith(suffix0) should ===(true)
+      byteStrings.endsWith(notSuffix) should ===(false)
+      byteStrings.endsWith(Array.emptyByteArray) should ===(true)
+
+      // suffix spanning the segment boundary
+      val crossBoundary = 
"xyzabcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)
+      byteStrings.endsWith(crossBoundary) should ===(true)
+
+      // ByteString1C
+      val byteString1C = 
ByteString1C("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8))
+      byteString1C.endsWith(suffix0) should ===(true)
+      byteString1C.endsWith(notSuffix) should ===(false)
+      byteString1C.endsWith(Array.emptyByteArray) should ===(true)
+
+      // ByteString1 with internal offset
+      val byteStringWithOffset = ByteString1(
+        "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 20)
+      // ByteString1(bytes, 2, 20) represents "cdefghijklmnopqrstuv"
+      val offsetSuffix = "rstuv".getBytes(StandardCharsets.UTF_8)
+      byteStringWithOffset.endsWith(offsetSuffix) should ===(true)
+      byteStringWithOffset.endsWith(notSuffix) should ===(false)
+
+      // empty ByteString
+      ByteString.empty.endsWith(Array.emptyByteArray) should ===(true)
+      ByteString.empty.endsWith(Array[Byte]('a')) should ===(false)
+
+      val concat1 = makeMultiByteStringsWithEmptyComponents()
+      concat1.endsWith(Array[Byte](16.toByte, 0xFF.toByte)) should ===(true)
+      concat1.endsWith(Array[Byte](15.toByte, 0xFF.toByte)) should ===(false)
+
+      // SWAR-optimised path: needles spanning full 8-byte chunks (ByteString1)
+      val byteStringLong2 = 
ByteString1.fromString("abcdefghijklmnopqrstuvwxyz")
+      // exactly 8 bytes: one SWAR iteration, no tail
+      val last8 = "stuvwxyz".getBytes(StandardCharsets.UTF_8)
+      byteStringLong2.endsWith(last8) should ===(true)
+      byteStringLong2.endsWith("12345678".getBytes(StandardCharsets.UTF_8)) 
should ===(false)
+      // 16 bytes: two SWAR iterations, no tail
+      val last16 = "klmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)
+      byteStringLong2.endsWith(last16) should ===(true)
+      
byteStringLong2.endsWith("klmnopqrstuvwxy_".getBytes(StandardCharsets.UTF_8)) 
should ===(false)
+      // 9 bytes: one SWAR iteration + 1-byte tail
+      val last9 = "rstuvwxyz".getBytes(StandardCharsets.UTF_8)
+      byteStringLong2.endsWith(last9) should ===(true)
+      // mismatch buried inside the first 8-byte chunk
+      val mismatchInFirstChunk = 
"_lmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)
+      byteStringLong2.endsWith(mismatchInFirstChunk) should ===(false)
+      // ByteString1 with internal offset
+      val bs1WithOffset2 = 
ByteString1("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 
20)
+      // represents "cdefghijklmnopqrstuv"
+      bs1WithOffset2.endsWith("mnopqrstuv".getBytes(StandardCharsets.UTF_8)) 
should ===(true)
+      bs1WithOffset2.endsWith("12345678".getBytes(StandardCharsets.UTF_8)) 
should ===(false)
+      // ByteString1C SWAR path
+      val bs1c2 = 
ByteString1C("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8))
+      bs1c2.endsWith(last8) should ===(true)
+      bs1c2.endsWith(last16) should ===(true)
+      bs1c2.endsWith(last9) should ===(true)
+      bs1c2.endsWith("12345678".getBytes(StandardCharsets.UTF_8)) should 
===(false)
+      bs1c2.endsWith(mismatchInFirstChunk) should ===(false)
     }
     "return same hashCode" in {
       val slice0 = ByteString1.fromString("xyz")
@@ -2244,4 +2493,10 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
     )
     ByteStrings(byteStrings)
   }
+
+  private def makeMultiByteStringsWithEmptyComponents(): ByteString = {
+    ByteString1(Array.emptyByteArray) ++
+    makeMultiByteStringsSample() ++
+    ByteString1(Array.emptyByteArray)
+  }
 }
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 c1d25d8bcb..9b24d88160 100644
--- a/actor/src/main/scala/org/apache/pekko/util/ByteString.scala
+++ b/actor/src/main/scala/org/apache/pekko/util/ByteString.scala
@@ -374,6 +374,34 @@ object ByteString {
       else -1
     }
 
+    override def startsWith(bytes: Array[Byte], offset: Int): Boolean = {
+      val needleLen = bytes.length
+      if (length - offset < needleLen) return false
+      var hIdx = offset
+      var nIdx = 0
+      while (needleLen - nIdx >= 8) {
+        if (SWARUtil.getLong(this.bytes, hIdx, ByteOrder.BIG_ENDIAN) !=
+            SWARUtil.getLong(bytes, nIdx, ByteOrder.BIG_ENDIAN)) return false
+        hIdx += 8
+        nIdx += 8
+      }
+      java.util.Arrays.equals(this.bytes, hIdx, hIdx + (needleLen - nIdx), 
bytes, nIdx, needleLen)
+    }
+
+    override def endsWith(bytes: Array[Byte]): Boolean = {
+      val needleLen = bytes.length
+      if (length < needleLen) return false
+      var hIdx = length - needleLen
+      var nIdx = 0
+      while (needleLen - nIdx >= 8) {
+        if (SWARUtil.getLong(this.bytes, hIdx, ByteOrder.BIG_ENDIAN) !=
+            SWARUtil.getLong(bytes, nIdx, ByteOrder.BIG_ENDIAN)) return false
+        hIdx += 8
+        nIdx += 8
+      }
+      java.util.Arrays.equals(this.bytes, hIdx, hIdx + (needleLen - nIdx), 
bytes, nIdx, needleLen)
+    }
+
     override def slice(from: Int, until: Int): ByteString =
       if (from <= 0 && until >= length) this
       else if (from >= length || until <= 0 || from >= until) ByteString.empty
@@ -709,6 +737,34 @@ object ByteString {
       else -1
     }
 
+    override def startsWith(bytes: Array[Byte], offset: Int): Boolean = {
+      val needleLen = bytes.length
+      if (length - offset < needleLen) return false
+      var hIdx = startIndex + offset
+      var nIdx = 0
+      while (needleLen - nIdx >= 8) {
+        if (SWARUtil.getLong(this.bytes, hIdx, ByteOrder.BIG_ENDIAN) !=
+            SWARUtil.getLong(bytes, nIdx, ByteOrder.BIG_ENDIAN)) return false
+        hIdx += 8
+        nIdx += 8
+      }
+      java.util.Arrays.equals(this.bytes, hIdx, hIdx + (needleLen - nIdx), 
bytes, nIdx, needleLen)
+    }
+
+    override def endsWith(bytes: Array[Byte]): Boolean = {
+      val needleLen = bytes.length
+      if (length < needleLen) return false
+      var hIdx = startIndex + length - needleLen
+      var nIdx = 0
+      while (needleLen - nIdx >= 8) {
+        if (SWARUtil.getLong(this.bytes, hIdx, ByteOrder.BIG_ENDIAN) !=
+            SWARUtil.getLong(bytes, nIdx, ByteOrder.BIG_ENDIAN)) return false
+        hIdx += 8
+        nIdx += 8
+      }
+      java.util.Arrays.equals(this.bytes, hIdx, hIdx + (needleLen - nIdx), 
bytes, nIdx, needleLen)
+    }
+
     override def copyToArray[B >: Byte](dest: Array[B], start: Int, len: Int): 
Int = {
       // min of the bytes available to copy, bytes there is room for in dest 
and the requested number of bytes
       val toCopy = math.min(math.min(len, length), dest.length - start)
@@ -1473,6 +1529,16 @@ sealed abstract class ByteString
    */
   def contains(elem: Byte): Boolean = indexOf(elem, 0) != -1
 
+  override def startsWith[B >: Byte](iterable: 
scala.collection.IterableOnce[B], offset: Int): Boolean = {
+    var i = offset
+    val it = iterable.iterator
+    while (it.hasNext) {
+      if (i >= length || apply(i) != it.next()) return false
+      i += 1
+    }
+    true
+  }
+
   /**
    * Tests whether this ByteString starts with the given slice.
    *
@@ -1506,6 +1572,41 @@ sealed abstract class ByteString
    */
   def startsWith(bytes: Array[Byte]): Boolean = startsWith(bytes, 0)
 
+  override def endsWith[B >: Byte](iterable: scala.collection.Iterable[B]): 
Boolean = {
+    val size = iterable.size
+    if (length < size) false
+    else {
+      var i = length - size
+      val iterator = iterable.iterator
+      while (iterator.hasNext) {
+        if (apply(i) != iterator.next()) return false
+        i += 1
+      }
+      true
+    }
+  }
+
+  /**
+   * Tests whether this ByteString ends with the given bytes.
+   *
+   * @param bytes the slice to test
+   * @return true if this ByteString ends with the given bytes
+   * @since 2.0.0
+   */
+  def endsWith(bytes: Array[Byte]): Boolean = {
+    if (length < bytes.length) false
+    else {
+      var i = length - bytes.length
+      var j = 0
+      while (j < bytes.length) {
+        if (apply(i) != bytes(j)) return false
+        i += 1
+        j += 1
+      }
+      true
+    }
+  }
+
   override def grouped(size: Int): Iterator[ByteString] = {
     if (size <= 0) {
       throw new IllegalArgumentException(s"size=$size must be positive")
diff --git 
a/bench-jmh/src/main/scala/org/apache/pekko/util/ByteString_startEnd_Benchmark.scala
 
b/bench-jmh/src/main/scala/org/apache/pekko/util/ByteString_startEnd_Benchmark.scala
new file mode 100644
index 0000000000..f98e552b03
--- /dev/null
+++ 
b/bench-jmh/src/main/scala/org/apache/pekko/util/ByteString_startEnd_Benchmark.scala
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2014-2022 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.util
+
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.TimeUnit
+
+import org.openjdk.jmh.annotations._
+
+@State(Scope.Benchmark)
+@Measurement(timeUnit = TimeUnit.MILLISECONDS)
+class ByteString_startEnd_Benchmark {
+  val start = ByteString("abcdefg") ++ ByteString("hijklmno") ++ 
ByteString("pqrstuv")
+  val bss = start ++ start ++ start ++ start ++ start ++ ByteString("xyz")
+
+  val bs = bss.compact // compacted
+  val startCheck = "abcdefghijk"
+  val startBytes = startCheck.getBytes(StandardCharsets.UTF_8)
+  val endCheck = "pqrstuvxyz"
+  val endBytes = endCheck.getBytes(StandardCharsets.UTF_8)
+
+  @Benchmark
+  def bss_startsWith: Boolean = bss.startsWith(startCheck)
+
+  @Benchmark
+  def bss_endsWith: Boolean = bss.endsWith(endCheck)
+
+  @Benchmark
+  def bs_startsWith: Boolean = bs.startsWith(startCheck)
+
+  @Benchmark
+  def bs_endsWith: Boolean = bs.endsWith(endCheck)
+
+  @Benchmark
+  def bs_startsWithBytes: Boolean = bs.startsWith(startBytes)
+
+  @Benchmark
+  def bs_endsWithBytes: Boolean = bs.endsWith(endBytes)
+}


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

Reply via email to