Skip to content

Commit d623534

Browse files
authored
Merge pull request #377 from scodec/topic/hexdump
Incremental printing of hex dumps
2 parents 95d3b62 + 122b033 commit d623534

File tree

4 files changed

+285
-199
lines changed

4 files changed

+285
-199
lines changed

build.sbt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ ThisBuild / tlVersionIntroduced := Map(
1919
"2.11" -> "1.1.99" // Ignore 2.11 in mima
2020
)
2121

22+
ThisBuild / tlMimaPreviousVersions ~= (_.filterNot(_ == "1.1.32"))
23+
2224
ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("8"))
2325

2426
ThisBuild / tlFatalWarningsInCi := false
@@ -67,10 +69,7 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq(
6769
ProblemFilters.exclude[DirectMissingMethodProblem]("scodec.bits.crc.vectorTable"),
6870
ProblemFilters.exclude[IncompatibleMethTypeProblem](
6971
"scodec.bits.ByteVector#ByteVectorInputStream#CustomAtomicInteger.getAndUpdate_"
70-
),
71-
ProblemFilters.exclude[DirectMissingMethodProblem]("scodec.bits.ByteVector#HexDumpFormat.Ansi"),
72-
ProblemFilters.exclude[DirectMissingMethodProblem]("scodec.bits.ByteVector.toHexDumpNoAnsi"),
73-
ProblemFilters.exclude[InaccessibleClassProblem]("scodec.bits.ByteVector$HexDumpFormat$Ansi$")
72+
)
7473
)
7574

7675
lazy val root = tlCrossRootProject.aggregate(core, benchmark)

core/shared/src/main/scala/scodec/bits/BitVector.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,25 @@ sealed abstract class BitVector
730730
}
731731
}
732732

