67 lines
1.9 KiB
Go
67 lines
1.9 KiB
Go
package nativepath
|
|
|
|
import (
|
|
"path/filepath"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// On macOS, we use open + fcntl(F_GETPATH) to resolve the canonical path in
|
|
// O(1) syscalls instead of Go's filepath.EvalSymlinks which does an lstat per
|
|
// path component — O(depth).
|
|
//
|
|
// How it works:
|
|
// - open(path, O_EVTONLY|O_NONBLOCK|O_CLOEXEC) follows all symlinks and gives
|
|
// us a lightweight fd. O_EVTONLY is macOS's event-only descriptor — it
|
|
// doesn't require read permission (similar to Linux's O_PATH) but still
|
|
// references the vnode. O_NONBLOCK prevents blocking on FIFOs.
|
|
// - fcntl(fd, F_GETPATH, buf) asks the kernel for the canonical path of the
|
|
// open file descriptor, written into a MAXPATHLEN buffer.
|
|
//
|
|
// unix.FcntlInt takes an int arg, so call it through a uintptr-escaping wrapper
|
|
// to keep the buffer pointer valid until fcntl returns.
|
|
|
|
var hasFGetPath = sync.OnceValue(func() bool {
|
|
// Verify that F_GETPATH is supported by this kernel version.
|
|
var buf [unix.PathMax]byte
|
|
fd, err := unix.Open(".", unix.O_EVTONLY|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer unix.Close(fd)
|
|
_, err = fcntlGetPath(fd, &buf)
|
|
return err == nil
|
|
})
|
|
|
|
func fcntlGetPath(fd int, buf *[unix.PathMax]byte) (int, error) {
|
|
return ignoringEINTR(func() (int, error) {
|
|
return fcntlGetPathPtr(uintptr(fd), uintptr(unsafe.Pointer(&buf[0])))
|
|
})
|
|
}
|
|
|
|
//go:uintptrescapes
|
|
func fcntlGetPathPtr(fd uintptr, buf uintptr) (int, error) {
|
|
return unix.FcntlInt(fd, unix.F_GETPATH, int(buf))
|
|
}
|
|
|
|
func Realpath(path string) (string, error) {
|
|
if !hasFGetPath() {
|
|
return filepath.EvalSymlinks(path)
|
|
}
|
|
|
|
fd, err := unix.Open(path, unix.O_EVTONLY|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer unix.Close(fd)
|
|
|
|
var buf [unix.PathMax]byte
|
|
if _, err := fcntlGetPath(fd, &buf); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return unix.ByteSliceToString(buf[:]), nil
|
|
}
|