Skip to content

Commit

Permalink
Merge pull request #377 from scodec/topic/hexdump
Browse files Browse the repository at this point in the history
Incremental printing of hex dumps
  • Loading branch information
mpilquist committed May 30, 2022
2 parents 95d3b62 + 122b033 commit d623534
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 199 deletions.
7 changes: 3 additions & 4 deletions build.sbt
Expand Up @@ -19,6 +19,8 @@ ThisBuild / tlVersionIntroduced := Map(
"2.11" -> "1.1.99" // Ignore 2.11 in mima
)

ThisBuild / tlMimaPreviousVersions ~= (_.filterNot(_ == "1.1.32"))

ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("8"))

ThisBuild / tlFatalWarningsInCi := false
Expand Down Expand Up @@ -67,10 +69,7 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[DirectMissingMethodProblem]("scodec.bits.crc.vectorTable"),
ProblemFilters.exclude[IncompatibleMethTypeProblem](
"scodec.bits.ByteVector#ByteVectorInputStream#CustomAtomicInteger.getAndUpdate_"
),
ProblemFilters.exclude[DirectMissingMethodProblem]("scodec.bits.ByteVector#HexDumpFormat.Ansi"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scodec.bits.ByteVector.toHexDumpNoAnsi"),
ProblemFilters.exclude[InaccessibleClassProblem]("scodec.bits.ByteVector$HexDumpFormat$Ansi$")
)
)

lazy val root = tlCrossRootProject.aggregate(core, benchmark)
Expand Down
19 changes: 19 additions & 0 deletions core/shared/src/main/scala/scodec/bits/BitVector.scala
Expand Up @@ -730,6 +730,25 @@ sealed abstract class BitVector
}
}

/** Generates a hex dump of this vector using the default format (with no color / ANSI escapes).
* To customize the output, use the `HexDumpFormat` class instead.
* For example, `HexDumpFormat.NoAscii.render(bytes)` or
* `HexDumpFormat.Default.withIncludeAddressColumn(false).render(bytes)`.
*
* @group conversions
*/
final def toHexDump: String = HexDumpFormat.NoAnsi.render(this)

/** Colorized version of [[toHexDump]] that uses ANSI escape codes.
*
* @group conversions
*/
final def toHexDumpColorized: String = HexDumpFormat.Default.render(this)

/** Prints this vector as a colorized hex dump to standard out.
*/
final def printHexDump(): Unit = HexDumpFormat.Default.print(this)

/** Helper alias of [[toHex():String*]]
*
* @group conversions
Expand Down
199 changes: 4 additions & 195 deletions core/shared/src/main/scala/scodec/bits/ByteVector.scala
Expand Up @@ -807,9 +807,9 @@ sealed abstract class ByteVector
}

/** Generates a hex dump of this vector using the default format (with no color / ANSI escapes).
* To customize the output, use the `ByteVector.HexDumpFormat` class instead.
* For example, `ByteVector.HexDumpFormat.NoAscii.render(bytes)` or
* `ByteVector.HexDumpFormat.Default.withIncludeAddressColumn(false).render(bytes)`.
* To customize the output, use the `HexDumpFormat` class instead.
* For example, `HexDumpFormat.NoAscii.render(bytes)` or
* `HexDumpFormat.Default.withIncludeAddressColumn(false).render(bytes)`.
*
* @group conversions
*/
Expand All @@ -823,7 +823,7 @@ sealed abstract class ByteVector

/** Prints this vector as a colorized hex dump to standard out.
*/
final def printHexDump(): Unit = print(toHexDumpColorized)
final def printHexDump(): Unit = HexDumpFormat.Default.print(this)

/** Helper alias for [[toHex:String*]]
*
Expand Down Expand Up @@ -2344,195 +2344,4 @@ object ByteVector extends ByteVectorCompanionCrossPlatform {
}
}
}

final class HexDumpFormat private (
includeAddressColumn: Boolean,
dataColumnCount: Int,
dataColumnWidthInBytes: Int,
includeAsciiColumn: Boolean,
alphabet: Bases.HexAlphabet,
ansiEnabled: Boolean
) {
def withIncludeAddressColumn(newIncludeAddressColumn: Boolean): HexDumpFormat =
new HexDumpFormat(
newIncludeAddressColumn,
dataColumnCount,
dataColumnWidthInBytes,
includeAsciiColumn,
alphabet,
ansiEnabled
)
def withDataColumnCount(newDataColumnCount: Int): HexDumpFormat =
new HexDumpFormat(
includeAddressColumn,
newDataColumnCount,
dataColumnWidthInBytes,
includeAsciiColumn,
alphabet,
ansiEnabled
)
def withDataColumnWidthInBytes(newDataColumnWidthInBytes: Int): HexDumpFormat =
new HexDumpFormat(
includeAddressColumn,
dataColumnCount,
newDataColumnWidthInBytes,
includeAsciiColumn,
alphabet,
ansiEnabled
)
def withIncludeAsciiColumn(newIncludeAsciiColumn: Boolean): HexDumpFormat =
new HexDumpFormat(
includeAddressColumn,
dataColumnCount,
dataColumnWidthInBytes,
newIncludeAsciiColumn,
alphabet,
ansiEnabled
)
def withAlphabet(newAlphabet: Bases.HexAlphabet): HexDumpFormat =
new HexDumpFormat(
includeAddressColumn,
dataColumnCount,
dataColumnWidthInBytes,
includeAsciiColumn,
newAlphabet,
ansiEnabled
)
def withAnsi(newAnsiEnabled: Boolean): HexDumpFormat =
new HexDumpFormat(
includeAddressColumn,
dataColumnCount,
dataColumnWidthInBytes,
includeAsciiColumn,
alphabet,
newAnsiEnabled
)

def render(bytes: ByteVector): String = {
val bldr = new StringBuilder
val numBytesPerLine = dataColumnWidthInBytes * dataColumnCount
val bytesPerLine = bytes.groupedIterator(numBytesPerLine.toLong)
bytesPerLine.zipWithIndex.foreach { case (bytesInLine, index) =>
renderLine(bldr, bytesInLine, index * numBytesPerLine)
}
bldr.toString
}

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(bldr: StringBuilder, bytes: ByteVector, address: Int): Unit = {
if (includeAddressColumn) {
if (ansiEnabled) bldr.append(Ansi.Faint)
bldr.append(ByteVector.fromInt(address).toHex(alphabet))
if (ansiEnabled) bldr.append(Ansi.Normal)
bldr.append(" ")
}
bytes.groupedIterator(dataColumnWidthInBytes.toLong).foreach { columnBytes =>
renderHex(bldr, columnBytes)
bldr.append(" ")
}
if (ansiEnabled)
bldr.append(Ansi.Reset)
if (includeAsciiColumn) {
val padding = {
val bytesOnFullLine = dataColumnWidthInBytes * dataColumnCount
val bytesOnThisLine = bytes.size.toInt
val dataBytePadding = (bytesOnFullLine - bytesOnThisLine) * 3 - 1
val numFullDataColumns = bytesOnThisLine / dataColumnWidthInBytes
val numAdditionalColumnSpacers = dataColumnCount - numFullDataColumns
dataBytePadding + numAdditionalColumnSpacers
}
bldr.append(" " * padding)
bldr.append('│')
renderAsciiBestEffort(bldr, bytes)
bldr.append('│')
}
bldr.append('\n')
()
}

private def renderHex(bldr: StringBuilder, bytes: ByteVector): Unit =
bytes.foreachS {
new F1BU {
def apply(b: Byte) = {
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)

/** 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)
}
}

0 comments on commit d623534

Please sign in to comment.