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

scodec.bits.HexDumpFormat.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2013, Scodec
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package scodec.bits

/** Creates hex dumps for bit and byte vectors.
  *
  * Formatting options can be specified by starting with `HexDumpFormat.Default`
  * and then calling various `withXyz` methods.
  */
final class HexDumpFormat private (
    val includeAddressColumn: Boolean,
    val dataColumnCount: Int,
    val dataColumnWidthInBytes: Int,
    val includeAsciiColumn: Boolean,
    val alphabet: Bases.HexAlphabet,
    val ansiEnabled: Boolean,
    val addressOffset: Int,
    val lengthLimit: Long
) {
  @inline private def numBytesPerLine = dataColumnWidthInBytes * dataColumnCount
  @inline private def numBitsPerLine = numBytesPerLine * 8L

  def withIncludeAddressColumn(newIncludeAddressColumn: Boolean): HexDumpFormat =
    new HexDumpFormat(
      newIncludeAddressColumn,
      dataColumnCount,
      dataColumnWidthInBytes,
      includeAsciiColumn,
      alphabet,
      ansiEnabled,
      addressOffset,
      lengthLimit
    )
  def withDataColumnCount(newDataColumnCount: Int): HexDumpFormat =
    new HexDumpFormat(
      includeAddressColumn,
      newDataColumnCount,
      dataColumnWidthInBytes,
      includeAsciiColumn,
      alphabet,
      ansiEnabled,
      addressOffset,
      lengthLimit
    )
  def withDataColumnWidthInBytes(newDataColumnWidthInBytes: Int): HexDumpFormat =
    new HexDumpFormat(
      includeAddressColumn,
      dataColumnCount,
      newDataColumnWidthInBytes,
      includeAsciiColumn,
      alphabet,
      ansiEnabled,
      addressOffset,
      lengthLimit
    )
  def withIncludeAsciiColumn(newIncludeAsciiColumn: Boolean): HexDumpFormat =
    new HexDumpFormat(
      includeAddressColumn,
      dataColumnCount,
      dataColumnWidthInBytes,
      newIncludeAsciiColumn,
      alphabet,
      ansiEnabled,
      addressOffset,
      lengthLimit
    )
  def withAlphabet(newAlphabet: Bases.HexAlphabet): HexDumpFormat =
    new HexDumpFormat(
      includeAddressColumn,
      dataColumnCount,
      dataColumnWidthInBytes,
      includeAsciiColumn,
      newAlphabet,
      ansiEnabled,
      addressOffset,
      lengthLimit
    )
  def withAnsi(newAnsiEnabled: Boolean): HexDumpFormat =
    new HexDumpFormat(
      includeAddressColumn,
      dataColumnCount,
      dataColumnWidthInBytes,
      includeAsciiColumn,
      alphabet,
      newAnsiEnabled,
      addressOffset,
      lengthLimit
    )
  def withAddressOffset(newAddressOffset: Int): HexDumpFormat =
    new HexDumpFormat(
      includeAddressColumn,
      dataColumnCount,
      dataColumnWidthInBytes,
      includeAsciiColumn,
      alphabet,
      ansiEnabled,
      newAddressOffset,
      lengthLimit
    )
  def withLengthLimit(newLengthLimit: Long): HexDumpFormat =
    new HexDumpFormat(
      includeAddressColumn,
      dataColumnCount,
      dataColumnWidthInBytes,
      includeAsciiColumn,
      alphabet,
      ansiEnabled,
      addressOffset,
      newLengthLimit
    )

  def render(bytes: ByteVector): String =
    render(bytes.bits)

  def render(bits: => BitVector): String = {
    val bldr = new StringBuilder
    render(bits, line => { bldr.append(line); () })
    bldr.toString
  }

  def render(bits: => BitVector, onLine: String => Unit): Unit =
    render(bits, 0L, onLine)

  @annotation.tailrec
  private def render(bits: BitVector, position: Long, onLine: String => Unit): Unit = {
    // Note: we don't use grouped(numBitsPerLine) here to avoid holding on to a reference to original vector
    val takeFullLine = position + numBytesPerLine <= lengthLimit
    val bitsToTake = if (takeFullLine) numBitsPerLine else (lengthLimit - position) * 8L
    if (bits.nonEmpty && bitsToTake > 0L) {
      val bitsInLine = bits.take(bitsToTake)
      val line = renderLine(bitsInLine.bytes, (addressOffset + position).toInt)
      onLine(line)
      if (takeFullLine)
        render(bits.drop(numBitsPerLine), position + bitsInLine.size / 8, onLine)
    }
  }

  def print(bytes: ByteVector): Unit =
    print(bytes.bits)

  def print(bits: => BitVector): Unit =
    render(bits, line => Console.print(line))

  private object Ansi {
    val Faint = "\u001b[;2m"
    val Normal = "\u001b[;22m"
    val Reset = "\u001b[0m"
    def foregroundColor(bldr: StringBuilder, rgb: (Int, Int, Int)): Unit = {
      bldr
        .append("\u001b[38;2;")
        .append(rgb._1)
        .append(";")
        .append(rgb._2)
        .append(";")
        .append(rgb._3)
        .append("m")
      ()
    }
  }

  private def renderLine(bytes: ByteVector, address: Int): String = {
    val bldr = new StringBuilder
    if (includeAddressColumn) {
      if (ansiEnabled) bldr.append(Ansi.Faint)
      bldr.append(ByteVector.fromInt(address).toHex(alphabet))
      if (ansiEnabled) bldr.append(Ansi.Normal)
      bldr.append("  ")
    }
    bytes.grouped(dataColumnWidthInBytes.toLong).foreach { columnBytes =>
      renderHex(bldr, columnBytes)
      bldr.append(" ")
    }
    if (ansiEnabled)
      bldr.append(Ansi.Reset)
    if (includeAsciiColumn) {
      val padding = {
        val bytesOnThisLine = bytes.size.toInt
        val dataBytePadding = (numBytesPerLine - bytesOnThisLine) * 3 - 1
        val numFullDataColumns = (bytesOnThisLine - 1) / dataColumnWidthInBytes
        val numAdditionalColumnSpacers = dataColumnCount - numFullDataColumns
        dataBytePadding + numAdditionalColumnSpacers
      }
      bldr.append(" " * padding)
      bldr.append('│')
      renderAsciiBestEffort(bldr, bytes)
      bldr.append('│')
    }
    bldr.append('\n')
    bldr.toString
  }

  private def renderHex(bldr: StringBuilder, bytes: ByteVector): Unit =
    bytes.foreachS {
      new ByteVector.F1BU {
        def apply(b: Byte): Unit = {
          if (ansiEnabled) Ansi.foregroundColor(bldr, rgbForByte(b))
          bldr
            .append(alphabet.toChar((b >> 4 & 0x0f).toByte.toInt))
            .append(alphabet.toChar((b & 0x0f).toByte.toInt))
            .append(' ')
          ()
        }
      }
    }

  private def rgbForByte(b: Byte): (Int, Int, Int) = {
    val saturation = 0.4
    val value = 0.75
    val hue = ((b & 0xff) / 256.0) * 360.0
    hsvToRgb(hue, saturation, value)
  }

  // From https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
  private def hsvToRgb(hue: Double, saturation: Double, value: Double): (Int, Int, Int) = {
    val c = saturation * value
    val h = hue / 60
    val x = c * (1 - (h % 2 - 1).abs)
    val z = 0d
    val (r1, g1, b1) = h.toInt match {
      case 0 => (c, x, z)
      case 1 => (x, c, z)
      case 2 => (z, c, x)
      case 3 => (z, x, c)
      case 4 => (x, z, c)
      case 5 => (c, z, x)
    }
    val m = value - c
    val (r, g, b) = (r1 + m, g1 + m, b1 + m)
    def scale(v: Double) = (v * 256).toInt
    (scale(r), scale(g), scale(b))
  }

  private val FaintDot = s"${Ansi.Faint}.${Ansi.Normal}"
  private val FaintUnmappable = s"${Ansi.Faint}�${Ansi.Normal}"
  private val NonPrintablePattern = "[^�\\p{Print}]".r

  private def renderAsciiBestEffort(bldr: StringBuilder, bytes: ByteVector): Unit = {
    val decoded = bytes.decodeAsciiLenient
    val nonPrintableReplacement = if (ansiEnabled) FaintDot else "."
    val printable = NonPrintablePattern.replaceAllIn(decoded, nonPrintableReplacement)
    val colorized = if (ansiEnabled) printable.replaceAll("�", FaintUnmappable) else printable
    bldr.append(colorized)
    ()
  }
}

object HexDumpFormat {

  /** Colorized hex dump that displays 2 columns of 8 bytes each, along with the address column and ASCII column. */
  val Default: HexDumpFormat =
    new HexDumpFormat(true, 2, 8, true, Bases.Alphabets.HexLowercase, true, 0, Long.MaxValue)

  /** Like [[Default]] but with ANSI color disabled. */
  val NoAnsi: HexDumpFormat =
    Default.withAnsi(false)

  /** Like [[Default]] but with 3 columns of data and no ASCII column. */
  val NoAscii: HexDumpFormat =
    Default.withIncludeAsciiColumn(false).withDataColumnCount(3)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy