All Downloads are FREE. Search and download functionalities are using the official Maven repository.

japgolly.webapputil.binary.BinaryData.scala Maven / Gradle / Ivy

There is a newer version: 2.0.0-RC12
Show newest version
package japgolly.webapputil.binary

import japgolly.univeq.UnivEq
import japgolly.webapputil.general.ErrorMsg
import java.io.OutputStream
import java.lang.{StringBuilder => JStringBuilder}
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.{Arrays, Base64}
import scala.collection.immutable.ArraySeq

object BinaryData extends BinaryData_PlatformSpecific_Object {

  implicit def univEq: UnivEq[BinaryData] =
    UnivEq.force

  final val DefaultByteLimitInDesc = 50

  def empty: BinaryData =
    unsafeFromArray(new Array(0))

  def byte(b: Byte): BinaryData = {
    val a = new Array[Byte](1)
    a(0) = b
    unsafeFromArray(a)
  }

  def fromArray(a: Array[Byte]): BinaryData = {
    val a2 = Arrays.copyOf(a, a.length)
    unsafeFromArray(a2)
  }

  def fromArraySeq(a: ArraySeq[Byte]): BinaryData =
    unsafeFromArray(a.unsafeArray.asInstanceOf[Array[Byte]])

  def fromBase64(base64: String): Either[ErrorMsg, BinaryData] =
    try
      Right(fromBase64OrThrow(base64))
    catch {
      case e: IllegalArgumentException =>
        Left(ErrorMsg("Invalid base64 data: " + e.getMessage))
    }

  def fromBase64OrThrow(base64: String): BinaryData =
    unsafeFromArray(Base64.getDecoder.decode(base64))

  def fromByteBuffer(bb: ByteBuffer): BinaryData =
    if (bb.hasArray) {
      val offset = bb.arrayOffset()
      val a = Arrays.copyOfRange(bb.array(), offset, offset + bb.limit())
      unsafeFromArray(a)
    } else {
      val a = new Array[Byte](bb.remaining)
      bb.get(a)
      unsafeFromArray(a)
    }

  def fromHex(hex: String): BinaryData = {
    assert((hex.length & 1) == 0, "Hex strings must have an even length.")
    var i = hex.length >> 1
    val bytes = new Array[Byte](i)
    while (i > 0) {
      i -= 1
      val si = i << 1
      val byteStr = hex.substring(si, si + 2)
      val byte = java.lang.Integer.parseUnsignedInt(byteStr, 16).byteValue()
      bytes(i) = byte
    }
    unsafeFromArray(bytes)
  }

  /** unsafe because the array could be modified later and affect the underlying array we use here */
  def unsafeFromArray(a: Array[Byte]): BinaryData =
    new BinaryData(a, 0, a.length)

  /** unsafe because the ByteBuffer could be modified later and affect the underlying array we use here */
  def unsafeFromByteBuffer(bb: ByteBuffer): BinaryData =
    if (bb.hasArray)
      new BinaryData(bb.array(), bb.arrayOffset(), bb.limit())
    else
      fromByteBuffer(bb)

  def fromStringAsUtf8(str: String): BinaryData =
    unsafeFromArray(str.getBytes(StandardCharsets.UTF_8))
}

