@@ -32,7 +32,7 @@ package scodec.bits
32
32
33
33
import java .io .{InputStream , OutputStream }
34
34
import java .nio .{ByteBuffer , CharBuffer }
35
- import java .nio .charset .{CharacterCodingException , Charset }
35
+ import java .nio .charset .{CharacterCodingException , Charset , CodingErrorAction }
36
36
import java .util .UUID
37
37
import java .util .concurrent .atomic .{AtomicInteger , AtomicLong }
38
38
@@ -806,6 +806,20 @@ sealed abstract class ByteVector
806
806
bldr.toString
807
807
}
808
808
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
+
809
823
/** Helper alias for [[toHex:String* ]]
810
824
*
811
825
* @group conversions
@@ -1067,18 +1081,45 @@ sealed abstract class ByteVector
1067
1081
}
1068
1082
}
1069
1083
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
+
1070
1103
/** Decodes this vector as a string using the UTF-8 charset.
1071
1104
* @group conversions
1072
1105
*/
1073
1106
final def decodeUtf8 : Either [CharacterCodingException , String ] =
1074
1107
decodeString(Charset .forName(" UTF-8" ))
1075
1108
1109
+ /** Like [[decodeUtf8 ]] but does not fail on bad input. */
1110
+ final def decodeUtf8Lenient : String =
1111
+ decodeStringLenient()(Charset .forName(" UTF-8" ))
1112
+
1076
1113
/** Decodes this vector as a string using the US-ASCII charset.
1077
1114
* @group conversions
1078
1115
*/
1079
1116
final def decodeAscii : Either [CharacterCodingException , String ] =
1080
1117
decodeString(Charset .forName(" US-ASCII" ))
1081
1118
1119
+ /** Like [[decodeAscii ]] but does not fail on bad input. */
1120
+ final def decodeAsciiLenient : String =
1121
+ decodeStringLenient()(Charset .forName(" US-ASCII" ))
1122
+
1082
1123
final def not : ByteVector = mapS(new F1B { def apply (b : Byte ) = (~ b).toByte })
1083
1124
1084
1125
final def or (other : ByteVector ): ByteVector =
@@ -2298,4 +2339,185 @@ object ByteVector extends ByteVectorCompanionCrossPlatform {
2298
2339
}
2299
2340
}
2300
2341
}
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
+ }
2301
2523
}
0 commit comments