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]