File: nh.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2024 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 Single-file source-code for nh: this version has no http(s) support. Even
  27 the unit-tests from the original nh are omitted.
  28 
  29 To compile a smaller-sized command-line app, you can use the `go` command as
  30 follows:
  31 
  32 go build -ldflags "-s -w" -trimpath nh.go
  33 */
  34 
  35 package main
  36 
  37 import (
  38     "bufio"
  39     "bytes"
  40     "flag"
  41     "fmt"
  42     "io"
  43     "math"
  44     "os"
  45     "strconv"
  46     "strings"
  47 )
  48 
  49 const usage = `
  50 nh [options...] [filenames...]
  51 
  52 
  53 Nice Hexadecimal is a simple hexadecimal viewer to easily inspect bytes
  54 from files/data.
  55 
  56 Each line shows the starting offset for the bytes shown, 16 of the bytes
  57 themselves in base-16 notation, and any ASCII codes when the byte values
  58 are in the typical ASCII range. The offsets shown are base-10.
  59 
  60 The base-16 codes are color-coded, with most bytes shown in gray, while
  61 all-1 and all-0 bytes are shown in orange and blue respectively.
  62 
  63 All-0 bytes are the commonest kind in most binary file types and, along
  64 with all-1 bytes are also a special case worth noticing when exploring
  65 binary data, so it makes sense for them to stand out right away.
  66 `
  67 
  68 func main() {
  69     err := run(parseFlags(usage[1:]))
  70     if err != nil {
  71         os.Stderr.WriteString(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74     }
  75 }
  76 
  77 func run(cfg config) error {
  78     // f, _ := os.Create(`nh.prof`)
  79     // defer f.Close()
  80     // pprof.StartCPUProfile(f)
  81     // defer pprof.StopCPUProfile()
  82 
  83     w := bufio.NewWriterSize(os.Stdout, 32*1024)
  84     defer w.Flush()
  85 
  86     // with no filenames given, handle stdin and quit
  87     if len(cfg.Filenames) == 0 {
  88         return handle(w, os.Stdin, `<stdin>`, -1, cfg)
  89     }
  90 
  91     // show all files given
  92     for i, fname := range cfg.Filenames {
  93         if i > 0 {
  94             w.WriteString("\n")
  95             w.WriteString("\n")
  96         }
  97 
  98         err := handleFile(w, fname, cfg)
  99         if err != nil {
 100             return err
 101         }
 102     }
 103 
 104     return nil
 105 }
 106 
 107 // handleFile is like handleReader, except it also shows file-related info
 108 func handleFile(w *bufio.Writer, fname string, cfg config) error {
 109     f, err := os.Open(fname)
 110     if err != nil {
 111         return err
 112     }
 113     defer f.Close()
 114 
 115     stat, err := f.Stat()
 116     if err != nil {
 117         return handle(w, f, fname, -1, cfg)
 118     }
 119 
 120     fsize := int(stat.Size())
 121     return handle(w, f, fname, fsize, cfg)
 122 }
 123 
 124 // handle shows some messages related to the input and the cmd-line options
 125 // used, and then follows them by the hexadecimal byte-view
 126 func handle(w *bufio.Writer, r io.Reader, name string, size int, cfg config) error {
 127     skip(r, cfg.Skip)
 128     if cfg.MaxBytes > 0 {
 129         r = io.LimitReader(r, int64(cfg.MaxBytes))
 130     }
 131 
 132     // finish config setup based on the filesize, if a valid one was given
 133     if cfg.OffsetCounterWidth < 1 {
 134         if size < 1 {
 135             cfg.OffsetCounterWidth = defaultOffsetCounterWidth
 136         } else {
 137             w := math.Log10(float64(size))
 138             w = math.Max(math.Ceil(w), 1)
 139             cfg.OffsetCounterWidth = uint(w)
 140         }
 141     }
 142 
 143     switch cfg.To {
 144     case plainOutput:
 145         writeMetaPlain(w, name, size, cfg)
 146         // when done, emit a new line in case only part of the last line is
 147         // shown, which means no newline was emitted for it
 148         defer w.WriteString("\n")
 149         return render(w, r, cfg, writeBufferPlain)
 150 
 151     case ansiOutput:
 152         writeMetaANSI(w, name, size, cfg)
 153         // when done, emit a new line in case only part of the last line is
 154         // shown, which means no newline was emitted for it
 155         defer w.WriteString("\x1b[0m\n")
 156         return render(w, r, cfg, writeBufferANSI)
 157 
 158     default:
 159         const fs = `unsupported output format %q`
 160         return fmt.Errorf(fs, cfg.To)
 161     }
 162 }
 163 
 164 // skip ignores n bytes from the reader given
 165 func skip(r io.Reader, n int) {
 166     if n < 1 {
 167         return
 168     }
 169 
 170     // use func Seek for input files, except for stdin, which you can't seek
 171     if f, ok := r.(*os.File); ok && r != os.Stdin {
 172         f.Seek(int64(n), io.SeekCurrent)
 173         return
 174     }
 175     io.CopyN(io.Discard, r, int64(n))
 176 }
 177 
 178 // renderer is the type for the hex-view render funcs
 179 type renderer func(rc rendererConfig, first, second []byte) error
 180 
 181 // render reads all input and shows the hexadecimal byte-view for the input
 182 // data via the rendering callback given
 183 func render(w *bufio.Writer, r io.Reader, cfg config, fn renderer) error {
 184     if cfg.PerLine < 1 {
 185         cfg.PerLine = 16
 186     }
 187 
 188     rc := rendererConfig{
 189         out:     w,
 190         offset:  uint(cfg.Skip),
 191         chunks:  0,
 192         perLine: uint(cfg.PerLine),
 193         ruler:   cfg.Ruler,
 194 
 195         offsetWidth: cfg.OffsetCounterWidth,
 196         showOffsets: cfg.ShowOffsets,
 197         showASCII:   cfg.ShowASCII,
 198     }
 199 
 200     // calling func Read directly can sometimes result in chunks shorter
 201     // than the max chunk-size, even when there are plenty of bytes yet
 202     // to read; to avoid that, use a buffered-reader to explicitly fill
 203     // a slice instead
 204     br := bufio.NewReader(r)
 205 
 206     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 207     cur := make([]byte, 0, cfg.PerLine)
 208     ahead := make([]byte, 0, cfg.PerLine)
 209 
 210     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 211     // so to speak
 212     cur, err := fillChunk(cur[:0], cfg.PerLine, br)
 213     if len(cur) == 0 {
 214         if err == io.EOF {
 215             err = nil
 216         }
 217         return err
 218     }
 219 
 220     for {
 221         ahead, err := fillChunk(ahead[:0], cfg.PerLine, br)
 222         if err != nil && err != io.EOF {
 223             return err
 224         }
 225 
 226         if len(ahead) == 0 {
 227             // done, maybe except for an extra line of output
 228             break
 229         }
 230 
 231         // show the byte-chunk on its own output line
 232         err = fn(rc, cur, ahead)
 233         if err != nil {
 234             // probably a pipe was closed
 235             return nil
 236         }
 237 
 238         rc.chunks++
 239         rc.offset += uint(len(cur))
 240         cur = cur[:copy(cur, ahead)]
 241     }
 242 
 243     // don't forget the last output line
 244     if rc.chunks > 0 && len(cur) > 0 {
 245         return fn(rc, cur, nil)
 246     }
 247     return nil
 248 }
 249 
 250 // fillChunk tries to read the number of bytes given, appending them to the
 251 // byte-slice given; this func returns an EOF error only when no bytes are
 252 // read, which somewhat simplifies error-handling for the func caller
 253 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 254     // read buffered-bytes up to the max chunk-size
 255     for i := 0; i < n; i++ {
 256         b, err := br.ReadByte()
 257         if err == nil {
 258             chunk = append(chunk, b)
 259             continue
 260         }
 261 
 262         if err == io.EOF && i > 0 {
 263             return chunk, nil
 264         }
 265         return chunk, err
 266     }
 267 
 268     // got the full byte-count asked for
 269     return chunk, nil
 270 }
 271 
 272 const (
 273     usageMaxBytes   = `limit input up to n bytes; negative to disable`
 274     usagePerLine    = `how many bytes to show on each line`
 275     usageSkip       = `how many leading bytes to skip/ignore`
 276     usageTitle      = `use this to show a title/description`
 277     usageTo         = `the output format to use (plain or ansi)`
 278     usagePlain      = `show plain-text output, as opposed to ansi-styled output`
 279     usageShowOffset = `start lines with the offset of the 1st byte shown on each`
 280     usageShowASCII  = `repeat all ASCII strings on the side, so they're searcheable`
 281 )
 282 
 283 const defaultOffsetCounterWidth = 8
 284 
 285 const (
 286     plainOutput = `plain`
 287     ansiOutput  = `ansi`
 288 )
 289 
 290 // config is the parsed cmd-line options given to the app
 291 type config struct {
 292     // MaxBytes limits how many bytes are shown; a negative value means no limit
 293     MaxBytes int
 294 
 295     // PerLine is how many bytes are shown per output line
 296     PerLine int
 297 
 298     // Skip is how many leading bytes to skip/ignore
 299     Skip int
 300 
 301     // OffsetCounterWidth is the max string-width; not exposed as a cmd-line option
 302     OffsetCounterWidth uint
 303 
 304     // Title is an optional title preceding the output proper
 305     Title string
 306 
 307     // To is the output format
 308     To string
 309 
 310     // Filenames is the list of input filenames
 311     Filenames []string
 312 
 313     // Ruler is a prerendered ruler to emit every few output lines
 314     Ruler []byte
 315 
 316     // ShowOffsets starts lines with the offset of the 1st byte shown on each
 317     ShowOffsets bool
 318 
 319     // ShowASCII shows a side-panel with searcheable ASCII-runs
 320     ShowASCII bool
 321 }
 322 
 323 // parseFlags is the constructor for type config
 324 func parseFlags(usage string) config {
 325     flag.Usage = func() {
 326         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
 327         flag.PrintDefaults()
 328     }
 329 
 330     cfg := config{
 331         MaxBytes:           -1,
 332         PerLine:            16,
 333         OffsetCounterWidth: 0,
 334         To:                 ansiOutput,
 335         ShowOffsets:        true,
 336         ShowASCII:          true,
 337     }
 338 
 339     plain := false
 340     flag.IntVar(&cfg.MaxBytes, `max`, cfg.MaxBytes, usageMaxBytes)
 341     flag.IntVar(&cfg.PerLine, `width`, cfg.PerLine, usagePerLine)
 342     flag.IntVar(&cfg.Skip, `skip`, cfg.Skip, usageSkip)
 343     flag.StringVar(&cfg.Title, `title`, cfg.Title, usageTitle)
 344     flag.StringVar(&cfg.To, `to`, cfg.To, usageTo)
 345     flag.BoolVar(&cfg.ShowOffsets, `n`, cfg.ShowOffsets, usageShowOffset)
 346     flag.BoolVar(&cfg.ShowASCII, `ascii`, cfg.ShowASCII, usageShowASCII)
 347     flag.BoolVar(&plain, `p`, plain, "alias for option `plain`")
 348     flag.BoolVar(&plain, `plain`, plain, usagePlain)
 349     flag.Parse()
 350 
 351     if plain {
 352         cfg.To = plainOutput
 353     }
 354 
 355     // normalize values for option -to
 356     switch cfg.To {
 357     case `text`, `plaintext`, `plain-text`:
 358         cfg.To = plainOutput
 359     }
 360 
 361     cfg.Ruler = makeRuler(cfg.PerLine)
 362     cfg.Filenames = flag.Args()
 363     return cfg
 364 }
 365 
 366 // makeRuler prerenders a ruler-line, used to make the output lines breathe
 367 func makeRuler(numitems int) []byte {
 368     if n := numitems / 4; n > 0 {
 369         var pat = []byte(`           ·`)
 370         return bytes.Repeat(pat, n)
 371     }
 372     return nil
 373 }
 374 
 375 // rendererConfig groups several arguments given to any of the rendering funcs
 376 type rendererConfig struct {
 377     // out is writer to send all output to
 378     out *bufio.Writer
 379 
 380     // offset is the byte-offset of the first byte shown on the current output
 381     // line: if shown at all, it's shown at the start the line
 382     offset uint
 383 
 384     // chunks is the 0-based counter for byte-chunks/lines shown so far, which
 385     // indirectly keeps track of when it's time to show a `breather` line
 386     chunks uint
 387 
 388     // ruler is the `ruler` content to show on `breather` lines
 389     ruler []byte
 390 
 391     // perLine is how many hex-encoded bytes are shown per line
 392     perLine uint
 393 
 394     // offsetWidth is the max string-width for the byte-offsets shown at the
 395     // start of output lines, and determines those values' left-padding
 396     offsetWidth uint
 397 
 398     // showOffsets determines whether byte-offsets are shown at all
 399     showOffsets bool
 400 
 401     // showASCII determines whether the ASCII-panels are shown at all
 402     showASCII bool
 403 }
 404 
 405 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
 406 // handles negatives, even though this app only uses it with non-negatives.
 407 func loopThousandsGroups(n int, fn func(i, n int)) {
 408     // 0 doesn't have a log10
 409     if n == 0 {
 410         fn(0, 0)
 411         return
 412     }
 413 
 414     sign := +1
 415     if n < 0 {
 416         n = -n
 417         sign = -1
 418     }
 419 
 420     intLog1000 := int(math.Log10(float64(n)) / 3)
 421     remBase := int(math.Pow10(3 * intLog1000))
 422 
 423     for i := 0; remBase > 0; i++ {
 424         group := (1000 * n) / remBase / 1000
 425         fn(i, sign*group)
 426         // if original number was negative, ensure only first
 427         // group gives a negative input to the callback
 428         sign = +1
 429 
 430         n %= remBase
 431         remBase /= 1000
 432     }
 433 }
 434 
 435 // sprintCommas turns the non-negative number given into a readable string,
 436 // where digits are grouped-separated by commas
 437 func sprintCommas(n int) string {
 438     var sb strings.Builder
 439     loopThousandsGroups(n, func(i, n int) {
 440         if i == 0 {
 441             var buf [4]byte
 442             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
 443             return
 444         }
 445         sb.WriteByte(',')
 446         writePad0Sub1000Counter(&sb, uint(n))
 447     })
 448     return sb.String()
 449 }
 450 
 451 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
 452 func writePad0Sub1000Counter(w io.Writer, n uint) {
 453     // precondition is 0...999
 454     if n > 999 {
 455         w.Write([]byte(`???`))
 456         return
 457     }
 458 
 459     var buf [3]byte
 460     buf[0] = byte(n/100) + '0'
 461     n %= 100
 462     buf[1] = byte(n/10) + '0'
 463     buf[2] = byte(n%10) + '0'
 464     w.Write(buf[:])
 465 }
 466 
 467 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
 468 // matters because it's called for every byte of input which isn't
 469 // all 0s or all 1s
 470 func writeHex(w *bufio.Writer, b byte) {
 471     const hexDigits = `0123456789abcdef`
 472     w.WriteByte(hexDigits[b>>4])
 473     w.WriteByte(hexDigits[b&0x0f])
 474 }
 475 
 476 // padding is the padding/spacing emitted across each output line, except for
 477 // the breather/ruler lines
 478 const padding = `  `
 479 
 480 // writeMetaPlain shows metadata right before the plain-text hex byte-view
 481 func writeMetaPlain(w *bufio.Writer, fname string, fsize int, cfg config) {
 482     if cfg.Title != `` {
 483         w.WriteString(cfg.Title)
 484         w.WriteString("\n")
 485         w.WriteString("\n")
 486     }
 487 
 488     if fsize < 0 {
 489         fmt.Fprintf(w, "%s\n", fname)
 490     } else {
 491         const fs = "%s  (%s bytes)\n"
 492         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
 493     }
 494 
 495     if cfg.Skip > 0 {
 496         const fs = "   skipping first %s bytes\n"
 497         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
 498     }
 499     if cfg.MaxBytes > 0 {
 500         const fs = "   showing only up to %s bytes\n"
 501         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
 502     }
 503     w.WriteString("\n")
 504 }
 505 
 506 // writeBufferPlain shows the hex byte-view withOUT using ANSI colors/styles
 507 func writeBufferPlain(rc rendererConfig, first, second []byte) error {
 508     // show a ruler every few lines to make eye-scanning easier
 509     if rc.chunks%5 == 0 && rc.chunks > 0 {
 510         rc.out.WriteByte('\n')
 511     }
 512 
 513     return writeLinePlain(rc, first, second)
 514 }
 515 
 516 func writeLinePlain(rc rendererConfig, first, second []byte) error {
 517     w := rc.out
 518 
 519     // start each line with the byte-offset for the 1st item shown on it
 520     if rc.showOffsets {
 521         writePlainCounter(w, int(rc.offsetWidth), rc.offset)
 522         w.WriteByte(' ')
 523     } else {
 524         w.WriteString(padding)
 525     }
 526 
 527     for _, b := range first {
 528         // fmt.Fprintf(w, ` %02x`, b)
 529         //
 530         // the commented part above was a performance bottleneck, since
 531         // the slow/generic fmt.Fprintf was called for each input byte
 532         w.WriteByte(' ')
 533         writeHex(w, b)
 534     }
 535 
 536     if rc.showASCII {
 537         writePlainASCII(w, first, second, int(rc.perLine))
 538     }
 539 
 540     return w.WriteByte('\n')
 541 }
 542 
 543 // writePlainCounter just emits a left-padded number
 544 func writePlainCounter(w *bufio.Writer, width int, n uint) {
 545     var buf [32]byte
 546     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 547     writeSpaces(w, width-len(str))
 548     w.Write(str)
 549 }
 550 
 551 // writeRulerPlain emits a breather line using a ruler-like pattern of spaces
 552 // and dots, to guide the eye across the main output lines
 553 // func writeRulerPlain(w *bufio.Writer, indent int, offset int, numitems int) {
 554 //  writeSpaces(w, indent)
 555 //  for i := 0; i < numitems-1; i++ {
 556 //      if (i+offset+1)%5 == 0 {
 557 //          w.WriteString(`   `)
 558 //      } else {
 559 //          w.WriteString(`  ·`)
 560 //      }
 561 //  }
 562 // }
 563 
 564 // writeSpaces bulk-emits the number of spaces given
 565 func writeSpaces(w *bufio.Writer, n int) {
 566     const spaces = `                                `
 567     for ; n > len(spaces); n -= len(spaces) {
 568         w.WriteString(spaces)
 569     }
 570     if n > 0 {
 571         w.WriteString(spaces[:n])
 572     }
 573 }
 574 
 575 // writePlainASCII emits the side-panel showing all ASCII runs for each line
 576 func writePlainASCII(w *bufio.Writer, first, second []byte, perline int) {
 577     // prev keeps track of the previous byte, so spaces are added
 578     // when bytes change from non-visible-ASCII to visible-ASCII
 579     prev := byte(0)
 580 
 581     spaces := 3*(perline-len(first)) + len(padding)
 582 
 583     for _, b := range first {
 584         if 32 < b && b < 127 {
 585             if !(32 < prev && prev < 127) {
 586                 writeSpaces(w, spaces)
 587                 spaces = 1
 588             }
 589             w.WriteByte(b)
 590         }
 591         prev = b
 592     }
 593 
 594     for _, b := range second {
 595         if 32 < b && b < 127 {
 596             if !(32 < prev && prev < 127) {
 597                 writeSpaces(w, spaces)
 598                 spaces = 1
 599             }
 600             w.WriteByte(b)
 601         }
 602         prev = b
 603     }
 604 }
 605 
 606 // styledHexBytes is a super-fast direct byte-to-result lookup table, and was
 607 // autogenerated by running the command
 608 //
 609 // seq 0 255 | ./hex-styles.awk
 610 var styledHexBytes = [256]string{
 611     "\x1b[38;5;111m00 ", "\x1b[38;5;246m01 ",
 612     "\x1b[38;5;246m02 ", "\x1b[38;5;246m03 ",
 613     "\x1b[38;5;246m04 ", "\x1b[38;5;246m05 ",
 614     "\x1b[38;5;246m06 ", "\x1b[38;5;246m07 ",
 615     "\x1b[38;5;246m08 ", "\x1b[38;5;246m09 ",
 616     "\x1b[38;5;246m0a ", "\x1b[38;5;246m0b ",
 617     "\x1b[38;5;246m0c ", "\x1b[38;5;246m0d ",
 618     "\x1b[38;5;246m0e ", "\x1b[38;5;246m0f ",
 619     "\x1b[38;5;246m10 ", "\x1b[38;5;246m11 ",
 620     "\x1b[38;5;246m12 ", "\x1b[38;5;246m13 ",
 621     "\x1b[38;5;246m14 ", "\x1b[38;5;246m15 ",
 622     "\x1b[38;5;246m16 ", "\x1b[38;5;246m17 ",
 623     "\x1b[38;5;246m18 ", "\x1b[38;5;246m19 ",
 624     "\x1b[38;5;246m1a ", "\x1b[38;5;246m1b ",
 625     "\x1b[38;5;246m1c ", "\x1b[38;5;246m1d ",
 626     "\x1b[38;5;246m1e ", "\x1b[38;5;246m1f ",
 627     "\x1b[38;5;72m20\x1b[38;5;239m ", "\x1b[38;5;72m21\x1b[38;5;239m!",
 628     "\x1b[38;5;72m22\x1b[38;5;239m\"", "\x1b[38;5;72m23\x1b[38;5;239m#",
 629     "\x1b[38;5;72m24\x1b[38;5;239m$", "\x1b[38;5;72m25\x1b[38;5;239m%",
 630     "\x1b[38;5;72m26\x1b[38;5;239m&", "\x1b[38;5;72m27\x1b[38;5;239m'",
 631     "\x1b[38;5;72m28\x1b[38;5;239m(", "\x1b[38;5;72m29\x1b[38;5;239m)",
 632     "\x1b[38;5;72m2a\x1b[38;5;239m*", "\x1b[38;5;72m2b\x1b[38;5;239m+",
 633     "\x1b[38;5;72m2c\x1b[38;5;239m,", "\x1b[38;5;72m2d\x1b[38;5;239m-",
 634     "\x1b[38;5;72m2e\x1b[38;5;239m.", "\x1b[38;5;72m2f\x1b[38;5;239m/",
 635     "\x1b[38;5;72m30\x1b[38;5;239m0", "\x1b[38;5;72m31\x1b[38;5;239m1",
 636     "\x1b[38;5;72m32\x1b[38;5;239m2", "\x1b[38;5;72m33\x1b[38;5;239m3",
 637     "\x1b[38;5;72m34\x1b[38;5;239m4", "\x1b[38;5;72m35\x1b[38;5;239m5",
 638     "\x1b[38;5;72m36\x1b[38;5;239m6", "\x1b[38;5;72m37\x1b[38;5;239m7",
 639     "\x1b[38;5;72m38\x1b[38;5;239m8", "\x1b[38;5;72m39\x1b[38;5;239m9",
 640     "\x1b[38;5;72m3a\x1b[38;5;239m:", "\x1b[38;5;72m3b\x1b[38;5;239m;",
 641     "\x1b[38;5;72m3c\x1b[38;5;239m<", "\x1b[38;5;72m3d\x1b[38;5;239m=",
 642     "\x1b[38;5;72m3e\x1b[38;5;239m>", "\x1b[38;5;72m3f\x1b[38;5;239m?",
 643     "\x1b[38;5;72m40\x1b[38;5;239m@", "\x1b[38;5;72m41\x1b[38;5;239mA",
 644     "\x1b[38;5;72m42\x1b[38;5;239mB", "\x1b[38;5;72m43\x1b[38;5;239mC",
 645     "\x1b[38;5;72m44\x1b[38;5;239mD", "\x1b[38;5;72m45\x1b[38;5;239mE",
 646     "\x1b[38;5;72m46\x1b[38;5;239mF", "\x1b[38;5;72m47\x1b[38;5;239mG",
 647     "\x1b[38;5;72m48\x1b[38;5;239mH", "\x1b[38;5;72m49\x1b[38;5;239mI",
 648     "\x1b[38;5;72m4a\x1b[38;5;239mJ", "\x1b[38;5;72m4b\x1b[38;5;239mK",
 649     "\x1b[38;5;72m4c\x1b[38;5;239mL", "\x1b[38;5;72m4d\x1b[38;5;239mM",
 650     "\x1b[38;5;72m4e\x1b[38;5;239mN", "\x1b[38;5;72m4f\x1b[38;5;239mO",
 651     "\x1b[38;5;72m50\x1b[38;5;239mP", "\x1b[38;5;72m51\x1b[38;5;239mQ",
 652     "\x1b[38;5;72m52\x1b[38;5;239mR", "\x1b[38;5;72m53\x1b[38;5;239mS",
 653     "\x1b[38;5;72m54\x1b[38;5;239mT", "\x1b[38;5;72m55\x1b[38;5;239mU",
 654     "\x1b[38;5;72m56\x1b[38;5;239mV", "\x1b[38;5;72m57\x1b[38;5;239mW",
 655     "\x1b[38;5;72m58\x1b[38;5;239mX", "\x1b[38;5;72m59\x1b[38;5;239mY",
 656     "\x1b[38;5;72m5a\x1b[38;5;239mZ", "\x1b[38;5;72m5b\x1b[38;5;239m[",
 657     "\x1b[38;5;72m5c\x1b[38;5;239m\\", "\x1b[38;5;72m5d\x1b[38;5;239m]",
 658     "\x1b[38;5;72m5e\x1b[38;5;239m^", "\x1b[38;5;72m5f\x1b[38;5;239m_",
 659     "\x1b[38;5;72m60\x1b[38;5;239m`", "\x1b[38;5;72m61\x1b[38;5;239ma",
 660     "\x1b[38;5;72m62\x1b[38;5;239mb", "\x1b[38;5;72m63\x1b[38;5;239mc",
 661     "\x1b[38;5;72m64\x1b[38;5;239md", "\x1b[38;5;72m65\x1b[38;5;239me",
 662     "\x1b[38;5;72m66\x1b[38;5;239mf", "\x1b[38;5;72m67\x1b[38;5;239mg",
 663     "\x1b[38;5;72m68\x1b[38;5;239mh", "\x1b[38;5;72m69\x1b[38;5;239mi",
 664     "\x1b[38;5;72m6a\x1b[38;5;239mj", "\x1b[38;5;72m6b\x1b[38;5;239mk",
 665     "\x1b[38;5;72m6c\x1b[38;5;239ml", "\x1b[38;5;72m6d\x1b[38;5;239mm",
 666     "\x1b[38;5;72m6e\x1b[38;5;239mn", "\x1b[38;5;72m6f\x1b[38;5;239mo",
 667     "\x1b[38;5;72m70\x1b[38;5;239mp", "\x1b[38;5;72m71\x1b[38;5;239mq",
 668     "\x1b[38;5;72m72\x1b[38;5;239mr", "\x1b[38;5;72m73\x1b[38;5;239ms",
 669     "\x1b[38;5;72m74\x1b[38;5;239mt", "\x1b[38;5;72m75\x1b[38;5;239mu",
 670     "\x1b[38;5;72m76\x1b[38;5;239mv", "\x1b[38;5;72m77\x1b[38;5;239mw",
 671     "\x1b[38;5;72m78\x1b[38;5;239mx", "\x1b[38;5;72m79\x1b[38;5;239my",
 672     "\x1b[38;5;72m7a\x1b[38;5;239mz", "\x1b[38;5;72m7b\x1b[38;5;239m{",
 673     "\x1b[38;5;72m7c\x1b[38;5;239m|", "\x1b[38;5;72m7d\x1b[38;5;239m}",
 674     "\x1b[38;5;72m7e\x1b[38;5;239m~", "\x1b[38;5;246m7f ",
 675     "\x1b[38;5;246m80 ", "\x1b[38;5;246m81 ",
 676     "\x1b[38;5;246m82 ", "\x1b[38;5;246m83 ",
 677     "\x1b[38;5;246m84 ", "\x1b[38;5;246m85 ",
 678     "\x1b[38;5;246m86 ", "\x1b[38;5;246m87 ",
 679     "\x1b[38;5;246m88 ", "\x1b[38;5;246m89 ",
 680     "\x1b[38;5;246m8a ", "\x1b[38;5;246m8b ",
 681     "\x1b[38;5;246m8c ", "\x1b[38;5;246m8d ",
 682     "\x1b[38;5;246m8e ", "\x1b[38;5;246m8f ",
 683     "\x1b[38;5;246m90 ", "\x1b[38;5;246m91 ",
 684     "\x1b[38;5;246m92 ", "\x1b[38;5;246m93 ",
 685     "\x1b[38;5;246m94 ", "\x1b[38;5;246m95 ",
 686     "\x1b[38;5;246m96 ", "\x1b[38;5;246m97 ",
 687     "\x1b[38;5;246m98 ", "\x1b[38;5;246m99 ",
 688     "\x1b[38;5;246m9a ", "\x1b[38;5;246m9b ",
 689     "\x1b[38;5;246m9c ", "\x1b[38;5;246m9d ",
 690     "\x1b[38;5;246m9e ", "\x1b[38;5;246m9f ",
 691     "\x1b[38;5;246ma0 ", "\x1b[38;5;246ma1 ",
 692     "\x1b[38;5;246ma2 ", "\x1b[38;5;246ma3 ",
 693     "\x1b[38;5;246ma4 ", "\x1b[38;5;246ma5 ",
 694     "\x1b[38;5;246ma6 ", "\x1b[38;5;246ma7 ",
 695     "\x1b[38;5;246ma8 ", "\x1b[38;5;246ma9 ",
 696     "\x1b[38;5;246maa ", "\x1b[38;5;246mab ",
 697     "\x1b[38;5;246mac ", "\x1b[38;5;246mad ",
 698     "\x1b[38;5;246mae ", "\x1b[38;5;246maf ",
 699     "\x1b[38;5;246mb0 ", "\x1b[38;5;246mb1 ",
 700     "\x1b[38;5;246mb2 ", "\x1b[38;5;246mb3 ",
 701     "\x1b[38;5;246mb4 ", "\x1b[38;5;246mb5 ",
 702     "\x1b[38;5;246mb6 ", "\x1b[38;5;246mb7 ",
 703     "\x1b[38;5;246mb8 ", "\x1b[38;5;246mb9 ",
 704     "\x1b[38;5;246mba ", "\x1b[38;5;246mbb ",
 705     "\x1b[38;5;246mbc ", "\x1b[38;5;246mbd ",
 706     "\x1b[38;5;246mbe ", "\x1b[38;5;246mbf ",
 707     "\x1b[38;5;246mc0 ", "\x1b[38;5;246mc1 ",
 708     "\x1b[38;5;246mc2 ", "\x1b[38;5;246mc3 ",
 709     "\x1b[38;5;246mc4 ", "\x1b[38;5;246mc5 ",
 710     "\x1b[38;5;246mc6 ", "\x1b[38;5;246mc7 ",
 711     "\x1b[38;5;246mc8 ", "\x1b[38;5;246mc9 ",
 712     "\x1b[38;5;246mca ", "\x1b[38;5;246mcb ",
 713     "\x1b[38;5;246mcc ", "\x1b[38;5;246mcd ",
 714     "\x1b[38;5;246mce ", "\x1b[38;5;246mcf ",
 715     "\x1b[38;5;246md0 ", "\x1b[38;5;246md1 ",
 716     "\x1b[38;5;246md2 ", "\x1b[38;5;246md3 ",
 717     "\x1b[38;5;246md4 ", "\x1b[38;5;246md5 ",
 718     "\x1b[38;5;246md6 ", "\x1b[38;5;246md7 ",
 719     "\x1b[38;5;246md8 ", "\x1b[38;5;246md9 ",
 720     "\x1b[38;5;246mda ", "\x1b[38;5;246mdb ",
 721     "\x1b[38;5;246mdc ", "\x1b[38;5;246mdd ",
 722     "\x1b[38;5;246mde ", "\x1b[38;5;246mdf ",
 723     "\x1b[38;5;246me0 ", "\x1b[38;5;246me1 ",
 724     "\x1b[38;5;246me2 ", "\x1b[38;5;246me3 ",
 725     "\x1b[38;5;246me4 ", "\x1b[38;5;246me5 ",
 726     "\x1b[38;5;246me6 ", "\x1b[38;5;246me7 ",
 727     "\x1b[38;5;246me8 ", "\x1b[38;5;246me9 ",
 728     "\x1b[38;5;246mea ", "\x1b[38;5;246meb ",
 729     "\x1b[38;5;246mec ", "\x1b[38;5;246med ",
 730     "\x1b[38;5;246mee ", "\x1b[38;5;246mef ",
 731     "\x1b[38;5;246mf0 ", "\x1b[38;5;246mf1 ",
 732     "\x1b[38;5;246mf2 ", "\x1b[38;5;246mf3 ",
 733     "\x1b[38;5;246mf4 ", "\x1b[38;5;246mf5 ",
 734     "\x1b[38;5;246mf6 ", "\x1b[38;5;246mf7 ",
 735     "\x1b[38;5;246mf8 ", "\x1b[38;5;246mf9 ",
 736     "\x1b[38;5;246mfa ", "\x1b[38;5;246mfb ",
 737     "\x1b[38;5;246mfc ", "\x1b[38;5;246mfd ",
 738     "\x1b[38;5;246mfe ", "\x1b[38;5;209mff ",
 739 }
 740 
 741 // hexSymbols is a direct lookup table combining 2 hex digits with either a
 742 // space or a displayable ASCII symbol matching the byte's own ASCII value;
 743 // this table was autogenerated by running the command
 744 //
 745 // seq 0 255 | ./hex-symbols.awk
 746 var hexSymbols = [256]string{
 747     `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
 748     `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
 749     `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
 750     `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
 751     `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
 752     `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
 753     `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
 754     `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
 755     `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
 756     `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
 757     `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
 758     `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
 759     "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
 760     `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
 761     `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
 762     `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
 763     `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
 764     `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
 765     `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
 766     `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
 767     `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
 768     `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
 769     `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
 770     `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
 771     `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
 772     `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
 773     `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
 774     `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
 775     `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
 776     `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
 777     `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
 778     `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
 779 }
 780 
 781 const (
 782     unknownStyle = 0
 783     zeroStyle    = 1
 784     otherStyle   = 2
 785     asciiStyle   = 3
 786     allOnStyle   = 4
 787 )
 788 
 789 // byteStyles turns bytes into one of several distinct visual types, which
 790 // allows quickly telling when ANSI styles codes are repetitive and when
 791 // they're actually needed
 792 var byteStyles = [256]int{
 793     zeroStyle, otherStyle, otherStyle, otherStyle,
 794     otherStyle, otherStyle, otherStyle, otherStyle,
 795     otherStyle, otherStyle, otherStyle, otherStyle,
 796     otherStyle, otherStyle, otherStyle, otherStyle,
 797     otherStyle, otherStyle, otherStyle, otherStyle,
 798     otherStyle, otherStyle, otherStyle, otherStyle,
 799     otherStyle, otherStyle, otherStyle, otherStyle,
 800     otherStyle, otherStyle, otherStyle, otherStyle,
 801     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 802     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 803     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 804     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 805     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 806     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 807     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 808     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 809     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 810     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 811     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 812     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 813     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 814     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 815     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 816     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 817     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 818     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 819     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 820     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 821     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 822     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 823     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 824     asciiStyle, asciiStyle, asciiStyle, otherStyle,
 825     otherStyle, otherStyle, otherStyle, otherStyle,
 826     otherStyle, otherStyle, otherStyle, otherStyle,
 827     otherStyle, otherStyle, otherStyle, otherStyle,
 828     otherStyle, otherStyle, otherStyle, otherStyle,
 829     otherStyle, otherStyle, otherStyle, otherStyle,
 830     otherStyle, otherStyle, otherStyle, otherStyle,
 831     otherStyle, otherStyle, otherStyle, otherStyle,
 832     otherStyle, otherStyle, otherStyle, otherStyle,
 833     otherStyle, otherStyle, otherStyle, otherStyle,
 834     otherStyle, otherStyle, otherStyle, otherStyle,
 835     otherStyle, otherStyle, otherStyle, otherStyle,
 836     otherStyle, otherStyle, otherStyle, otherStyle,
 837     otherStyle, otherStyle, otherStyle, otherStyle,
 838     otherStyle, otherStyle, otherStyle, otherStyle,
 839     otherStyle, otherStyle, otherStyle, otherStyle,
 840     otherStyle, otherStyle, otherStyle, otherStyle,
 841     otherStyle, otherStyle, otherStyle, otherStyle,
 842     otherStyle, otherStyle, otherStyle, otherStyle,
 843     otherStyle, otherStyle, otherStyle, otherStyle,
 844     otherStyle, otherStyle, otherStyle, otherStyle,
 845     otherStyle, otherStyle, otherStyle, otherStyle,
 846     otherStyle, otherStyle, otherStyle, otherStyle,
 847     otherStyle, otherStyle, otherStyle, otherStyle,
 848     otherStyle, otherStyle, otherStyle, otherStyle,
 849     otherStyle, otherStyle, otherStyle, otherStyle,
 850     otherStyle, otherStyle, otherStyle, otherStyle,
 851     otherStyle, otherStyle, otherStyle, otherStyle,
 852     otherStyle, otherStyle, otherStyle, otherStyle,
 853     otherStyle, otherStyle, otherStyle, otherStyle,
 854     otherStyle, otherStyle, otherStyle, otherStyle,
 855     otherStyle, otherStyle, otherStyle, otherStyle,
 856     otherStyle, otherStyle, otherStyle, allOnStyle,
 857 }
 858 
 859 // writeMetaANSI shows metadata right before the ANSI-styled hex byte-view
 860 func writeMetaANSI(w *bufio.Writer, fname string, fsize int, cfg config) {
 861     if cfg.Title != "" {
 862         fmt.Fprintf(w, "\x1b[4m%s\x1b[0m\n", cfg.Title)
 863         w.WriteString("\n")
 864     }
 865 
 866     if fsize < 0 {
 867         fmt.Fprintf(w, "%s\n", fname)
 868     } else {
 869         const fs = "%s  \x1b[38;5;248m(%s bytes)\x1b[0m\n"
 870         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
 871     }
 872 
 873     if cfg.Skip > 0 {
 874         const fs = "   \x1b[38;5;5mskipping first %s bytes\x1b[0m\n"
 875         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
 876     }
 877     if cfg.MaxBytes > 0 {
 878         const fs = "   \x1b[38;5;5mshowing only up to %s bytes\x1b[0m\n"
 879         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
 880     }
 881     w.WriteString("\n")
 882 }
 883 
 884 // writeBufferANSI shows the hex byte-view using ANSI colors/styles
 885 func writeBufferANSI(rc rendererConfig, first, second []byte) error {
 886     // show a ruler every few lines to make eye-scanning easier
 887     if rc.chunks%5 == 0 && rc.chunks > 0 {
 888         writeRulerANSI(rc)
 889     }
 890 
 891     return writeLineANSI(rc, first, second)
 892 }
 893 
 894 // writeRulerANSI emits an indented ANSI-styled line showing spaced-out dots,
 895 // so as to help eye-scan items on nearby output lines
 896 func writeRulerANSI(rc rendererConfig) {
 897     w := rc.out
 898     if len(rc.ruler) == 0 {
 899         w.WriteByte('\n')
 900         return
 901     }
 902 
 903     w.WriteString("\x1b[38;5;248m")
 904     indent := int(rc.offsetWidth) + len(padding)
 905     writeSpaces(w, indent)
 906     w.Write(rc.ruler)
 907     w.WriteString("\x1b[0m\n")
 908 }
 909 
 910 func writeLineANSI(rc rendererConfig, first, second []byte) error {
 911     w := rc.out
 912 
 913     // start each line with the byte-offset for the 1st item shown on it
 914     if rc.showOffsets {
 915         writeStyledCounter(w, int(rc.offsetWidth), rc.offset)
 916         w.WriteString(padding + "\x1b[48;5;254m")
 917     } else {
 918         w.WriteString(padding)
 919     }
 920 
 921     prevStyle := unknownStyle
 922     for _, b := range first {
 923         // using the slow/generic fmt.Fprintf is a performance bottleneck,
 924         // since it's called for each input byte
 925         // w.WriteString(styledHexBytes[b])
 926 
 927         // this more complicated way of emitting output avoids repeating
 928         // ANSI styles when dealing with bytes which aren't displayable
 929         // ASCII symbols, thus emitting fewer bytes when dealing with
 930         // general binary datasets; it makes no difference for plain-text
 931         // ASCII input
 932         style := byteStyles[b]
 933         if style != prevStyle {
 934             w.WriteString(styledHexBytes[b])
 935             if style == asciiStyle {
 936                 // styling displayable ASCII symbols uses multiple different
 937                 // styles each time it happens, always forcing ANSI-style
 938                 // updates
 939                 style = unknownStyle
 940             }
 941         } else {
 942             w.WriteString(hexSymbols[b])
 943         }
 944         prevStyle = style
 945     }
 946 
 947     w.WriteString("\x1b[0m")
 948     if rc.showASCII {
 949         writePlainASCII(w, first, second, int(rc.perLine))
 950     }
 951 
 952     return w.WriteByte('\n')
 953 }
 954 
 955 func writeStyledCounter(w *bufio.Writer, width int, n uint) {
 956     var buf [32]byte
 957     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 958 
 959     // left-pad the final result with leading spaces
 960     writeSpaces(w, width-len(str))
 961 
 962     var style bool
 963     // emit leading part with 1 or 2 digits unstyled, ensuring the
 964     // rest or the rendered number's string is a multiple of 3 long
 965     if rem := len(str) % 3; rem != 0 {
 966         w.Write(str[:rem])
 967         str = str[rem:]
 968         // next digit-group needs some styling
 969         style = true
 970     } else {
 971         style = false
 972     }
 973 
 974     // alternate between styled/unstyled 3-digit groups
 975     for len(str) > 0 {
 976         if !style {
 977             w.Write(str[:3])
 978         } else {
 979             w.WriteString("\x1b[38;5;248m")
 980             w.Write(str[:3])
 981             w.WriteString("\x1b[0m")
 982         }
 983 
 984         style = !style
 985         str = str[3:]
 986     }
 987 }