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

fanningpj 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 6f3de3b50a ByteString lastIndexOfSlice (#2855)
6f3de3b50a is described below

commit 6f3de3b50af01d525d0f04397fd3c2480b9fcf7c
Author: PJ Fanning <[email protected]>
AuthorDate: Mon Apr 13 13:02:10 2026 +0200

    ByteString lastIndexOfSlice (#2855)
    
    * Add lastIndexOfSlice to ByteString
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/5bb99eb9-ef1a-4d68-a4f1-1d556423f93e
    
    Co-authored-by: pjfanning <[email protected]>
    
    Address code review: use length-1 default end, clarify unrolledLastIndexOf 
comment
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/5bb99eb9-ef1a-4d68-a4f1-1d556423f93e
    
    Co-authored-by: pjfanning <[email protected]>
    
    merge issue
    
    * Update ByteStringSpec.scala
    
    * Create ByteString_lastIndexOfSlice_Benchmark.scala
    
    * reorder code
    
    * scalafmt
    
    Update ByteString.scala
    
    * add extra tests and fix issues in indexOfSlice
    
    * Add edge case tests for indexOfSlice/lastIndexOfSlice; fix empty-slice 
boundary bugs
    
    Agent-Logs-Url: 
https://github.com/pjfanning/incubator-pekko/sessions/8d88160e-6241-4825-a6e9-0c7ad0e5ac56
    
    Co-authored-by: pjfanning <[email protected]>
    
    * scalafmt
    
    ---------
    
    Co-authored-by: copilot-swe-agent[bot] 
<[email protected]>
    Co-authored-by: pjfanning <[email protected]>
---
 .../org/apache/pekko/util/ByteStringSpec.scala     | 215 +++++++++++++++++++++
 .../scala/org/apache/pekko/util/ByteString.scala   | 147 +++++++++++---
 .../ByteString_lastIndexOfSlice_Benchmark.scala    |  39 ++++
 3 files changed, 379 insertions(+), 22 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 1601b581f1..0891437a14 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
@@ -830,6 +830,12 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       val concat0 = ByteStrings(ByteString1.fromString("ab"), 
ByteString1.fromString("dd"))
       concat0.lastIndexOf('d'.toByte, 2) should ===(2)
       concat0.lastIndexOf('d'.toByte, 3) should ===(3)
+
+      // end larger than length - 1 should be clamped to the last valid index
+      val bs5 = ByteString1.fromString("abcde")
+      bs5.lastIndexOf('e', 100) should ===(4)
+      bs5.lastIndexOf('a', 100) should ===(0)
+      bs5.lastIndexOf('z', 100) should ===(-1)
     }
     "lastIndexOf (specialized)" in {
       ByteString.empty.lastIndexOf(5.toByte, -1) should ===(-1)
@@ -894,6 +900,50 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
       concat0.lastIndexOf(0xFF.toByte, 18) should ===(18)
       concat0.lastIndexOf(0xFF.toByte, 17) should ===(0)
       concat0.lastIndexOf(0xFE.toByte) should ===(-1)
+
+      // Single-byte ByteString
+      val single = ByteString1(Array[Byte]('x'))
+      single.lastIndexOf('x'.toByte) should ===(0)
+      single.lastIndexOf('y'.toByte) should ===(-1)
+
+      // SWAR boundary: exactly 7 bytes (only tail, no full chunk)
+      val len7 = ByteString1.fromString("abcdefg")
+      len7.lastIndexOf('g'.toByte) should ===(6)
+      len7.lastIndexOf('a'.toByte) should ===(0)
+      len7.lastIndexOf('d'.toByte) should ===(3)
+      len7.lastIndexOf('z'.toByte) should ===(-1)
+
+      // SWAR boundary: exactly 8 bytes (one full chunk, no tail)
+      val len8 = ByteString1.fromString("abcdefgh")
+      len8.lastIndexOf('h'.toByte) should ===(7)
+      len8.lastIndexOf('a'.toByte) should ===(0)
+      len8.lastIndexOf('d'.toByte) should ===(3)
+      len8.lastIndexOf('z'.toByte) should ===(-1)
+
+      // SWAR boundary: exactly 9 bytes (1-byte tail + 1 full chunk)
+      val len9 = ByteString1.fromString("abcdefghi")
+      len9.lastIndexOf('i'.toByte) should ===(8) // found in tail
+      len9.lastIndexOf('a'.toByte) should ===(0) // found in chunk
+      len9.lastIndexOf('h'.toByte) should ===(7) // last byte of chunk
+      len9.lastIndexOf('z'.toByte) should ===(-1)
+
+      // SWAR boundary: exactly 16 bytes (2 full chunks, no tail)
+      val len16 = ByteString1.fromString("abcdefghijklmnop")
+      len16.lastIndexOf('p'.toByte) should ===(15) // last byte of rightmost 
chunk
+      len16.lastIndexOf('a'.toByte) should ===(0) // first byte of leftmost 
chunk
+      len16.lastIndexOf('h'.toByte) should ===(7) // last byte of leftmost 
chunk
+      len16.lastIndexOf('i'.toByte) should ===(8) // first byte of rightmost 
chunk
+
+      // All-same-byte array longer than 8: last index is the highest
+      val allSame = ByteString(Array[Byte](7, 7, 7, 7, 7, 7, 7, 7, 7)) // 9 
bytes
+      allSame.lastIndexOf(7.toByte) should ===(8)
+      allSame.lastIndexOf(7.toByte, 5) should ===(5)
+      allSame.lastIndexOf(7.toByte, 0) should ===(0)
+
+      // ByteString1 with startIndex offset: find a byte residing in the SWAR 
chunk
+      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
     }
     "indexOf (specialized)" in {
       ByteString.empty.indexOf(5.toByte) should ===(-1)
@@ -1140,7 +1190,34 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
 
       val byteStringXxyz = ByteString1.fromString("xxyz")
       byteStringXxyz.indexOfSlice(slice0) should ===(1)
+      byteStringXxyz.indexOfSlice(slice0, -1) should ===(1)
       byteStringXxyz.indexOfSlice(slice0, 2) should ===(-1)
+      byteStringXxyz.indexOfSlice(Seq.empty) should ===(0)
+      byteStringXxyz.indexOfSlice(Seq.empty, -1) should ===(0)
+      byteStringXxyz.indexOfSlice(Seq.empty, 1) should ===(1)
+
+      // empty slice at from == length: should return length (consistent with 
Scala standard library)
+      byteStringXxyz.indexOfSlice(Seq.empty, 4) should ===(4)
+      // empty slice at from > length: should return -1
+      byteStringXxyz.indexOfSlice(Seq.empty, 5) should ===(-1)
+
+      // Empty source
+      ByteString.empty.indexOfSlice(Seq.empty) should ===(0)
+      ByteString.empty.indexOfSlice(ByteString1.fromString("a")) should ===(-1)
+
+      // Slice equals entire source
+      
ByteString1.fromString("abc").indexOfSlice(ByteString1.fromString("abc")) 
should ===(0)
+
+      // Slice longer than source
+      ByteString1.fromString("ab").indexOfSlice(ByteString1.fromString("abc")) 
should ===(-1)
+
+      // Cross-segment match in ByteStrings: "bc" spans segment boundary of 
("ab" ++ "cd")
+      ByteStrings(ByteString1.fromString("ab"), ByteString1.fromString("cd"))
+        .indexOfSlice(ByteString1.fromString("bc")) should ===(1)
+
+      // False match: first byte matches multiple times before the full slice 
matches
+      // "ab" in "aabc": 'a' at index 0 (check: bytes[1]='a' != 'b' ✗), then 
'a' at 1 (bytes[2]='b' == 'b' ✓) -> 1
+      
ByteString1.fromString("aabc").indexOfSlice(ByteString1.fromString("ab")) 
should ===(1)
     }
     "indexOfSlice (specialized)" in {
       val slice0 = "xyz".getBytes(StandardCharsets.UTF_8)
@@ -1162,12 +1239,150 @@ class ByteStringSpec extends AnyWordSpec with Matchers 
with Checkers {
 
       val byteStringXxyz = ByteString1.fromString("xxyz")
       byteStringXxyz.indexOfSlice(slice0) should ===(1)
+      byteStringXxyz.indexOfSlice(slice0, -1) should ===(1)
       byteStringXxyz.indexOfSlice(slice0, 2) should ===(-1)
+      byteStringXxyz.indexOfSlice(Seq.empty) should ===(0)
+      byteStringXxyz.indexOfSlice(Seq.empty, -1) should ===(0)
+      byteStringXxyz.indexOfSlice(Seq.empty, 1) should ===(1)
+
+      // empty slice at from == length: should return length (consistent with 
Scala standard library)
+      byteStringXxyz.indexOfSlice(Seq.empty, 4) should ===(4)
+      // empty slice at from > length: should return -1
+      byteStringXxyz.indexOfSlice(Seq.empty, 5) should ===(-1)
+
+      // Empty source
+      ByteString.empty.indexOfSlice(Array.empty[Byte]) should ===(0)
+      ByteString.empty.indexOfSlice(Array[Byte]('a')) should ===(-1)
+
+      // Slice equals entire source
+      
ByteString1.fromString("abc").indexOfSlice("abc".getBytes(StandardCharsets.UTF_8))
 should ===(0)
+
+      // Slice longer than source
+      
ByteString1.fromString("ab").indexOfSlice("abc".getBytes(StandardCharsets.UTF_8))
 should ===(-1)
+
+      // Cross-segment match in ByteStrings: "bc" spans segment boundary of 
("ab" ++ "cd")
+      ByteStrings(ByteString1.fromString("ab"), ByteString1.fromString("cd"))
+        .indexOfSlice(Array[Byte]('b', 'c')) should ===(1)
+
+      // False match: first byte matches multiple times before the full slice 
matches
+      ByteString1.fromString("aabc").indexOfSlice(Array[Byte]('a', 'b')) 
should ===(1)
 
       val byteStringWithOffset = ByteString1(
         "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 24)
       byteStringWithOffset.indexOfSlice(slice0) should ===(21)
     }
+    "lastIndexOfSlice" in {
+      val slice0 = ByteString1.fromString("xyz")
+      val slice1 = ByteString1.fromString("xyzabc")
+      val notSlice = ByteString1.fromString("12345")
+      val pByte = ByteString1.fromString("p")
+      val byteStringLong = ByteString1.fromString("abcdefghijklmnopqrstuvwxyz")
+      val byteStrings = ByteStrings(byteStringLong, byteStringLong)
+      byteStringLong.lastIndexOfSlice(slice0) should ===(23)
+      byteStringLong.lastIndexOfSlice(slice1) should ===(-1)
+      byteStringLong.lastIndexOfSlice(notSlice) should ===(-1)
+      byteStringLong.lastIndexOfSlice(pByte) should ===(15)
+
+      byteStrings.lastIndexOfSlice(slice0) should ===(49)
+      byteStrings.lastIndexOfSlice(slice1) should ===(23)
+      byteStrings.lastIndexOfSlice(notSlice) should ===(-1)
+      byteStrings.lastIndexOfSlice(pByte) should ===(41)
+      byteStrings.lastIndexOfSlice(pByte, 40) should ===(15)
+
+      val byteStringXxyz = ByteString1.fromString("xxyz")
+      byteStringXxyz.lastIndexOfSlice(slice0) should ===(1)
+      byteStringXxyz.lastIndexOfSlice(slice0, 10) should ===(1)
+      byteStringXxyz.lastIndexOfSlice(slice0, 0) should ===(-1)
+      byteStringXxyz.lastIndexOfSlice(slice0, 2) should ===(1)
+      byteStringXxyz.lastIndexOfSlice(Seq.empty) should ===(4)
+      byteStringXxyz.lastIndexOfSlice(Seq.empty, 2) should ===(2)
+
+      val xyzx = ByteString1.fromString("xyzx")
+      xyzx.lastIndexOfSlice(slice0) should ===(0)
+
+      // Empty source with empty slice -> 0; with non-empty slice -> -1
+      ByteString.empty.lastIndexOfSlice(ByteString.empty) should ===(0)
+      ByteString.empty.lastIndexOfSlice(ByteString1.fromString("a")) should 
===(-1)
+
+      // Slice equals entire source -> 0
+      
ByteString1.fromString("abc").lastIndexOfSlice(ByteString1.fromString("abc")) 
should ===(0)
+
+      // Slice longer than source -> -1
+      
ByteString1.fromString("ab").lastIndexOfSlice(ByteString1.fromString("abc")) 
should ===(-1)
+
+      // end = -1 for non-empty slice -> -1
+      
ByteString1.fromString("abc").lastIndexOfSlice(ByteString1.fromString("a"), -1) 
should ===(-1)
+
+      // False-match retry: tail byte 'b' matches at index 2, but bytes[1]='b' 
!= 'a'; retries, finds at index 1, bytes[0]='a' == 'a'
+      
ByteString1.fromString("abb").lastIndexOfSlice(ByteString1.fromString("ab")) 
should ===(0)
+
+      // Repeated pattern: "aa" in "aaaa" -> last occurrence starts at index 2
+      
ByteString1.fromString("aaaa").lastIndexOfSlice(ByteString1.fromString("aa")) 
should ===(2)
+
+      // Cross-segment match in ByteStrings: slice "bc" spans segment boundary 
of ("ab" ++ "cd")
+      ByteStrings(ByteString1.fromString("ab"), ByteString1.fromString("cd"))
+        .lastIndexOfSlice(ByteString1.fromString("bc")) should ===(1)
+    }
+    "lastIndexOfSlice (specialized)" in {
+      val slice0 = "xyz".getBytes(StandardCharsets.UTF_8)
+      val slice1 = "xyzabc".getBytes(StandardCharsets.UTF_8)
+      val notSlice = "12345".getBytes(StandardCharsets.UTF_8)
+      val pByte = Array('p'.toByte)
+      val byteStringLong = ByteString1.fromString("abcdefghijklmnopqrstuvwxyz")
+      val byteStrings = ByteStrings(byteStringLong, byteStringLong)
+      byteStringLong.lastIndexOfSlice(slice0) should ===(23)
+      byteStringLong.lastIndexOfSlice(slice1) should ===(-1)
+      byteStringLong.lastIndexOfSlice(notSlice) should ===(-1)
+      byteStringLong.lastIndexOfSlice(pByte) should ===(15)
+
+      byteStrings.lastIndexOfSlice(slice0) should ===(49)
+      byteStrings.lastIndexOfSlice(slice1) should ===(23)
+      byteStrings.lastIndexOfSlice(notSlice) should ===(-1)
+      byteStrings.lastIndexOfSlice(pByte) should ===(41)
+      byteStrings.lastIndexOfSlice(pByte, 40) should ===(15)
+
+      val byteStringXxyz = ByteString1.fromString("xxyz")
+      byteStringXxyz.lastIndexOfSlice(slice0) should ===(1)
+      byteStringXxyz.lastIndexOfSlice(slice0, 10) should ===(1)
+      byteStringXxyz.lastIndexOfSlice(slice0, 0) should ===(-1)
+      byteStringXxyz.lastIndexOfSlice(slice0, 2) should ===(1)
+      byteStringXxyz.lastIndexOfSlice(Seq.empty) should ===(4)
+      byteStringXxyz.lastIndexOfSlice(Seq.empty, 2) should ===(2)
+
+      val xyzx = ByteString1.fromString("xyzx")
+      xyzx.lastIndexOfSlice(slice0) should ===(0)
+
+      val byteStringWithOffset = ByteString1(
+        "abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8), 2, 24)
+      byteStringWithOffset.lastIndexOfSlice(slice0) should ===(21)
+
+      val concat0 = makeMultiByteStringsSample()
+      concat0.lastIndexOfSlice(Array(16.toByte, 0xFF.toByte)) should ===(17)
+      concat0.lastIndexOfSlice(Array(16.toByte, 0xFE.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)
+
+      // Slice equals entire source -> 0
+      
ByteString1.fromString("abc").lastIndexOfSlice("abc".getBytes(StandardCharsets.UTF_8))
 should ===(0)
+
+      // Slice longer than source -> -1
+      
ByteString1.fromString("ab").lastIndexOfSlice("abc".getBytes(StandardCharsets.UTF_8))
 should ===(-1)
+
+      // end = -1 for non-empty slice -> -1
+      ByteString1.fromString("abc").lastIndexOfSlice(Array[Byte]('a'), -1) 
should ===(-1)
+
+      // False-match retry: tail byte 'b' matches at index 2, but bytes[1]='b' 
!= 'a'; retries, finds at index 1
+      ByteString1.fromString("abb").lastIndexOfSlice(Array[Byte]('a', 'b')) 
should ===(0)
+
+      // Repeated pattern: "aa" in "aaaa" -> last occurrence starts at index 2
+      ByteString1.fromString("aaaa").lastIndexOfSlice(Array[Byte]('a', 'a')) 
should ===(2)
+
+      // Cross-segment match in ByteStrings: slice "bc" spans segment boundary 
of ("ab" ++ "cd")
+      ByteStrings(ByteString1.fromString("ab"), ByteString1.fromString("cd"))
+        .lastIndexOfSlice(Array[Byte]('b', 'c')) should ===(1)
+    }
     "startsWith (specialized)" in {
       val slice0 = "abcdefghijk".getBytes(StandardCharsets.UTF_8)
       val slice1 = "xyz".getBytes(StandardCharsets.UTF_8)
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 982227ee94..c36ac54b70 100644
--- a/actor/src/main/scala/org/apache/pekko/util/ByteString.scala
+++ b/actor/src/main/scala/org/apache/pekko/util/ByteString.scala
@@ -1259,10 +1259,10 @@ sealed abstract class ByteString
   /**
    * Finds index of last occurrence of some byte in this ByteString before or 
at some end index.
    *
-   * Similar to lastIndexOf, but it avoids boxing if the value is already a 
byte.
+   * Similar to `lastIndexOf`, but it avoids boxing if the value is already a 
byte.
    *
    *  @param   elem   the element value to search for.
-   *  @param   end    the end index
+   *  @param   end    the end index (inclusive)
    *  @return  the index `<= end` of the last element of this ByteString that 
is equal (as determined by `==`)
    *           to `elem`, or `-1`, if none exists.
    *  @since 2.0.0
@@ -1272,7 +1272,7 @@ sealed abstract class ByteString
   /**
    * Finds index of last occurrence of some byte in this ByteString.
    *
-   * Similar to lastIndexOf, but it avoids boxing if the value is already a 
byte.
+   * Similar to `lastIndexOf`, but it avoids boxing if the value is already a 
byte.
    *
    *  @param   elem   the element value to search for.
    *  @return  the index of the last element of this ByteString that is equal 
(as determined by `==`)
@@ -1294,17 +1294,19 @@ sealed abstract class ByteString
       }
       true
     }
-    val headByte = slice.head.asInstanceOf[Byte]
-    @tailrec def rec(from: Int): Int = {
-      val startPos = indexOf(headByte, from, length - slice.length + 1)
-      if (startPos == -1) -1
-      else if (check(startPos)) startPos
-      else rec(startPos + 1)
-    }
     val sliceLength = slice.length
-    if (sliceLength == 0) 0
-    else if (sliceLength == 1) indexOf(headByte, from)
-    else rec(math.max(0, from))
+    if (sliceLength == 0) if (from > length) -1 else math.max(from, 0)
+    else {
+      val headByte = slice.head.asInstanceOf[Byte]
+      @tailrec def rec(from: Int): Int = {
+        val startPos = indexOf(headByte, from, length - slice.length + 1)
+        if (startPos == -1) -1
+        else if (check(startPos)) startPos
+        else rec(startPos + 1)
+      }
+      if (sliceLength == 1) indexOf(headByte, from)
+      else rec(math.max(0, from))
+    }
   }
 
   /**
@@ -1330,16 +1332,20 @@ sealed abstract class ByteString
       }
       true
     }
-    @tailrec def rec(from: Int): Int = {
-      val startPos = indexOf(slice.head, from, length - slice.length + 1)
-      if (startPos == -1) -1
-      else if (check(startPos)) startPos
-      else rec(startPos + 1)
-    }
     val sliceLength = slice.length
-    if (sliceLength == 0) 0
-    else if (sliceLength == 1) indexOf(slice.head, from)
-    else rec(math.max(0, from))
+    if (sliceLength == 0) if (from > length) -1 else math.max(from, 0)
+    else if (sliceLength > length) -1
+    else {
+      val headByte = slice.head
+      @tailrec def rec(from: Int): Int = {
+        val startPos = indexOf(headByte, from, length - slice.length + 1)
+        if (startPos == -1) -1
+        else if (check(startPos)) startPos
+        else rec(startPos + 1)
+      }
+      if (sliceLength == 1) indexOf(headByte, from)
+      else rec(math.max(0, from))
+    }
   }
 
   /**
@@ -1353,6 +1359,103 @@ sealed abstract class ByteString
    */
   def indexOfSlice(slice: Array[Byte]): Int = indexOfSlice(slice, 0)
 
+  override def lastIndexOfSlice[B >: Byte](slice: scala.collection.Seq[B], 
end: Int): Int = {
+    val sliceLength = slice.length
+    if (sliceLength == 0) if (end < 0) -1 else math.min(end, length)
+    else if (sliceLength > length) -1
+    else {
+      val tailByte = slice(sliceLength - 1).asInstanceOf[Byte]
+      // Check all bytes of the slice except the last one (which was matched 
by lastIndexOf)
+      def check(startPos: Int): Boolean = {
+        var i = startPos
+        var j = 0
+        while (j < sliceLength - 1) {
+          // let's trust the calling code has ensured that we have enough 
bytes in this ByteString
+          if (apply(i) != slice(j)) return false
+          i += 1
+          j += 1
+        }
+        true
+      }
+      // Cap end to the max valid slice start position to avoid Int overflow 
when end is very large
+      val effectiveEnd = math.min(end, length - sliceLength)
+      val maxEndPos = effectiveEnd + sliceLength - 1
+      @tailrec def rec(currEnd: Int): Int = {
+        val endPos = lastIndexOf(tailByte, currEnd)
+        if (endPos < sliceLength - 1) -1
+        else {
+          val startPos = endPos - sliceLength + 1
+          if (check(startPos)) startPos
+          else rec(endPos - 1)
+        }
+      }
+      if (sliceLength == 1) lastIndexOf(tailByte, effectiveEnd)
+      else rec(maxEndPos)
+    }
+  }
+
+  /**
+   * Finds index of last occurrence of some slice in this ByteString before or 
at some end index.
+   *
+   * Uses `lastIndexOf` on the last byte of the slice to efficiently find 
candidate positions,
+   * then verifies the full slice match.
+   *
+   * @param   slice   the slice to search for.
+   * @param   end     the end index — the largest index at which the slice may 
start
+   * @return  the largest index `<= end` of the first element of this
+   *          ByteString that starts a slice equal (as determined by `==`)
+   *          to `slice`, or `-1`, if none exists.
+   * @since 2.0.0
+   */
+  def lastIndexOfSlice(slice: Array[Byte], end: Int): Int = {
+    val sliceLength = slice.length
+    if (sliceLength == 0) if (end < 0) -1 else math.min(end, length)
+    else if (sliceLength > length) -1
+    else {
+      val tailByte = slice(sliceLength - 1)
+      // Check all bytes of the slice except the last one (which was matched 
by lastIndexOf)
+      def check(startPos: Int): Boolean = {
+        var i = startPos
+        var j = 0
+        while (j < sliceLength - 1) {
+          // let's trust the calling code has ensured that we have enough 
bytes in this ByteString
+          if (apply(i) != slice(j)) return false
+          i += 1
+          j += 1
+        }
+        true
+      }
+      // Cap end to the max valid slice start position to avoid Int overflow 
when end is very large
+      val effectiveEnd = math.min(end, length - sliceLength)
+      val maxEndPos = effectiveEnd + sliceLength - 1
+      @tailrec def rec(currEnd: Int): Int = {
+        val endPos = lastIndexOf(tailByte, currEnd)
+        if (endPos < sliceLength - 1) -1
+        else {
+          val startPos = endPos - sliceLength + 1
+          if (check(startPos)) startPos
+          else rec(endPos - 1)
+        }
+      }
+      if (sliceLength == 1) lastIndexOf(tailByte, effectiveEnd)
+      else rec(maxEndPos)
+    }
+  }
+
+  /**
+   * Finds index of last occurrence of some slice in this ByteString.
+   *
+   * Uses `lastIndexOf` on the last byte of the slice to efficiently find 
candidate positions,
+   * then verifies the full slice match.
+   *
+   * @param   slice   the slice to search for.
+   * @return  the index of the last element of this
+   *          ByteString that starts a slice equal (as determined by `==`)
+   *          to `slice`, or `-1`, if none exists.
+   * @since 2.0.0
+   */
+  def lastIndexOfSlice(slice: Array[Byte]): Int = lastIndexOfSlice(slice, 
length)
+
   override def contains[B >: Byte](elem: B): Boolean = indexOf(elem, 0) != -1
 
   /**
diff --git 
a/bench-jmh/src/main/scala/org/apache/pekko/util/ByteString_lastIndexOfSlice_Benchmark.scala
 
b/bench-jmh/src/main/scala/org/apache/pekko/util/ByteString_lastIndexOfSlice_Benchmark.scala
new file mode 100644
index 0000000000..f3695ec6e6
--- /dev/null
+++ 
b/bench-jmh/src/main/scala/org/apache/pekko/util/ByteString_lastIndexOfSlice_Benchmark.scala
@@ -0,0 +1,39 @@
+/*
+ * 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_lastIndexOfSlice_Benchmark {
+  val start = ByteString("abcdefg") ++ ByteString("hijklmno") ++ 
ByteString("pqrstuv")
+  val bss = start ++ start ++ start ++ start ++ start ++ ByteString("xyz")
+
+  val bs = bss.compact // compacted
+  val abc = "abc".getBytes(StandardCharsets.UTF_8)
+  val hv = "hijklmnopqrstuv".getBytes(StandardCharsets.UTF_8)
+
+  @Benchmark
+  def bss_lastIndexOfSlice: Int = bss.lastIndexOfSlice(abc)
+
+  @Benchmark
+  def bs_lastIndexOfSlice_abc: Int = bs.lastIndexOfSlice(abc)
+
+  @Benchmark
+  def bs_lastIndexOfSlice_hv: Int = bs.lastIndexOfSlice(hv)
+}


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

Reply via email to