hexdump.gno
3.12 Kb · 113 lines
1// Package hexdump renders bytes in the classic xxd/hexdump -C layout, as a
2// pure, reusable package.
3//
4// The format is the familiar one: an 8-digit hex offset, then 16 bytes as hex
5// in two 8-byte groups separated by an extra space, then the same bytes as
6// ASCII between pipes with non-printables shown as ".". Short final lines are
7// padded so the ASCII column stays aligned — the whole point of the layout.
8//
9// Useful on chain for exactly the reason it is useful off it: when a value is
10// not what you expected, the bytes tell you why. Encoding bugs, stray NULs,
11// UTF-8 that is not what it claims, trailing whitespace — all invisible in a
12// rendered string and obvious in a dump.
13//
14// A live demo of this package is at
15// [r/moul/x/daily/hexdumpdemo](/r/moul/x/daily/hexdumpdemo/v0).
16package hexdump
17
18import "strings"
19
20// MaxBytes bounds a dump so gas stays predictable. Input beyond this is
21// truncated and reported by Dump's second return value.
22const MaxBytes = 4096
23
24// BytesPerLine is the classic 16.
25const BytesPerLine = 16
26
27const hexDigits = "0123456789abcdef"
28
29// Dump renders b in the xxd -C layout. It returns the dump and the number of
30// bytes rendered, which is less than len(b) when the input exceeds MaxBytes.
31func Dump(b []byte) (string, int) {
32 n := len(b)
33 if n > MaxBytes {
34 n = MaxBytes
35 }
36 if n == 0 {
37 return "", 0
38 }
39
40 var sb strings.Builder
41 for off := 0; off < n; off += BytesPerLine {
42 end := off + BytesPerLine
43 if end > n {
44 end = n
45 }
46 sb.WriteString(Line(off, b[off:end]))
47 sb.WriteString("\n")
48 }
49 return sb.String(), n
50}
51
52// DumpString is Dump over a string's bytes.
53func DumpString(s string) (string, int) { return Dump([]byte(s)) }
54
55// Line renders one line: offset, hex columns, ASCII gutter. chunk must hold at
56// most BytesPerLine bytes; a shorter chunk is padded so columns stay aligned.
57func Line(offset int, chunk []byte) string {
58 var sb strings.Builder
59 sb.WriteString(Offset(offset))
60 sb.WriteString(" ")
61
62 for i := 0; i < BytesPerLine; i++ {
63 if i == BytesPerLine/2 {
64 sb.WriteString(" ") // the classic gap between the two 8-byte groups
65 }
66 if i < len(chunk) {
67 sb.WriteString(Hex(chunk[i]))
68 } else {
69 sb.WriteString(" ") // pad so the ASCII gutter never shifts
70 }
71 sb.WriteString(" ")
72 }
73
74 sb.WriteString(" |")
75 sb.WriteString(ASCII(chunk))
76 sb.WriteString("|")
77 return sb.String()
78}
79
80// Offset formats an 8-digit lowercase hex offset.
81func Offset(n int) string {
82 if n < 0 {
83 n = 0
84 }
85 out := make([]byte, 8)
86 for i := 7; i >= 0; i-- {
87 out[i] = hexDigits[n&0xf]
88 n >>= 4
89 }
90 return string(out)
91}
92
93// Hex formats one byte as two lowercase hex digits.
94func Hex(b byte) string {
95 return string([]byte{hexDigits[b>>4], hexDigits[b&0xf]})
96}
97
98// ASCII renders the printable-ASCII view of chunk: bytes outside 0x20..0x7e
99// become ".". It is NOT padded — Line handles alignment.
100func ASCII(chunk []byte) string {
101 out := make([]byte, len(chunk))
102 for i, c := range chunk {
103 if c >= 0x20 && c <= 0x7e {
104 out[i] = c
105 } else {
106 out[i] = '.'
107 }
108 }
109 return string(out)
110}
111
112// Printable reports whether c renders as itself rather than as ".".
113func Printable(c byte) bool { return c >= 0x20 && c <= 0x7e }