733+
/** Generates a hex dump of this vector using the default format (with no color / ANSI escapes).
734+
* To customize the output, use the `HexDumpFormat` class instead.
735+
* For example, `HexDumpFormat.NoAscii.render(bytes)` or
736+
* `HexDumpFormat.Default.withIncludeAddressColumn(false).render(bytes)`.
737+
*
738+
* @group conversions
739+
*/
740+
final def toHexDump: String = HexDumpFormat.NoAnsi.render(this)
741+
742+
/** Colorized version of [[toHexDump]] that uses ANSI escape codes.
743+
*
744+
* @group conversions
745+
*/
746+
final def toHexDumpColorized: String = HexDumpFormat.Default.render(this)
747+
748+
/** Prints this vector as a colorized hex dump to standard out.
749+
*/
750+
final def printHexDump(): Unit = HexDumpFormat.Default.print(this)
751+
733752
/** Helper alias of [[toHex():String*]]
734753
*
735754
* @group conversions

core/shared/src/main/scala/scodec/bits/ByteVector.scala

Lines changed: 4 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -807,9 +807,9 @@ sealed abstract class ByteVector
807807
}
808808

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

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

828828
/** Helper alias for [[toHex:String*]]
829829
*
@@ -2344,195 +2344,4 @@ object ByteVector extends ByteVectorCompanionCrossPlatform {
23442344
}
23452345
}
23462346
}
2347-
2348-
final class HexDumpFormat private (
2349-
includeAddressColumn: Boolean,
2350-
dataColumnCount: Int,
2351-
dataColumnWidthInBytes: Int,
2352-
includeAsciiColumn: Boolean,
2353-
alphabet: Bases.HexAlphabet,
2354-
ansiEnabled: Boolean
2355-
) {
2356-
def withIncludeAddressColumn(newIncludeAddressColumn: Boolean): HexDumpFormat =
2357-
new HexDumpFormat(
2358-
newIncludeAddressColumn,
2359-
dataColumnCount,
2360-
dataColumnWidthInBytes,
2361-
includeAsciiColumn,
2362-
alphabet,
2363-
ansiEnabled
2364-
)
2365-
def withDataColumnCount(newDataColumnCount: Int): HexDumpFormat =
2366-
new HexDumpFormat(
2367-
includeAddressColumn,
2368-
newDataColumnCount,
2369-
dataColumnWidthInBytes,
2370-
includeAsciiColumn,
2371-
alphabet,
2372-
ansiEnabled
2373-
)
2374-
def withDataColumnWidthInBytes(newDataColumnWidthInBytes: Int): HexDumpFormat =
2375-
new HexDumpFormat(
2376-
includeAddressColumn,
2377-
dataColumnCount,
2378-
newDataColumnWidthInBytes,
2379-
includeAsciiColumn,
2380-
alphabet,
2381-
ansiEnabled
2382-
)
2383-
def withIncludeAsciiColumn(newIncludeAsciiColumn: Boolean): HexDumpFormat =
2384-
new HexDumpFormat(
2385-
includeAddressColumn,
2386-
dataColumnCount,
2387-
dataColumnWidthInBytes,
2388-
newIncludeAsciiColumn,
2389-
alphabet,
2390-
ansiEnabled
2391-
)
2392-
def withAlphabet(newAlphabet: Bases.HexAlphabet): HexDumpFormat =
2393-
new HexDumpFormat(
2394-
includeAddressColumn,
2395-
dataColumnCount,
2396-
dataColumnWidthInBytes,
2397-
includeAsciiColumn,
2398-
newAlphabet,
2399-
ansiEnabled
2400-
)
2401-
def withAnsi(newAnsiEnabled: Boolean): HexDumpFormat =
2402-
new HexDumpFormat(
2403-
includeAddressColumn,
2404-
dataColumnCount,
2405-
dataColumnWidthInBytes,
2406-
includeAsciiColumn,
2407-
alphabet,
2408-
newAnsiEnabled
2409-
)
2410-
2411-
def render(bytes: ByteVector): String = {
2412-
val bldr = new StringBuilder
2413-
val numBytesPerLine = dataColumnWidthInBytes * dataColumnCount
2414-
val bytesPerLine = bytes.groupedIterator(numBytesPerLine.toLong)
2415-
bytesPerLine.zipWithIndex.foreach { case (bytesInLine, index) =>
2416-
renderLine(bldr, bytesInLine, index * numBytesPerLine)
2417-
}
2418-
bldr.toString
2419-
}
2420-
2421-
private object Ansi {
2422-
val Faint = "\u001b[;2m"
2423-
val Normal = "\u001b[;22m"
2424-
val Reset = "\u001b[0m"
2425-
def foregroundColor(bldr: StringBuilder, rgb: (Int, Int, Int)): Unit = {
2426-
bldr
2427-
.append("\u001b[38;2;")
2428-
.append(rgb._1)
2429-
.append(";")
2430-
.append(rgb._2)
2431-
.append(";")
2432-
.append(rgb._3)
2433-
.append("m")
2434-
()
2435-
}
2436-
}
2437-
2438-
private def renderLine(bldr: StringBuilder, bytes: ByteVector, address: Int): Unit = {
2439-
if (includeAddressColumn) {
2440-
if (ansiEnabled) bldr.append(Ansi.Faint)
2441-
bldr.append(ByteVector.fromInt(address).toHex(alphabet))
2442-
if (ansiEnabled) bldr.append(Ansi.Normal)
2443-
bldr.append(" ")
2444-
}
2445-
bytes.groupedIterator(dataColumnWidthInBytes.toLong).foreach { columnBytes =>
2446-
renderHex(bldr, columnBytes)
2447-
bldr.append(" ")
2448-
}
2449-
if (ansiEnabled)
2450-
bldr.append(Ansi.Reset)
2451-
if (includeAsciiColumn) {
2452-
val padding = {
2453-
val bytesOnFullLine = dataColumnWidthInBytes * dataColumnCount
2454-
val bytesOnThisLine = bytes.size.toInt
2455-
val dataBytePadding = (bytesOnFullLine - bytesOnThisLine) * 3 - 1
2456-
val numFullDataColumns = bytesOnThisLine / dataColumnWidthInBytes
2457-
val numAdditionalColumnSpacers = dataColumnCount - numFullDataColumns
2458-
dataBytePadding + numAdditionalColumnSpacers
2459-
}
2460-
bldr.append(" " * padding)
2461-
bldr.append('│')
2462-
renderAsciiBestEffort(bldr, bytes)
2463-
bldr.append('│')
2464-
}
2465-
bldr.append('\n')
2466-
()
2467-
}
2468-
2469-
private def renderHex(bldr: StringBuilder, bytes: ByteVector): Unit =
2470-
bytes.foreachS {
2471-
new F1BU {
2472-
def apply(b: Byte) = {
2473-
if (ansiEnabled) Ansi.foregroundColor(bldr, rgbForByte(b))
2474-
bldr
2475-
.append(alphabet.toChar((b >> 4 & 0x0f).toByte.toInt))
2476-
.append(alphabet.toChar((b & 0x0f).toByte.toInt))
2477-
.append(' ')
2478-
()
2479-
}
2480-
}
2481-
}
2482-
2483-
private def rgbForByte(b: Byte): (Int, Int, Int) = {
2484-
val saturation = 0.4
2485-
val value = 0.75
2486-
val hue = ((b & 0xff) / 256.0) * 360.0
2487-
hsvToRgb(hue, saturation, value)
2488-
}
2489-
2490-
// From https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
2491-
private def hsvToRgb(hue: Double, saturation: Double, value: Double): (Int, Int, Int) = {
2492-
val c = saturation * value
2493-
val h = hue / 60
2494-
val x = c * (1 - (h % 2 - 1).abs)
2495-
val z = 0d
2496-
val (r1, g1, b1) = h.toInt match {
2497-
case 0 => (c, x, z)
2498-
case 1 => (x, c, z)
2499-
case 2 => (z, c, x)
2500-
case 3 => (z, x, c)
2501-
case 4 => (x, z, c)
2502-
case 5 => (c, z, x)
2503-
}
2504-
val m = value - c
2505-
val (r, g, b) = (r1 + m, g1 + m, b1 + m)
2506-
def scale(v: Double) = (v * 256).toInt
2507-
(scale(r), scale(g), scale(b))
2508-
}
2509-
2510-
private val FaintDot = s"${Ansi.Faint}.${Ansi.Normal}"
2511-
private val FaintUnmappable = s"${Ansi.Faint}${Ansi.Normal}"
2512-
private val NonPrintablePattern = "[^�\\p{Print}]".r
2513-
2514-
private def renderAsciiBestEffort(bldr: StringBuilder, bytes: ByteVector): Unit = {
2515-
val decoded = bytes.decodeAsciiLenient
2516-
val nonPrintableReplacement = if (ansiEnabled) FaintDot else "."
2517-
val printable = NonPrintablePattern.replaceAllIn(decoded, nonPrintableReplacement)
2518-
val colorized = if (ansiEnabled) printable.replaceAll("", FaintUnmappable) else printable
2519-
bldr.append(colorized)
2520-
()
2521-
}
2522-
}
2523-
2524-
object HexDumpFormat {
2525-
2526-
/** Colorized hex dump that displays 2 columns of 8 bytes each, along with the address column and ASCII column. */
2527-
val Default: HexDumpFormat =
2528-
new HexDumpFormat(true, 2, 8, true, Bases.Alphabets.HexLowercase, true)
2529-
2530-
/** Like [[Default]] but with ANSI color disabled. */
2531-
val NoAnsi: HexDumpFormat =
2532-
Default.withAnsi(false)
2533-
2534-
/** Like [[Default]] but with 3 columns of data and no ASCII column. */
2535-
val NoAscii: HexDumpFormat =
2536-
Default.withIncludeAsciiColumn(false).withDataColumnCount(3)
2537-
}
25382347
}

0 commit comments

Comments
 (0)