summaryrefslogtreecommitdiff
path: root/internal/file
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
committerPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
commit1561987330cb898f5ff64383a9c78e7e6559f118 (patch)
tree69a823e8f98dce572566c97e6879c11c9d591bda /internal/file
parent96225fb6159212a8851043a08d781aba721b4e78 (diff)
parent110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff)
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/file')
-rw-r--r--internal/file/doc.go2
-rw-r--r--internal/file/file.go33
-rw-r--r--internal/file/file_test.go141
-rw-r--r--internal/file/flags.go11
-rw-r--r--internal/file/flags_test.go40
5 files changed, 142 insertions, 85 deletions
diff --git a/internal/file/doc.go b/internal/file/doc.go
new file mode 100644
index 0000000..dd3c2dd
--- /dev/null
+++ b/internal/file/doc.go
@@ -0,0 +1,2 @@
+// Package file provides file metadata helpers used by trace rendering and export code.
+package file
diff --git a/internal/file/file.go b/internal/file/file.go
index b1bd84c..b95d40b 100644
--- a/internal/file/file.go
+++ b/internal/file/file.go
@@ -4,12 +4,14 @@ import (
"bufio"
"bytes"
"fmt"
- "ior/internal/types"
"os"
"strconv"
"strings"
+
+ "ior/internal/types"
)
+// File is the common interface for file-like syscall payload representations.
type File interface {
String() string
Name() string
@@ -17,6 +19,7 @@ type File interface {
FD() int32
}
+// FdFile represents a file descriptor-backed file reference.
type FdFile struct {
fd int32
name string
@@ -24,8 +27,9 @@ type FdFile struct {
flagsFromProcFS bool
}
-func NewFd(fd int32, name string, flags int32) FdFile {
- f := FdFile{
+// NewFd constructs an FdFile from explicit descriptor metadata.
+func NewFd(fd int32, name string, flags int32) *FdFile {
+ f := &FdFile{
fd: fd,
name: name,
flags: Flags(flags),
@@ -36,10 +40,13 @@ func NewFd(fd int32, name string, flags int32) FdFile {
return f
}
-func NewFdWithPid(fd int32, pid uint32) (f FdFile) {
+// NewFdWithPid resolves descriptor metadata from /proc/<pid>/fd.
+func NewFdWithPid(fd int32, pid uint32) *FdFile {
+ f := &FdFile{
+ fd: fd,
+ }
var err error
- f.fd = fd
procPath := fmt.Sprintf("/proc/%d/fd/%d", pid, fd)
f.name, err = os.Readlink(procPath)
if err != nil {
@@ -55,10 +62,10 @@ func NewFdWithPid(fd int32, pid uint32) (f FdFile) {
return f
}
-func (f FdFile) Dup(fd int32) FdFile {
- dupFd := f
+func (f *FdFile) Dup(fd int32) *FdFile {
+ dupFd := *f
dupFd.fd = fd
- return dupFd
+ return &dupFd
}
func readFlagsFromFdInfo(fd int32, pid uint32) (Flags, error) {
@@ -78,11 +85,11 @@ func readFlagsFromFdInfo(fd int32, pid uint32) (Flags, error) {
return unknownFlag, scanner.Err()
}
-func (f FdFile) Name() string {
+func (f *FdFile) Name() string {
return f.name
}
-func (f FdFile) String() string {
+func (f *FdFile) String() string {
var sb strings.Builder
if len(f.name) == 0 {
@@ -99,11 +106,11 @@ func (f FdFile) String() string {
return sb.String()
}
-func (f FdFile) Flags() Flags {
+func (f *FdFile) Flags() Flags {
return f.flags
}
-func (f FdFile) FD() int32 {
+func (f *FdFile) FD() int32 {
return f.fd
}
@@ -119,6 +126,7 @@ type oldnameNewnameFile struct {
Oldname, Newname string
}
+// NewOldnameNewname creates a file representation for rename-like syscalls.
func NewOldnameNewname(oldname, newname []byte) oldnameNewnameFile {
return oldnameNewnameFile{types.StringValue(oldname), types.StringValue(newname)}
}
@@ -153,6 +161,7 @@ type pathnameFile struct {
Pathname string
}
+// NewPathname creates a path-only file representation.
func NewPathname(pathname []byte) pathnameFile {
return pathnameFile{types.StringValue(pathname)}
}
diff --git a/internal/file/file_test.go b/internal/file/file_test.go
index 684a7d8..f9025fe 100644
--- a/internal/file/file_test.go
+++ b/internal/file/file_test.go
@@ -1,108 +1,109 @@
package file
import (
- "ior/internal/types"
"strings"
"syscall"
"testing"
+
+ "ior/internal/types"
)
func TestStringValue(t *testing.T) {
- var array [128]byte
- copy(array[:], "test string")
+ var array [128]byte
+ copy(array[:], "test string")
- if str := types.StringValue(array[:]); str != "test string" {
- t.Errorf("epxected 'test string' but got '%s' with bytes '%v'", str, []byte(str))
- }
+ if str := types.StringValue(array[:]); str != "test string" {
+ t.Errorf("epxected 'test string' but got '%s' with bytes '%v'", str, []byte(str))
+ }
}
func TestNewFdUnknownFlags(t *testing.T) {
- fdFile := NewFd(1, "test.txt", -1)
- if fdFile.Flags() != unknownFlag {
- t.Errorf("expected unknown flags, got %v", fdFile.Flags())
- }
+ fdFile := NewFd(1, "test.txt", -1)
+ if fdFile.Flags() != unknownFlag {
+ t.Errorf("expected unknown flags, got %v", fdFile.Flags())
+ }
}
func TestNewFdEmptyName(t *testing.T) {
- fdFile := NewFd(1, "", 0)
- str := fdFile.String()
- if !strings.Contains(str, "E:name") {
- t.Errorf("expected String() to contain 'E:name' for empty name, got '%s'", str)
- }
+ fdFile := NewFd(1, "", 0)
+ str := fdFile.String()
+ if !strings.Contains(str, "E:name") {
+ t.Errorf("expected String() to contain 'E:name' for empty name, got '%s'", str)
+ }
}
func TestFlagsIsUnknown(t *testing.T) {
- f := unknownFlag
- if f.Is(syscall.O_RDONLY) {
- t.Errorf("expected Is(O_RDONLY) to be false for unknownFlag")
- }
- if f.Is(syscall.O_WRONLY) {
- t.Errorf("expected Is(O_WRONLY) to be false for unknownFlag")
- }
- if f.Is(syscall.O_RDWR) {
- t.Errorf("expected Is(O_RDWR) to be false for unknownFlag")
- }
+ f := unknownFlag
+ if f.Is(syscall.O_RDONLY) {
+ t.Errorf("expected Is(O_RDONLY) to be false for unknownFlag")
+ }
+ if f.Is(syscall.O_WRONLY) {
+ t.Errorf("expected Is(O_WRONLY) to be false for unknownFlag")
+ }
+ if f.Is(syscall.O_RDWR) {
+ t.Errorf("expected Is(O_RDWR) to be false for unknownFlag")
+ }
}
func TestFlagsStringUnknown(t *testing.T) {
- f := Flags(-1)
- if f.String() != "O_NONE" {
- t.Errorf("expected 'O_NONE' for unknown flags, got '%s'", f.String())
- }
+ f := Flags(-1)
+ if f.String() != "O_NONE" {
+ t.Errorf("expected 'O_NONE' for unknown flags, got '%s'", f.String())
+ }
}
func TestNewOldnameNewnameEmpty(t *testing.T) {
- var oldname, newname [128]byte
- f := NewOldnameNewname(oldname[:], newname[:])
- if f.Name() != "" {
- t.Errorf("expected empty Name(), got '%s'", f.Name())
- }
- if !strings.Contains(f.String(), "old:") || !strings.Contains(f.String(), "->new:") {
- t.Errorf("expected String() to contain 'old:' and '->new:', got '%s'", f.String())
- }
+ var oldname, newname [128]byte
+ f := NewOldnameNewname(oldname[:], newname[:])
+ if f.Name() != "" {
+ t.Errorf("expected empty Name(), got '%s'", f.Name())
+ }
+ if !strings.Contains(f.String(), "old:") || !strings.Contains(f.String(), "->new:") {
+ t.Errorf("expected String() to contain 'old:' and '->new:', got '%s'", f.String())
+ }
}
func TestNewPathnameEmpty(t *testing.T) {
- var pathname [128]byte
- f := NewPathname(pathname[:])
- if f.Name() != "" {
- t.Errorf("expected empty Name(), got '%s'", f.Name())
- }
- if !strings.Contains(f.String(), "pathname:") {
- t.Errorf("expected String() to contain 'pathname:', got '%s'", f.String())
- }
+ var pathname [128]byte
+ f := NewPathname(pathname[:])
+ if f.Name() != "" {
+ t.Errorf("expected empty Name(), got '%s'", f.Name())
+ }
+ if !strings.Contains(f.String(), "pathname:") {
+ t.Errorf("expected String() to contain 'pathname:', got '%s'", f.String())
+ }
}
func TestFdFileSetFlags(t *testing.T) {
- fdFile := NewFd(1, "test.txt", 0)
- if fdFile.Flags() != Flags(0) {
- t.Errorf("expected flags 0, got %v", fdFile.Flags())
- }
- fdFile.SetFlags(syscall.O_WRONLY)
- if fdFile.Flags() != Flags(syscall.O_WRONLY) {
- t.Errorf("expected O_WRONLY after SetFlags, got %v", fdFile.Flags())
- }
+ fdFile := NewFd(1, "test.txt", 0)
+ if fdFile.Flags() != Flags(0) {
+ t.Errorf("expected flags 0, got %v", fdFile.Flags())
+ }
+ fdFile.SetFlags(syscall.O_WRONLY)
+ if fdFile.Flags() != Flags(syscall.O_WRONLY) {
+ t.Errorf("expected O_WRONLY after SetFlags, got %v", fdFile.Flags())
+ }
}
func TestFdFileAddFlags(t *testing.T) {
- fdFile := NewFd(1, "test.txt", syscall.O_RDWR)
- fdFile.AddFlags(syscall.O_APPEND)
- expected := Flags(syscall.O_RDWR | syscall.O_APPEND)
- if fdFile.Flags() != expected {
- t.Errorf("expected O_RDWR|O_APPEND after AddFlags, got %v", fdFile.Flags())
- }
+ fdFile := NewFd(1, "test.txt", syscall.O_RDWR)
+ fdFile.AddFlags(syscall.O_APPEND)
+ expected := Flags(syscall.O_RDWR | syscall.O_APPEND)
+ if fdFile.Flags() != expected {
+ t.Errorf("expected O_RDWR|O_APPEND after AddFlags, got %v", fdFile.Flags())
+ }
}
func TestFdFileDup(t *testing.T) {
- fdFile := NewFd(1, "original.txt", syscall.O_RDONLY)
- duped := fdFile.Dup(42)
- if duped.Name() != "original.txt" {
- t.Errorf("expected duped name 'original.txt', got '%s'", duped.Name())
- }
- if !strings.Contains(duped.String(), "42") {
- t.Errorf("expected duped String() to contain fd 42, got '%s'", duped.String())
- }
- if strings.Contains(duped.String(), "%(1,") {
- t.Errorf("expected duped String() to NOT contain old fd 1, got '%s'", duped.String())
- }
+ fdFile := NewFd(1, "original.txt", syscall.O_RDONLY)
+ duped := fdFile.Dup(42)
+ if duped.Name() != "original.txt" {
+ t.Errorf("expected duped name 'original.txt', got '%s'", duped.Name())
+ }
+ if !strings.Contains(duped.String(), "42") {
+ t.Errorf("expected duped String() to contain fd 42, got '%s'", duped.String())
+ }
+ if strings.Contains(duped.String(), "%(1,") {
+ t.Errorf("expected duped String() to NOT contain old fd 1, got '%s'", duped.String())
+ }
}
diff --git a/internal/file/flags.go b/internal/file/flags.go
index ca749e1..c06c27b 100644
--- a/internal/file/flags.go
+++ b/internal/file/flags.go
@@ -3,12 +3,13 @@ package file
import (
"os"
"strings"
+ "sync"
"syscall"
)
type Flags int32
-var flagsToHumanCache = map[Flags]string{}
+var flagsToHumanCache sync.Map
var unknownFlag = Flags(-1)
type tuple struct {
@@ -49,12 +50,16 @@ func (f Flags) Is(flag int) bool {
}
func (f Flags) BuildString(sb *strings.Builder) {
- if str, ok := flagsToHumanCache[f]; ok {
+ if cached, ok := flagsToHumanCache.Load(f); ok {
+ str, _ := cached.(string)
sb.WriteString(str)
return
}
str := f.String()
- flagsToHumanCache[f] = str
+ cached, loaded := flagsToHumanCache.LoadOrStore(f, str)
+ if loaded {
+ str, _ = cached.(string)
+ }
sb.WriteString(str)
}
diff --git a/internal/file/flags_test.go b/internal/file/flags_test.go
new file mode 100644
index 0000000..120e432
--- /dev/null
+++ b/internal/file/flags_test.go
@@ -0,0 +1,40 @@
+package file
+
+import (
+ "strings"
+ "sync"
+ "syscall"
+ "testing"
+)
+
+func TestFlagsBuildStringConcurrent(t *testing.T) {
+ flagsToHumanCache = sync.Map{}
+
+ const workers = 32
+ const iterations = 500
+ const want = "O_WRONLY|O_APPEND"
+ flag := Flags(syscall.O_WRONLY | syscall.O_APPEND)
+
+ var wg sync.WaitGroup
+ errs := make(chan string, workers)
+ for i := 0; i < workers; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := 0; j < iterations; j++ {
+ var sb strings.Builder
+ flag.BuildString(&sb)
+ if got := sb.String(); got != want {
+ errs <- got
+ return
+ }
+ }
+ }()
+ }
+ wg.Wait()
+ close(errs)
+
+ for got := range errs {
+ t.Fatalf("unexpected BuildString output %q, want %q", got, want)
+ }
+}