From 66f4d3009866cd7aab0838303564382b45c0ae72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Klang=20=28=E2=88=9A=29?= Date: Fri, 6 Mar 2020 14:14:34 +0100 Subject: [PATCH] Improving the performance of ByteString.decodeString, and adding base64 * Improving the performance of ByteString.decodeString, and adding ByteString.encodeBase64 and ByteString.decodeBase64 * ByteString.take should return itself whenever possible * Implementing fallback for the rare case where the JDKs Base64 returns a non-array-backed ByteBuffer * Adding mima excludes for encodeBase64/decodeBase64 --- .../test/scala/akka/util/ByteStringSpec.scala | 14 +++++ ...663-wip-bytestring-improvements-√.excludes | 2 + .../scala-2.13+/akka/util/ByteString.scala | 51 +++++++++++++++++- .../scala-2.13-/akka/util/ByteString.scala | 54 +++++++++++++++++-- 4 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 akka-actor/src/main/mima-filters/2.6.3.backwards.excludes/pr-28663-wip-bytestring-improvements-√.excludes diff --git a/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala b/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala index 8a816cd657..ac3fc8a16c 100644 --- a/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala @@ -788,6 +788,20 @@ class ByteStringSpec extends AnyWordSpec with Matchers with Checkers { } } + "taking its own length" in { + check { b: ByteString => + b.take(b.length) eq b + } + } + + "created from and decoding to Base64" in { + check { a: ByteString => + val encoded = a.encodeBase64 + encoded == ByteString(java.util.Base64.getEncoder.encode(a.toArray)) && + encoded.decodeBase64 == a + } + } + "compacting" in { check { a: ByteString => val wasCompact = a.isCompact diff --git a/akka-actor/src/main/mima-filters/2.6.3.backwards.excludes/pr-28663-wip-bytestring-improvements-√.excludes b/akka-actor/src/main/mima-filters/2.6.3.backwards.excludes/pr-28663-wip-bytestring-improvements-√.excludes new file mode 100644 index 0000000000..b3797d493a --- /dev/null +++ b/akka-actor/src/main/mima-filters/2.6.3.backwards.excludes/pr-28663-wip-bytestring-improvements-√.excludes @@ -0,0 +1,2 @@ +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.util.ByteString.decodeBase64") +ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.util.ByteString.encodeBase64") \ No newline at end of file diff --git a/akka-actor/src/main/scala-2.13+/akka/util/ByteString.scala b/akka-actor/src/main/scala-2.13+/akka/util/ByteString.scala index 614d0e1bdc..bba66669c5 100644 --- a/akka-actor/src/main/scala-2.13+/akka/util/ByteString.scala +++ b/akka-actor/src/main/scala-2.13+/akka/util/ByteString.scala @@ -8,6 +8,7 @@ import java.io.{ ObjectInputStream, ObjectOutputStream } import java.nio.{ ByteBuffer, ByteOrder } import java.lang.{ Iterable => JIterable } import java.nio.charset.{ Charset, StandardCharsets } +import java.util.Base64 import scala.annotation.{ tailrec, varargs } import scala.collection.mutable.{ Builder, WrappedArray } @@ -196,6 +197,12 @@ object ByteString { override def decodeString(charset: Charset): String = if (isEmpty) "" else new String(bytes, charset) + override def decodeBase64: ByteString = + if (isEmpty) this else ByteString1C(Base64.getDecoder.decode(bytes)) + + override def encodeBase64: ByteString = + if (isEmpty) this else ByteString1C(Base64.getEncoder.encode(bytes)) + override def ++(that: ByteString): ByteString = { if (that.isEmpty) this else if (this.isEmpty) that @@ -204,6 +211,7 @@ object ByteString { override def take(n: Int): ByteString = if (n <= 0) ByteString.empty + else if (n >= length) this else toByteString1.take(n) override def dropRight(n: Int): ByteString = @@ -361,10 +369,34 @@ object ByteString { def asByteBuffers: scala.collection.immutable.Iterable[ByteBuffer] = List(asByteBuffer) override def decodeString(charset: String): String = - new String(if (length == bytes.length) bytes else toArray, charset) + if (isEmpty) "" + else new String(bytes, startIndex, length, charset) override def decodeString(charset: Charset): String = // avoids Charset.forName lookup in String internals - new String(if (length == bytes.length) bytes else toArray, charset) + if (isEmpty) "" + else new String(bytes, startIndex, length, charset) + + override def decodeBase64: ByteString = + if (isEmpty) this + else if (isCompact) ByteString1C(Base64.getDecoder.decode(bytes)) + else { + val dst = Base64.getDecoder.decode(ByteBuffer.wrap(bytes, startIndex, length)) + if (dst.hasArray) { + if (dst.array.length == dst.remaining) ByteString1C(dst.array) + else ByteString1(dst.array, dst.arrayOffset + dst.position, dst.remaining) + } else CompactByteString(dst) + } + + override def encodeBase64: ByteString = + if (isEmpty) this + else if (isCompact) ByteString1C(Base64.getEncoder.encode(bytes)) + else { + val dst = Base64.getEncoder.encode(ByteBuffer.wrap(bytes, startIndex, length)) + if (dst.hasArray) { + if (dst.array.length == dst.remaining) ByteString1C(dst.array) + else ByteString1(dst.array, dst.arrayOffset + dst.position, dst.remaining) + } else CompactByteString(dst) + } def ++(that: ByteString): ByteString = { if (that.isEmpty) this @@ -535,6 +567,10 @@ object ByteString { def decodeString(charset: Charset): String = compact.decodeString(charset) + override def decodeBase64: ByteString = compact.decodeBase64 + + override def encodeBase64: ByteString = compact.encodeBase64 + private[akka] def writeToOutputStream(os: ObjectOutputStream): Unit = { os.writeInt(bytestrings.length) bytestrings.foreach(_.writeToOutputStream(os)) @@ -887,6 +923,17 @@ sealed abstract class ByteString */ def decodeString(charset: Charset): String + /* + * Returns a ByteString which is the binary representation of this ByteString + * if this ByteString is Base64-encoded. + */ + def decodeBase64: ByteString + + /** + * Returns a ByteString which is the Base64 representation of this ByteString + */ + def encodeBase64: ByteString + /** * map method that will automatically cast Int back into Byte. */ diff --git a/akka-actor/src/main/scala-2.13-/akka/util/ByteString.scala b/akka-actor/src/main/scala-2.13-/akka/util/ByteString.scala index 94680d2899..3ce187fd07 100644 --- a/akka-actor/src/main/scala-2.13-/akka/util/ByteString.scala +++ b/akka-actor/src/main/scala-2.13-/akka/util/ByteString.scala @@ -7,6 +7,7 @@ package akka.util import java.io.{ ObjectInputStream, ObjectOutputStream } import java.nio.{ ByteBuffer, ByteOrder } import java.lang.{ Iterable => JIterable } +import java.util.Base64 import scala.annotation.{ tailrec, varargs } import scala.collection.IndexedSeqOptimized @@ -193,6 +194,12 @@ object ByteString { override def decodeString(charset: Charset): String = if (isEmpty) "" else new String(bytes, charset) + override def decodeBase64: ByteString = + if (isEmpty) this else ByteString1C(Base64.getDecoder.decode(bytes)) + + override def encodeBase64: ByteString = + if (isEmpty) this else ByteString1C(Base64.getEncoder.encode(bytes)) + override def ++(that: ByteString): ByteString = { if (that.isEmpty) this else if (this.isEmpty) that @@ -201,6 +208,7 @@ object ByteString { override def take(n: Int): ByteString = if (n <= 0) ByteString.empty + else if (n >= length) this else toByteString1.take(n) override def dropRight(n: Int): ByteString = @@ -351,10 +359,34 @@ object ByteString { def asByteBuffers: scala.collection.immutable.Iterable[ByteBuffer] = List(asByteBuffer) override def decodeString(charset: String): String = - new String(if (length == bytes.length) bytes else toArray, charset) + if (isEmpty) "" + else new String(bytes, startIndex, length, charset) override def decodeString(charset: Charset): String = // avoids Charset.forName lookup in String internals - new String(if (length == bytes.length) bytes else toArray, charset) + if (isEmpty) "" + else new String(bytes, startIndex, length, charset) + + override def decodeBase64: ByteString = + if (isEmpty) this + else if (isCompact) ByteString1C(Base64.getDecoder.decode(bytes)) + else { + val dst = Base64.getDecoder.decode(ByteBuffer.wrap(bytes, startIndex, length)) + if (dst.hasArray) { + if (dst.array.length == dst.remaining) ByteString1C(dst.array) + else ByteString1(dst.array, dst.arrayOffset + dst.position, dst.remaining) + } else CompactByteString(dst) + } + + override def encodeBase64: ByteString = + if (isEmpty) this + else if (isCompact) ByteString1C(Base64.getEncoder.encode(bytes)) + else { + val dst = Base64.getEncoder.encode(ByteBuffer.wrap(bytes, startIndex, length)) + if (dst.hasArray) { + if (dst.array.length == dst.remaining) ByteString1C(dst.array) + else ByteString1(dst.array, dst.arrayOffset + dst.position, dst.remaining) + } else CompactByteString(dst) + } def ++(that: ByteString): ByteString = { if (that.isEmpty) this @@ -517,6 +549,10 @@ object ByteString { def decodeString(charset: Charset): String = compact.decodeString(charset) + override def decodeBase64: ByteString = compact.decodeBase64 + + override def encodeBase64: ByteString = compact.encodeBase64 + private[akka] def writeToOutputStream(os: ObjectOutputStream): Unit = { os.writeInt(bytestrings.length) bytestrings.foreach(_.writeToOutputStream(os)) @@ -835,6 +871,17 @@ sealed abstract class ByteString extends IndexedSeq[Byte] with IndexedSeqOptimiz */ def decodeString(charset: Charset): String + /* + * Returns a ByteString which is the binary representation of this ByteString + * if this ByteString is Base64-encoded. + */ + def decodeBase64: ByteString + + /** + * Returns a ByteString which is the Base64 representation of this ByteString + */ + def encodeBase64: ByteString + /** * map method that will automatically cast Int back into Byte. */ @@ -1237,8 +1284,7 @@ final class ByteStringBuilder extends Builder[Byte, ByteString] { * operations on the stream are forwarded to the builder. */ def asOutputStream: java.io.OutputStream = new java.io.OutputStream { - def write(b: Int): Unit = builder += b.toByte - + override def write(b: Int): Unit = builder += b.toByte override def write(b: Array[Byte], off: Int, len: Int): Unit = { builder.putBytes(b, off, len) } }