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 : "github.com/cockroachdb/pebble/internal/treeprinter"
19 : )
20 :
21 : // New constructs a new binary formatter.
22 1 : func New(data []byte) *Formatter {
23 1 : offsetWidth := strconv.Itoa(max(int(math.Log10(float64(len(data)-1)))+1, 1))
24 1 : return &Formatter{
25 1 : data: data,
26 1 : lineWidth: 40,
27 1 : offsetFormatStr: "%0" + offsetWidth + "d-%0" + offsetWidth + "d: ",
28 1 : }
29 1 : }
30 :
31 : // Formatter is a utility for formatting binary data with descriptive comments.
32 : type Formatter struct {
33 : buf bytes.Buffer
34 : lines [][2]string // (binary data, comment) tuples
35 : data []byte
36 : off int
37 : anchorOff int
38 :
39 : // config
40 : lineWidth int
41 : linePrefix string
42 : offsetFormatStr string
43 : }
44 :
45 : // SetLinePrefix sets a prefix for each line of formatted output.
46 0 : func (f *Formatter) SetLinePrefix(prefix string) {
47 0 : f.linePrefix = prefix
48 0 : }
49 :
50 : // SetAnchorOffset sets the reference point for relative offset calculations to
51 : // the current offset. Future calls to RelativeOffset() will return an offset
52 : // relative to the current offset.
53 1 : func (f *Formatter) SetAnchorOffset() {
54 1 : f.anchorOff = f.off
55 1 : }
56 :
57 : // RelativeOffset retrieves the current offset relative to the offset at the
58 : // last time SetAnchorOffset was called. If SetAnchorOffset was never called,
59 : // RelativeOffset is equivalent to Offset.
60 1 : func (f *Formatter) RelativeOffset() int {
61 1 : return f.off - f.anchorOff
62 1 : }
63 :
64 : // RelativeData returns the subslice of the original data slice beginning at the
65 : // offset at which SetAnchorOffset was last called. If SetAnchorOffset was never
66 : // called, RelativeData is equivalent to Data.
67 1 : func (f *Formatter) RelativeData() []byte {
68 1 : return f.data[f.anchorOff:]
69 1 : }
70 :
71 : // LineWidth sets the Formatter's maximum line width for binary data.
72 1 : func (f *Formatter) LineWidth(width int) *Formatter {
73 1 : f.lineWidth = width
74 1 : return f
75 1 : }
76 :
77 : // More returns true if there is more data in the byte slice that can be formatted.
78 0 : func (f *Formatter) More() bool {
79 0 : return f.off < len(f.data)
80 0 : }
81 :
82 : // Remaining returns the number of unformatted bytes remaining in the byte slice.
83 0 : func (f *Formatter) Remaining() int {
84 0 : return len(f.data) - f.off
85 0 : }
86 :
87 : // Offset returns the current offset within the original data slice.
88 1 : func (f *Formatter) Offset() int {
89 1 : return f.off
90 1 : }
91 :
92 : // PeekUint reads a little-endian unsigned integer of the specified width at the
93 : // current offset.
94 1 : func (f *Formatter) PeekUint(w int) uint64 {
95 1 : switch w {
96 1 : case 1:
97 1 : return uint64(f.data[f.off])
98 1 : case 2:
99 1 : return uint64(binary.LittleEndian.Uint16(f.data[f.off:]))
100 1 : case 4:
101 1 : return uint64(binary.LittleEndian.Uint32(f.data[f.off:]))
102 1 : case 8:
103 1 : return binary.LittleEndian.Uint64(f.data[f.off:])
104 0 : default:
105 0 : panic("unsupported width")
106 : }
107 : }
108 :
109 : // Byte formats a single byte in binary format, displaying each bit as a zero or
110 : // one.
111 1 : func (f *Formatter) Byte(format string, args ...interface{}) int {
112 1 : f.printOffsets(1)
113 1 : f.printf("b %08b", f.data[f.off])
114 1 : f.off++
115 1 : f.newline(f.buf.String(), fmt.Sprintf(format, args...))
116 1 : return 1
117 1 : }
118 :
119 : // HexBytesln formats the next n bytes in hexadecimal format, appending the
120 : // formatted comment string to each line and ending on a newline.
121 1 : func (f *Formatter) HexBytesln(n int, format string, args ...interface{}) int {
122 1 : commentLine := strings.TrimSpace(fmt.Sprintf(format, args...))
123 1 : printLine := func() {
124 1 : bytesInLine := min(f.lineWidth/2, n)
125 1 : if f.buf.Len() == 0 {
126 1 : f.printOffsets(bytesInLine)
127 1 : }
128 1 : f.printf("x %0"+strconv.Itoa(bytesInLine*2)+"x", f.data[f.off:f.off+bytesInLine])
129 1 : f.newline(f.buf.String(), commentLine)
130 1 : f.off += bytesInLine
131 1 : n -= bytesInLine
132 : }
133 1 : printLine()
134 1 : commentLine = "(continued...)"
135 1 : for n > 0 {
136 1 : printLine()
137 1 : }
138 1 : return n
139 : }
140 :
141 : // HexTextln formats the next n bytes in hexadecimal format, appending a comment
142 : // to each line showing the ASCII equivalent characters for each byte for bytes
143 : // that are human-readable.
144 0 : func (f *Formatter) HexTextln(n int) int {
145 0 : printLine := func() {
146 0 : bytesInLine := min(f.lineWidth/2, n)
147 0 : if f.buf.Len() == 0 {
148 0 : f.printOffsets(bytesInLine)
149 0 : }
150 0 : f.printf("x %0"+strconv.Itoa(bytesInLine*2)+"x", f.data[f.off:f.off+bytesInLine])
151 0 : commentLine := asciiChars(f.data[f.off : f.off+bytesInLine])
152 0 : f.newline(f.buf.String(), commentLine)
153 0 : f.off += bytesInLine
154 0 : n -= bytesInLine
155 : }
156 0 : printLine()
157 0 : for n > 0 {
158 0 : printLine()
159 0 : }
160 0 : return n
161 : }
162 :
163 : // Uvarint decodes the bytes at the current offset as a uvarint, formatting them
164 : // in hexadecimal and prefixing the comment with the encoded decimal value.
165 0 : func (f *Formatter) Uvarint(format string, args ...interface{}) {
166 0 : comment := fmt.Sprintf(format, args...)
167 0 : v, n := binary.Uvarint(f.data[f.off:])
168 0 : f.HexBytesln(n, "uvarint(%d): %s", v, comment)
169 0 : }
170 :
171 : // Line prepares a single line of formatted output that will consume n bytes,
172 : // but formatting those n bytes in multiple ways. The line will be prefixed with
173 : // the offsets for the line's entire data.
174 1 : func (f *Formatter) Line(n int) Line {
175 1 : f.printOffsets(n)
176 1 : return Line{f: f, n: n, i: 0}
177 1 : }
178 :
179 : // String returns the current formatted output.
180 1 : func (f *Formatter) String() string {
181 1 : f.buf.Reset()
182 1 : // Identify the max width of the binary data so that we can add padding to
183 1 : // align comments on the right.
184 1 : binaryLineWidth := 0
185 1 : for _, lineData := range f.lines {
186 1 : binaryLineWidth = max(binaryLineWidth, len(lineData[0]))
187 1 : }
188 1 : for _, lineData := range f.lines {
189 1 : fmt.Fprint(&f.buf, f.linePrefix)
190 1 : fmt.Fprint(&f.buf, lineData[0])
191 1 : if len(lineData[1]) > 0 {
192 1 : if len(lineData[0]) == 0 {
193 0 : // There's no binary data on this line, just a comment. Print
194 0 : // the comment left-aligned.
195 0 : fmt.Fprint(&f.buf, "# ")
196 1 : } else {
197 1 : // Align the comment to the right of the binary data.
198 1 : fmt.Fprint(&f.buf, strings.Repeat(" ", binaryLineWidth-len(lineData[0])))
199 1 : fmt.Fprint(&f.buf, " # ")
200 1 : }
201 1 : fmt.Fprint(&f.buf, lineData[1])
202 : }
203 1 : fmt.Fprintln(&f.buf)
204 : }
205 1 : return f.buf.String()
206 : }
207 :
208 : // ToTreePrinter formats the current output and creates a treeprinter child node
209 : // for each line. The current output is reset; the position within the binary
210 : // buffer is not.
211 1 : func (f *Formatter) ToTreePrinter(tp treeprinter.Node) {
212 1 : for _, l := range strings.Split(strings.TrimRight(f.String(), "\n"), "\n") {
213 1 : tp.Child(l)
214 1 : }
215 1 : f.buf.Reset()
216 1 : f.lines = f.lines[:0]
217 : }
218 :
219 : // Pointer returns a pointer into the original data slice at the specified
220 : // offset.
221 0 : func (f *Formatter) Pointer(off int) unsafe.Pointer {
222 0 : return unsafe.Pointer(&f.data[f.off+off])
223 0 : }
224 :
225 : // Data returns the original data slice. Offset may be used to retrieve the
226 : // current offset within the slice.
227 0 : func (f *Formatter) Data() []byte {
228 0 : return f.data
229 0 : }
230 :
231 1 : func (f *Formatter) newline(binaryData, comment string) {
232 1 : f.lines = append(f.lines, [2]string{binaryData, comment})
233 1 : f.buf.Reset()
234 1 : }
235 :
236 1 : func (f *Formatter) printOffsets(n int) {
237 1 : f.printf(f.offsetFormatStr, f.off, f.off+n)
238 1 : }
239 :
240 1 : func (f *Formatter) printf(format string, args ...interface{}) {
241 1 : fmt.Fprintf(&f.buf, format, args...)
242 1 : }
243 :
244 : // Line is a pending line of formatted binary output.
245 : type Line struct {
246 : f *Formatter
247 : n int
248 : i int
249 : }
250 :
251 : // Append appends the provided string to the current line.
252 1 : func (l Line) Append(s string) Line {
253 1 : fmt.Fprint(&l.f.buf, s)
254 1 : return l
255 1 : }
256 :
257 : // Binary formats the next n bytes in binary format, displaying each bit as
258 : // a zero or one.
259 1 : func (l Line) Binary(n int) Line {
260 1 : if n+l.i > l.n {
261 0 : panic("binary data exceeds consumed line length")
262 : }
263 1 : for i := 0; i < n; i++ {
264 1 : l.f.printf("%08b", l.f.data[l.f.off+l.i])
265 1 : l.i++
266 1 : }
267 1 : return l
268 : }
269 :
270 : // HexBytes formats the next n bytes in hexadecimal format.
271 1 : func (l Line) HexBytes(n int) Line {
272 1 : if n+l.i > l.n {
273 0 : panic("binary data exceeds consumed line length")
274 : }
275 1 : l.f.printf("%0"+strconv.Itoa(n*2)+"x", l.f.data[l.f.off+l.i:l.f.off+l.i+n])
276 1 : l.i += n
277 1 : return l
278 : }
279 :
280 : // Done finishes the line, appending the provided comment if any.
281 1 : func (l Line) Done(format string, args ...interface{}) int {
282 1 : if l.n != l.i {
283 0 : panic("unconsumed data in line")
284 : }
285 1 : l.f.newline(l.f.buf.String(), fmt.Sprintf(format, args...))
286 1 : l.f.off += l.n
287 1 : return l.n
288 : }
289 :
290 0 : func asciiChars(b []byte) string {
291 0 : s := make([]byte, len(b))
292 0 : for i := range b {
293 0 : if b[i] >= 32 && b[i] <= 126 {
294 0 : s[i] = b[i]
295 0 : } else {
296 0 : s[i] = '.'
297 0 : }
298 : }
299 0 : return string(s)
300 : }
|