Line data Source code
1 : // Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2 : // of this source code is governed by a BSD-style license that can be found in
3 : // the LICENSE file.
4 :
5 : // Package binfmt exposes utilities for formatting binary data with descriptive
6 : // comments.
7 : package binfmt
8 :
9 : import (
10 : "bytes"
11 : "encoding/binary"
12 : "fmt"
13 : "math"
14 : "strconv"
15 : "strings"
16 : "unsafe"
17 : )
18 :
19 : // New constructs a new binary formatter.
20 1 : func New(data []byte) *Formatter {
21 1 : offsetWidth := strconv.Itoa(max(int(math.Log10(float64(len(data)-1)))+1, 1))
22 1 : return &Formatter{
23 1 : data: data,
24 1 : lineWidth: 40,
25 1 : offsetFormatStr: "%0" + offsetWidth + "d-%0" + offsetWidth + "d: ",
26 1 : }
27 1 : }
28 :
29 : // Formatter is a utility for formatting binary data with descriptive comments.
30 : type Formatter struct {
31 : buf bytes.Buffer
32 : lines [][2]string // (binary data, comment) tuples
33 : data []byte
34 : off int
35 :
36 : // config
37 : lineWidth int
38 : offsetFormatStr string
39 : }
40 :
41 : // LineWidth sets the Formatter's maximum line width for binary data.
42 1 : func (f *Formatter) LineWidth(width int) *Formatter {
43 1 : f.lineWidth = width
44 1 : return f
45 1 : }
46 :
47 : // More returns true if there is more data in the byte slice that can be formatted.
48 0 : func (f *Formatter) More() bool {
49 0 : return f.off < len(f.data)
50 0 : }
51 :
52 : // Remaining returns the number of unformatted bytes remaining in the byte slice.
53 0 : func (f *Formatter) Remaining() int {
54 0 : return len(f.data) - f.off
55 0 : }
56 :
57 : // Offset returns the current offset within the original data slice.
58 1 : func (f *Formatter) Offset() int {
59 1 : return f.off
60 1 : }
61 :
62 : // PeekUint reads a little-endian unsigned integer of the specified width at the
63 : // current offset.
64 1 : func (f *Formatter) PeekUint(w int) uint64 {
65 1 : switch w {
66 1 : case 1:
67 1 : return uint64(f.data[f.off])
68 1 : case 2:
69 1 : return uint64(binary.LittleEndian.Uint16(f.data[f.off:]))
70 1 : case 4:
71 1 : return uint64(binary.LittleEndian.Uint32(f.data[f.off:]))
72 1 : case 8:
73 1 : return binary.LittleEndian.Uint64(f.data[f.off:])
74 0 : default:
75 0 : panic("unsupported width")
76 : }
77 : }
78 :
79 : // Byte formats a single byte in binary format, displaying each bit as a zero or
80 : // one.
81 1 : func (f *Formatter) Byte(format string, args ...interface{}) int {
82 1 : f.printOffsets(1)
83 1 : f.printf("b %08b", f.data[f.off])
84 1 : f.off++
85 1 : f.newline(f.buf.String(), fmt.Sprintf(format, args...))
86 1 : return 1
87 1 : }
88 :
89 : // CommentLine adds a full-width comment line to the output.
90 1 : func (f *Formatter) CommentLine(format string, args ...interface{}) {
91 1 : f.newline("", strings.TrimSpace(fmt.Sprintf(format, args...)))
92 1 : }
93 :
94 : // HexBytesln formats the next n bytes in hexadecimal format, appending the
95 : // formatted comment string to each line and ending on a newline.
96 1 : func (f *Formatter) HexBytesln(n int, format string, args ...interface{}) int {
97 1 : commentLine := strings.TrimSpace(fmt.Sprintf(format, args...))
98 1 : printLine := func() {
99 1 : bytesInLine := min(f.lineWidth/2, n)
100 1 : if f.buf.Len() == 0 {
101 1 : f.printOffsets(bytesInLine)
102 1 : }
103 1 : f.printf("x %0"+strconv.Itoa(bytesInLine*2)+"x", f.data[f.off:f.off+bytesInLine])
104 1 : f.newline(f.buf.String(), commentLine)
105 1 : f.off += bytesInLine
106 1 : n -= bytesInLine
107 : }
108 1 : printLine()
109 1 : commentLine = "(continued...)"
110 1 : for n > 0 {
111 1 : printLine()
112 1 : }
113 1 : return n
114 : }
115 :
116 : // Line prepares a single line of formatted output that will consume n bytes,
117 : // but formatting those n bytes in multiple ways. The line will be prefixed with
118 : // the offsets for the line's entire data.
119 1 : func (f *Formatter) Line(n int) Line {
120 1 : f.printOffsets(n)
121 1 : return Line{f: f, n: n, i: 0}
122 1 : }
123 :
124 : // String returns the current formatted output.
125 1 : func (f *Formatter) String() string {
126 1 : f.buf.Reset()
127 1 : // Identify the max width of the binary data so that we can add padding to
128 1 : // align comments on the right.
129 1 : binaryLineWidth := 0
130 1 : for _, lineData := range f.lines {
131 1 : binaryLineWidth = max(binaryLineWidth, len(lineData[0]))
132 1 : }
133 1 : for _, lineData := range f.lines {
134 1 : fmt.Fprint(&f.buf, lineData[0])
135 1 : if len(lineData[1]) > 0 {
136 1 : if len(lineData[0]) == 0 {
137 1 : // There's no binary data on this line, just a comment. Print
138 1 : // the comment left-aligned.
139 1 : fmt.Fprint(&f.buf, "# ")
140 1 : } else {
141 1 : // Align the comment to the right of the binary data.
142 1 : fmt.Fprint(&f.buf, strings.Repeat(" ", binaryLineWidth-len(lineData[0])))
143 1 : fmt.Fprint(&f.buf, " # ")
144 1 : }
145 1 : fmt.Fprint(&f.buf, lineData[1])
146 : }
147 1 : fmt.Fprintln(&f.buf)
148 : }
149 1 : return f.buf.String()
150 : }
151 :
152 : // Pointer returns a pointer into the original data slice at the specified
153 : // offset.
154 0 : func (f *Formatter) Pointer(off int) unsafe.Pointer {
155 0 : return unsafe.Pointer(&f.data[f.off+off])
156 0 : }
157 :
158 : // Data returns the original data slice. Offset may be used to retrieve the
159 : // current offset within the slice.
160 1 : func (f *Formatter) Data() []byte {
161 1 : return f.data
162 1 : }
163 :
164 1 : func (f *Formatter) newline(binaryData, comment string) {
165 1 : f.lines = append(f.lines, [2]string{binaryData, comment})
166 1 : f.buf.Reset()
167 1 : }
168 :
169 1 : func (f *Formatter) printOffsets(n int) {
170 1 : f.printf(f.offsetFormatStr, f.off, f.off+n)
171 1 : }
172 :
173 1 : func (f *Formatter) printf(format string, args ...interface{}) {
174 1 : fmt.Fprintf(&f.buf, format, args...)
175 1 : }
176 :
177 : // Line is a pending line of formatted binary output.
178 : type Line struct {
179 : f *Formatter
180 : n int
181 : i int
182 : }
183 :
184 : // Append appends the provided string to the current line.
185 1 : func (l Line) Append(s string) Line {
186 1 : fmt.Fprint(&l.f.buf, s)
187 1 : return l
188 1 : }
189 :
190 : // Binary formats the next n bytes in binary format, displaying each bit as
191 : // a zero or one.
192 1 : func (l Line) Binary(n int) Line {
193 1 : if n+l.i > l.n {
194 0 : panic("binary data exceeds consumed line length")
195 : }
196 1 : for i := 0; i < n; i++ {
197 1 : l.f.printf("%08b", l.f.data[l.f.off+l.i])
198 1 : l.i++
199 1 : }
200 1 : return l
201 : }
202 :
203 : // HexBytes formats the next n bytes in hexadecimal format.
204 1 : func (l Line) HexBytes(n int) Line {
205 1 : if n+l.i > l.n {
206 0 : panic("binary data exceeds consumed line length")
207 : }
208 1 : l.f.printf("%0"+strconv.Itoa(n*2)+"x", l.f.data[l.f.off+l.i:l.f.off+l.i+n])
209 1 : l.i += n
210 1 : return l
211 : }
212 :
213 : // Done finishes the line, appending the provided comment if any.
214 1 : func (l Line) Done(format string, args ...interface{}) int {
215 1 : if l.n != l.i {
216 0 : panic("unconsumed data in line")
217 : }
218 1 : l.f.newline(l.f.buf.String(), fmt.Sprintf(format, args...))
219 1 : l.f.off += l.n
220 1 : return l.n
221 : }
|