package file import ( "bufio" "bytes" "fmt" "os" "strconv" "strings" "ior/internal/types" ) // File is the common interface for file-like syscall payload representations. // // Implementations may represent either a live file descriptor-backed handle // (FdFile) or partial path metadata for syscalls that do not resolve to a // stable descriptor (for example rename-like or pathname-only events). // // Semantics: // - Name returns the best single-path identifier for the event. For // rename-like events this is the "new" path; for pathname-only events it is // the observed pathname. // - Flags returns open flags when known, otherwise unknownFlag. // - FD returns the tracked file descriptor when one exists, otherwise -1. // - String returns a human-readable representation suitable for TUI/CSV use // and should remain informative even when Name or FD are unavailable. type File interface { String() string Name() string Flags() Flags FD() int32 } // FdFile represents a file descriptor-backed file reference. type FdFile struct { fd int32 name string flags Flags flagsFromProcFS bool } // 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), } if f.flags == -1 { f.flags = unknownFlag } return f } // NewFdWithPid resolves descriptor metadata from /proc//fd. func NewFdWithPid(fd int32, pid uint32) *FdFile { f := &FdFile{ fd: fd, } var err error procPath := fmt.Sprintf("/proc/%d/fd/%d", pid, fd) f.name, err = os.Readlink(procPath) if err != nil { f.name = "" f.flags = unknownFlag f.flagsFromProcFS = true return f } flags, err := readFlagsFromFdInfo(fd, pid) if err != nil { f.flags = unknownFlag } else { f.flags = flags } f.flagsFromProcFS = true return f } func (f *FdFile) Dup(fd int32) *FdFile { dupFd := *f dupFd.fd = fd return &dupFd } func readFlagsFromFdInfo(fd int32, pid uint32) (Flags, error) { data, err := os.ReadFile(fmt.Sprintf("/proc/%d/fdinfo/%d", pid, fd)) if err != nil { return unknownFlag, err } return parseFlagsFromFdInfo(data) } func parseFlagsFromFdInfo(data []byte) (Flags, error) { scanner := bufio.NewScanner(bytes.NewReader(data)) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "flags:") { fields := strings.Fields(line) if len(fields) < 2 { return unknownFlag, fmt.Errorf("malformed flags line in fdinfo: %q", line) } flags, err := strconv.ParseUint(fields[1], 8, 32) return Flags(flags), err } } if err := scanner.Err(); err != nil { return unknownFlag, err } return unknownFlag, fmt.Errorf("flags field not found in fdinfo") } func (f *FdFile) Name() string { return f.name } func (f *FdFile) String() string { var sb strings.Builder if len(f.name) == 0 { sb.WriteString("E:name") // Empty name string } else { sb.WriteString(f.name) } sb.WriteString("%(") sb.WriteString(strconv.FormatInt(int64(f.fd), 10)) sb.WriteString(",") sb.WriteString(f.Flags().String()) sb.WriteString(")") return sb.String() } func (f *FdFile) Flags() Flags { return f.flags } func (f *FdFile) FD() int32 { return f.fd } func (f *FdFile) SetFlags(flags int32) { f.flags = Flags(flags) } func (f *FdFile) AddFlags(flags int32) { f.flags = Flags(int32(f.flags) | flags) } 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)} } func (f oldnameNewnameFile) Name() string { return f.Newname } func (f oldnameNewnameFile) Flags() Flags { return unknownFlag } func (f oldnameNewnameFile) FD() int32 { return -1 } func (f oldnameNewnameFile) String() string { var sb strings.Builder sb.WriteString("old:") sb.WriteString(f.Oldname) sb.WriteString(" ->new:") sb.WriteString(f.Newname) sb.WriteString("%(") sb.WriteString(f.Flags().String()) sb.WriteString(")") return sb.String() } type pathnameFile struct { Pathname string } // NewPathname creates a path-only file representation. func NewPathname(pathname []byte) pathnameFile { return pathnameFile{types.StringValue(pathname)} } func (f pathnameFile) Name() string { return f.Pathname } func (f pathnameFile) Flags() Flags { return unknownFlag } func (f pathnameFile) FD() int32 { return -1 } func (f pathnameFile) String() string { var sb strings.Builder sb.WriteString("pathname:") sb.WriteString(f.Pathname) sb.WriteString("%(") sb.WriteString(f.Flags().String()) sb.WriteString(")") return sb.String() } // --- compile-time interface satisfaction assertions --- // // *FdFile is the primary public implementation of File used throughout the // codebase. The assertion causes a build error if FdFile drifts out of sync // with the File interface contract. var _ File = (*FdFile)(nil)