mirror of
https://github.com/wader/fq.git
synced 2024-12-26 06:51:35 +03:00
501 lines
10 KiB
Go
501 lines
10 KiB
Go
package interp
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"math/big"
|
|
|
|
"github.com/wader/fq/internal/aheadreadseeker"
|
|
"github.com/wader/fq/internal/bitioex"
|
|
"github.com/wader/fq/internal/ctxreadseeker"
|
|
"github.com/wader/fq/internal/gojqex"
|
|
"github.com/wader/fq/internal/ioex"
|
|
"github.com/wader/fq/internal/progressreadseeker"
|
|
"github.com/wader/fq/pkg/bitio"
|
|
"github.com/wader/fq/pkg/ranges"
|
|
"github.com/wader/gojq"
|
|
)
|
|
|
|
func init() {
|
|
RegisterFunc1("_tobits", (*Interp)._toBits)
|
|
RegisterFunc0("open", (*Interp)._open)
|
|
}
|
|
|
|
type ToBinary interface {
|
|
ToBinary() (Binary, error)
|
|
}
|
|
|
|
func toBinary(v any) (Binary, error) {
|
|
switch vv := v.(type) {
|
|
case ToBinary:
|
|
return vv.ToBinary()
|
|
default:
|
|
br, err := ToBitReader(v)
|
|
if err != nil {
|
|
return Binary{}, err
|
|
}
|
|
return NewBinaryFromBitReader(br, 8, 0)
|
|
}
|
|
}
|
|
|
|
func ToBitReader(v any) (bitio.ReaderAtSeeker, error) {
|
|
return toBitReaderEx(v, false)
|
|
}
|
|
|
|
type byteRangeError int
|
|
|
|
func (b byteRangeError) Error() string {
|
|
return fmt.Sprintf("byte in binary list must be bytes (0-255) got %d", int(b))
|
|
|
|
}
|
|
|
|
func toBitReaderEx(v any, inArray bool) (bitio.ReaderAtSeeker, error) {
|
|
switch vv := v.(type) {
|
|
case ToBinary:
|
|
bv, err := vv.ToBinary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bitioex.Range(bv.br, bv.r.Start, bv.r.Len)
|
|
case string:
|
|
return bitio.NewBitReader([]byte(vv), -1), nil
|
|
case int, float64, *big.Int:
|
|
bi, err := toBigInt(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if inArray {
|
|
if bi.Cmp(big.NewInt(255)) > 0 || bi.Cmp(big.NewInt(0)) < 0 {
|
|
return nil, byteRangeError(bi.Int64())
|
|
}
|
|
n := bi.Uint64()
|
|
b := [1]byte{byte(n)}
|
|
return bitio.NewBitReader(b[:], -1), nil
|
|
}
|
|
|
|
bitLen := int64(bi.BitLen())
|
|
// bit.Int "The bit length of 0 is 0."
|
|
if bitLen == 0 {
|
|
var z [1]byte
|
|
return bitio.NewBitReader(z[:], 1), nil
|
|
}
|
|
// TODO: how should this work? "0xf | tobytes" 4bits or 8bits? now 4
|
|
padBefore := (8 - (bitLen % 8)) % 8
|
|
// padBefore := 0
|
|
br, err := bitioex.Range(bitio.NewBitReader(bi.Bytes(), -1), padBefore, bitLen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return br, nil
|
|
case []any:
|
|
rr := make([]bitio.ReadAtSeeker, 0, len(vv))
|
|
|
|
// fast path for slice containing only 0-255 numbers and strings
|
|
bs := &bytes.Buffer{}
|
|
for _, e := range vv {
|
|
if bs == nil {
|
|
break
|
|
}
|
|
switch ev := e.(type) {
|
|
case int:
|
|
if ev >= 0 && ev <= 255 {
|
|
bs.WriteByte(byte(ev))
|
|
continue
|
|
}
|
|
case float64:
|
|
b := int(ev)
|
|
if b >= 0 && b <= 255 {
|
|
bs.WriteByte(byte(ev))
|
|
continue
|
|
}
|
|
case *big.Int:
|
|
if ev.Cmp(big.NewInt(0)) >= 0 && ev.Cmp(big.NewInt(255)) <= 0 {
|
|
bs.WriteByte(byte(ev.Uint64()))
|
|
continue
|
|
}
|
|
case string:
|
|
// TODO: maybe only if less then some length?
|
|
bs.WriteString(ev)
|
|
continue
|
|
}
|
|
bs = nil
|
|
}
|
|
if bs != nil {
|
|
return bitio.NewBitReader(bs.Bytes(), -1), nil
|
|
}
|
|
|
|
// TODO: optimize byte array case, flatten into one slice
|
|
for _, e := range vv {
|
|
eBR, eErr := toBitReaderEx(e, true)
|
|
if eErr != nil {
|
|
return nil, eErr
|
|
}
|
|
rr = append(rr, eBR)
|
|
}
|
|
|
|
mb, err := bitio.NewMultiReader(rr...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mb, nil
|
|
default:
|
|
return nil, fmt.Errorf("value can't be a binary")
|
|
}
|
|
}
|
|
|
|
type toBitsOpts struct {
|
|
Unit int
|
|
KeepRange bool
|
|
PadToUnits int
|
|
}
|
|
|
|
// note is used to implement tobytes* also
|
|
func (i *Interp) _toBits(c any, opts toBitsOpts) any {
|
|
// TODO: unit > 8?
|
|
|
|
bv, err := toBinary(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pad := int64(opts.Unit * opts.PadToUnits)
|
|
if pad == 0 {
|
|
pad = int64(opts.Unit)
|
|
}
|
|
|
|
bv.unit = opts.Unit
|
|
bv.pad = (pad - bv.r.Len%pad) % pad
|
|
|
|
if opts.KeepRange {
|
|
return bv
|
|
}
|
|
|
|
br, err := bv.toReader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bb, err := NewBinaryFromBitReader(br, bv.unit, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return bb
|
|
}
|
|
|
|
type openFile struct {
|
|
Binary
|
|
filename string
|
|
progressFn progressreadseeker.ProgressFn
|
|
}
|
|
|
|
var _ Value = (*openFile)(nil)
|
|
var _ ToBinary = (*openFile)(nil)
|
|
|
|
func (of *openFile) Display(w io.Writer, opts *Options) error {
|
|
_, err := fmt.Fprintf(w, "<openfile %q>\n", of.filename)
|
|
return err
|
|
}
|
|
|
|
func (of *openFile) ToBinary() (Binary, error) {
|
|
return NewBinaryFromBitReader(of.br, 8, 0)
|
|
}
|
|
|
|
// opens a file for reading from filesystem
|
|
// TODO: when to close? when br loses all refs? need to use finalizer somehow?
|
|
func (i *Interp) _open(c any) any {
|
|
if i.EvalInstance.IsCompleting {
|
|
// TODO: have dummy values for each type for completion?
|
|
br, _ := NewBinaryFromBitReader(bitio.NewBitReader([]byte{}, -1), 8, 0)
|
|
return br
|
|
}
|
|
|
|
var err error
|
|
var f fs.File
|
|
var path string
|
|
|
|
switch c.(type) {
|
|
case nil:
|
|
path = "<stdin>"
|
|
f = i.OS.Stdin()
|
|
default:
|
|
path, err = toString(c)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
f, err = i.OS.FS().Open(path)
|
|
if err != nil {
|
|
// path context added in jq error code
|
|
var pe *fs.PathError
|
|
if errors.As(err, &pe) {
|
|
return pe.Err
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
var bEnd int64
|
|
var fRS io.ReadSeeker
|
|
|
|
fFI, err := f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
|
|
// ctxreadseeker is used to make sure any io calls can be canceled
|
|
// TODO: ctxreadseeker might leak if the underlying call hangs forever
|
|
|
|
// a regular file should be seekable but fallback below to read whole file if not
|
|
if fFI.Mode().IsRegular() {
|
|
if rs, ok := f.(io.ReadSeeker); ok {
|
|
fRS = ctxreadseeker.New(i.EvalInstance.Ctx, rs)
|
|
bEnd = fFI.Size()
|
|
}
|
|
}
|
|
|
|
if fRS == nil {
|
|
buf, err := io.ReadAll(ctxreadseeker.New(i.EvalInstance.Ctx, &ioex.ReadErrSeeker{Reader: f}))
|
|
if err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
fRS = bytes.NewReader(buf)
|
|
bEnd = int64(len(buf))
|
|
}
|
|
|
|
bbf := &openFile{
|
|
filename: path,
|
|
}
|
|
|
|
const progressPrecision = 1024
|
|
fRS = progressreadseeker.New(fRS, progressPrecision, bEnd,
|
|
func(approxReadBytes int64, totalSize int64) {
|
|
// progressFn is assign by decode etc
|
|
if bbf.progressFn != nil {
|
|
bbf.progressFn(approxReadBytes, totalSize)
|
|
}
|
|
},
|
|
)
|
|
|
|
const cacheReadAheadSize = 512 * 1024
|
|
aheadRs := aheadreadseeker.New(fRS, cacheReadAheadSize)
|
|
|
|
// bitio.Buffer -> (bitio.Reader) -> aheadreadseeker -> progressreadseeker -> ctxreadseeker -> readseeker
|
|
|
|
bbf.br = bitio.NewIOBitReadSeeker(aheadRs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return bbf
|
|
}
|
|
|
|
var _ Value = Binary{}
|
|
var _ ToBinary = Binary{}
|
|
|
|
type Binary struct {
|
|
br bitio.ReaderAtSeeker
|
|
r ranges.Range
|
|
unit int
|
|
pad int64
|
|
}
|
|
|
|
func NewBinaryFromBitReader(br bitio.ReaderAtSeeker, unit int, pad int64) (Binary, error) {
|
|
l, err := bitioex.Len(br)
|
|
if err != nil {
|
|
return Binary{}, err
|
|
}
|
|
|
|
return Binary{
|
|
br: br,
|
|
r: ranges.Range{Start: 0, Len: l},
|
|
unit: unit,
|
|
pad: pad,
|
|
}, nil
|
|
}
|
|
|
|
func (b Binary) toBytesBuffer(r ranges.Range) (*bytes.Buffer, error) {
|
|
br, err := bitioex.Range(b.br, r.Start, r.Len)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf := &bytes.Buffer{}
|
|
if _, err := bitioex.CopyBits(buf, br); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf, nil
|
|
}
|
|
|
|
func (Binary) ExtType() string { return "binary" }
|
|
|
|
func (Binary) ExtKeys() []string {
|
|
return []string{
|
|
"bits",
|
|
"bytes",
|
|
"name",
|
|
"size",
|
|
"start",
|
|
"stop",
|
|
"unit",
|
|
}
|
|
}
|
|
|
|
func (b Binary) ToBinary() (Binary, error) {
|
|
return b, nil
|
|
}
|
|
|
|
func (b Binary) JQValueLength() any {
|
|
return int(b.r.Len / int64(b.unit))
|
|
}
|
|
func (b Binary) JQValueSliceLen() any {
|
|
return b.JQValueLength()
|
|
}
|
|
|
|
func (b Binary) JQValueIndex(index int) any {
|
|
if index < 0 {
|
|
return nil
|
|
}
|
|
|
|
buf, err := b.toBytesBuffer(ranges.Range{Start: b.r.Start + int64(index*b.unit), Len: int64(b.unit)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
extraBits := uint((8 - b.unit%8) % 8)
|
|
|
|
return new(big.Int).Rsh(new(big.Int).SetBytes(buf.Bytes()), extraBits)
|
|
}
|
|
func (b Binary) JQValueSlice(start int, end int) any {
|
|
rStart := int64(start * b.unit)
|
|
rLen := int64((end - start) * b.unit)
|
|
|
|
return Binary{
|
|
br: b.br,
|
|
r: ranges.Range{Start: b.r.Start + rStart, Len: rLen},
|
|
unit: b.unit,
|
|
}
|
|
}
|
|
func (b Binary) JQValueKey(name string) any {
|
|
switch name {
|
|
case "bits":
|
|
if b.unit == 1 {
|
|
return b
|
|
}
|
|
return Binary{br: b.br, r: b.r, unit: 1}
|
|
case "bytes":
|
|
if b.unit == 8 {
|
|
return b
|
|
}
|
|
return Binary{br: b.br, r: b.r, unit: 8}
|
|
|
|
case "name":
|
|
f := ioex.Unwrap(b.br)
|
|
// this exploits the fact that *os.File has Name()
|
|
if n, ok := f.(interface{ Name() string }); ok {
|
|
return n.Name()
|
|
}
|
|
return nil
|
|
case "size":
|
|
return new(big.Int).SetInt64(b.r.Len / int64(b.unit))
|
|
case "start":
|
|
return new(big.Int).SetInt64(b.r.Start / int64(b.unit))
|
|
case "stop":
|
|
stop := b.r.Stop()
|
|
stopUnits := stop / int64(b.unit)
|
|
if stop%int64(b.unit) != 0 {
|
|
stopUnits++
|
|
}
|
|
return new(big.Int).SetInt64(stopUnits)
|
|
case "unit":
|
|
return b.unit
|
|
}
|
|
return nil
|
|
}
|
|
func (b Binary) JQValueEach() any {
|
|
return nil
|
|
}
|
|
func (b Binary) JQValueType() string {
|
|
return gojq.JQTypeString
|
|
}
|
|
func (b Binary) JQValueKeys() any {
|
|
return gojqex.FuncTypeNameError{Name: "keys", Typ: gojq.JQTypeString}
|
|
}
|
|
func (b Binary) JQValueHas(key any) any {
|
|
return gojqex.HasKeyTypeError{L: gojq.JQTypeString, R: fmt.Sprintf("%v", key)}
|
|
}
|
|
func (b Binary) JQValueToNumber() any {
|
|
buf, err := b.toBytesBuffer(b.r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extraBits := uint((8 - b.r.Len%8) % 8)
|
|
return new(big.Int).Rsh(new(big.Int).SetBytes(buf.Bytes()), extraBits)
|
|
}
|
|
func (b Binary) JQValueToString() any {
|
|
return b.JQValueToGoJQ()
|
|
}
|
|
func (b Binary) JQValueToGoJQEx(optsFn func() (*Options, error)) any {
|
|
br, err := b.toReader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
brC, err := bitio.CloneReaderAtSeeker(br)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opts, err := optsFn()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s, err := opts.BitsFormatFn(brC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (b Binary) JQValueToGoJQ() any {
|
|
buf, err := b.toBytesBuffer(b.r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (b Binary) Display(w io.Writer, opts *Options) error {
|
|
if opts.RawOutput {
|
|
br, err := b.toReader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := bitioex.CopyBits(w, br); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return hexdump(w, b, opts)
|
|
}
|
|
|
|
func (b Binary) toReader() (bitio.ReaderAtSeeker, error) {
|
|
br, err := bitioex.Range(b.br, b.r.Start, b.r.Len)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if b.pad == 0 {
|
|
return br, nil
|
|
}
|
|
return bitio.NewMultiReader(bitioex.NewZeroAtSeeker(b.pad), br)
|
|
}
|