diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-06 17:32:24 +0200 |
| commit | 1561987330cb898f5ff64383a9c78e7e6559f118 (patch) | |
| tree | 69a823e8f98dce572566c97e6879c11c9d591bda /internal/file | |
| parent | 96225fb6159212a8851043a08d781aba721b4e78 (diff) | |
| parent | 110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff) | |
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/file')
| -rw-r--r-- | internal/file/doc.go | 2 | ||||
| -rw-r--r-- | internal/file/file.go | 33 | ||||
| -rw-r--r-- | internal/file/file_test.go | 141 | ||||
| -rw-r--r-- | internal/file/flags.go | 11 | ||||
| -rw-r--r-- | internal/file/flags_test.go | 40 |
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) + } +} |
