Skip to content

Commit 1af9fdb

Browse files
authored
Merge pull request #375 from scodec/topic/hexdump
Add support for generating hex dumps
2 parents 4ea2d16 + 41a9fab commit 1af9fdb

File tree

1 file changed

+223
-1
lines changed

1 file changed

+223
-1
lines changed

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

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ package scodec.bits
3232

3333
import java.io.{InputStream, OutputStream}
3434
import java.nio.{ByteBuffer, CharBuffer}
35-
import java.nio.charset.{CharacterCodingException, Charset}
35+
import java.nio.charset.{CharacterCodingException, Charset, CodingErrorAction}
3636
import java.util.UUID
3737
import java.util.concurrent.atomic.{AtomicInteger, AtomicLong}
3838

@@ -806,6 +806,20 @@ sealed abstract class ByteVector
806806
bldr.toString
807807
}
808808

809+
/** Generates a hex dump of this vector using the default format (with ANSI enabled).
810+
* To customize the output, use the `ByteVector.HexDumpFormat` class instead.
811+
* For example, `ByteVector.HexDumpFormat.NoAnsi.render(bytes)` or
812+
* `ByteVector.HexDumpFormat.Default.withIncludeAddressColumn(false).render(bytes)`.
813+
*
814+
* @group conversions
815+
*/
816+
final def toHexDump: String = HexDumpFormat.Default.render(this)
817+
818+
/** Like [[toHexDump]] but no ANSI escape codes are included.
819+
* @group conversions
820+
*/
821+
final def toHexDumpNoAnsi: String = HexDumpFormat.NoAnsi.render(this)
822+
809823
/** Helper alias for [[toHex:String*]]
810824
*
811825
* @group conversions
@@ -1067,18 +1081,45 @@ sealed abstract class ByteVector
10671081
}
10681082
}
10691083

1084+
/** Like [[decodeString]] but does not fail on bad input.
1085+
* @group conversions
1086+
*/
1087+
final def decodeStringLenient(
1088+
replaceMalformedInput: Boolean = true,
1089+
replaceUnmappableChars: Boolean = true,
1090+
replacement: String = ""
1091+
)(implicit charset: Charset): String = {
1092+
val decoder = charset.newDecoder
1093+
.replaceWith(replacement)
1094+
.onMalformedInput(
1095+
if (replaceMalformedInput) CodingErrorAction.REPLACE else CodingErrorAction.IGNORE
1096+
)
1097+
.onUnmappableCharacter(
1098+
if (replaceUnmappableChars) CodingErrorAction.REPLACE else CodingErrorAction.IGNORE
1099+
)
1100+
decoder.decode(toByteBuffer).toString
1101+
}
1102+
10701103
/** Decodes this vector as a string using the UTF-8 charset.
10711104
* @group conversions
10721105
*/
10731106
final def decodeUtf8: Either[CharacterCodingException, String] =
10741107
decodeString(Charset.forName("UTF-8"))
10751108

1109+
/** Like [[decodeUtf8]] but does not fail on bad input. */
1110+
final def decodeUtf8Lenient: String =
1111+
decodeStringLenient()(Charset.forName("UTF-8"))
1112+
10761113
/** Decodes this vector as a string using the US-ASCII charset.
10771114
* @group conversions
10781115
*/
10791116
final def decodeAscii: Either[CharacterCodingException, String] =
10801117
decodeString(Charset.forName("US-ASCII"))
10811118

1119+
/** Like [[decodeAscii]] but does not fail on bad input. */
1120+
final def decodeAsciiLenient: String =
1121+
decodeStringLenient()(Charset.forName("US-ASCII"))
1122+
10821123
final def not: ByteVector = mapS(new F1B { def apply(b: Byte) = (~b).toByte })
10831124

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

0 commit comments

Comments
 (0)