/** Immutable blob of binary data. */
final class BinaryData(private[BinaryData] val bytes: Array[Byte],
                       private[BinaryData] val offset: Int,
                       val length: Int) extends BinaryData_PlatformSpecific_Instance {

  private val lastIndExcl = offset + length

  // Note: It's acceptable to have excess bytes beyond the declared length
  assert(lastIndExcl <= bytes.length, s"offset($offset) + length ($length) exceeds number of bytes (${bytes.length})")

  override def toString = s"BinaryData(${describe()})"

  override def hashCode =
    // Should use Arrays.hashCode() but have to copy to use provided length instead of array.length
    offset * -947 + length

  override def equals(o: Any): Boolean =
    o match {
      case b: BinaryData =>
        @inline def sameRef = this eq b
        @inline def sameLen = length == b.length
        @inline def sameBin = (0 until length).forall(i => bytes(offset + i) == b.bytes(b.offset + i))
        sameRef || (sameLen && sameBin)
      case _ =>
        false
    }

  @inline def isEmpty: Boolean =
    length == 0

  @inline def nonEmpty: Boolean =
    length != 0

  def duplicate: BinaryData =
    BinaryData.unsafeFromArray(toNewArray)

  def describe(byteLimit: Int = BinaryData.DefaultByteLimitInDesc, sep: String = ",") = {
    val byteDesc = describeBytes(byteLimit, sep)
    val len = "%,d".format(length)
    s"$len bytes: $byteDesc"
  }

  def describeBytes(limit: Int = BinaryData.DefaultByteLimitInDesc, sep: String = ",") = {
    var i = bytes.iterator.drop(offset).map(b => "%02X".format(b & 0xff))
    if (length > limit)
      i = i.take(limit) ++ Iterator.single("…")
    else
      i = i.take(length)
    i.mkString(sep)
  }

  def writeTo(os: OutputStream): Unit =
    os.write(bytes, offset, length)

  // Note: the below must remain a `def` because ByteBuffers themselves have mutable state
  /** unsafe in that the underlying bytes could be modified via access to unsafeArray */
  def unsafeByteBuffer: ByteBuffer =
    if (offset > 0)
      ByteBuffer.wrap(bytes, 0, lastIndExcl).position(offset).slice()
    else
      ByteBuffer.wrap(bytes, 0, length)

  def toNewByteBuffer: ByteBuffer =
    ByteBuffer.wrap(toNewArray, 0, length)

  def toNewArray: Array[Byte] =
    Arrays.copyOfRange(bytes, offset, lastIndExcl)

  /** unsafe in that you might get back the underlying array which is mutable */
  lazy val unsafeArray: Array[Byte] =
    if (offset == 0 && length == bytes.length)
      bytes
    else
      toNewArray

  def binaryLikeString: String = {
    val chars = new Array[Char](length)
    var j = length
    while (j > 0) {
      j -= 1
      val b = bytes(offset + j)
      val i = b.toInt & 0xff
      chars.update(j, i.toChar)
    }
    String.valueOf(chars)
  }

  def hex: String =
    bytes
      .iterator
      .slice(offset, lastIndExcl)
      .map(b => "%02X".format(b & 0xff))
      .mkString

  def ++(that: BinaryData): BinaryData =
    if (this.isEmpty)
      that
    else if (that.isEmpty)
      this
    else {
      val a = new Array[Byte](length + that.length)
      Array.copy(this.bytes, this.offset, a, 0, this.length)
      Array.copy(that.bytes, that.offset, a, this.length, that.length)
      BinaryData.unsafeFromArray(a)
    }

  def drop(n: Int): BinaryData = {
    val m = n.min(length)
    new BinaryData(bytes, offset + m, length - m)
  }

  def take(n: Int): BinaryData = {
    val m = n.min(length)
    new BinaryData(bytes, offset, m)
  }

  def dropRight(n: Int): BinaryData = {
    val m = n.min(length)
    take(length - m)
  }

  def takeRight(n: Int): BinaryData = {
    val m = n.min(length)
    drop(length - m)
  }

  def toBase64: String =
    Base64.getEncoder.encodeToString(unsafeArray)

  def appendBase64(sb: JStringBuilder): Unit = {
    val b64 = Base64.getEncoder.encode(unsafeArray)
    var i = 0
    while (i < b64.length) {
      sb.append(b64(i).toChar)
      i += 1
    }
  }

  @inline def appendBase64(sb: StringBuilder): Unit =
    appendBase64(sb.underlying)

  def toStringAsUtf8: String =
    new String(unsafeArray, StandardCharsets.UTF_8)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy