summaryrefslogtreecommitdiff
path: root/internal/generate
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-21 11:51:01 +0200
committerPaul Buetow <paul@buetow.org>2026-02-21 11:51:01 +0200
commit6c912a9d72ae2a43923c638538d320e6bf585952 (patch)
tree727f66d158210e01abf8c18a83ef4db6066e0c1a /internal/generate
parent32136b8cb18944157ff1f361bc0755f6b627fd47 (diff)
Migrate make targets to mage
Amp-Thread-ID: https://ampcode.com/threads/T-019c7f4e-cc5f-76f1-aaf0-dd7cbaabbb18 Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal/generate')
-rw-r--r--internal/generate/bpfhandler.go152
-rw-r--r--internal/generate/classify.go214
-rw-r--r--internal/generate/classify_test.go332
-rw-r--r--internal/generate/codegen.go152
-rw-r--r--internal/generate/codegen_test.go263
-rw-r--r--internal/generate/format.go145
-rw-r--r--internal/generate/format_test.go174
-rw-r--r--internal/generate/retclassify_test.go59
-rw-r--r--internal/generate/testdata.go666
-rw-r--r--internal/generate/tracepointsgo.go43
-rw-r--r--internal/generate/tracepointsgo_test.go92
-rw-r--r--internal/generate/typesgo.go341
-rw-r--r--internal/generate/typesgo_test.go257
13 files changed, 2890 insertions, 0 deletions
diff --git a/internal/generate/bpfhandler.go b/internal/generate/bpfhandler.go
new file mode 100644
index 0000000..1ce6d3e
--- /dev/null
+++ b/internal/generate/bpfhandler.go
@@ -0,0 +1,152 @@
+package generate
+
+import (
+ "fmt"
+ "strings"
+)
+
+func generateBPFHandler(tp GeneratedTracepoint) string {
+ f := tp.Format
+ isEnter := strings.Split(f.Name, "_")[1] == "enter"
+
+ ctxStruct := "trace_event_raw_sys_exit"
+ if isEnter {
+ ctxStruct = "trace_event_raw_sys_enter"
+ }
+
+ eventStruct := eventStructName(tp.Classification.Kind)
+ comment := eventStruct
+ if tp.Classification.Kind == KindRet {
+ comment = fmt.Sprintf("%s (%s)", eventStruct, ClassifyRet(f.Name))
+ }
+
+ eventTypeConst := eventTypeConstant(tp.Classification.Kind, isEnter)
+ extra := generateExtra(tp, isEnter)
+
+ return renderHandler(f.Name, ctxStruct, eventStruct, comment, eventTypeConst, extra)
+}
+
+func renderHandler(name, ctxStruct, eventStruct, comment, eventTypeConst, extra string) string {
+ var b strings.Builder
+ fmt.Fprintf(&b, "/// %s is a struct %s\n", name, comment)
+ fmt.Fprintf(&b, "SEC(\"tracepoint/syscalls/%s\")\n", name)
+ fmt.Fprintf(&b, "int handle_%s(struct %s *ctx) {\n", strings.ToLower(name), ctxStruct)
+ b.WriteString(" __u32 pid, tid;\n")
+ b.WriteString(" if (filter(&pid, &tid))\n")
+ b.WriteString(" return 0;\n")
+ b.WriteString("\n")
+ fmt.Fprintf(&b, " struct %s *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct %s), 0);\n", eventStruct, eventStruct)
+ b.WriteString(" if (!ev)\n")
+ b.WriteString(" return 0;\n")
+ b.WriteString("\n")
+ fmt.Fprintf(&b, " ev->event_type = %s;\n", eventTypeConst)
+ fmt.Fprintf(&b, " ev->trace_id = %s;\n", strings.ToUpper(name))
+ b.WriteString(" ev->pid = pid;\n")
+ b.WriteString(" ev->tid = tid;\n")
+ b.WriteString(" ev->time = bpf_ktime_get_boot_ns();\n")
+ if extra != "" {
+ b.WriteString(extra)
+ }
+ b.WriteString("\n")
+ b.WriteString(" bpf_ringbuf_submit(ev, 0);\n")
+ b.WriteString(" return 0;\n")
+ b.WriteString("}\n")
+ return b.String()
+}
+
+func generateExtra(tp GeneratedTracepoint, isEnter bool) string {
+ f := tp.Format
+
+ switch tp.Classification.Kind {
+ case KindFd:
+ return " ev->fd = (__s32)ctx->args[0];\n"
+
+ case KindDup3:
+ return " ev->fd = (__s32)ctx->args[0];\n ev->flags = (__s32)ctx->args[2];\n"
+
+ case KindOpenByHandleAt:
+ return " ev->flags = (__s32)ctx->args[2];\n"
+
+ case KindOpen:
+ filenameIdx := f.FieldNumber("filename")
+ flagsIdx := f.FieldNumber("flags")
+ var b strings.Builder
+ b.WriteString(" __builtin_memset(&(ev->filename), 0, sizeof(ev->filename) + sizeof(ev->comm));\n")
+ fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->filename, sizeof(ev->filename), (void *)ctx->args[%d]);\n", filenameIdx)
+ b.WriteString(" bpf_get_current_comm(&ev->comm, sizeof(ev->comm));\n")
+ if flagsIdx > -1 {
+ fmt.Fprintf(&b, " ev->flags = ctx->args[%d];\n", flagsIdx)
+ } else {
+ b.WriteString(" ev->flags = -1; // Probably OK\n")
+ }
+ return b.String()
+
+ case KindPathname:
+ fieldName := tp.Classification.PathnameField
+ fieldIdx := f.FieldNumber(fieldName)
+ var b strings.Builder
+ b.WriteString(" __builtin_memset(&(ev->pathname), 0, sizeof(ev->pathname));\n")
+ fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[%d]);\n", fieldIdx)
+ return b.String()
+
+ case KindName:
+ oldIdx := f.FieldNumber("oldname")
+ newIdx := f.FieldNumber("newname")
+ var b strings.Builder
+ b.WriteString(" __builtin_memset(&(ev->oldname), 0, sizeof(ev->oldname) + sizeof(ev->newname));\n")
+ fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->oldname, sizeof(ev->oldname), (void*)ctx->args[%d]);\n", oldIdx)
+ fmt.Fprintf(&b, " bpf_probe_read_user_str(ev->newname, sizeof(ev->newname), (void*)ctx->args[%d]);\n", newIdx)
+ return b.String()
+
+ case KindFcntl:
+ fdIdx := f.FieldNumber("fd")
+ cmdIdx := f.FieldNumber("cmd")
+ argIdx := f.FieldNumber("arg")
+ return fmt.Sprintf(
+ " ev->fd = ctx->args[%d];\n ev->cmd = ctx->args[%d];\n ev->arg = ctx->args[%d];\n",
+ fdIdx, cmdIdx, argIdx,
+ )
+
+ case KindRet:
+ classification := ClassifyRet(f.Name)
+ return fmt.Sprintf(" ev->ret = ctx->ret;\n ev->ret_type = %s;\n", classification)
+
+ case KindNull:
+ return ""
+ }
+
+ return ""
+}
+
+func eventStructName(kind TracepointKind) string {
+ switch kind {
+ case KindFd:
+ return "fd_event"
+ case KindOpen:
+ return "open_event"
+ case KindPathname:
+ return "path_event"
+ case KindName:
+ return "name_event"
+ case KindRet:
+ return "ret_event"
+ case KindFcntl:
+ return "fcntl_event"
+ case KindNull:
+ return "null_event"
+ case KindDup3:
+ return "dup3_event"
+ case KindOpenByHandleAt:
+ return "open_by_handle_at_event"
+ default:
+ return "unknown_event"
+ }
+}
+
+func eventTypeConstant(kind TracepointKind, isEnter bool) string {
+ prefix := "EXIT_"
+ if isEnter {
+ prefix = "ENTER_"
+ }
+ return prefix + strings.ToUpper(eventStructName(kind))
+}
diff --git a/internal/generate/classify.go b/internal/generate/classify.go
new file mode 100644
index 0000000..75a12fe
--- /dev/null
+++ b/internal/generate/classify.go
@@ -0,0 +1,214 @@
+package generate
+
+import "strings"
+
+type TracepointKind int
+
+const (
+ KindNone TracepointKind = iota
+ KindFd
+ KindOpen
+ KindPathname
+ KindName
+ KindRet
+ KindFcntl
+ KindNull
+ KindDup3
+ KindOpenByHandleAt
+)
+
+type RetClassification string
+
+const (
+ Unclassified RetClassification = "UNCLASSIFIED"
+ ReadClassified RetClassification = "READ_CLASSIFIED"
+ WriteClassified RetClassification = "WRITE_CLASSIFIED"
+ TransferClassified RetClassification = "TRANSFER_CLASSIFIED"
+)
+
+type ClassificationResult struct {
+ Kind TracepointKind
+ PathnameField string // for KindPathname: "pathname", "path", or "filename"
+}
+
+// ClassifyFormat determines the tracepoint kind for a parsed format section.
+// It mirrors the Raku multi-dispatch: name-based ignores take priority,
+// then name-only mappings, then each external field is tried in order until
+// one matches a name+field or generic field pattern.
+func ClassifyFormat(f *Format) ClassificationResult {
+ if len(f.ExternalFields) == 0 {
+ return ClassificationResult{Kind: KindNone}
+ }
+
+ if shouldIgnore(f.Name) {
+ return ClassificationResult{Kind: KindNone}
+ }
+
+ if r, ok := classifyNameOnly(f.Name); ok {
+ return r
+ }
+
+ for _, field := range f.ExternalFields {
+ if field.Name == "__syscall_nr" {
+ continue
+ }
+ if r, ok := classifyNameAndField(f.Name, field.Type, field.Name); ok {
+ return r
+ }
+ if r, ok := classifyByField(field.Type, field.Name); ok {
+ return r
+ }
+ }
+
+ return ClassificationResult{Kind: KindNone}
+}
+
+func shouldIgnore(name string) bool {
+ prefixIgnores := []string{
+ "sys_enter_mknod",
+ "sys_enter_execve",
+ "sys_enter_accept",
+ "sys_enter_listen",
+ "sys_enter_epoll",
+ }
+ for _, p := range prefixIgnores {
+ if strings.HasPrefix(name, p) {
+ return true
+ }
+ }
+
+ if strings.HasPrefix(name, "sys_enter_") {
+ containsIgnores := []string{"recv", "send", "sock", "inotify", "pidfd"}
+ for _, sub := range containsIgnores {
+ if strings.Contains(name, sub) {
+ return true
+ }
+ }
+ }
+
+ exactIgnores := map[string]bool{
+ "sys_enter_bind": true,
+ "sys_enter_setns": true,
+ "sys_enter_shutdown": true,
+ "sys_enter_connect": true,
+ "sys_enter_fanotify_init": true,
+ "sys_enter_getpeername": true,
+ }
+ return exactIgnores[name]
+}
+
+// classifyNameOnly handles tracepoints classified by name alone,
+// independent of any field.
+func classifyNameOnly(name string) (ClassificationResult, bool) {
+ switch name {
+ case "sys_enter_open_by_handle_at":
+ return ClassificationResult{Kind: KindOpenByHandleAt}, true
+ case "sys_enter_fcntl":
+ return ClassificationResult{Kind: KindFcntl}, true
+ case "sys_enter_syslog":
+ return ClassificationResult{Kind: KindNull}, true
+ case "sys_enter_sync":
+ return ClassificationResult{Kind: KindNull}, true
+ }
+ if strings.HasPrefix(name, "sys_enter_io_") {
+ return ClassificationResult{Kind: KindNull}, true
+ }
+ return ClassificationResult{}, false
+}
+
+// classifyNameAndField handles tracepoints that need both the name and
+// a specific field to classify.
+func classifyNameAndField(name, fieldType, fieldName string) (ClassificationResult, bool) {
+ switch name {
+ case "sys_enter_dup":
+ if fieldType == "unsigned int" && fieldName == "fildes" {
+ return ClassificationResult{Kind: KindFd}, true
+ }
+ case "sys_enter_dup2":
+ if fieldType == "unsigned int" && fieldName == "oldfd" {
+ return ClassificationResult{Kind: KindFd}, true
+ }
+ case "sys_enter_dup3":
+ if fieldType == "unsigned int" && fieldName == "oldfd" {
+ return ClassificationResult{Kind: KindDup3}, true
+ }
+ }
+
+ if strings.HasPrefix(name, "sys_enter") &&
+ strings.Contains(name, "open") &&
+ fieldType == "const char *" && fieldName == "filename" {
+ return ClassificationResult{Kind: KindOpen}, true
+ }
+
+ return ClassificationResult{}, false
+}
+
+func classifyByField(fieldType, fieldName string) (ClassificationResult, bool) {
+ switch {
+ case fieldName == "fd" && isFdType(fieldType):
+ return ClassificationResult{Kind: KindFd}, true
+ case fieldType == "const char *" && fieldName == "newname":
+ return ClassificationResult{Kind: KindName}, true
+ case fieldType == "const char *" && fieldName == "pathname":
+ return ClassificationResult{Kind: KindPathname, PathnameField: "pathname"}, true
+ case fieldType == "const char *" && fieldName == "path":
+ return ClassificationResult{Kind: KindPathname, PathnameField: "path"}, true
+ case fieldType == "const char *" && fieldName == "filename":
+ return ClassificationResult{Kind: KindPathname, PathnameField: "filename"}, true
+ case fieldType == "long" && fieldName == "ret":
+ return ClassificationResult{Kind: KindRet}, true
+ }
+ return ClassificationResult{}, false
+}
+
+func isFdType(t string) bool {
+ return t == "unsigned int" || t == "unsigned long" || t == "int"
+}
+
+// ClassifyRet returns the RetClassification for a syscall exit name.
+func ClassifyRet(name string) RetClassification {
+ syscall := strings.ToLower(strings.TrimPrefix(name, "sys_exit_"))
+ if c, ok := retClassifications[syscall]; ok {
+ return c
+ }
+ return Unclassified
+}
+
+var retClassifications = map[string]RetClassification{
+ "fgetxattr": ReadClassified,
+ "flistxattr": ReadClassified,
+ "getdents": ReadClassified,
+ "getdents64": ReadClassified,
+ "getxattr": ReadClassified,
+ "lgetxattr": ReadClassified,
+ "listxattr": ReadClassified,
+ "llistxattr": ReadClassified,
+ "pread64": ReadClassified,
+ "preadv": ReadClassified,
+ "preadv2": ReadClassified,
+ "process_vm_readv": ReadClassified,
+ "read": ReadClassified,
+ "readlink": ReadClassified,
+ "readlinkat": ReadClassified,
+ "readv": ReadClassified,
+ "recvmmsg": ReadClassified,
+ "recvmsg": ReadClassified,
+ "recvfrom": ReadClassified,
+ "syslog": ReadClassified,
+
+ "copy_file_range": TransferClassified,
+ "sendfile64": TransferClassified,
+ "splice": TransferClassified,
+ "tee": TransferClassified,
+ "vmsplice": TransferClassified,
+
+ "process_vm_writev": WriteClassified,
+ "pwrite64": WriteClassified,
+ "pwritev": WriteClassified,
+ "pwritev2": WriteClassified,
+ "sendmmsg": WriteClassified,
+ "sendmsg": WriteClassified,
+ "sendto": WriteClassified,
+ "write": WriteClassified,
+ "writev": WriteClassified,
+}
diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go
new file mode 100644
index 0000000..c94e359
--- /dev/null
+++ b/internal/generate/classify_test.go
@@ -0,0 +1,332 @@
+package generate
+
+import (
+ "strings"
+ "testing"
+)
+
+func classifyFromData(t *testing.T, data string) ClassificationResult {
+ t.Helper()
+ f := mustParseOne(t, data)
+ return ClassifyFormat(&f)
+}
+
+func TestClassifyFdRead(t *testing.T) {
+ r := classifyFromData(t, FormatRead)
+ if r.Kind != KindFd {
+ t.Errorf("read: got kind %d, want KindFd", r.Kind)
+ }
+}
+
+func TestClassifyFdClose(t *testing.T) {
+ r := classifyFromData(t, FormatClose)
+ if r.Kind != KindFd {
+ t.Errorf("close: got kind %d, want KindFd", r.Kind)
+ }
+}
+
+func TestClassifyFdPread64(t *testing.T) {
+ r := classifyFromData(t, FormatPread64)
+ if r.Kind != KindFd {
+ t.Errorf("pread64: got kind %d, want KindFd", r.Kind)
+ }
+}
+
+func TestClassifyFdWrite(t *testing.T) {
+ r := classifyFromData(t, FormatWrite)
+ if r.Kind != KindFd {
+ t.Errorf("write: got kind %d, want KindFd", r.Kind)
+ }
+}
+
+func TestClassifyOpenOpenat(t *testing.T) {
+ r := classifyFromData(t, FormatOpenat)
+ if r.Kind != KindOpen {
+ t.Errorf("openat: got kind %d, want KindOpen", r.Kind)
+ }
+}
+
+func TestClassifyOpenOpen(t *testing.T) {
+ r := classifyFromData(t, FormatOpen)
+ if r.Kind != KindOpen {
+ t.Errorf("open: got kind %d, want KindOpen", r.Kind)
+ }
+}
+
+func TestClassifyOpenOpenat2(t *testing.T) {
+ r := classifyFromData(t, FormatOpenat2)
+ if r.Kind != KindOpen {
+ t.Errorf("openat2: got kind %d, want KindOpen", r.Kind)
+ }
+}
+
+func TestClassifyPathnameCreat(t *testing.T) {
+ r := classifyFromData(t, FormatCreat)
+ if r.Kind != KindPathname {
+ t.Errorf("creat: got kind %d, want KindPathname", r.Kind)
+ }
+ if r.PathnameField != "pathname" {
+ t.Errorf("creat: PathnameField = %q, want pathname", r.PathnameField)
+ }
+}
+
+func TestClassifyPathnameUnlink(t *testing.T) {
+ r := classifyFromData(t, FormatUnlink)
+ if r.Kind != KindPathname {
+ t.Errorf("unlink: got kind %d, want KindPathname", r.Kind)
+ }
+ if r.PathnameField != "pathname" {
+ t.Errorf("unlink: PathnameField = %q, want pathname", r.PathnameField)
+ }
+}
+
+func TestClassifyNameRename(t *testing.T) {
+ r := classifyFromData(t, FormatRename)
+ if r.Kind != KindName {
+ t.Errorf("rename: got kind %d, want KindName", r.Kind)
+ }
+}
+
+func TestClassifyNameLinkat(t *testing.T) {
+ r := classifyFromData(t, FormatLinkat)
+ if r.Kind != KindName {
+ t.Errorf("linkat: got kind %d, want KindName", r.Kind)
+ }
+}
+
+func TestClassifyNameSymlink(t *testing.T) {
+ r := classifyFromData(t, FormatSymlink)
+ if r.Kind != KindName {
+ t.Errorf("symlink: got kind %d, want KindName", r.Kind)
+ }
+}
+
+func TestClassifyFcntl(t *testing.T) {
+ r := classifyFromData(t, FormatFcntl)
+ if r.Kind != KindFcntl {
+ t.Errorf("fcntl: got kind %d, want KindFcntl", r.Kind)
+ }
+}
+
+func TestClassifyDup(t *testing.T) {
+ r := classifyFromData(t, FormatDup)
+ if r.Kind != KindFd {
+ t.Errorf("dup: got kind %d, want KindFd", r.Kind)
+ }
+}
+
+func TestClassifyDup2(t *testing.T) {
+ r := classifyFromData(t, FormatDup2)
+ if r.Kind != KindFd {
+ t.Errorf("dup2: got kind %d, want KindFd", r.Kind)
+ }
+}
+
+func TestClassifyDup3(t *testing.T) {
+ r := classifyFromData(t, FormatDup3)
+ if r.Kind != KindDup3 {
+ t.Errorf("dup3: got kind %d, want KindDup3", r.Kind)
+ }
+}
+
+func TestClassifyOpenByHandleAt(t *testing.T) {
+ r := classifyFromData(t, FormatOpenByHandleAt)
+ if r.Kind != KindOpenByHandleAt {
+ t.Errorf("open_by_handle_at: got kind %d, want KindOpenByHandleAt", r.Kind)
+ }
+}
+
+func TestClassifyNullSync(t *testing.T) {
+ r := classifyFromData(t, FormatSync)
+ if r.Kind != KindNull {
+ t.Errorf("sync: got kind %d, want KindNull", r.Kind)
+ }
+}
+
+func TestClassifyNullSyslog(t *testing.T) {
+ r := classifyFromData(t, FormatSyslog)
+ if r.Kind != KindNull {
+ t.Errorf("syslog: got kind %d, want KindNull", r.Kind)
+ }
+}
+
+func TestClassifyNullIoUring(t *testing.T) {
+ r := classifyFromData(t, FormatIoUringEnter)
+ if r.Kind != KindNull {
+ t.Errorf("io_uring_enter: got kind %d, want KindNull", r.Kind)
+ }
+}
+
+func TestClassifyRetExitRead(t *testing.T) {
+ r := classifyFromData(t, FormatExitRead)
+ if r.Kind != KindRet {
+ t.Errorf("exit_read: got kind %d, want KindRet", r.Kind)
+ }
+}
+
+func TestClassifyRetExitWrite(t *testing.T) {
+ r := classifyFromData(t, FormatExitWrite)
+ if r.Kind != KindRet {
+ t.Errorf("exit_write: got kind %d, want KindRet", r.Kind)
+ }
+}
+
+func TestClassifyRetExitOpenat(t *testing.T) {
+ r := classifyFromData(t, FormatExitOpenat)
+ if r.Kind != KindRet {
+ t.Errorf("exit_openat: got kind %d, want KindRet", r.Kind)
+ }
+}
+
+func TestClassifyRetExitPread64(t *testing.T) {
+ r := classifyFromData(t, FormatExitPread64)
+ if r.Kind != KindRet {
+ t.Errorf("exit_pread64: got kind %d, want KindRet", r.Kind)
+ }
+}
+
+func TestClassifyRetExitSymlink(t *testing.T) {
+ r := classifyFromData(t, FormatExitSymlink)
+ if r.Kind != KindRet {
+ t.Errorf("exit_symlink: got kind %d, want KindRet", r.Kind)
+ }
+}
+
+// --- Ignore tests ---
+
+func TestIgnoreMknod(t *testing.T) {
+ r := classifyFromData(t, FormatMknod)
+ if r.Kind != KindNone {
+ t.Errorf("mknod: got kind %d, want KindNone (ignored)", r.Kind)
+ }
+}
+
+func TestIgnoreExecve(t *testing.T) {
+ r := classifyFromData(t, FormatExecve)
+ if r.Kind != KindNone {
+ t.Errorf("execve: got kind %d, want KindNone (ignored)", r.Kind)
+ }
+}
+
+func TestIgnoreAccept(t *testing.T) {
+ r := classifyFromData(t, FormatAccept)
+ if r.Kind != KindNone {
+ t.Errorf("accept: got kind %d, want KindNone (ignored)", r.Kind)
+ }
+}
+
+func TestIgnoreSocket(t *testing.T) {
+ r := classifyFromData(t, FormatSocket)
+ if r.Kind != KindNone {
+ t.Errorf("socket: got kind %d, want KindNone (ignored)", r.Kind)
+ }
+}
+
+func TestIgnoreKill(t *testing.T) {
+ r := classifyFromData(t, FormatKill)
+ if r.Kind != KindNone {
+ t.Errorf("kill: got kind %d, want KindNone (no matching type)", r.Kind)
+ }
+}
+
+func TestShouldIgnorePatterns(t *testing.T) {
+ ignoreNames := []string{
+ "sys_enter_mknod", "sys_enter_mknodat",
+ "sys_enter_execve", "sys_enter_execveat",
+ "sys_enter_accept", "sys_enter_accept4",
+ "sys_enter_listen",
+ "sys_enter_epoll_ctl", "sys_enter_epoll_pwait",
+ "sys_enter_recvfrom", "sys_enter_recvmsg", "sys_enter_recvmmsg",
+ "sys_enter_sendto", "sys_enter_sendmsg", "sys_enter_sendmmsg",
+ "sys_enter_socket", "sys_enter_socketpair", "sys_enter_getsockname",
+ "sys_enter_inotify_init", "sys_enter_inotify_add_watch",
+ "sys_enter_pidfd_open", "sys_enter_pidfd_getfd",
+ "sys_enter_bind", "sys_enter_setns", "sys_enter_shutdown",
+ "sys_enter_connect", "sys_enter_fanotify_init", "sys_enter_getpeername",
+ }
+ for _, name := range ignoreNames {
+ if !shouldIgnore(name) {
+ t.Errorf("shouldIgnore(%q) = false, want true", name)
+ }
+ }
+}
+
+func TestShouldNotIgnore(t *testing.T) {
+ noIgnore := []string{
+ "sys_enter_read", "sys_enter_write", "sys_enter_openat",
+ "sys_enter_close", "sys_enter_rename", "sys_enter_unlink",
+ "sys_exit_read", "sys_exit_openat",
+ }
+ for _, name := range noIgnore {
+ if shouldIgnore(name) {
+ t.Errorf("shouldIgnore(%q) = true, want false", name)
+ }
+ }
+}
+
+// --- End-to-end classification with enter+exit pairs ---
+
+func TestClassifySyscallPairAccepted(t *testing.T) {
+ tests := []struct {
+ name string
+ enter string
+ exit string
+ enterKind TracepointKind
+ }{
+ {"read", FormatRead, FormatExitRead, KindFd},
+ {"openat", FormatOpenat, FormatExitOpenat, KindOpen},
+ {"rename", FormatRename, FormatExitRename, KindName},
+ {"close", FormatClose, FormatExitClose, KindFd},
+ {"dup3", FormatDup3, FormatExitDup3, KindDup3},
+ {"fcntl", FormatFcntl, FormatExitFcntl, KindFcntl},
+ {"sync", FormatSync, FormatExitSync, KindNull},
+ {"syslog", FormatSyslog, FormatExitSyslog, KindNull},
+ {"open_by_handle_at", FormatOpenByHandleAt, FormatExitOpenByHandleAt, KindOpenByHandleAt},
+ {"io_uring_enter", FormatIoUringEnter, FormatExitIoUringEnter, KindNull},
+ {"pread64", FormatPread64, FormatExitPread64, KindFd},
+ {"symlink", FormatSymlink, FormatExitSymlink, KindName},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ input := tt.enter + "\n" + tt.exit
+ output := GenerateTracepointsC(mustParseAll(t, input))
+ if strings.Contains(output, "Ignoring") {
+ t.Errorf("syscall %s was ignored, expected accepted", tt.name)
+ }
+ })
+ }
+}
+
+func TestClassifySyscallPairIgnored(t *testing.T) {
+ tests := []struct {
+ name string
+ enter string
+ exit string
+ }{
+ {"mknod", FormatMknod, FormatExitMknod},
+ {"execve", FormatExecve, FormatExitExecve},
+ {"accept", FormatAccept, FormatExitAccept},
+ {"socket", FormatSocket, FormatExitSocket},
+ {"kill", FormatKill, FormatExitKill},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ input := tt.enter + "\n" + tt.exit
+ output := GenerateTracepointsC(mustParseAll(t, input))
+ if !strings.Contains(output, "Ignoring") {
+ t.Errorf("syscall %s was accepted, expected ignored", tt.name)
+ }
+ })
+ }
+}
+
+func mustParseAll(t *testing.T, data string) []Format {
+ t.Helper()
+ formats, err := ParseFormats(strings.NewReader(data))
+ if err != nil {
+ t.Fatalf("ParseFormats failed: %v", err)
+ }
+ return formats
+}
diff --git a/internal/generate/codegen.go b/internal/generate/codegen.go
new file mode 100644
index 0000000..9b9f52c
--- /dev/null
+++ b/internal/generate/codegen.go
@@ -0,0 +1,152 @@
+package generate
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// Syscall groups enter+exit formats by syscall name.
+type Syscall struct {
+ Name string
+ Enter *Format
+ Exit *Format
+}
+
+// GeneratedTracepoint holds a classified format ready for code generation.
+type GeneratedTracepoint struct {
+ Format *Format
+ Classification ClassificationResult
+}
+
+// GenerateTracepointsC produces the full generated_tracepoints.c content from
+// concatenated sysfs format data parsed into formats.
+func GenerateTracepointsC(formats []Format) string {
+ syscalls := groupBySyscall(formats)
+ var b strings.Builder
+
+ b.WriteString("// Code generated - don't change manually!\n\n")
+
+ var accepted []GeneratedTracepoint
+ for _, sc := range syscalls {
+ tracepoints, reason := classifySyscall(sc)
+ if reason != "" {
+ fmt.Fprintf(&b, "/// %s\n", reason)
+ continue
+ }
+ accepted = append(accepted, tracepoints...)
+ }
+
+ sort.Slice(accepted, func(i, j int) bool {
+ return accepted[i].Format.ID > accepted[j].Format.ID
+ })
+
+ b.WriteString("\n")
+ for _, tp := range accepted {
+ fmt.Fprintf(&b, "#define %s %d\n", strings.ToUpper(tp.Format.Name), tp.Format.ID)
+ }
+ b.WriteString("\n")
+
+ for _, tp := range accepted {
+ b.WriteString(generateBPFHandler(tp))
+ b.WriteString("\n")
+ }
+
+ return b.String()
+}
+
+func groupBySyscall(formats []Format) []Syscall {
+ m := make(map[string]*Syscall)
+ var order []string
+
+ for i := range formats {
+ f := &formats[i]
+ parts := strings.SplitN(f.Name, "_", 3)
+ if len(parts) < 3 {
+ continue
+ }
+ enterExit := parts[1]
+ what := parts[2]
+
+ sc, ok := m[what]
+ if !ok {
+ sc = &Syscall{Name: what}
+ m[what] = sc
+ order = append(order, what)
+ }
+ if enterExit == "enter" {
+ sc.Enter = f
+ } else {
+ sc.Exit = f
+ }
+ }
+
+ result := make([]Syscall, 0, len(order))
+ for _, name := range order {
+ result = append(result, *m[name])
+ }
+ return result
+}
+
+func classifySyscall(sc Syscall) ([]GeneratedTracepoint, string) {
+ var enterClass, exitClass ClassificationResult
+ allCanGenerate := true
+
+ if sc.Enter != nil {
+ enterClass = ClassifyFormat(sc.Enter)
+ if enterClass.Kind == KindNone {
+ allCanGenerate = false
+ }
+ } else {
+ allCanGenerate = false
+ }
+
+ if sc.Exit != nil {
+ exitClass = ClassifyFormat(sc.Exit)
+ if exitClass.Kind == KindNone {
+ allCanGenerate = false
+ }
+ } else {
+ allCanGenerate = false
+ }
+
+ if !allCanGenerate {
+ names := syscallFormatNames(sc)
+ return nil, fmt.Sprintf("Ignoring %s as possibly not file I/O related", strings.Join(names, " "))
+ }
+
+ if isEnterRejected(enterClass.Kind) {
+ names := syscallFormatNames(sc)
+ return nil, fmt.Sprintf("Ignoring %s as enter-rejected", strings.Join(names, " "))
+ }
+
+ var result []GeneratedTracepoint
+ if sc.Enter != nil {
+ result = append(result, GeneratedTracepoint{Format: sc.Enter, Classification: enterClass})
+ }
+ if sc.Exit != nil {
+ result = append(result, GeneratedTracepoint{Format: sc.Exit, Classification: exitClass})
+ }
+ return result, ""
+}
+
+func isEnterRejected(kind TracepointKind) bool {
+ switch kind {
+ case KindFd, KindName, KindOpen, KindPathname, KindFcntl, KindNull, KindDup3, KindOpenByHandleAt:
+ return false
+ default:
+ return true
+ }
+}
+
+func syscallFormatNames(sc Syscall) []string {
+ var names []string
+ if sc.Enter != nil {
+ names = append(names, sc.Enter.Name)
+ }
+ if sc.Exit != nil {
+ names = append(names, sc.Exit.Name)
+ }
+ sort.Strings(names)
+ return names
+}
diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go
new file mode 100644
index 0000000..b19a824
--- /dev/null
+++ b/internal/generate/codegen_test.go
@@ -0,0 +1,263 @@
+package generate
+
+import (
+ "strings"
+ "testing"
+)
+
+func generateFromPair(t *testing.T, enter, exit string) string {
+ t.Helper()
+ input := enter + "\n" + exit
+ formats := mustParseAll(t, input)
+ return GenerateTracepointsC(formats)
+}
+
+func TestGenerateFdHandler(t *testing.T) {
+ output := generateFromPair(t, FormatRead, FormatExitRead)
+
+ requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_read")`)
+ requireContains(t, output, "struct trace_event_raw_sys_enter *ctx")
+ requireContains(t, output, "struct fd_event *ev = bpf_ringbuf_reserve(&event_map, sizeof(struct fd_event), 0);")
+ requireContains(t, output, "ev->event_type = ENTER_FD_EVENT;")
+ requireContains(t, output, "ev->trace_id = SYS_ENTER_READ;")
+ requireContains(t, output, "ev->fd = (__s32)ctx->args[0];")
+ requireContains(t, output, "#define SYS_ENTER_READ 844")
+}
+
+func TestGenerateOpenHandler(t *testing.T) {
+ output := generateFromPair(t, FormatOpenat, FormatExitOpenat)
+
+ requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_openat")`)
+ requireContains(t, output, "struct open_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_OPEN_EVENT;")
+ requireContains(t, output, "ev->trace_id = SYS_ENTER_OPENAT;")
+ requireContains(t, output, "__builtin_memset(&(ev->filename), 0, sizeof(ev->filename) + sizeof(ev->comm));")
+ requireContains(t, output, "bpf_probe_read_user_str(ev->filename, sizeof(ev->filename), (void *)ctx->args[1]);")
+ requireContains(t, output, "bpf_get_current_comm(&ev->comm, sizeof(ev->comm));")
+ requireContains(t, output, "ev->flags = ctx->args[2];")
+}
+
+func TestGenerateOpenHandlerDirect(t *testing.T) {
+ output := generateFromPair(t, FormatOpen, FormatExitOpen)
+
+ requireContains(t, output, "bpf_probe_read_user_str(ev->filename, sizeof(ev->filename), (void *)ctx->args[0]);")
+ requireContains(t, output, "ev->flags = ctx->args[1];")
+}
+
+func TestGenerateOpenat2Handler(t *testing.T) {
+ f := mustParseOne(t, FormatOpenat2)
+ r := ClassifyFormat(&f)
+ if r.Kind != KindOpen {
+ t.Fatalf("openat2 classified as %d, want KindOpen", r.Kind)
+ }
+ // openat2 has filename at args[1] but flags field name = "how" (not "flags"),
+ // so FieldNumber("flags") returns -1
+ if n := f.FieldNumber("flags"); n != -1 {
+ t.Errorf("openat2 FieldNumber(flags) = %d, want -1", n)
+ }
+}
+
+func TestGenerateRetHandlerRead(t *testing.T) {
+ output := generateFromPair(t, FormatRead, FormatExitRead)
+
+ requireContains(t, output, `SEC("tracepoint/syscalls/sys_exit_read")`)
+ requireContains(t, output, "struct trace_event_raw_sys_exit *ctx")
+ requireContains(t, output, "struct ret_event *ev")
+ requireContains(t, output, "ev->event_type = EXIT_RET_EVENT;")
+ requireContains(t, output, "ev->trace_id = SYS_EXIT_READ;")
+ requireContains(t, output, "ev->ret = ctx->ret;")
+ requireContains(t, output, "ev->ret_type = READ_CLASSIFIED;")
+}
+
+func TestGenerateRetHandlerWrite(t *testing.T) {
+ output := generateFromPair(t, FormatWrite, FormatExitWrite)
+
+ requireContains(t, output, "ev->ret_type = WRITE_CLASSIFIED;")
+ requireContains(t, output, "ev->trace_id = SYS_EXIT_WRITE;")
+}
+
+func TestGenerateRetHandlerOpenat(t *testing.T) {
+ output := generateFromPair(t, FormatOpenat, FormatExitOpenat)
+
+ requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
+ requireContains(t, output, "ev->trace_id = SYS_EXIT_OPENAT;")
+}
+
+func TestGenerateNameHandler(t *testing.T) {
+ output := generateFromPair(t, FormatRename, FormatExitRename)
+
+ requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_rename")`)
+ requireContains(t, output, "struct name_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_NAME_EVENT;")
+ requireContains(t, output, "ev->trace_id = SYS_ENTER_RENAME;")
+ requireContains(t, output, "__builtin_memset(&(ev->oldname), 0, sizeof(ev->oldname) + sizeof(ev->newname));")
+ requireContains(t, output, "bpf_probe_read_user_str(ev->oldname, sizeof(ev->oldname), (void*)ctx->args[0]);")
+ requireContains(t, output, "bpf_probe_read_user_str(ev->newname, sizeof(ev->newname), (void*)ctx->args[1]);")
+}
+
+func TestGeneratePathnameHandler(t *testing.T) {
+ // Use exit_unlink (same structure as exit_read) paired with enter_unlink
+ exitUnlink := strings.Replace(FormatExitRead, "sys_exit_read", "sys_exit_unlink", 1)
+ exitUnlink = strings.Replace(exitUnlink, "ID: 843", "ID: 883", 1)
+ output := generateFromPair(t, FormatUnlink, exitUnlink)
+
+ requireContains(t, output, "struct path_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_PATH_EVENT;")
+ requireContains(t, output, "__builtin_memset(&(ev->pathname), 0, sizeof(ev->pathname));")
+ requireContains(t, output, "bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[0]);")
+}
+
+func TestGenerateFcntlHandler(t *testing.T) {
+ output := generateFromPair(t, FormatFcntl, FormatExitFcntl)
+
+ requireContains(t, output, "struct fcntl_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_FCNTL_EVENT;")
+ requireContains(t, output, "ev->fd = ctx->args[0];")
+ requireContains(t, output, "ev->cmd = ctx->args[1];")
+ requireContains(t, output, "ev->arg = ctx->args[2];")
+}
+
+func TestGenerateNullHandler(t *testing.T) {
+ output := generateFromPair(t, FormatSync, FormatExitSync)
+
+ requireContains(t, output, "struct null_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_NULL_EVENT;")
+ requireContains(t, output, "ev->trace_id = SYS_ENTER_SYNC;")
+ // Null handler should NOT have ev->fd, ev->filename, etc.
+ if strings.Contains(output, "ev->fd") {
+ t.Error("null handler should not have ev->fd")
+ }
+}
+
+func TestGenerateDup3Handler(t *testing.T) {
+ output := generateFromPair(t, FormatDup3, FormatExitDup3)
+
+ requireContains(t, output, "struct dup3_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_DUP3_EVENT;")
+ requireContains(t, output, "ev->fd = (__s32)ctx->args[0];")
+ requireContains(t, output, "ev->flags = (__s32)ctx->args[2];")
+}
+
+func TestGenerateOpenByHandleAtHandler(t *testing.T) {
+ output := generateFromPair(t, FormatOpenByHandleAt, FormatExitOpenByHandleAt)
+
+ requireContains(t, output, "struct open_by_handle_at_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_OPEN_BY_HANDLE_AT_EVENT;")
+ requireContains(t, output, "ev->flags = (__s32)ctx->args[2];")
+}
+
+func TestGenerateIgnoredComment(t *testing.T) {
+ output := generateFromPair(t, FormatKill, FormatExitKill)
+
+ requireContains(t, output, "/// Ignoring sys_enter_kill sys_exit_kill as possibly not file I/O related")
+}
+
+func TestGenerateDefineConstants(t *testing.T) {
+ output := generateFromPair(t, FormatRead, FormatExitRead)
+
+ requireContains(t, output, "#define SYS_ENTER_READ 844")
+ requireContains(t, output, "#define SYS_EXIT_READ 843")
+}
+
+func TestGenerateDefinesSortedByIDDesc(t *testing.T) {
+ input := FormatRead + "\n" + FormatExitRead + "\n" + FormatClose + "\n" + FormatExitClose
+ formats := mustParseAll(t, input)
+ output := GenerateTracepointsC(formats)
+
+ enterReadPos := strings.Index(output, "#define SYS_ENTER_READ")
+ enterClosePos := strings.Index(output, "#define SYS_ENTER_CLOSE")
+ if enterReadPos < 0 || enterClosePos < 0 {
+ t.Fatal("missing #define lines")
+ }
+ if enterReadPos > enterClosePos {
+ t.Error("#define SYS_ENTER_READ (844) should come before SYS_ENTER_CLOSE (778)")
+ }
+}
+
+func TestGenerateHandlerStructure(t *testing.T) {
+ output := generateFromPair(t, FormatClose, FormatExitClose)
+
+ requireContains(t, output, "int handle_sys_enter_close(struct trace_event_raw_sys_enter *ctx) {")
+ requireContains(t, output, "__u32 pid, tid;")
+ requireContains(t, output, "if (filter(&pid, &tid))")
+ requireContains(t, output, "ev->pid = pid;")
+ requireContains(t, output, "ev->tid = tid;")
+ requireContains(t, output, "ev->time = bpf_ktime_get_boot_ns();")
+ requireContains(t, output, "bpf_ringbuf_submit(ev, 0);")
+ requireContains(t, output, "return 0;")
+}
+
+func TestGenerateAllEventTypes(t *testing.T) {
+ // Verify every event type constant appears correctly
+ tests := []struct {
+ kind TracepointKind
+ enter string
+ exit string
+ }{
+ {KindFd, "ENTER_FD_EVENT", "EXIT_FD_EVENT"},
+ {KindOpen, "ENTER_OPEN_EVENT", "EXIT_OPEN_EVENT"},
+ {KindPathname, "ENTER_PATH_EVENT", "EXIT_PATH_EVENT"},
+ {KindName, "ENTER_NAME_EVENT", "EXIT_NAME_EVENT"},
+ {KindRet, "ENTER_RET_EVENT", "EXIT_RET_EVENT"},
+ {KindFcntl, "ENTER_FCNTL_EVENT", "EXIT_FCNTL_EVENT"},
+ {KindNull, "ENTER_NULL_EVENT", "EXIT_NULL_EVENT"},
+ {KindDup3, "ENTER_DUP3_EVENT", "EXIT_DUP3_EVENT"},
+ {KindOpenByHandleAt, "ENTER_OPEN_BY_HANDLE_AT_EVENT", "EXIT_OPEN_BY_HANDLE_AT_EVENT"},
+ }
+
+ for _, tt := range tests {
+ if got := eventTypeConstant(tt.kind, true); got != tt.enter {
+ t.Errorf("eventTypeConstant(%d, true) = %q, want %q", tt.kind, got, tt.enter)
+ }
+ if got := eventTypeConstant(tt.kind, false); got != tt.exit {
+ t.Errorf("eventTypeConstant(%d, false) = %q, want %q", tt.kind, got, tt.exit)
+ }
+ }
+}
+
+func TestEventStructNames(t *testing.T) {
+ tests := []struct {
+ kind TracepointKind
+ want string
+ }{
+ {KindFd, "fd_event"},
+ {KindOpen, "open_event"},
+ {KindPathname, "path_event"},
+ {KindName, "name_event"},
+ {KindRet, "ret_event"},
+ {KindFcntl, "fcntl_event"},
+ {KindNull, "null_event"},
+ {KindDup3, "dup3_event"},
+ {KindOpenByHandleAt, "open_by_handle_at_event"},
+ }
+
+ for _, tt := range tests {
+ if got := eventStructName(tt.kind); got != tt.want {
+ t.Errorf("eventStructName(%d) = %q, want %q", tt.kind, got, tt.want)
+ }
+ }
+}
+
+func TestEnterReject(t *testing.T) {
+ // RetTracepoint as enter type should be rejected
+ if !isEnterRejected(KindRet) {
+ t.Error("KindRet should be enter-rejected")
+ }
+ if !isEnterRejected(KindNone) {
+ t.Error("KindNone should be enter-rejected")
+ }
+
+ accepted := []TracepointKind{KindFd, KindOpen, KindPathname, KindName, KindFcntl, KindNull, KindDup3, KindOpenByHandleAt}
+ for _, k := range accepted {
+ if isEnterRejected(k) {
+ t.Errorf("kind %d should NOT be enter-rejected", k)
+ }
+ }
+}
+
+func requireContains(t *testing.T, haystack, needle string) {
+ t.Helper()
+ if !strings.Contains(haystack, needle) {
+ t.Errorf("output missing expected string: %q", needle)
+ }
+}
diff --git a/internal/generate/format.go b/internal/generate/format.go
new file mode 100644
index 0000000..ea579b6
--- /dev/null
+++ b/internal/generate/format.go
@@ -0,0 +1,145 @@
+package generate
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+)
+
+type Field struct {
+ Type string
+ Name string
+ Offset int
+ Size int
+ Signed bool
+}
+
+type Format struct {
+ Name string
+ ID int
+ InternalFields []Field
+ ExternalFields []Field
+}
+
+// FieldNumber returns the 0-based index of a named field in ExternalFields,
+// minus 1 (to skip __syscall_nr), matching the Raku behavior where args[0]
+// is the first field after __syscall_nr.
+func (f *Format) FieldNumber(name string) int {
+ for i, field := range f.ExternalFields {
+ if field.Name == name {
+ return i - 1
+ }
+ }
+ return 0 - 1
+}
+
+// ParseFormats parses concatenated sysfs tracepoint format files from r.
+// Each section has: name, ID, format fields, print fmt.
+func ParseFormats(r io.Reader) ([]Format, error) {
+ scanner := bufio.NewScanner(r)
+ var formats []Format
+ var current *Format
+ isExternal := false
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimmed := strings.TrimSpace(line)
+
+ switch {
+ case strings.HasPrefix(trimmed, "name:"):
+ f := Format{}
+ f.Name = strings.TrimSpace(strings.TrimPrefix(trimmed, "name:"))
+ formats = append(formats, f)
+ current = &formats[len(formats)-1]
+ isExternal = false
+
+ case strings.HasPrefix(trimmed, "ID:"):
+ if current == nil {
+ return nil, fmt.Errorf("ID without name")
+ }
+ id, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(trimmed, "ID:")))
+ if err != nil {
+ return nil, fmt.Errorf("parsing ID: %w", err)
+ }
+ current.ID = id
+
+ case strings.HasPrefix(trimmed, "field:"):
+ if current == nil {
+ return nil, fmt.Errorf("field without name")
+ }
+ field, err := parseField(trimmed)
+ if err != nil {
+ return nil, fmt.Errorf("parsing field in %s: %w", current.Name, err)
+ }
+ if field.Name == "__syscall_nr" {
+ isExternal = true
+ }
+ if isExternal {
+ current.ExternalFields = append(current.ExternalFields, field)
+ } else {
+ current.InternalFields = append(current.InternalFields, field)
+ }
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("scanning input: %w", err)
+ }
+ return formats, nil
+}
+
+func parseField(line string) (Field, error) {
+ // Format: "field:TYPE NAME; offset:N; size:N; signed:N;"
+ line = strings.TrimPrefix(line, "field:")
+ parts := strings.Split(line, ";")
+ if len(parts) < 4 {
+ return Field{}, fmt.Errorf("not enough field parts: %q", line)
+ }
+
+ decl := strings.TrimSpace(parts[0])
+ fieldType, fieldName := splitDeclaration(decl)
+
+ offset, err := parseFieldInt(parts[1], "offset:")
+ if err != nil {
+ return Field{}, err
+ }
+ size, err := parseFieldInt(parts[2], "size:")
+ if err != nil {
+ return Field{}, err
+ }
+ signedVal, err := parseFieldInt(parts[3], "signed:")
+ if err != nil {
+ return Field{}, err
+ }
+
+ return Field{
+ Type: fieldType,
+ Name: fieldName,
+ Offset: offset,
+ Size: size,
+ Signed: signedVal != 0,
+ }, nil
+}
+
+// splitDeclaration splits "const char * filename" into ("const char *", "filename").
+func splitDeclaration(decl string) (string, string) {
+ tokens := strings.Fields(decl)
+ if len(tokens) == 0 {
+ return "", ""
+ }
+ if len(tokens) == 1 {
+ return "", tokens[0]
+ }
+ name := tokens[len(tokens)-1]
+ typePart := strings.Join(tokens[:len(tokens)-1], " ")
+ return typePart, name
+}
+
+func parseFieldInt(s, prefix string) (int, error) {
+ s = strings.TrimSpace(s)
+ s = strings.TrimPrefix(s, prefix)
+ s = strings.TrimSpace(s)
+ return strconv.Atoi(s)
+}
diff --git a/internal/generate/format_test.go b/internal/generate/format_test.go
new file mode 100644
index 0000000..f8f078b
--- /dev/null
+++ b/internal/generate/format_test.go
@@ -0,0 +1,174 @@
+package generate
+
+import (
+ "strings"
+ "testing"
+)
+
+func mustParseOne(t *testing.T, data string) Format {
+ t.Helper()
+ formats, err := ParseFormats(strings.NewReader(data))
+ if err != nil {
+ t.Fatalf("ParseFormats failed: %v", err)
+ }
+ if len(formats) != 1 {
+ t.Fatalf("expected 1 format, got %d", len(formats))
+ }
+ return formats[0]
+}
+
+func TestParseFormatOpenat(t *testing.T) {
+ f := mustParseOne(t, FormatOpenat)
+
+ if f.Name != "sys_enter_openat" {
+ t.Errorf("name = %q, want sys_enter_openat", f.Name)
+ }
+ if f.ID != 784 {
+ t.Errorf("ID = %d, want 784", f.ID)
+ }
+ if len(f.InternalFields) != 4 {
+ t.Errorf("internal fields = %d, want 4", len(f.InternalFields))
+ }
+ if len(f.ExternalFields) != 5 {
+ t.Errorf("external fields = %d, want 5", len(f.ExternalFields))
+ }
+ if f.ExternalFields[0].Name != "__syscall_nr" {
+ t.Errorf("first external field = %q, want __syscall_nr", f.ExternalFields[0].Name)
+ }
+ if f.ExternalFields[2].Type != "const char *" {
+ t.Errorf("filename type = %q, want 'const char *'", f.ExternalFields[2].Type)
+ }
+ if f.ExternalFields[2].Name != "filename" {
+ t.Errorf("field 2 name = %q, want filename", f.ExternalFields[2].Name)
+ }
+}
+
+func TestParseFormatExitRead(t *testing.T) {
+ f := mustParseOne(t, FormatExitRead)
+
+ if f.Name != "sys_exit_read" {
+ t.Errorf("name = %q, want sys_exit_read", f.Name)
+ }
+ if f.ID != 843 {
+ t.Errorf("ID = %d, want 843", f.ID)
+ }
+ if len(f.ExternalFields) != 2 {
+ t.Errorf("external fields = %d, want 2", len(f.ExternalFields))
+ }
+ if f.ExternalFields[1].Type != "long" {
+ t.Errorf("ret type = %q, want long", f.ExternalFields[1].Type)
+ }
+ if f.ExternalFields[1].Name != "ret" {
+ t.Errorf("ret name = %q, want ret", f.ExternalFields[1].Name)
+ }
+ if !f.ExternalFields[1].Signed {
+ t.Error("ret should be signed")
+ }
+}
+
+func TestParseFormatSync(t *testing.T) {
+ f := mustParseOne(t, FormatSync)
+
+ if f.Name != "sys_enter_sync" {
+ t.Errorf("name = %q, want sys_enter_sync", f.Name)
+ }
+ if f.ID != 1027 {
+ t.Errorf("ID = %d, want 1027", f.ID)
+ }
+ if len(f.ExternalFields) != 1 {
+ t.Errorf("external fields = %d, want 1 (__syscall_nr only)", len(f.ExternalFields))
+ }
+}
+
+func TestParseMultiSection(t *testing.T) {
+ input := FormatRead + "\n" + FormatExitRead
+ formats, err := ParseFormats(strings.NewReader(input))
+ if err != nil {
+ t.Fatalf("ParseFormats failed: %v", err)
+ }
+ if len(formats) != 2 {
+ t.Fatalf("expected 2 formats, got %d", len(formats))
+ }
+ if formats[0].Name != "sys_enter_read" {
+ t.Errorf("first = %q", formats[0].Name)
+ }
+ if formats[1].Name != "sys_exit_read" {
+ t.Errorf("second = %q", formats[1].Name)
+ }
+}
+
+func TestParseFieldDetails(t *testing.T) {
+ f := mustParseOne(t, FormatOpenat)
+
+ // common_type: internal field
+ ct := f.InternalFields[0]
+ if ct.Name != "common_type" || ct.Type != "unsigned short" || ct.Offset != 0 || ct.Size != 2 || ct.Signed {
+ t.Errorf("common_type = %+v", ct)
+ }
+
+ // dfd: first non-syscall_nr external field
+ dfd := f.ExternalFields[1]
+ if dfd.Name != "dfd" || dfd.Type != "int" || dfd.Offset != 16 || dfd.Size != 8 {
+ t.Errorf("dfd = %+v", dfd)
+ }
+}
+
+func TestFieldNumber(t *testing.T) {
+ f := mustParseOne(t, FormatOpenat)
+
+ if n := f.FieldNumber("filename"); n != 1 {
+ t.Errorf("FieldNumber(filename) = %d, want 1", n)
+ }
+ if n := f.FieldNumber("flags"); n != 2 {
+ t.Errorf("FieldNumber(flags) = %d, want 2", n)
+ }
+ if n := f.FieldNumber("dfd"); n != 0 {
+ t.Errorf("FieldNumber(dfd) = %d, want 0", n)
+ }
+ if n := f.FieldNumber("nonexistent"); n != -1 {
+ t.Errorf("FieldNumber(nonexistent) = %d, want -1", n)
+ }
+}
+
+func TestFieldNumberFcntl(t *testing.T) {
+ f := mustParseOne(t, FormatFcntl)
+
+ if n := f.FieldNumber("fd"); n != 0 {
+ t.Errorf("FieldNumber(fd) = %d, want 0", n)
+ }
+ if n := f.FieldNumber("cmd"); n != 1 {
+ t.Errorf("FieldNumber(cmd) = %d, want 1", n)
+ }
+ if n := f.FieldNumber("arg"); n != 2 {
+ t.Errorf("FieldNumber(arg) = %d, want 2", n)
+ }
+}
+
+func TestFieldNumberRename(t *testing.T) {
+ f := mustParseOne(t, FormatRename)
+
+ if n := f.FieldNumber("oldname"); n != 0 {
+ t.Errorf("FieldNumber(oldname) = %d, want 0", n)
+ }
+ if n := f.FieldNumber("newname"); n != 1 {
+ t.Errorf("FieldNumber(newname) = %d, want 1", n)
+ }
+}
+
+func TestFieldNumberLinkat(t *testing.T) {
+ f := mustParseOne(t, FormatLinkat)
+
+ if n := f.FieldNumber("oldname"); n != 1 {
+ t.Errorf("FieldNumber(oldname) = %d, want 1", n)
+ }
+ if n := f.FieldNumber("newname"); n != 3 {
+ t.Errorf("FieldNumber(newname) = %d, want 3", n)
+ }
+}
+
+func TestParseFormatError(t *testing.T) {
+ _, err := ParseFormats(strings.NewReader("field:broken"))
+ if err == nil {
+ t.Error("expected error for field without name")
+ }
+}
diff --git a/internal/generate/retclassify_test.go b/internal/generate/retclassify_test.go
new file mode 100644
index 0000000..3152005
--- /dev/null
+++ b/internal/generate/retclassify_test.go
@@ -0,0 +1,59 @@
+package generate
+
+import "testing"
+
+func TestClassifyRetRead(t *testing.T) {
+ reads := []string{
+ "fgetxattr", "flistxattr", "getdents", "getdents64", "getxattr",
+ "lgetxattr", "listxattr", "llistxattr", "pread64", "preadv",
+ "preadv2", "process_vm_readv", "read", "readlink", "readlinkat",
+ "readv", "recvmmsg", "recvmsg", "recvfrom", "syslog",
+ }
+ for _, name := range reads {
+ if got := ClassifyRet("sys_exit_" + name); got != ReadClassified {
+ t.Errorf("ClassifyRet(sys_exit_%s) = %q, want READ_CLASSIFIED", name, got)
+ }
+ }
+}
+
+func TestClassifyRetWrite(t *testing.T) {
+ writes := []string{
+ "process_vm_writev", "pwrite64", "pwritev", "pwritev2",
+ "sendmmsg", "sendmsg", "sendto", "write", "writev",
+ }
+ for _, name := range writes {
+ if got := ClassifyRet("sys_exit_" + name); got != WriteClassified {
+ t.Errorf("ClassifyRet(sys_exit_%s) = %q, want WRITE_CLASSIFIED", name, got)
+ }
+ }
+}
+
+func TestClassifyRetTransfer(t *testing.T) {
+ transfers := []string{
+ "copy_file_range", "sendfile64", "splice", "tee", "vmsplice",
+ }
+ for _, name := range transfers {
+ if got := ClassifyRet("sys_exit_" + name); got != TransferClassified {
+ t.Errorf("ClassifyRet(sys_exit_%s) = %q, want TRANSFER_CLASSIFIED", name, got)
+ }
+ }
+}
+
+func TestClassifyRetUnclassified(t *testing.T) {
+ unclassified := []string{
+ "openat", "close", "rename", "unlink", "fcntl", "dup", "dup2", "dup3",
+ "mkdir", "rmdir", "chmod", "chown", "chdir", "stat", "lseek",
+ "truncate", "fallocate", "mmap", "fsync", "flock",
+ }
+ for _, name := range unclassified {
+ if got := ClassifyRet("sys_exit_" + name); got != Unclassified {
+ t.Errorf("ClassifyRet(sys_exit_%s) = %q, want UNCLASSIFIED", name, got)
+ }
+ }
+}
+
+func TestClassifyRetCaseInsensitive(t *testing.T) {
+ if got := ClassifyRet("sys_exit_READ"); got != ReadClassified {
+ t.Errorf("ClassifyRet(sys_exit_READ) = %q, want READ_CLASSIFIED", got)
+ }
+}
diff --git a/internal/generate/testdata.go b/internal/generate/testdata.go
new file mode 100644
index 0000000..7ca29cd
--- /dev/null
+++ b/internal/generate/testdata.go
@@ -0,0 +1,666 @@
+package generate
+
+// Real sysfs tracepoint format data captured from a Linux 6.18 kernel,
+// used as test fixtures.
+
+const FormatOpenat = `name: sys_enter_openat
+ID: 784
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int dfd; offset:16; size:8; signed:0;
+ field:const char * filename; offset:24; size:8; signed:0;
+ field:int flags; offset:32; size:8; signed:0;
+ field:umode_t mode; offset:40; size:8; signed:0;
+
+print fmt: "dfd: 0x%08lx, filename: 0x%08lx, flags: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->dfd)), ((unsigned long)(REC->filename)), ((unsigned long)(REC->flags)), ((unsigned long)(REC->mode))
+`
+
+const FormatExitOpenat = `name: sys_exit_openat
+ID: 783
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatOpen = `name: sys_enter_open
+ID: 786
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:const char * filename; offset:16; size:8; signed:0;
+ field:int flags; offset:24; size:8; signed:0;
+ field:umode_t mode; offset:32; size:8; signed:0;
+
+print fmt: "filename: 0x%08lx, flags: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->flags)), ((unsigned long)(REC->mode))
+`
+
+const FormatExitOpen = `name: sys_exit_open
+ID: 785
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatRead = `name: sys_enter_read
+ID: 844
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int fd; offset:16; size:8; signed:0;
+ field:char * buf; offset:24; size:8; signed:0;
+ field:size_t count; offset:32; size:8; signed:0;
+
+print fmt: "fd: 0x%08lx, buf: 0x%08lx, count: 0x%08lx", ((unsigned long)(REC->fd)), ((unsigned long)(REC->buf)), ((unsigned long)(REC->count))
+`
+
+const FormatExitRead = `name: sys_exit_read
+ID: 843
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatWrite = `name: sys_enter_write
+ID: 842
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int fd; offset:16; size:8; signed:0;
+ field:const char * buf; offset:24; size:8; signed:0;
+ field:size_t count; offset:32; size:8; signed:0;
+
+print fmt: "fd: 0x%08lx, buf: 0x%08lx, count: 0x%08lx", ((unsigned long)(REC->fd)), ((unsigned long)(REC->buf)), ((unsigned long)(REC->count))
+`
+
+const FormatExitWrite = `name: sys_exit_write
+ID: 841
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatClose = `name: sys_enter_close
+ID: 778
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int fd; offset:16; size:8; signed:0;
+
+print fmt: "fd: 0x%08lx", ((unsigned long)(REC->fd))
+`
+
+const FormatExitClose = `name: sys_exit_close
+ID: 777
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatRename = `name: sys_enter_rename
+ID: 870
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:const char * oldname; offset:16; size:8; signed:0;
+ field:const char * newname; offset:24; size:8; signed:0;
+
+print fmt: "oldname: 0x%08lx, newname: 0x%08lx", ((unsigned long)(REC->oldname)), ((unsigned long)(REC->newname))
+`
+
+const FormatExitRename = `name: sys_exit_rename
+ID: 869
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatLinkat = `name: sys_enter_linkat
+ID: 878
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int olddfd; offset:16; size:8; signed:0;
+ field:const char * oldname; offset:24; size:8; signed:0;
+ field:int newdfd; offset:32; size:8; signed:0;
+ field:const char * newname; offset:40; size:8; signed:0;
+ field:int flags; offset:48; size:8; signed:0;
+
+print fmt: "olddfd: 0x%08lx, oldname: 0x%08lx, newdfd: 0x%08lx, newname: 0x%08lx, flags: 0x%08lx", ((unsigned long)(REC->olddfd)), ((unsigned long)(REC->oldname)), ((unsigned long)(REC->newdfd)), ((unsigned long)(REC->newname)), ((unsigned long)(REC->flags))
+`
+
+const FormatUnlink = `name: sys_enter_unlink
+ID: 884
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:const char * pathname; offset:16; size:8; signed:0;
+
+print fmt: "pathname: 0x%08lx", ((unsigned long)(REC->pathname))
+`
+
+const FormatDup3 = `name: sys_enter_dup3
+ID: 922
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int oldfd; offset:16; size:8; signed:0;
+ field:unsigned int newfd; offset:24; size:8; signed:0;
+ field:int flags; offset:32; size:8; signed:0;
+
+print fmt: "oldfd: 0x%08lx, newfd: 0x%08lx, flags: 0x%08lx", ((unsigned long)(REC->oldfd)), ((unsigned long)(REC->newfd)), ((unsigned long)(REC->flags))
+`
+
+const FormatExitDup3 = `name: sys_exit_dup3
+ID: 921
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatDup = `name: sys_enter_dup
+ID: 918
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int fildes; offset:16; size:8; signed:0;
+
+print fmt: "fildes: 0x%08lx", ((unsigned long)(REC->fildes))
+`
+
+const FormatDup2 = `name: sys_enter_dup2
+ID: 920
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int oldfd; offset:16; size:8; signed:0;
+ field:unsigned int newfd; offset:24; size:8; signed:0;
+
+print fmt: "oldfd: 0x%08lx, newfd: 0x%08lx", ((unsigned long)(REC->oldfd)), ((unsigned long)(REC->newfd))
+`
+
+const FormatFcntl = `name: sys_enter_fcntl
+ID: 898
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int fd; offset:16; size:8; signed:0;
+ field:unsigned int cmd; offset:24; size:8; signed:0;
+ field:unsigned long arg; offset:32; size:8; signed:0;
+
+print fmt: "fd: 0x%08lx, cmd: 0x%08lx, arg: 0x%08lx", ((unsigned long)(REC->fd)), ((unsigned long)(REC->cmd)), ((unsigned long)(REC->arg))
+`
+
+const FormatExitFcntl = `name: sys_exit_fcntl
+ID: 897
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatSync = `name: sys_enter_sync
+ID: 1027
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+
+print fmt: ""
+`
+
+const FormatExitSync = `name: sys_exit_sync
+ID: 1026
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatSyslog = `name: sys_enter_syslog
+ID: 347
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int type; offset:16; size:8; signed:0;
+ field:char * buf; offset:24; size:8; signed:0;
+ field:int len; offset:32; size:8; signed:0;
+
+print fmt: "type: 0x%08lx, buf: 0x%08lx, len: 0x%08lx", ((unsigned long)(REC->type)), ((unsigned long)(REC->buf)), ((unsigned long)(REC->len))
+`
+
+const FormatExitSyslog = `name: sys_exit_syslog
+ID: 346
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatOpenByHandleAt = `name: sys_enter_open_by_handle_at
+ID: 1133
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int mountdirfd; offset:16; size:8; signed:0;
+ field:struct file_handle * handle; offset:24; size:8; signed:0;
+ field:int flags; offset:32; size:8; signed:0;
+
+print fmt: "mountdirfd: 0x%08lx, handle: 0x%08lx, flags: 0x%08lx", ((unsigned long)(REC->mountdirfd)), ((unsigned long)(REC->handle)), ((unsigned long)(REC->flags))
+`
+
+const FormatExitOpenByHandleAt = `name: sys_exit_open_by_handle_at
+ID: 1132
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatIoUringEnter = `name: sys_enter_io_uring_enter
+ID: 1496
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int fd; offset:16; size:8; signed:0;
+ field:u32 to_submit; offset:24; size:8; signed:0;
+ field:u32 min_complete; offset:32; size:8; signed:0;
+ field:u32 flags; offset:40; size:8; signed:0;
+ field:const void * argp; offset:48; size:8; signed:0;
+ field:size_t argsz; offset:56; size:8; signed:0;
+
+print fmt: "fd: 0x%08lx, to_submit: 0x%08lx, min_complete: 0x%08lx, flags: 0x%08lx, argp: 0x%08lx, argsz: 0x%08lx", ((unsigned long)(REC->fd)), ((unsigned long)(REC->to_submit)), ((unsigned long)(REC->min_complete)), ((unsigned long)(REC->flags)), ((unsigned long)(REC->argp)), ((unsigned long)(REC->argsz))
+`
+
+const FormatExitIoUringEnter = `name: sys_exit_io_uring_enter
+ID: 1495
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatOpenat2 = `name: sys_enter_openat2
+ID: 782
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int dfd; offset:16; size:8; signed:0;
+ field:const char * filename; offset:24; size:8; signed:0;
+ field:struct open_how * how; offset:32; size:8; signed:0;
+ field:size_t usize; offset:40; size:8; signed:0;
+
+print fmt: "dfd: 0x%08lx, filename: 0x%08lx, how: 0x%08lx, usize: 0x%08lx", ((unsigned long)(REC->dfd)), ((unsigned long)(REC->filename)), ((unsigned long)(REC->how)), ((unsigned long)(REC->usize))
+`
+
+const FormatCreat = `name: sys_enter_creat
+ID: 780
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:const char * pathname; offset:16; size:8; signed:0;
+ field:umode_t mode; offset:24; size:8; signed:0;
+
+print fmt: "pathname: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->pathname)), ((unsigned long)(REC->mode))
+`
+
+// Ignored tracepoints
+
+const FormatExecve = `name: sys_enter_execve
+ID: 864
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:const char * filename; offset:16; size:8; signed:0;
+ field:const char *const * argv; offset:24; size:8; signed:0;
+ field:const char *const * envp; offset:32; size:8; signed:0;
+
+print fmt: "filename: 0x%08lx, argv: 0x%08lx, envp: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->argv)), ((unsigned long)(REC->envp))
+`
+
+const FormatExitExecve = `name: sys_exit_execve
+ID: 863
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatMknod = `name: sys_enter_mknod
+ID: 894
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:const char * filename; offset:16; size:8; signed:0;
+ field:umode_t mode; offset:24; size:8; signed:0;
+ field:unsigned dev; offset:32; size:8; signed:0;
+
+print fmt: "filename: 0x%08lx, mode: 0x%08lx, dev: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->mode)), ((unsigned long)(REC->dev))
+`
+
+const FormatExitMknod = `name: sys_exit_mknod
+ID: 893
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatKill = `name: sys_enter_kill
+ID: 183
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:pid_t pid; offset:16; size:8; signed:0;
+ field:int sig; offset:24; size:8; signed:0;
+
+print fmt: "pid: 0x%08lx, sig: 0x%08lx", ((unsigned long)(REC->pid)), ((unsigned long)(REC->sig))
+`
+
+const FormatExitKill = `name: sys_exit_kill
+ID: 182
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatAccept = `name: sys_enter_accept
+ID: 1808
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int fd; offset:16; size:8; signed:0;
+ field:struct sockaddr * upeer_sockaddr; offset:24; size:8; signed:0;
+ field:int * upeer_addrlen; offset:32; size:8; signed:0;
+
+print fmt: "fd: 0x%08lx, upeer_sockaddr: 0x%08lx, upeer_addrlen: 0x%08lx", ((unsigned long)(REC->fd)), ((unsigned long)(REC->upeer_sockaddr)), ((unsigned long)(REC->upeer_addrlen))
+`
+
+const FormatExitAccept = `name: sys_exit_accept
+ID: 1807
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatSocket = `name: sys_enter_socket
+ID: 1818
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int family; offset:16; size:8; signed:0;
+ field:int type; offset:24; size:8; signed:0;
+ field:int protocol; offset:32; size:8; signed:0;
+
+print fmt: "family: 0x%08lx, type: 0x%08lx, protocol: 0x%08lx", ((unsigned long)(REC->family)), ((unsigned long)(REC->type)), ((unsigned long)(REC->protocol))
+`
+
+const FormatExitSocket = `name: sys_exit_socket
+ID: 1817
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatPread64 = `name: sys_enter_pread64
+ID: 840
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:unsigned int fd; offset:16; size:8; signed:0;
+ field:char * buf; offset:24; size:8; signed:0;
+ field:size_t count; offset:32; size:8; signed:0;
+ field:loff_t pos; offset:40; size:8; signed:0;
+
+print fmt: "fd: 0x%08lx, buf: 0x%08lx, count: 0x%08lx, pos: 0x%08lx", ((unsigned long)(REC->fd)), ((unsigned long)(REC->buf)), ((unsigned long)(REC->count)), ((unsigned long)(REC->pos))
+`
+
+const FormatExitPread64 = `name: sys_exit_pread64
+ID: 839
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
+const FormatSymlink = `name: sys_enter_symlink
+ID: 880
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:const char * oldname; offset:16; size:8; signed:0;
+ field:const char * newname; offset:24; size:8; signed:0;
+
+print fmt: "oldname: 0x%08lx, newname: 0x%08lx", ((unsigned long)(REC->oldname)), ((unsigned long)(REC->newname))
+`
+
+const FormatExitSymlink = `name: sys_exit_symlink
+ID: 879
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
diff --git a/internal/generate/tracepointsgo.go b/internal/generate/tracepointsgo.go
new file mode 100644
index 0000000..0542f64
--- /dev/null
+++ b/internal/generate/tracepointsgo.go
@@ -0,0 +1,43 @@
+package generate
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+)
+
+var secRe = regexp.MustCompile(`^SEC.*sys_((?:enter|exit)_[a-z_0-9]+)`)
+
+// ExtractTracepoints reads generated C code and extracts tracepoint names from
+// SEC annotations, producing the generated_tracepoints.go content.
+func ExtractTracepoints(r io.Reader) (string, error) {
+ scanner := bufio.NewScanner(r)
+ var tracepoints []string
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ if m := secRe.FindStringSubmatch(line); m != nil {
+ tracepoints = append(tracepoints, "sys_"+m[1])
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return "", fmt.Errorf("scanning input: %w", err)
+ }
+
+ return formatTracepointsGo(tracepoints), nil
+}
+
+func formatTracepointsGo(tracepoints []string) string {
+ var b strings.Builder
+ b.WriteString("// Code generated - don't change manually!\n")
+ b.WriteString("package tracepoints\n\n")
+ b.WriteString("var List = []string{\n")
+ for _, tp := range tracepoints {
+ fmt.Fprintf(&b, "\t%q,\n", tp)
+ }
+ b.WriteString("}\n")
+ return b.String()
+}
diff --git a/internal/generate/tracepointsgo_test.go b/internal/generate/tracepointsgo_test.go
new file mode 100644
index 0000000..d0f90db
--- /dev/null
+++ b/internal/generate/tracepointsgo_test.go
@@ -0,0 +1,92 @@
+package generate
+
+import (
+ "strings"
+ "testing"
+)
+
+const sampleGeneratedC = `// Code generated - don't change manually!
+
+/// Ignoring sys_enter_kill sys_exit_kill as possibly not file I/O related
+
+#define SYS_ENTER_READ 844
+#define SYS_EXIT_READ 843
+#define SYS_ENTER_CLOSE 778
+#define SYS_EXIT_CLOSE 777
+
+/// sys_enter_read is a struct fd_event
+SEC("tracepoint/syscalls/sys_enter_read")
+int handle_sys_enter_read(struct trace_event_raw_sys_enter *ctx) {
+ return 0;
+}
+
+/// sys_exit_read is a struct ret_event (READ_CLASSIFIED)
+SEC("tracepoint/syscalls/sys_exit_read")
+int handle_sys_exit_read(struct trace_event_raw_sys_exit *ctx) {
+ return 0;
+}
+
+/// sys_enter_close is a struct fd_event
+SEC("tracepoint/syscalls/sys_enter_close")
+int handle_sys_enter_close(struct trace_event_raw_sys_enter *ctx) {
+ return 0;
+}
+
+/// sys_exit_close is a struct ret_event (UNCLASSIFIED)
+SEC("tracepoint/syscalls/sys_exit_close")
+int handle_sys_exit_close(struct trace_event_raw_sys_exit *ctx) {
+ return 0;
+}
+`
+
+func TestExtractTracepoints(t *testing.T) {
+ output, err := ExtractTracepoints(strings.NewReader(sampleGeneratedC))
+ if err != nil {
+ t.Fatalf("ExtractTracepoints failed: %v", err)
+ }
+
+ requireContains(t, output, "package tracepoints")
+ requireContains(t, output, `"sys_enter_read",`)
+ requireContains(t, output, `"sys_exit_read",`)
+ requireContains(t, output, `"sys_enter_close",`)
+ requireContains(t, output, `"sys_exit_close",`)
+ requireContains(t, output, "var List = []string{")
+
+ // Should NOT contain ignore comments or defines
+ if strings.Contains(output, "kill") {
+ t.Error("output should not contain ignored tracepoints")
+ }
+}
+
+func TestExtractTracepointsOrder(t *testing.T) {
+ output, err := ExtractTracepoints(strings.NewReader(sampleGeneratedC))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ enterReadPos := strings.Index(output, `"sys_enter_read"`)
+ exitReadPos := strings.Index(output, `"sys_exit_read"`)
+ enterClosePos := strings.Index(output, `"sys_enter_close"`)
+ if enterReadPos > exitReadPos || exitReadPos > enterClosePos {
+ t.Error("tracepoints should maintain source order")
+ }
+}
+
+func TestExtractTracepointsEmpty(t *testing.T) {
+ output, err := ExtractTracepoints(strings.NewReader("// no SEC lines here\n"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ requireContains(t, output, "var List = []string{")
+ requireContains(t, output, "}")
+}
+
+func TestExtractTracepointsPackageHeader(t *testing.T) {
+ output, err := ExtractTracepoints(strings.NewReader(sampleGeneratedC))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.HasPrefix(output, "// Code generated - don't change manually!\npackage tracepoints\n") {
+ t.Errorf("unexpected header: %s", output[:60])
+ }
+}
diff --git a/internal/generate/typesgo.go b/internal/generate/typesgo.go
new file mode 100644
index 0000000..ee24845
--- /dev/null
+++ b/internal/generate/typesgo.go
@@ -0,0 +1,341 @@
+package generate
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+)
+
+type CConstant struct {
+ Name string
+ Value string
+}
+
+type CMember struct {
+ TypeName string
+ FieldName string
+ ArraySize string
+}
+
+type CStruct struct {
+ Name string
+ Members []CMember
+}
+
+// ParseCTypesInput parses C struct definitions and #define constants.
+func ParseCTypesInput(r io.Reader) ([]CStruct, []CConstant, error) {
+ scanner := bufio.NewScanner(r)
+ var structs []CStruct
+ var constants []CConstant
+ var current *CStruct
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+
+ if strings.HasPrefix(line, "#define") {
+ c, ok := parseDefine(line)
+ if ok {
+ constants = append(constants, c)
+ }
+ continue
+ }
+
+ if isCommentLine(line) || line == "" {
+ continue
+ }
+
+ if strings.HasPrefix(line, "struct") && strings.HasSuffix(line, "{") {
+ name := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "struct"), "{"))
+ current = &CStruct{Name: name}
+ continue
+ }
+
+ if line == "};" && current != nil {
+ structs = append(structs, *current)
+ current = nil
+ continue
+ }
+
+ if current != nil {
+ m, ok := parseMember(line)
+ if ok {
+ current.Members = append(current.Members, m)
+ }
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, nil, fmt.Errorf("scanning C input: %w", err)
+ }
+ return structs, constants, nil
+}
+
+// GenerateTypesGo produces the generated_types.go content.
+func GenerateTypesGo(structs []CStruct, constants []CConstant) string {
+ var b strings.Builder
+
+ b.WriteString("// Code generated - don't change manually!\n")
+ b.WriteString("package types\n\n")
+
+ writeTypeDefsAndMaps(&b, constants)
+
+ for _, c := range constants {
+ constType := ""
+ if strings.HasPrefix(c.Name, "SYS_") {
+ constType = " TraceId "
+ }
+ fmt.Fprintf(&b, "const %s%s = %s\n", c.Name, constType, c.Value)
+ }
+
+ for _, s := range structs {
+ writeGoStruct(&b, s)
+ }
+
+ return b.String()
+}
+
+// AddTypesImports inserts the import block needed by the generated types code.
+func AddTypesImports(code string) string {
+ needsImports := strings.Contains(code, "fmt.") ||
+ strings.Contains(code, "sync.") ||
+ strings.Contains(code, "binary.") ||
+ strings.Contains(code, "bytes.")
+
+ if !needsImports {
+ return code
+ }
+
+ importBlock := `import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "sync"
+)
+
+`
+ return strings.Replace(code, "package types\n\n", "package types\n\n"+importBlock, 1)
+}
+
+func parseDefine(line string) (CConstant, bool) {
+ fields := strings.Fields(line)
+ if len(fields) < 3 {
+ return CConstant{}, false
+ }
+ return CConstant{Name: fields[1], Value: fields[2]}, true
+}
+
+func isCommentLine(line string) bool {
+ return strings.HasPrefix(line, "//") || strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*")
+}
+
+var arrayRe = regexp.MustCompile(`^(\w+)\s+(\w+)\[(\w+)\];?$`)
+var simpleRe = regexp.MustCompile(`^(\w+)\s+(\w+);?$`)
+
+func parseMember(line string) (CMember, bool) {
+ line = strings.TrimSuffix(strings.TrimSpace(line), ";")
+ line = strings.TrimSpace(line)
+
+ if m := arrayRe.FindStringSubmatch(line + ";"); m != nil {
+ return CMember{TypeName: m[1], FieldName: m[2], ArraySize: m[3]}, true
+ }
+ if m := simpleRe.FindStringSubmatch(line + ";"); m != nil {
+ return CMember{TypeName: m[1], FieldName: m[2]}, true
+ }
+ return CMember{}, false
+}
+
+func writeTypeDefsAndMaps(b *strings.Builder, constants []CConstant) {
+ b.WriteString("type EventType uint32\n")
+ b.WriteString("type TraceId uint32\n\n")
+
+ var sysConstants []CConstant
+ for _, c := range constants {
+ if strings.HasPrefix(c.Name, "SYS_") {
+ sysConstants = append(sysConstants, c)
+ }
+ }
+
+ writeTraceIdMap(b, "traceId2String", sysConstants, func(name string) string {
+ return strings.ToLower(strings.TrimPrefix(name, "SYS_"))
+ })
+
+ writeTraceIdMap(b, "traceId2Name", sysConstants, func(name string) string {
+ s := strings.TrimPrefix(name, "SYS_ENTER_")
+ s = strings.TrimPrefix(s, "SYS_EXIT_")
+ return strings.ToLower(s)
+ })
+
+ writeTraceIdStringMethod(b)
+ writeTraceIdNameMethod(b)
+ b.WriteString("\n")
+}
+
+func writeTraceIdMap(b *strings.Builder, mapName string, constants []CConstant, transform func(string) string) {
+ fmt.Fprintf(b, "var %s = map[TraceId]string{\n\t", mapName)
+ entries := make([]string, 0, len(constants))
+ for _, c := range constants {
+ entries = append(entries, fmt.Sprintf("%s: %q", c.Value, transform(c.Name)))
+ }
+ b.WriteString(strings.Join(entries, ", "))
+ b.WriteString(",\n}\n\n")
+}
+
+func writeTraceIdStringMethod(b *strings.Builder) {
+ b.WriteString(`func (s TraceId) String() string {
+ str, ok := traceId2String[s]
+ if !ok {
+ panic(fmt.Sprintf("no string representation for trace ID %d found", s))
+ }
+ return str
+}
+
+`)
+}
+
+func writeTraceIdNameMethod(b *strings.Builder) {
+ b.WriteString(`func (s TraceId) Name() string {
+ str, ok := traceId2Name[s]
+ if !ok {
+ panic(fmt.Sprintf("no name for trace ID %d found", s))
+ }
+ return str
+}
+
+`)
+}
+
+func writeGoStruct(b *strings.Builder, s CStruct) {
+ goName := snakeToCamel(s.Name)
+ selfRef := strings.ToLower(goName[:1])
+
+ b.WriteString("\n")
+ fmt.Fprintf(b, "type %s struct {\n\t", goName)
+ memberDefs := make([]string, 0, len(s.Members))
+ for _, m := range s.Members {
+ memberDefs = append(memberDefs, goMemberDef(m))
+ }
+ b.WriteString(strings.Join(memberDefs, "; "))
+ b.WriteString(" \n}\n\n")
+
+ writeStringMethod(b, goName, selfRef, s.Members)
+ writeEqualsMethod(b, goName, selfRef, s.Members)
+ writeGetterMethods(b, goName, selfRef)
+
+ if strings.HasSuffix(goName, "Event") {
+ b.WriteString("\n")
+ writeSyncPool(b, goName, selfRef)
+ }
+}
+
+func goMemberDef(m CMember) string {
+ goField := snakeToCamel(m.FieldName)
+ goType := cTypeToGoType(m.TypeName)
+
+ if goField == "TraceId" {
+ goType = "TraceId"
+ }
+ if goField == "EventType" {
+ goType = "EventType"
+ }
+
+ if m.ArraySize != "" {
+ return fmt.Sprintf("%s [%s]%s", goField, m.ArraySize, goType)
+ }
+ return fmt.Sprintf("%s %s", goField, goType)
+}
+
+func writeStringMethod(b *strings.Builder, goName, selfRef string, members []CMember) {
+ fmtParts := make([]string, 0, len(members))
+ argParts := make([]string, 0, len(members))
+ for _, m := range members {
+ goField := snakeToCamel(m.FieldName)
+ fmtParts = append(fmtParts, goField+":%v")
+ ref := selfRef + "." + goField
+ if m.TypeName == "char" && m.ArraySize != "" {
+ ref = fmt.Sprintf("string(%s[:])", ref)
+ }
+ argParts = append(argParts, ref)
+ }
+ fmt.Fprintf(b, "func (%s %s) String() string {\n", selfRef, goName)
+ fmt.Fprintf(b, "\treturn fmt.Sprintf(\"%s\", %s)\n", strings.Join(fmtParts, " "), strings.Join(argParts, ", "))
+ b.WriteString("}\n\n")
+}
+
+func writeEqualsMethod(b *strings.Builder, goName, selfRef string, members []CMember) {
+ fmt.Fprintf(b, "func (%s %s) Equals(other any) bool {\n", selfRef, goName)
+ fmt.Fprintf(b, "\totherConcrete, ok := other.(*%s)\n", goName)
+ b.WriteString("\tif !ok {\n\t\treturn false\n\t}\n")
+ conds := make([]string, 0, len(members))
+ for _, m := range members {
+ goField := snakeToCamel(m.FieldName)
+ conds = append(conds, fmt.Sprintf("%s.%s == otherConcrete.%s", selfRef, goField, goField))
+ }
+ fmt.Fprintf(b, "\treturn %s\n", strings.Join(conds, " && "))
+ b.WriteString("}\n\n")
+}
+
+func writeGetterMethods(b *strings.Builder, goName, selfRef string) {
+ getters := []struct {
+ method string
+ returnType string
+ field string
+ }{
+ {"GetEventType", "EventType", "EventType"},
+ {"GetTraceId", "TraceId", "TraceId"},
+ {"GetPid", "uint32", "Pid"},
+ {"GetTid", "uint32", "Tid"},
+ {"GetTime", "uint64", "Time"},
+ }
+ for _, g := range getters {
+ fmt.Fprintf(b, "func (%s *%s) %s() %s {\n\treturn %s.%s\n}\n\n",
+ selfRef, goName, g.method, g.returnType, selfRef, g.field)
+ }
+}
+
+func writeSyncPool(b *strings.Builder, goName, selfRef string) {
+ fmt.Fprintf(b, "var poolOf%ss = sync.Pool{\n\tNew: func() interface{} { return &%s{} },\n}\n\n", goName, goName)
+ fmt.Fprintf(b, "func New%s(raw []byte) *%s {\n", goName, goName)
+ fmt.Fprintf(b, "\t%s := poolOf%ss.Get().(*%s)\n", selfRef, goName, goName)
+ fmt.Fprintf(b, "\tif err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, %s); err != nil {\n", selfRef)
+ fmt.Fprintf(b, "\t\tfmt.Println(%s, raw, len(raw), err)\n", selfRef)
+ b.WriteString("\t\tpanic(raw)\n\t}\n")
+ fmt.Fprintf(b, "\treturn %s\n}\n\n", selfRef)
+
+ fmt.Fprintf(b, "func (%s *%s) Bytes() ([]byte, error) {\n", selfRef, goName)
+ b.WriteString("\tbuf := new(bytes.Buffer)\n")
+ fmt.Fprintf(b, "\terr := binary.Write(buf, binary.LittleEndian, %s)\n", selfRef)
+ b.WriteString("\tif err != nil {\n\t\treturn nil, err\n\t}\n")
+ b.WriteString("\treturn buf.Bytes(), nil\n}\n\n")
+
+ fmt.Fprintf(b, "func (%s *%s) Recycle() {\n\tpoolOf%ss.Put(%s)\n}\n", selfRef, goName, goName, selfRef)
+}
+
+func snakeToCamel(s string) string {
+ parts := strings.Split(s, "_")
+ for i, p := range parts {
+ if p == "" {
+ continue
+ }
+ parts[i] = strings.ToUpper(p[:1]) + p[1:]
+ }
+ return strings.Join(parts, "")
+}
+
+func cTypeToGoType(t string) string {
+ switch t {
+ case "char":
+ return "byte"
+ case "__s32":
+ return "int32"
+ case "__u32":
+ return "uint32"
+ case "__s64":
+ return "int64"
+ case "__u64":
+ return "uint64"
+ default:
+ return t
+ }
+}
diff --git a/internal/generate/typesgo_test.go b/internal/generate/typesgo_test.go
new file mode 100644
index 0000000..f1085b5
--- /dev/null
+++ b/internal/generate/typesgo_test.go
@@ -0,0 +1,257 @@
+package generate
+
+import (
+ "strings"
+ "testing"
+)
+
+const testTypesH = `//+build ignore
+
+#define MAX_FILENAME_LENGTH 256
+#define MAX_PROGNAME_LENGTH 16
+
+#define ENTER_OPEN_EVENT 1
+#define EXIT_OPEN_EVENT 2
+
+#define UNCLASSIFIED 0
+#define READ_CLASSIFIED 1
+
+struct open_event {
+ __u32 event_type;
+ __u32 trace_id;
+ __u64 time;
+ __u32 pid;
+ __u32 tid;
+ __s32 flags;
+ char filename[MAX_FILENAME_LENGTH];
+ char comm[MAX_PROGNAME_LENGTH];
+};
+
+struct null_event {
+ __u32 event_type;
+ __u32 trace_id;
+ __u64 time;
+ __u32 pid;
+ __u32 tid;
+};
+
+struct fd_event {
+ __u32 event_type;
+ __u32 trace_id;
+ __u64 time;
+ __u32 pid;
+ __u32 tid;
+ __s32 fd;
+};
+`
+
+const testDefines = `#define SYS_ENTER_OPENAT 784
+#define SYS_EXIT_OPENAT 783
+`
+
+func TestParseCTypesInput(t *testing.T) {
+ input := testTypesH + testDefines
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatalf("ParseCTypesInput failed: %v", err)
+ }
+ if len(structs) != 3 {
+ t.Fatalf("expected 3 structs, got %d", len(structs))
+ }
+ if structs[0].Name != "open_event" {
+ t.Errorf("first struct name = %q, want open_event", structs[0].Name)
+ }
+ if len(structs[0].Members) != 8 {
+ t.Errorf("open_event members = %d, want 8", len(structs[0].Members))
+ }
+
+ // Check array member
+ filenameMember := structs[0].Members[6]
+ if filenameMember.FieldName != "filename" || filenameMember.ArraySize != "MAX_FILENAME_LENGTH" {
+ t.Errorf("filename member = %+v", filenameMember)
+ }
+
+ // Check constants
+ expectedConsts := 8 // MAX_FILENAME_LENGTH, MAX_PROGNAME_LENGTH, 2 event types, 2 classified, 2 SYS_
+ if len(constants) != expectedConsts {
+ t.Errorf("constants = %d, want %d", len(constants), expectedConsts)
+ }
+}
+
+func TestParseCStructMembers(t *testing.T) {
+ input := testTypesH
+ structs, _, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fd := structs[2] // fd_event
+ if fd.Name != "fd_event" {
+ t.Fatalf("third struct = %q, want fd_event", fd.Name)
+ }
+ if len(fd.Members) != 6 {
+ t.Fatalf("fd_event members = %d, want 6", len(fd.Members))
+ }
+ if fd.Members[5].TypeName != "__s32" || fd.Members[5].FieldName != "fd" {
+ t.Errorf("fd member = %+v", fd.Members[5])
+ }
+}
+
+func TestSnakeToCamel(t *testing.T) {
+ tests := []struct {
+ input, want string
+ }{
+ {"open_event", "OpenEvent"},
+ {"trace_id", "TraceId"},
+ {"event_type", "EventType"},
+ {"fd", "Fd"},
+ {"pid", "Pid"},
+ {"open_by_handle_at_event", "OpenByHandleAtEvent"},
+ {"filename", "Filename"},
+ }
+ for _, tt := range tests {
+ if got := snakeToCamel(tt.input); got != tt.want {
+ t.Errorf("snakeToCamel(%q) = %q, want %q", tt.input, got, tt.want)
+ }
+ }
+}
+
+func TestCTypeToGoType(t *testing.T) {
+ tests := []struct {
+ input, want string
+ }{
+ {"char", "byte"},
+ {"__s32", "int32"},
+ {"__u32", "uint32"},
+ {"__s64", "int64"},
+ {"__u64", "uint64"},
+ }
+ for _, tt := range tests {
+ if got := cTypeToGoType(tt.input); got != tt.want {
+ t.Errorf("cTypeToGoType(%q) = %q, want %q", tt.input, got, tt.want)
+ }
+ }
+}
+
+func TestGenerateTypesGoStructs(t *testing.T) {
+ input := testTypesH + testDefines
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := GenerateTypesGo(structs, constants)
+
+ requireContains(t, output, "type OpenEvent struct {")
+ requireContains(t, output, "EventType EventType")
+ requireContains(t, output, "TraceId TraceId")
+ requireContains(t, output, "Time uint64")
+ requireContains(t, output, "Pid uint32")
+ requireContains(t, output, "Flags int32")
+ requireContains(t, output, "Filename [MAX_FILENAME_LENGTH]byte")
+ requireContains(t, output, "Comm [MAX_PROGNAME_LENGTH]byte")
+}
+
+func TestGenerateTypesGoMethods(t *testing.T) {
+ input := testTypesH + testDefines
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := GenerateTypesGo(structs, constants)
+
+ // String method with char array conversion
+ requireContains(t, output, `string(o.Filename[:])`)
+ requireContains(t, output, `string(o.Comm[:])`)
+ requireContains(t, output, "func (o OpenEvent) String() string")
+
+ // Equals method
+ requireContains(t, output, "func (o OpenEvent) Equals(other any) bool")
+ requireContains(t, output, "other.(*OpenEvent)")
+
+ // Getters
+ requireContains(t, output, "func (o *OpenEvent) GetEventType() EventType")
+ requireContains(t, output, "func (o *OpenEvent) GetTraceId() TraceId")
+ requireContains(t, output, "func (o *OpenEvent) GetPid() uint32")
+ requireContains(t, output, "func (o *OpenEvent) GetTid() uint32")
+ requireContains(t, output, "func (o *OpenEvent) GetTime() uint64")
+}
+
+func TestGenerateTypesGoSyncPool(t *testing.T) {
+ input := testTypesH + testDefines
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := GenerateTypesGo(structs, constants)
+
+ requireContains(t, output, "var poolOfOpenEvents = sync.Pool{")
+ requireContains(t, output, "func NewOpenEvent(raw []byte) *OpenEvent")
+ requireContains(t, output, "func (o *OpenEvent) Bytes() ([]byte, error)")
+ requireContains(t, output, "func (o *OpenEvent) Recycle()")
+ requireContains(t, output, "poolOfOpenEvents.Put(o)")
+
+ requireContains(t, output, "var poolOfNullEvents = sync.Pool{")
+ requireContains(t, output, "func NewNullEvent(raw []byte) *NullEvent")
+
+ requireContains(t, output, "var poolOfFdEvents = sync.Pool{")
+ requireContains(t, output, "func NewFdEvent(raw []byte) *FdEvent")
+}
+
+func TestGenerateTypesGoConstants(t *testing.T) {
+ input := testTypesH + testDefines
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := GenerateTypesGo(structs, constants)
+
+ requireContains(t, output, "const MAX_FILENAME_LENGTH = 256")
+ requireContains(t, output, "const MAX_PROGNAME_LENGTH = 16")
+ requireContains(t, output, "const ENTER_OPEN_EVENT = 1")
+ requireContains(t, output, "const SYS_ENTER_OPENAT TraceId = 784")
+ requireContains(t, output, "const SYS_EXIT_OPENAT TraceId = 783")
+}
+
+func TestGenerateTypesGoTraceIdMaps(t *testing.T) {
+ input := testTypesH + testDefines
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := GenerateTypesGo(structs, constants)
+
+ requireContains(t, output, "type EventType uint32")
+ requireContains(t, output, "type TraceId uint32")
+ requireContains(t, output, "var traceId2String = map[TraceId]string{")
+ requireContains(t, output, `784: "enter_openat"`)
+ requireContains(t, output, `783: "exit_openat"`)
+ requireContains(t, output, "var traceId2Name = map[TraceId]string{")
+ requireContains(t, output, `784: "openat"`)
+ requireContains(t, output, `783: "openat"`)
+}
+
+func TestGenerateTypesGoTraceIdMethods(t *testing.T) {
+ input := testTypesH + testDefines
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := GenerateTypesGo(structs, constants)
+
+ requireContains(t, output, "func (s TraceId) String() string")
+ requireContains(t, output, "func (s TraceId) Name() string")
+ requireContains(t, output, `panic(fmt.Sprintf("no string representation for trace ID %d found", s))`)
+}
+
+func TestGenerateTypesGoPackageDecl(t *testing.T) {
+ input := testTypesH
+ structs, constants, err := ParseCTypesInput(strings.NewReader(input))
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := GenerateTypesGo(structs, constants)
+
+ if !strings.HasPrefix(output, "// Code generated - don't change manually!\npackage types\n") {
+ t.Errorf("unexpected package header: %s", output[:80])
+ }
+}