kitty/tools/utils/file_at_fd.go
copilot-swe-agent[bot] 7757d160bc
RemoveChildren: use RefCountedFile instead of DupFile, skip seek for child dirs
Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/e648c02e-d4cc-4f6c-8188-17941e8a1dd8

Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
2026-04-27 04:01:02 +00:00

704 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package utils
import (
"container/list"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sync/atomic"
"syscall"
"time"
"github.com/kovidgoyal/go-parallel"
"golang.org/x/sys/unix"
)
var _ = fmt.Print
// MkdirAt creates a new subdirectory named 'name' inside the directory
// pointed to by parentDir.
func MkdirAt(parentDir *os.File, name string, perm os.FileMode) (err error) {
// parentDir.Fd() gives us the base directory handle
fd := int(parentDir.Fd())
// unix.Mkdirat(dirfd, path, mode)
// We convert the os.FileMode to a uint32 for the syscall
for {
if err = unix.Mkdirat(fd, name, uint32(perm)); err != unix.EINTR {
break
}
}
if err != nil {
return &fs.PathError{
Op: "mkdirat",
Path: filepath.Join(parentDir.Name(), name),
Err: err,
}
}
return nil
}
// OpenAt opens a file relative to the directory pointed to by dirFile.
// Matches the behavior of os.Open (read-only).
func OpenAt(dirFile *os.File, name string) (*os.File, error) {
return openAt(dirFile, name, unix.O_RDONLY, 0)
}
// Opens a directory relative to the directory pointed to by dirFile.
// Matches the behavior of os.Open (read-only).
func OpenDirAt(dirFile *os.File, name string) (*os.File, error) {
return openAt(dirFile, name, unix.O_RDONLY|unix.O_DIRECTORY, 0)
}
// Create a symlink named name in the directory pointed to by dirFile. The
// target of the symlink is set to target
func SymlinkAt(dirFile *os.File, name, target string) (err error) {
for {
if err = unix.Symlinkat(target, int(dirFile.Fd()), name); err != unix.EINTR {
break
}
}
if err != nil {
return &fs.PathError{
Op: "symlinkat",
Path: filepath.Join(dirFile.Name(), name),
Err: err,
}
}
return
}
// CreateAt creates or truncates a file relative to the directory pointed to by dirFile.
// Matches the behavior of os.Create (read-write, creates if doesn't exist, truncates).
func CreateAt(dirFile *os.File, name string, permissions os.FileMode) (*os.File, error) {
return openAt(dirFile, name, unix.O_RDWR|unix.O_CREAT|unix.O_TRUNC, permissions)
}
// Create the specified directory, open it and return the file object. If the
// directory already exists, it is opened and returned, without changing its
// permissions, matching the behavior of CreateAt().
func CreateDirAt(parent *os.File, name string, permissions os.FileMode) (*os.File, error) {
if err := MkdirAt(parent, name, permissions); err != nil {
if errors.Is(err, unix.EEXIST) {
return OpenDirAt(parent, name)
}
return nil, err
}
return OpenDirAt(parent, name)
}
// Internal helper to wrap the unix.Openat syscall
func openAt(dirFile *os.File, name string, flags int, perm os.FileMode) (ans *os.File, err error) {
dirFd := int(dirFile.Fd())
// Call the underlying system call
var fd int
for {
if fd, err = unix.Openat(dirFd, name, flags|unix.O_CLOEXEC, uint32(perm)); err != unix.EINTR {
break
}
}
name = filepath.Join(dirFile.Name(), name)
if err != nil {
return nil, &os.PathError{Op: "openat", Path: name, Err: err}
}
return os.NewFile(uintptr(fd), name), nil
}
type UnixFileInfo struct {
name string
stat *unix.Stat_t
mode os.FileMode
}
func NewUnixFileInfo(name string, stat *unix.Stat_t) os.FileInfo {
rawMode := stat.Mode
// Start with the standard 9-bit permissions
mode := os.FileMode(rawMode & 0777)
// Map the file type bits using S_IFMT
switch rawMode & unix.S_IFMT {
case unix.S_IFDIR:
mode |= os.ModeDir
case unix.S_IFLNK:
mode |= os.ModeSymlink
case unix.S_IFBLK:
mode |= os.ModeDevice
case unix.S_IFCHR:
// Go uses ModeDevice | ModeCharDevice for character devices
mode |= os.ModeDevice | os.ModeCharDevice
case unix.S_IFIFO:
mode |= os.ModeNamedPipe
case unix.S_IFSOCK:
mode |= os.ModeSocket
}
// Map setuid, setgid, and sticky bits
if rawMode&unix.S_ISUID != 0 {
mode |= os.ModeSetuid
}
if rawMode&unix.S_ISGID != 0 {
mode |= os.ModeSetgid
}
if rawMode&unix.S_ISVTX != 0 {
mode |= os.ModeSticky
}
return &UnixFileInfo{name, stat, mode}
}
func (m *UnixFileInfo) Name() string { return m.name }
func (m *UnixFileInfo) Size() int64 { return m.stat.Size }
func (m *UnixFileInfo) Mode() os.FileMode { return m.mode }
func (m *UnixFileInfo) ModTime() time.Time { return time.Unix(m.stat.Mtim.Unix()) }
func (m *UnixFileInfo) IsDir() bool { return m.Mode().IsDir() }
func (m *UnixFileInfo) Sys() any { return m.stat }
func (m *UnixFileInfo) Dev() uint64 { return uint64(m.stat.Rdev) }
// Get file info relative to the parent FD, follows symlinks
func StatAt(dirFile *os.File, name string) (ans os.FileInfo, err error) {
var stat unix.Stat_t
for {
if err = unix.Fstatat(int(dirFile.Fd()), name, &stat, 0); err != unix.EINTR {
break
}
}
if err != nil {
name = filepath.Join(dirFile.Name(), name)
return nil, &os.PathError{Op: "statat", Path: name, Err: err}
}
return NewUnixFileInfo(name, &stat), nil
}
// Get file info relative to the parent FD, do not follows symlinks
func LstatAt(dirFile *os.File, name string) (ans os.FileInfo, err error) {
var stat unix.Stat_t
for {
if err = unix.Fstatat(int(dirFile.Fd()), name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != unix.EINTR {
break
}
}
if err != nil {
name = filepath.Join(dirFile.Name(), name)
return nil, &os.PathError{Op: "lstatat", Path: name, Err: err}
}
return NewUnixFileInfo(name, &stat), nil
}
// Remove file relative to parent fd
func UnlinkAt(parent *os.File, name string) (err error) {
for {
if err = unix.Unlinkat(int(parent.Fd()), name, 0); err != unix.EINTR {
break
}
}
if err != nil {
err = &os.PathError{Op: "unlinkat", Path: filepath.Join(parent.Name(), name), Err: err}
}
return
}
// Remove empty directory relative to parent fd
func RemoveDirAt(parent *os.File, name string) (err error) {
for {
if err = unix.Unlinkat(int(parent.Fd()), name, unix.AT_REMOVEDIR); err != unix.EINTR {
break
}
}
if err != nil {
err = &os.PathError{Op: "unlinkat", Path: filepath.Join(parent.Name(), name), Err: err}
}
return
}
// Create a hardlink pointing to oldname called newname relative to the
// specified directories. If oldname is a symlink,
// a new symlink is created pointing to its target when follow_symlinks is true otherwise to it.
func LinkAt(oldparent *os.File, oldname string, newparent *os.File, newname string, follow_symlinks bool) (err error) {
flags := IfElse(follow_symlinks, unix.AT_SYMLINK_FOLLOW, 0)
for {
if err = unix.Linkat(int(oldparent.Fd()), oldname, int(newparent.Fd()), newname, flags); err != unix.EINTR {
break
}
}
if err != nil {
err = &os.PathError{Op: "linkat", Path: fmt.Sprintf("%s -> %s", filepath.Join(newparent.Name(), newname), filepath.Join(oldparent.Name(), oldname)), Err: err}
}
return
}
// RemoveChildren removes all files and subdirectories within the directory
// pointed to by dirFile using an explicit stack instead of recursion. Removes
// all it can but returns the first error, if any.
func RemoveChildren(dirFile *os.File) error {
var firstErr error
// Each stack frame is one of two kinds:
// expand: dir != nil read dir's children, unlink files, push subdirs
// rmdir: dir == nil rmdir name from parent, then Unref parent
type frame struct {
dir *RefCountedFile // non-nil: expand this directory (Unref when done)
name string // rmdir sentinel: child name to remove from parent
parent *RefCountedFile // rmdir sentinel: ref to parent dir (Unref after rmdir)
}
// Only the root directory needs rewinding; it was passed in from outside
// and may have been read before. Child dirs are freshly opened by us.
if _, err := dirFile.Seek(0, io.SeekStart); err != nil {
return err
}
rcRoot := NewRefCountedFile(dirFile)
// Extra ref so the expand frame's Unref doesn't close the caller's fd.
rcRoot.NewRef()
stack := []frame{{dir: rcRoot}}
for len(stack) > 0 {
f := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if f.dir == nil {
// rmdir sentinel: the subdirectory is now empty, remove it.
if err := RemoveDirAt(f.parent.File(), f.name); err != nil && firstErr == nil {
firstErr = err
}
f.parent.Unref()
continue
}
for {
// Read entries in small chunks to handle very large directories.
entries, err := f.dir.File().ReadDir(64)
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
childFile, openErr := OpenDirAt(f.dir.File(), name)
if openErr != nil {
if firstErr == nil {
firstErr = openErr
}
continue
}
// Push rmdir sentinel first; LIFO ensures the child expand
// frame is processed before this rmdir sentinel.
// f.dir.NewRef() adds a ref to the parent for the sentinel.
stack = append(stack, frame{name: name, parent: f.dir.NewRef()}, frame{dir: NewRefCountedFile(childFile)})
} else {
if unlinkErr := UnlinkAt(f.dir.File(), name); unlinkErr != nil && firstErr == nil {
firstErr = unlinkErr
}
}
}
if err != nil {
if !errors.Is(err, io.EOF) && firstErr == nil {
firstErr = &os.PathError{Op: "readdir", Path: f.dir.File().Name(), Err: err}
}
break
}
}
f.dir.Unref()
}
_, _ = dirFile.Seek(0, io.SeekStart)
return firstErr
}
func DupFile(f *os.File) (ans *os.File, err error) {
var fd int
for {
if fd, err = unix.Dup(int(f.Fd())); err != unix.EINTR {
break
}
}
if err != nil {
return nil, &os.PathError{Op: "dup", Path: f.Name(), Err: err}
}
return os.NewFile(uintptr(fd), f.Name()), nil
}
func ReadLinkAt(parent *os.File, name string) (ans string, err error) {
buf := [unix.PathMax]byte{}
n, err := readLinkAt(parent, name, buf[:])
if err != nil {
return "", &os.PathError{Op: "readlinkat", Path: filepath.Join(parent.Name(), name), Err: err}
}
return UnsafeBytesToString(buf[:n]), nil
}
func RenameAt(old_parent *os.File, old_name string, new_parent *os.File, new_name string) (err error) {
for {
if err = unix.Renameat(int(old_parent.Fd()), old_name, int(new_parent.Fd()), new_name); err != unix.EINTR {
break
}
}
if err != nil {
err = &os.LinkError{Op: "renameat", Old: filepath.Join(old_parent.Name(), old_name), New: filepath.Join(new_parent.Name(), new_name), Err: err}
}
return
}
func ConvertFileModeToUnix(goMode os.FileMode) uint32 {
// 1. Start with the basic permission bits (0777)
unixMode := uint32(goMode.Perm())
// 2. Map the type bits
// We use the os.ModeXXX constants to identify the type
switch {
case goMode.IsDir():
unixMode |= unix.S_IFDIR
case goMode&os.ModeSymlink != 0:
unixMode |= unix.S_IFLNK
case goMode&os.ModeNamedPipe != 0:
unixMode |= unix.S_IFIFO
case goMode&os.ModeSocket != 0:
unixMode |= unix.S_IFSOCK
case goMode&os.ModeDevice != 0:
if goMode&os.ModeCharDevice != 0 {
unixMode |= unix.S_IFCHR
} else {
unixMode |= unix.S_IFBLK
}
default:
// Default to a regular file
unixMode |= unix.S_IFREG
}
// 3. Map special bits
if goMode&os.ModeSetuid != 0 {
unixMode |= unix.S_ISUID
}
if goMode&os.ModeSetgid != 0 {
unixMode |= unix.S_ISGID
}
if goMode&os.ModeSticky != 0 {
unixMode |= unix.S_ISVTX
}
return unixMode
}
func MknodAt(parent *os.File, name string, mode os.FileMode, dev uint64) (err error) {
unix_mode := ConvertFileModeToUnix(mode)
if err = mknodAt(parent, name, unix_mode, dev); err != nil {
err = &os.PathError{Op: "mknodat", Path: filepath.Join(parent.Name(), name), Err: err}
}
return
}
// Not thread safe reference counted wrapper for os.File
type RefCountedFile struct {
f *os.File
refcnt atomic.Int32
}
func NewRefCountedFile(f *os.File) *RefCountedFile {
ans := RefCountedFile{f: f}
ans.refcnt.Add(1)
return &ans
}
func (f *RefCountedFile) NewRef() *RefCountedFile {
f.refcnt.Add(1)
return f
}
func (f *RefCountedFile) Unref() *RefCountedFile {
if f.refcnt.Add(-1) == 0 {
f.f.Close()
f.f = nil
}
return nil
}
func (f *RefCountedFile) File() *os.File { return f.f }
type CopyFolderOptions struct {
Disallow_hardlinks bool
Follow_symlinks bool
Filter_files func(parent *os.File, child os.FileInfo) bool
}
// Copy the file objects as efficiently as possible with cancellation. The
// files are always closed before this function returns.
func CopyFileAndClose(ctx context.Context, src *os.File, dest *os.File) (err error) {
err_chan := make(chan error)
go func() {
defer func() {
if r := recover(); r != nil {
err_chan <- parallel.Format_stacktrace_on_panic(r, 1)
}
}()
// this go routine will automatically exit when src/dest are closed
// even if copying is not complete. io.Copy() automatically use
// sendfile() or similar mechanisms for efficiency.
_, err := io.Copy(dest, src)
err_chan <- err
}()
select {
case <-ctx.Done():
src.Close()
dest.Close()
// wait for go routine to exit
<-err_chan
return ctx.Err()
case err := <-err_chan:
src.Close()
dest.Close()
return err
}
}
// Copy the contents of src_folder to dest_folder, recursively, preserving file
// permissions. Behavior around hard and symbolic links and filtering is
// controlled via the provided options. When symlink following is enabled,
// symlink loops are avoided and any sumlink that points to an already copied
// entry becomes a symlink pointing to the copied entry using a relative path.
// Existing regular files are overwritten, without changing their permissions.
// Existing directories also do not have their permissions updated.
func CopyFolderContents(ctx context.Context, src_folder *os.File, dest_folder *os.File, opts CopyFolderOptions) (final_error error) {
// Ensure we get all dir contents
_, err := src_folder.Seek(0, io.SeekStart)
if err != nil {
return err
}
// When following symlinks, store previously seen source items with the
// abspaths they have been copied to in dest. Items are identified with
// device + inode number which should be globally unique. This ensures 1)
// no file/dir is copied more than once because of a symlink 2) symlinks
// are changed to point to the relative location of the previously copied
// target
type dir_ident struct{ dev, inode uint64 }
get_dir_ident := func(i os.FileInfo) dir_ident {
switch s := i.Sys().(type) {
case *syscall.Stat_t:
return dir_ident{uint64(s.Dev), uint64(s.Ino)}
case *unix.Stat_t:
return dir_ident{uint64(s.Dev), uint64(s.Ino)}
}
panic("unknown stat result type from os.FileInfo")
}
var seen_map map[dir_ident]string
if opts.Follow_symlinks {
seen_map = make(map[dir_ident]string)
if s, err := src_folder.Stat(); err != nil {
return err
} else {
seen_map[get_dir_ident(s)] = dest_folder.Name()
}
}
is_ok := opts.Filter_files
if is_ok == nil {
is_ok = func(*os.File, os.FileInfo) bool { return true }
}
type item struct {
src_parent, dest_parent *RefCountedFile
child os.FileInfo
}
queue := list.New()
is_cancelled := func() bool {
select {
case <-ctx.Done():
final_error = ctx.Err()
return true
default:
return false
}
}
defer func() {
for {
v := queue.Front()
if v == nil {
break
}
item := queue.Remove(v).(*item)
if item.src_parent != nil {
item.src_parent.Unref()
}
if item.dest_parent != nil {
item.dest_parent.Unref()
}
}
}()
src, dest := NewRefCountedFile(src_folder), NewRefCountedFile(dest_folder)
// Add an extra reference so that the files passed into this function are
// not closed in do_one()
src.NewRef()
dest.NewRef()
fail := func(lerr error) bool {
final_error = lerr
return false
}
mark_as_seen := func(dest_parent *os.File, child os.FileInfo) {
if opts.Follow_symlinks {
path := filepath.Join(dest_parent.Name(), child.Name())
seen_map[get_dir_ident(child)] = filepath.Clean(path)
}
}
var do_one_child func(src *RefCountedFile, dest *RefCountedFile, child os.FileInfo, from_symlink bool) bool
do_one_child = func(src *RefCountedFile, dest *RefCountedFile, child os.FileInfo, from_symlink bool) bool {
if child.IsDir() {
queue.PushBack(&item{src.NewRef(), dest.NewRef(), child})
return true
}
mark_as_seen(dest.File(), child)
// First try a hardlink which works for regular files and symlinks at least
if !opts.Disallow_hardlinks && LinkAt(src.File(), child.Name(), dest.File(), child.Name(), opts.Follow_symlinks) == nil {
return true
}
t := child.Mode().Type()
switch {
case t.IsRegular():
sf, err := OpenAt(src.File(), child.Name())
if err != nil {
return fail(err)
}
df, err := CreateAt(dest.File(), child.Name(), child.Mode().Perm())
if err != nil {
sf.Close()
return fail(err)
}
if err = CopyFileAndClose(ctx, sf, df); err != nil {
UnlinkAt(dest.File(), child.Name()) // dont leave partially copied files around
return fail(err)
}
case t&os.ModeSymlink != 0:
if opts.Follow_symlinks && !from_symlink {
rpath, err := filepath.EvalSymlinks(filepath.Join(src.File().Name(), child.Name()))
if err != nil {
return do_one_child(src, dest, child, true)
}
parent_dir := filepath.Dir(rpath)
pfd, err := unix.Open(parent_dir, unix.O_DIRECTORY|unix.O_RDONLY|unix.O_CLOEXEC, 0)
if err != nil {
return do_one_child(src, dest, child, true)
}
child_name := filepath.Base(rpath)
return func() bool {
pdf := os.NewFile(uintptr(pfd), parent_dir)
// Use a RefCountedFile so that if st is a directory the fd
// stays alive until the queued item is processed by next_dir.
rcf := NewRefCountedFile(pdf)
defer rcf.Unref()
st, err := StatAt(pdf, child_name)
if err != nil {
return do_one_child(src, dest, child, true)
}
id := get_dir_ident(st)
if existing_path, found := seen_map[id]; found {
target, err := filepath.Rel(dest.File().Name(), existing_path)
if err != nil {
return do_one_child(src, dest, child, true)
}
if err = SymlinkAt(dest.File(), child.Name(), target); err != nil {
return fail(err)
}
return true
}
return do_one_child(rcf, dest, st, true)
}()
} else {
target, err := ReadLinkAt(src.File(), child.Name())
if err != nil {
return fail(err)
}
if err = SymlinkAt(dest.File(), child.Name(), target); err != nil {
return fail(err)
}
}
case t&os.ModeDevice != 0:
if err := MknodAt(dest.File(), child.Name(), child.Mode(), child.(*UnixFileInfo).Dev()); err != nil {
return fail(err)
}
}
return true
}
do_one := func(src *RefCountedFile, dest *RefCountedFile) bool {
defer func() {
src = src.Unref()
dest = dest.Unref()
}()
if is_cancelled() {
return false
}
for {
dir_entries, err := src.File().ReadDir(64)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fail(err)
}
for _, entry := range dir_entries {
if is_cancelled() {
return false
}
child, err := entry.Info()
if err != nil {
return fail(err)
}
if !is_ok(src.File(), child) {
continue
}
if !do_one_child(src, dest, child, false) {
return false
}
}
}
return true
}
next_dir := func(src_parent *RefCountedFile, dest_parent *RefCountedFile, child os.FileInfo) (ok bool) {
src, dest = nil, nil
mark_as_seen(dest_parent.File(), child)
defer func() {
src_parent.Unref()
dest_parent.Unref()
if !ok {
if src != nil {
src = src.Unref()
}
if dest != nil {
dest = dest.Unref()
}
}
}()
sf, err := OpenDirAt(src_parent.File(), child.Name())
if err != nil {
final_error = err
return false
}
df, err := CreateDirAt(dest_parent.File(), child.Name(), child.Mode().Perm())
if err != nil {
sf.Close()
final_error = err
return false
}
src, dest = NewRefCountedFile(sf), NewRefCountedFile(df)
ok = true
return
}
for {
if !do_one(src, dest) {
return
}
v := queue.Front()
if v == nil {
break
}
n := queue.Remove(v).(*item)
if !next_dir(n.src_parent, n.dest_parent, n.child) {
return
}
}
return
}