mirror of
https://github.com/wader/fq.git
synced 2024-11-30 09:58:13 +03:00
913f5780f4
This removes bar column drawing responsility from already complicated dump code. Start of dump code refactor that will enable configurable columns and proper column truncate/wrap.
430 lines
12 KiB
Go
430 lines
12 KiB
Go
package interp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/wader/fq/internal/ansi"
|
|
"github.com/wader/fq/internal/asciiwriter"
|
|
"github.com/wader/fq/internal/bitioex"
|
|
"github.com/wader/fq/internal/columnwriter"
|
|
"github.com/wader/fq/internal/hexpairwriter"
|
|
"github.com/wader/fq/internal/mathex"
|
|
"github.com/wader/fq/pkg/bitio"
|
|
"github.com/wader/fq/pkg/decode"
|
|
"github.com/wader/fq/pkg/scalar"
|
|
)
|
|
|
|
// TODO: refactor this
|
|
// move more things to jq?
|
|
// select columns?
|
|
// smart line wrap instead of truncate?
|
|
// binary/octal/... dump?
|
|
|
|
// 0 12 34 56
|
|
// addr|hexdump|ascii|field
|
|
const (
|
|
colAddr = 0
|
|
colHex = 2
|
|
colASCII = 4
|
|
colField = 6
|
|
)
|
|
|
|
const rootIndentWidth = 2
|
|
const treeIndentWidth = 2
|
|
|
|
func isCompound(v *decode.Value) bool {
|
|
switch v.V.(type) {
|
|
case *decode.Compound:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type dumpCtx struct {
|
|
opts Options
|
|
buf []byte
|
|
cw *columnwriter.Writer
|
|
hexHeader string
|
|
asciiHeader string
|
|
}
|
|
|
|
func indentStr(n int) string {
|
|
const spaces = " "
|
|
for n > len(spaces) {
|
|
return strings.Repeat(" ", n)
|
|
}
|
|
return spaces[0:n]
|
|
}
|
|
|
|
func dumpEx(v *decode.Value, ctx *dumpCtx, depth int, rootV *decode.Value, rootDepth int, addrWidth int) error {
|
|
opts := ctx.opts
|
|
cw := ctx.cw
|
|
buf := ctx.buf
|
|
deco := ctx.opts.Decorator
|
|
|
|
// no error check as we write into buffering column
|
|
// we check for err later for Flush()
|
|
cprint := func(c int, a ...any) {
|
|
fmt.Fprint(cw.Columns[c], a...)
|
|
}
|
|
cfmt := func(c int, format string, a ...any) {
|
|
fmt.Fprintf(cw.Columns[c], format, a...)
|
|
}
|
|
|
|
isInArray := false
|
|
inArrayLen := 0
|
|
if v.Parent != nil {
|
|
if dc, ok := v.Parent.V.(*decode.Compound); ok {
|
|
isInArray = dc.IsArray
|
|
inArrayLen = len(dc.Children)
|
|
}
|
|
}
|
|
|
|
nameV := v
|
|
name := nameV.Name
|
|
if isInArray {
|
|
nameV = v.Parent
|
|
name = ""
|
|
}
|
|
if depth == 0 {
|
|
name = valuePathExprDecorated(nameV, deco)
|
|
} else {
|
|
name = deco.ObjectKey.Wrap(name)
|
|
}
|
|
|
|
rootIndent := indentStr(rootIndentWidth * rootDepth)
|
|
indent := indentStr(treeIndentWidth * depth)
|
|
|
|
if opts.ArrayTruncate != 0 && depth != 0 && isInArray && v.Index >= opts.ArrayTruncate {
|
|
cfmt(colField, "%s%s%s:%s%s: ...",
|
|
indent,
|
|
deco.Index.F("["),
|
|
deco.Number.F(strconv.Itoa(v.Index)),
|
|
deco.Number.F(strconv.Itoa(inArrayLen)),
|
|
deco.Index.F("]"),
|
|
)
|
|
cw.Flush()
|
|
return decode.ErrWalkBreak
|
|
}
|
|
|
|
innerRange := v.InnerRange()
|
|
willDisplayData := innerRange.Len > 0 && (!isCompound(v) || (opts.Depth != 0 && opts.Depth == depth))
|
|
|
|
// show address bar on root, nested root and format change
|
|
if depth == 0 || v.IsRoot || v.Format != nil {
|
|
cfmt(colHex, "%s", deco.DumpHeader.F(ctx.hexHeader))
|
|
cfmt(colASCII, "%s", deco.DumpHeader.F(ctx.asciiHeader))
|
|
|
|
if willDisplayData {
|
|
cw.Flush()
|
|
}
|
|
}
|
|
|
|
cfmt(colField, "%s%s", indent, name)
|
|
if isInArray {
|
|
cfmt(colField, "%s%s%s", deco.Index.F("["), deco.Number.F(strconv.Itoa(v.Index)), deco.Index.F("]"))
|
|
}
|
|
|
|
var valueErr error
|
|
|
|
// TODO: cleanup map[string]any []any or json format
|
|
// dump should use some internal interface instead?
|
|
switch vv := v.V.(type) {
|
|
case *decode.Compound:
|
|
if vv.IsArray {
|
|
cfmt(colField, "%s%s:%s%s", deco.Index.F("["), deco.Number.F("0"), deco.Number.F(strconv.Itoa(len(vv.Children))), deco.Index.F("]"))
|
|
} else {
|
|
cfmt(colField, "%s", deco.Object.F("{}"))
|
|
}
|
|
cprint(colField, ":")
|
|
if isInArray {
|
|
cfmt(colField, " %s", v.Name)
|
|
}
|
|
if vv.Description != "" {
|
|
cfmt(colField, " %s", deco.Value.F(vv.Description))
|
|
}
|
|
case *scalar.S:
|
|
switch av := vv.Actual.(type) {
|
|
case map[string]any:
|
|
cfmt(colField, ": %s", deco.Object.F("{}"))
|
|
case []any:
|
|
// TODO: format?
|
|
cfmt(colField, ": %s%s:%s%s", deco.Index.F("["), deco.Number.F("0"), deco.Number.F(strconv.Itoa(len(av))), deco.Index.F("]"))
|
|
default:
|
|
cprint(colField, ":")
|
|
if vv.Sym == nil {
|
|
cfmt(colField, " %s", deco.ValueColor(vv.Actual).F(previewValue(vv.Actual, vv.ActualDisplay)))
|
|
} else {
|
|
cfmt(colField, " %s", deco.ValueColor(vv.Sym).F(previewValue(vv.Sym, vv.SymDisplay)))
|
|
cfmt(colField, " (%s)", deco.ValueColor(vv.Actual).F(previewValue(vv.Actual, vv.ActualDisplay)))
|
|
}
|
|
}
|
|
|
|
if opts.Verbose && isInArray {
|
|
cfmt(colField, " %s", v.Name)
|
|
}
|
|
if vv.Description != "" {
|
|
cfmt(colField, " (%s)", deco.Value.F(vv.Description))
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unreachable vv %#+v", vv))
|
|
}
|
|
|
|
if v.Format != nil {
|
|
cfmt(colField, " (%s)", deco.Value.F(v.Format.Name))
|
|
}
|
|
valueErr = v.Err
|
|
|
|
if opts.Verbose {
|
|
cfmt(colField, " %s (%s)",
|
|
mathex.BitRange(innerRange).StringByteBits(opts.Addrbase), mathex.Bits(innerRange.Len).StringByteBits(opts.Sizebase))
|
|
}
|
|
|
|
cprint(colField, "\n")
|
|
|
|
if valueErr != nil {
|
|
var printErrs func(depth int, err error)
|
|
printErrs = func(depth int, err error) {
|
|
indent := indentStr(treeIndentWidth * depth)
|
|
|
|
var formatErr decode.FormatError
|
|
var decodeFormatsErr decode.FormatsError
|
|
|
|
switch {
|
|
case errors.As(err, &formatErr):
|
|
cfmt(colField, "%s %s: %s: %s\n", indent, deco.Error.F("error"), formatErr.Format.Name, formatErr.Err.Error())
|
|
|
|
if opts.Verbose {
|
|
for _, f := range formatErr.Stacktrace.Frames() {
|
|
cfmt(colField, "%s %s\n", indent, f.Function)
|
|
cfmt(colField, "%s %s:%d\n", indent, f.File, f.Line)
|
|
}
|
|
}
|
|
switch {
|
|
case errors.Is(formatErr.Err, decode.FormatsError{}):
|
|
printErrs(depth+1, formatErr.Err)
|
|
}
|
|
case errors.As(err, &decodeFormatsErr):
|
|
cfmt(colField, "%s %s\n", indent, err)
|
|
for _, e := range decodeFormatsErr.Errs {
|
|
printErrs(depth+1, e)
|
|
}
|
|
default:
|
|
cfmt(colField, "%s!%s\n", indent, deco.Error.F(err.Error()))
|
|
}
|
|
}
|
|
|
|
printErrs(depth, valueErr)
|
|
}
|
|
|
|
rootBitLen, err := bitioex.Len(rootV.RootReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bufferLastBit := rootBitLen - 1
|
|
startBit := innerRange.Start
|
|
stopBit := innerRange.Stop() - 1
|
|
sizeBits := innerRange.Len
|
|
lastDisplayBit := stopBit
|
|
|
|
if opts.DisplayBytes > 0 && sizeBits > int64(opts.DisplayBytes)*8 {
|
|
lastDisplayBit = startBit + (int64(opts.DisplayBytes)*8 - 1)
|
|
if lastDisplayBit%(int64(opts.LineBytes)*8) != 0 {
|
|
lastDisplayBit += (int64(opts.LineBytes) * 8) - lastDisplayBit%(int64(opts.LineBytes)*8) - 1
|
|
}
|
|
|
|
if lastDisplayBit > stopBit || stopBit-lastDisplayBit <= int64(opts.LineBytes)*8 {
|
|
lastDisplayBit = stopBit
|
|
}
|
|
}
|
|
|
|
bufferLastByte := bufferLastBit / 8
|
|
startByte := startBit / 8
|
|
stopByte := stopBit / 8
|
|
lastDisplayByte := lastDisplayBit / 8
|
|
displaySizeBytes := lastDisplayByte - startByte + 1
|
|
displaySizeBits := displaySizeBytes * 8
|
|
maxDisplaySizeBits := bufferLastBit - startByte*8 + 1
|
|
if sizeBits == 0 {
|
|
displaySizeBits = 0
|
|
}
|
|
if displaySizeBits > maxDisplaySizeBits {
|
|
displaySizeBits = maxDisplaySizeBits
|
|
}
|
|
|
|
startLine := startByte / int64(opts.LineBytes)
|
|
startLineByteOffset := startByte % int64(opts.LineBytes)
|
|
startLineByte := startLine * int64(opts.LineBytes)
|
|
lastDisplayLine := lastDisplayByte / int64(opts.LineBytes)
|
|
|
|
// has length and is not compound or a collapsed struct/array (max depth)
|
|
if willDisplayData {
|
|
cfmt(colAddr, "%s%s\n",
|
|
rootIndent, deco.DumpAddr.F(mathex.PadFormatInt(startLineByte, opts.Addrbase, true, addrWidth)))
|
|
|
|
vBR, err := bitioex.Range(rootV.RootReader, startByte*8, displaySizeBits)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addrLines := lastDisplayLine - startLine + 1
|
|
hexpairFn := func(b byte) string { return deco.ByteColor(b).Wrap(hexpairwriter.Pair(b)) }
|
|
asciiFn := func(b byte) string { return deco.ByteColor(b).Wrap(asciiwriter.SafeASCII(b)) }
|
|
|
|
hexBR, err := bitio.CloneReadSeeker(vBR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := bitioex.CopyBitsBuffer(
|
|
hexpairwriter.New(cw.Columns[colHex], opts.LineBytes, int(startLineByteOffset), hexpairFn),
|
|
hexBR,
|
|
buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
asciiBR, err := bitio.CloneReadSeeker(vBR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := bitioex.CopyBitsBuffer(
|
|
asciiwriter.New(cw.Columns[colASCII], opts.LineBytes, int(startLineByteOffset), asciiFn),
|
|
asciiBR,
|
|
buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := int64(1); i < addrLines; i++ {
|
|
lineStartByte := startLineByte + i*int64(opts.LineBytes)
|
|
cfmt(colAddr, "%s%s\n", rootIndent, deco.DumpAddr.F(mathex.PadFormatInt(lineStartByte, opts.Addrbase, true, addrWidth)))
|
|
}
|
|
// TODO: correct? should rethink columnwriter api maybe?
|
|
lastLineStopByte := startLineByte + addrLines*int64(opts.LineBytes) - 1
|
|
if lastDisplayByte == bufferLastByte && lastDisplayByte != lastLineStopByte {
|
|
// extra "|" as end markers
|
|
cfmt(colHex, "%s\n", deco.Column)
|
|
cfmt(colASCII, "%s\n", deco.Column)
|
|
}
|
|
|
|
if stopByte != lastDisplayByte {
|
|
isEnd := ""
|
|
if stopBit == bufferLastBit {
|
|
isEnd = " (end)"
|
|
}
|
|
|
|
cfmt(colAddr, "%s%s\n", rootIndent, deco.DumpAddr.F("*"))
|
|
cprint(colHex, "\n")
|
|
// TODO: truncate if display_bytes is small?
|
|
cfmt(colHex, "until %s%s (%s)",
|
|
mathex.Bits(stopBit).StringByteBits(opts.Addrbase),
|
|
isEnd,
|
|
mathex.PadFormatInt(bitio.BitsByteCount(sizeBits), opts.Sizebase, true, 0))
|
|
// TODO: dump last line?
|
|
}
|
|
}
|
|
|
|
if err := cw.Flush(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func dump(v *decode.Value, w io.Writer, opts Options) error {
|
|
maxAddrIndentWidth := 0
|
|
makeWalkFn := func(fn decode.WalkFn) decode.WalkFn {
|
|
return func(v *decode.Value, rootV *decode.Value, depth int, rootDepth int) error {
|
|
if opts.Depth != 0 && depth > opts.Depth {
|
|
return decode.ErrWalkSkipChildren
|
|
}
|
|
|
|
return fn(v, rootV, depth, rootDepth)
|
|
}
|
|
}
|
|
|
|
_ = v.WalkPreOrder(makeWalkFn(func(v *decode.Value, _ *decode.Value, _ int, rootDepth int) error {
|
|
maxAddrIndentWidth = mathex.Max(
|
|
maxAddrIndentWidth,
|
|
rootIndentWidth*rootDepth+mathex.DigitsInBase(bitio.BitsByteCount(v.InnerRange().Stop()), true, opts.Addrbase),
|
|
)
|
|
return nil
|
|
}))
|
|
|
|
var displayLenFn func(s string) int
|
|
var displayTruncateFn func(s string, start, stop int) string
|
|
if opts.Color {
|
|
displayLenFn = ansi.Len
|
|
displayTruncateFn = ansi.Slice
|
|
}
|
|
|
|
addrColumnWidth := maxAddrIndentWidth
|
|
hexColumnWidth := opts.LineBytes*3 - 1
|
|
asciiColumnWidth := opts.LineBytes
|
|
treeColumnWidth := -1
|
|
// TODO: set with and truncate/wrap properly
|
|
// if opts.Width != 0 {
|
|
// treeColumnWidth = mathex.Max(0, opts.Width-(addrColumnWidth+hexColumnWidth+asciiColumnWidth+3 /* bars */))
|
|
// }
|
|
|
|
cw := columnwriter.New(
|
|
w,
|
|
&columnwriter.MultiLineColumn{Width: addrColumnWidth, LenFn: displayLenFn, SliceFn: displayTruncateFn},
|
|
columnwriter.BarColumn(opts.Decorator.Column),
|
|
&columnwriter.MultiLineColumn{Width: hexColumnWidth, LenFn: displayLenFn, SliceFn: displayTruncateFn},
|
|
columnwriter.BarColumn(opts.Decorator.Column),
|
|
&columnwriter.MultiLineColumn{Width: asciiColumnWidth, LenFn: displayLenFn, SliceFn: displayTruncateFn},
|
|
columnwriter.BarColumn(opts.Decorator.Column),
|
|
&columnwriter.MultiLineColumn{Width: treeColumnWidth, Wrap: false, LenFn: displayLenFn, SliceFn: displayTruncateFn},
|
|
)
|
|
|
|
buf := make([]byte, 32*1024)
|
|
|
|
var hexHeader string
|
|
var asciiHeader string
|
|
for i := 0; i < opts.LineBytes; i++ {
|
|
s := mathex.PadFormatInt(int64(i), opts.Addrbase, false, 2)
|
|
hexHeader += s
|
|
if i < opts.LineBytes-1 {
|
|
hexHeader += " "
|
|
}
|
|
asciiHeader += s[len(s)-1:]
|
|
}
|
|
|
|
ctx := &dumpCtx{
|
|
opts: opts,
|
|
buf: buf,
|
|
cw: cw,
|
|
hexHeader: hexHeader,
|
|
asciiHeader: asciiHeader,
|
|
}
|
|
|
|
return v.WalkPreOrder(makeWalkFn(func(v *decode.Value, rootV *decode.Value, depth int, rootDepth int) error {
|
|
return dumpEx(v, ctx, depth, rootV, rootDepth, maxAddrIndentWidth-rootDepth)
|
|
}))
|
|
}
|
|
|
|
func hexdump(w io.Writer, bv Binary, opts Options) error {
|
|
br, err := bitioex.Range(bv.br, bv.r.Start, bv.r.Len)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: hack
|
|
opts.Verbose = true
|
|
return dump(
|
|
&decode.Value{
|
|
// TODO: hack
|
|
V: &scalar.S{Actual: br},
|
|
Range: bv.r,
|
|
RootReader: bv.br,
|
|
},
|
|
w,
|
|
opts,
|
|
)
|
|
}
|