feat(verify): add --call and --buf flags for single-function invocation
Assisted-by: GLM 5.2
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// BufSpec is one buffer allocation request parsed from the user's --buf spec.
|
||||
type BufSpec struct {
|
||||
Name string
|
||||
Size int // declared slice length and capacity
|
||||
Pattern string // "zero", "ones", "seq", or a hex blob
|
||||
}
|
||||
|
||||
// ParseBufSpec parses a "name:size:pattern[,name:size:pattern]" spec string
|
||||
// into individual buffer specs. Empty input yields an empty slice.
|
||||
func ParseBufSpec(spec string) ([]BufSpec, error) {
|
||||
if spec == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var out []BufSpec
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) != 3 {
|
||||
return nil, fmt.Errorf("verify: invalid buffer spec %q (expected name:size:pattern)", part)
|
||||
}
|
||||
var size int
|
||||
if _, err := fmt.Sscanf(fields[1], "%d", &size); err != nil || size <= 0 {
|
||||
return nil, fmt.Errorf("verify: invalid buffer size %q in %q", fields[1], part)
|
||||
}
|
||||
out = append(out, BufSpec{Name: fields[0], Size: size, Pattern: fields[2]})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// allocatedBuf is one live buffer in a pool.
|
||||
type allocatedBuf struct {
|
||||
spec BufSpec
|
||||
data []byte // Size + safetyMargin bytes; the first Size are the live region
|
||||
}
|
||||
|
||||
// safetyMargin is the extra bytes allocated past the declared size so SIMD
|
||||
// over-reads and functions that read slightly past len never touch unmapped
|
||||
// memory. Matches the margin used by the fuzz generator.
|
||||
const safetyMargin = 8192
|
||||
|
||||
// BufPool is a set of allocated buffers held alive for the duration of one or
|
||||
// more calls. Buffers live on the Go heap (the JIT call is in-process); the
|
||||
// pool keeps the backing slices referenced so the GC does not collect them
|
||||
// before the call returns.
|
||||
type BufPool struct {
|
||||
bufs []allocatedBuf
|
||||
}
|
||||
|
||||
// Alloc allocates and fills the buffers described by specs. The returned
|
||||
// pool must be kept alive until every call using it has returned.
|
||||
func (p *BufPool) Alloc(specs []BufSpec) error {
|
||||
for _, s := range specs {
|
||||
data := make([]byte, s.Size+safetyMargin)
|
||||
fillBuffer(data, s.Pattern)
|
||||
p.bufs = append(p.bufs, allocatedBuf{spec: s, data: data})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close releases the pool. No-op for Go-heap buffers, but keeps the API
|
||||
// symmetric with debug's mmap-backed pool.
|
||||
func (p *BufPool) Close() {
|
||||
p.bufs = nil
|
||||
}
|
||||
|
||||
// findByName returns the buffer with the given spec name, if any.
|
||||
func (p *BufPool) findByName(name string) *allocatedBuf {
|
||||
for i := range p.bufs {
|
||||
if p.bufs[i].spec.Name == name {
|
||||
return &p.bufs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildArgs constructs an ABI0 argument block of argSize bytes for the given
|
||||
// layout, placing each buffer's pointer/length/capacity at the matching
|
||||
// parameter offset. Parameters whose names match a buffer spec get the
|
||||
// buffer address; non-pointer parameters and unmatched pointers are zeroed.
|
||||
//
|
||||
// Matching is by exact name, then by prefix (a buffer named "src" matches a
|
||||
// parameter named "src" or "srcBuf"), mirroring the debug allocator.
|
||||
func (p *BufPool) BuildArgs(layout []ArgOffset, argSize int) []byte {
|
||||
args := make([]byte, argSize)
|
||||
for _, a := range layout {
|
||||
if !a.IsPtr {
|
||||
continue
|
||||
}
|
||||
buf := p.matchBuf(a.Name)
|
||||
if buf == nil {
|
||||
continue
|
||||
}
|
||||
if a.Offset+8 <= len(args) {
|
||||
binary.LittleEndian.PutUint64(args[a.Offset:a.Offset+8], uint64(uintptr(unsafe.Pointer(&buf.data[0]))))
|
||||
}
|
||||
if strings.HasPrefix(a.Typ, "[]") && a.Offset+24 <= len(args) {
|
||||
binary.LittleEndian.PutUint64(args[a.Offset+8:a.Offset+16], uint64(buf.spec.Size))
|
||||
binary.LittleEndian.PutUint64(args[a.Offset+16:a.Offset+24], uint64(buf.spec.Size))
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// matchBuf finds a buffer matching the parameter name (exact, then prefix).
|
||||
func (p *BufPool) matchBuf(name string) *allocatedBuf {
|
||||
if b := p.findByName(name); b != nil {
|
||||
return b
|
||||
}
|
||||
for i := range p.bufs {
|
||||
if strings.HasPrefix(name, p.bufs[i].spec.Name) {
|
||||
return &p.bufs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fillBuffer fills buf with the named pattern: "zero" (no-op, already zeroed),
|
||||
// "ones" (0xFF), "seq" (i mod 256), or a hex blob repeated to fill.
|
||||
func fillBuffer(buf []byte, pattern string) {
|
||||
switch pattern {
|
||||
case "zero":
|
||||
// Already zeroed by make.
|
||||
case "ones":
|
||||
for i := range buf {
|
||||
buf[i] = 0xFF
|
||||
}
|
||||
case "seq":
|
||||
for i := range buf {
|
||||
buf[i] = byte(i)
|
||||
}
|
||||
default:
|
||||
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
||||
for i := range buf {
|
||||
buf[i] = data[i%len(data)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseParamsExported(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
names []string
|
||||
types []string
|
||||
isPtr []bool
|
||||
}{
|
||||
{
|
||||
input: "dst []byte, src []byte",
|
||||
names: []string{"dst", "src"},
|
||||
types: []string{"[]byte", "[]byte"},
|
||||
isPtr: []bool{true, true},
|
||||
},
|
||||
{
|
||||
input: "dst, src []byte",
|
||||
names: []string{"dst", "src"},
|
||||
types: []string{"[]byte", "[]byte"},
|
||||
isPtr: []bool{true, true},
|
||||
},
|
||||
{
|
||||
input: "a, b int",
|
||||
names: []string{"a", "b"},
|
||||
types: []string{"int", "int"},
|
||||
isPtr: []bool{false, false},
|
||||
},
|
||||
{
|
||||
input: "src []byte, dst []byte",
|
||||
names: []string{"src", "dst"},
|
||||
types: []string{"[]byte", "[]byte"},
|
||||
isPtr: []bool{true, true},
|
||||
},
|
||||
{
|
||||
input: "swin []int32, dstP []uint32, hist *[32]uint16",
|
||||
names: []string{"swin", "dstP", "hist"},
|
||||
types: []string{"[]int32", "[]uint32", "*[32]uint16"},
|
||||
isPtr: []bool{true, true, true},
|
||||
},
|
||||
{
|
||||
input: "n int, code int",
|
||||
names: []string{"n", "code"},
|
||||
types: []string{"int", "int"},
|
||||
isPtr: []bool{false, false},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
params := parseParamsExported(tt.input)
|
||||
if len(params) != len(tt.names) {
|
||||
t.Errorf("parseParamsExported(%q): got %d params, want %d", tt.input, len(params), len(tt.names))
|
||||
continue
|
||||
}
|
||||
for i, p := range params {
|
||||
if p.Name != tt.names[i] {
|
||||
t.Errorf("parseParamsExported(%q)[%d].Name = %q, want %q", tt.input, i, p.Name, tt.names[i])
|
||||
}
|
||||
if p.Typ != tt.types[i] {
|
||||
t.Errorf("parseParamsExported(%q)[%d].Typ = %q, want %q", tt.input, i, p.Typ, tt.types[i])
|
||||
}
|
||||
if p.IsPointer() != tt.isPtr[i] {
|
||||
t.Errorf("parseParamsExported(%q)[%d].IsPointer() = %v, want %v", tt.input, i, p.IsPointer(), tt.isPtr[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBufSpec(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
specs, err := ParseBufSpec("")
|
||||
if err != nil || len(specs) != 0 {
|
||||
t.Errorf("ParseBufSpec(\"\") = %v, %v; want nil, nil", specs, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single", func(t *testing.T) {
|
||||
specs, err := ParseBufSpec("dst:64:zero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(specs) != 1 || specs[0].Name != "dst" || specs[0].Size != 64 || specs[0].Pattern != "zero" {
|
||||
t.Errorf("ParseBufSpec(\"dst:64:zero\") = %+v; want [{dst 64 zero}]", specs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple", func(t *testing.T) {
|
||||
specs, err := ParseBufSpec("dst:64:zero,src:128:seq")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(specs) != 2 {
|
||||
t.Fatalf("got %d specs, want 2", len(specs))
|
||||
}
|
||||
if specs[0].Name != "dst" || specs[1].Name != "src" {
|
||||
t.Errorf("names = %s, %s; want dst, src", specs[0].Name, specs[1].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
_, err := ParseBufSpec("bad")
|
||||
if err == nil {
|
||||
t.Error("ParseBufSpec(\"bad\") should error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero-size", func(t *testing.T) {
|
||||
_, err := ParseBufSpec("dst:0:zero")
|
||||
if err == nil {
|
||||
t.Error("ParseBufSpec(\"dst:0:zero\") should error on zero size")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBufPoolBuildArgs(t *testing.T) {
|
||||
specs, err := ParseBufSpec("dst:64:seq,src:128:zero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var pool BufPool
|
||||
if err := pool.Alloc(specs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Layout for wideCopyAVX2(dst, src []byte): dst at 0, src at 24.
|
||||
layout := []ArgOffset{
|
||||
{Name: "dst", Typ: "[]byte", Offset: 0, Size: 24, IsPtr: true},
|
||||
{Name: "src", Typ: "[]byte", Offset: 24, Size: 24, IsPtr: true},
|
||||
}
|
||||
args := pool.BuildArgs(layout, 48)
|
||||
|
||||
// dst.ptr should be non-zero.
|
||||
if args[0] == 0 && args[1] == 0 && args[2] == 0 && args[3] == 0 {
|
||||
t.Error("dst.ptr is zero; expected a buffer address")
|
||||
}
|
||||
// dst.len should be 64 (0x40).
|
||||
if args[8] != 0x40 {
|
||||
t.Errorf("dst.len = %d, want 64", args[8])
|
||||
}
|
||||
// dst.cap should be 64.
|
||||
if args[16] != 0x40 {
|
||||
t.Errorf("dst.cap = %d, want 64", args[16])
|
||||
}
|
||||
// src.ptr should be non-zero.
|
||||
if args[24] == 0 && args[25] == 0 && args[26] == 0 && args[27] == 0 {
|
||||
t.Error("src.ptr is zero; expected a buffer address")
|
||||
}
|
||||
// src.len should be 128 (0x80).
|
||||
if args[32] != 0x80 {
|
||||
t.Errorf("src.len = %d, want 128", args[32])
|
||||
}
|
||||
}
|
||||
+25
-8
@@ -79,22 +79,39 @@ func parseParams(s string) []param {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
fields := strings.Split(s, ",")
|
||||
// First pass: extract the type from each field (if present).
|
||||
types := make([]string, len(fields))
|
||||
for i, field := range fields {
|
||||
parts := strings.Fields(strings.TrimSpace(field))
|
||||
if len(parts) >= 2 {
|
||||
types[i] = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
// Propagate types backward: a field without a type inherits from the next
|
||||
// field that has one (e.g. "dst" inherits "[]byte" from "src []byte").
|
||||
for i := range fields {
|
||||
if types[i] == "" {
|
||||
for j := i + 1; j < len(fields); j++ {
|
||||
if types[j] != "" {
|
||||
types[i] = types[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var out []param
|
||||
for _, field := range strings.Split(s, ",") {
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(field)
|
||||
if len(parts) == 1 {
|
||||
// Unnamed: "int" or "[]byte".
|
||||
typ := types[i]
|
||||
if typ == "" {
|
||||
out = append(out, param{typ: parts[0]})
|
||||
} else {
|
||||
// Named: "a []byte" or shared "a, b []int32" (handled by the
|
||||
// comma split above — "a" alone means the type follows in the
|
||||
// next field; this is a simplification that covers the common
|
||||
// case where each param has its own type).
|
||||
out = append(out, param{name: parts[0], typ: parts[1]})
|
||||
out = append(out, param{name: parts[0], typ: typ})
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
+31
-7
@@ -44,25 +44,49 @@ func ParseFuncSig(comment string) (FuncSig, bool) {
|
||||
return sig, true
|
||||
}
|
||||
|
||||
// parseParamsExported splits "a []byte, b []int32" into typed parameters.
|
||||
// parseParamsExported splits a parameter list like "a []byte, b []int32" or
|
||||
// "dst, src []byte" into typed parameters. Go syntax allows grouped names
|
||||
// where the type at the end applies to every name in the group:// "dst, src []byte" means both dst and src are []byte.
|
||||
func parseParamsExported(s string) []Param {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
fields := strings.Split(s, ",")
|
||||
// First pass: extract the type from each field (if present).
|
||||
types := make([]string, len(fields))
|
||||
for i, field := range fields {
|
||||
parts := strings.Fields(strings.TrimSpace(field))
|
||||
if len(parts) >= 2 {
|
||||
types[i] = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
// Propagate types backward: a field without a type inherits the type from
|
||||
// the next field that has one (e.g. "dst" inherits "[]byte" from "src []byte").
|
||||
for i := range fields {
|
||||
if types[i] == "" {
|
||||
for j := i + 1; j < len(fields); j++ {
|
||||
if types[j] != "" {
|
||||
types[i] = types[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Second pass: build params.
|
||||
var out []Param
|
||||
for _, field := range strings.Split(s, ",") {
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(field)
|
||||
if len(parts) == 1 {
|
||||
// Unnamed: "int" or "[]byte".
|
||||
out = append(out, Param{Typ: parts[0]})
|
||||
typ := types[i]
|
||||
if typ == "" {
|
||||
typ = parts[0] // unnamed: the whole field is the type
|
||||
out = append(out, Param{Typ: typ})
|
||||
} else {
|
||||
// Named: "a []byte".
|
||||
out = append(out, Param{Name: parts[0], Typ: parts[1]})
|
||||
out = append(out, Param{Name: parts[0], Typ: typ})
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user