vendor tsgo
This commit is contained in:
14
tools/tsgo/internal/nativepath/eintr_unix.go
Normal file
14
tools/tsgo/internal/nativepath/eintr_unix.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package nativepath
|
||||
|
||||
import "syscall"
|
||||
|
||||
func ignoringEINTR[T any](fn func() (T, error)) (T, error) {
|
||||
for {
|
||||
v, err := fn()
|
||||
if err != syscall.EINTR { //nolint:errorlint // syscall functions return raw syscall.Errno, never wrapped
|
||||
return v, err
|
||||
}
|
||||
}
|
||||
}
|
||||
66
tools/tsgo/internal/nativepath/realpath_darwin.go
Normal file
66
tools/tsgo/internal/nativepath/realpath_darwin.go
Normal file
@@ -0,0 +1,66 @@
|
||||
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
|
||||
}
|
||||
70
tools/tsgo/internal/nativepath/realpath_linux.go
Normal file
70
tools/tsgo/internal/nativepath/realpath_linux.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package nativepath
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// On Linux, we use the O_PATH + /proc/self/fd trick to resolve the canonical
|
||||
// path in O(1) syscalls (open + readlink + close) instead of Go's
|
||||
// filepath.EvalSymlinks which does an lstat per path component — O(depth).
|
||||
//
|
||||
// This is the approach libuv/Node.js could use, though libuv currently just
|
||||
// calls C realpath(3) which itself does a readlink per component. On the Go
|
||||
// side, the per-component approach is even more expensive because each
|
||||
// os.Lstat call involves goroutine scheduling overhead (entersyscall /
|
||||
// exitsyscall).
|
||||
//
|
||||
// How it works:
|
||||
// - open(path, O_PATH|O_CLOEXEC) gives us a lightweight fd that follows all
|
||||
// symlinks to the final target. O_PATH requires only search permission on
|
||||
// directories (same as lstat), and works for both files and directories.
|
||||
// - readlink("/proc/self/fd/<fd>") returns the fully resolved canonical path
|
||||
// that the kernel computed during the open.
|
||||
//
|
||||
// Falls back to filepath.EvalSymlinks if /proc is not available (e.g. containers
|
||||
// or chroots without procfs mounted).
|
||||
|
||||
const _procSelfFD = "/proc/self/fd/"
|
||||
|
||||
var hasProcSelfFD = sync.OnceValue(func() bool {
|
||||
var stat unix.Stat_t
|
||||
return unix.Stat(_procSelfFD, &stat) == nil
|
||||
})
|
||||
|
||||
func Realpath(path string) (string, error) {
|
||||
if !hasProcSelfFD() {
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
|
||||
fd, err := ignoringEINTR(func() (int, error) {
|
||||
return unix.Open(path, unix.O_CLOEXEC|unix.O_PATH, 0)
|
||||
})
|
||||
if err != nil {
|
||||
return "", &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
var procBuf [len(_procSelfFD) + 20]byte // 20 digits is enough for any int64 fd
|
||||
n := copy(procBuf[:], _procSelfFD)
|
||||
n += copy(procBuf[n:], strconv.Itoa(fd))
|
||||
procPath := string(procBuf[:n])
|
||||
|
||||
buf := make([]byte, 256)
|
||||
for {
|
||||
nn, err := ignoringEINTR(func() (int, error) {
|
||||
return unix.Readlink(procPath, buf)
|
||||
})
|
||||
if err != nil {
|
||||
return "", &os.PathError{Op: "readlink", Path: path, Err: err}
|
||||
}
|
||||
if nn < len(buf) {
|
||||
return string(buf[:nn]), nil
|
||||
}
|
||||
buf = make([]byte, len(buf)*2)
|
||||
}
|
||||
}
|
||||
9
tools/tsgo/internal/nativepath/realpath_other.go
Normal file
9
tools/tsgo/internal/nativepath/realpath_other.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !windows && !linux && !darwin
|
||||
|
||||
package nativepath
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
func Realpath(path string) (string, error) {
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
100
tools/tsgo/internal/nativepath/realpath_windows.go
Normal file
100
tools/tsgo/internal/nativepath/realpath_windows.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package nativepath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// This implementation is based on what Node's fs.realpath.native does, via libuv: https://github.com/libuv/libuv/blob/ec5a4b54f7da7eeb01679005c615fee9633cdb3b/src/win/fs.c#L2937
|
||||
|
||||
func Realpath(path string) (string, error) {
|
||||
var h windows.Handle
|
||||
if len(path) < 248 {
|
||||
var err error
|
||||
h, err = openMetadata(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer windows.CloseHandle(h) //nolint:errcheck
|
||||
} else {
|
||||
// For long paths, defer to os.Open to run the path through fixLongPath.
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Works on directories too since https://go.dev/cl/405275.
|
||||
h = windows.Handle(f.Fd())
|
||||
}
|
||||
|
||||
// based on https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/os/file_windows.go#L389
|
||||
|
||||
const _VOLUME_NAME_DOS = 0
|
||||
|
||||
buf := make([]uint16, 310) // https://github.com/microsoft/go-winio/blob/3c9576c9346a1892dee136329e7e15309e82fb4f/internal/stringbuffer/wstring.go#L13
|
||||
for {
|
||||
n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), _VOLUME_NAME_DOS)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n < uint32(len(buf)) {
|
||||
break
|
||||
}
|
||||
buf = make([]uint16, n)
|
||||
}
|
||||
|
||||
s := syscall.UTF16ToString(buf)
|
||||
if len(s) > 4 && s[:4] == `\\?\` {
|
||||
s = s[4:]
|
||||
if len(s) > 3 && s[:3] == `UNC` {
|
||||
// return path like \\server\share\...
|
||||
return `\` + s[3:], nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s)
|
||||
}
|
||||
|
||||
func openMetadata(path string) (windows.Handle, error) {
|
||||
// based on https://github.com/microsoft/go-winio/blob/3c9576c9346a1892dee136329e7e15309e82fb4f/pkg/fs/resolve.go#L113
|
||||
|
||||
pathUTF16, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return windows.InvalidHandle, err
|
||||
}
|
||||
|
||||
const (
|
||||
_FILE_ANY_ACCESS = 0
|
||||
|
||||
_FILE_SHARE_READ = 0x01
|
||||
_FILE_SHARE_WRITE = 0x02
|
||||
_FILE_SHARE_DELETE = 0x04
|
||||
|
||||
_OPEN_EXISTING = 0x03
|
||||
|
||||
_FILE_FLAG_BACKUP_SEMANTICS = 0x0200_0000
|
||||
)
|
||||
|
||||
h, err := windows.CreateFile(
|
||||
pathUTF16,
|
||||
_FILE_ANY_ACCESS,
|
||||
_FILE_SHARE_READ|_FILE_SHARE_WRITE|_FILE_SHARE_DELETE,
|
||||
nil,
|
||||
_OPEN_EXISTING,
|
||||
_FILE_FLAG_BACKUP_SEMANTICS,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{
|
||||
Op: "CreateFile",
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
10
tools/tsgo/internal/nativepath/symlink_other.go
Normal file
10
tools/tsgo/internal/nativepath/symlink_other.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build !windows
|
||||
|
||||
package nativepath
|
||||
|
||||
import "os"
|
||||
|
||||
func IsSymlinkOrReparsePoint(path string) bool {
|
||||
info, err := os.Lstat(path)
|
||||
return err == nil && info.Mode()&os.ModeSymlink != 0
|
||||
}
|
||||
29
tools/tsgo/internal/nativepath/symlink_windows.go
Normal file
29
tools/tsgo/internal/nativepath/symlink_windows.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package nativepath
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func IsSymlinkOrReparsePoint(path string) bool {
|
||||
if len(path) >= 248 {
|
||||
path = `\\?\` + path
|
||||
}
|
||||
|
||||
pathUTF16, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var data syscall.Win32FileAttributeData
|
||||
err = syscall.GetFileAttributesEx(
|
||||
pathUTF16,
|
||||
syscall.GetFileExInfoStandard,
|
||||
(*byte)(unsafe.Pointer(&data)),
|
||||
)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return data.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0
|
||||
}
|
||||
185
tools/tsgo/internal/nativepath/symlink_windows_test.go
Normal file
185
tools/tsgo/internal/nativepath/symlink_windows_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package nativepath
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestIsSymlinkOrReparsePoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
|
||||
t.Run("regular file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
file := filepath.Join(tmp, "regular.txt")
|
||||
assert.NilError(t, os.WriteFile(file, []byte("hello"), 0o666))
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(file), false)
|
||||
})
|
||||
|
||||
t.Run("regular directory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := filepath.Join(tmp, "regular-dir")
|
||||
assert.NilError(t, os.MkdirAll(dir, 0o777))
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(dir), false)
|
||||
})
|
||||
|
||||
t.Run("junction point", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
target := filepath.Join(tmp, "junction-target")
|
||||
link := filepath.Join(tmp, "junction-link")
|
||||
assert.NilError(t, os.MkdirAll(target, 0o777))
|
||||
mklink(t, target, link, true)
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(link), true)
|
||||
})
|
||||
|
||||
t.Run("file symlink", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
target := filepath.Join(tmp, "symlink-target.txt")
|
||||
link := filepath.Join(tmp, "symlink-link.txt")
|
||||
assert.NilError(t, os.WriteFile(target, []byte("hello"), 0o666))
|
||||
mklink(t, target, link, false)
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(link), true)
|
||||
})
|
||||
|
||||
t.Run("directory symlink", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
target := filepath.Join(tmp, "dir-symlink-target")
|
||||
link := filepath.Join(tmp, "dir-symlink-link")
|
||||
assert.NilError(t, os.MkdirAll(target, 0o777))
|
||||
mklink(t, target, link, false)
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(link), true)
|
||||
})
|
||||
|
||||
t.Run("nonexistent path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
nonexistent := filepath.Join(tmp, "does-not-exist")
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(nonexistent), false)
|
||||
})
|
||||
|
||||
t.Run("empty path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(""), false)
|
||||
})
|
||||
|
||||
t.Run("invalid path with null byte", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint("invalid\x00path"), false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsSymlinkOrReparsePointLongPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
|
||||
// Create a deeply nested path that exceeds 248 characters
|
||||
longPathBase := tmp
|
||||
pathComponent := "very_long_directory_name_to_exceed_max_path_limit_abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
for len(longPathBase) < 250 {
|
||||
longPathBase = filepath.Join(longPathBase, pathComponent)
|
||||
}
|
||||
|
||||
target := filepath.Join(longPathBase, "target")
|
||||
link := filepath.Join(longPathBase, "link")
|
||||
|
||||
// Use \\?\ prefix to enable long path support for mklink
|
||||
longTarget := `\\?\` + target
|
||||
longLink := `\\?\` + link
|
||||
|
||||
assert.NilError(t, os.MkdirAll(longTarget, 0o777))
|
||||
assert.NilError(t, exec.Command("cmd", "/c", "mklink", "/J", longLink, longTarget).Run())
|
||||
|
||||
// With long path support enabled, this should work even for paths >= 248 chars
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(link), true)
|
||||
}
|
||||
|
||||
func TestIsSymlinkOrReparsePointNestedInSymlink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
|
||||
// Create a structure: target/inner-target, link -> target, then check link/inner-link
|
||||
target := filepath.Join(tmp, "target")
|
||||
innerTarget := filepath.Join(target, "inner-target")
|
||||
assert.NilError(t, os.MkdirAll(innerTarget, 0o777))
|
||||
|
||||
link := filepath.Join(tmp, "link")
|
||||
mklink(t, target, link, true)
|
||||
|
||||
// Create a junction inside the target
|
||||
innerLink := filepath.Join(target, "inner-link")
|
||||
mklink(t, innerTarget, innerLink, true)
|
||||
|
||||
// Check the junction through the symlink path
|
||||
nestedPath := filepath.Join(link, "inner-link")
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(nestedPath), true)
|
||||
}
|
||||
|
||||
func TestIsSymlinkOrReparsePointRelativePath(t *testing.T) { //nolint:paralleltest // Cannot use t.Parallel() with t.Chdir()
|
||||
tmp := t.TempDir()
|
||||
t.Chdir(tmp)
|
||||
|
||||
target := "target-rel"
|
||||
link := "link-rel"
|
||||
assert.NilError(t, os.MkdirAll(target, 0o777))
|
||||
mklink(t, target, link, true)
|
||||
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(link), true)
|
||||
assert.Equal(t, IsSymlinkOrReparsePoint(target), false)
|
||||
}
|
||||
|
||||
func BenchmarkIsSymlinkOrJunction(b *testing.B) {
|
||||
tmp := b.TempDir()
|
||||
|
||||
regularFile := filepath.Join(tmp, "regular.txt")
|
||||
assert.NilError(b, os.WriteFile(regularFile, []byte("hello"), 0o666))
|
||||
|
||||
target := filepath.Join(tmp, "target")
|
||||
link := filepath.Join(tmp, "link")
|
||||
assert.NilError(b, os.MkdirAll(target, 0o777))
|
||||
assert.NilError(b, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run())
|
||||
|
||||
b.Run("regular file", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
IsSymlinkOrReparsePoint(regularFile)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("junction", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
IsSymlinkOrReparsePoint(link)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("nonexistent", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
nonexistent := filepath.Join(tmp, "does-not-exist")
|
||||
for b.Loop() {
|
||||
IsSymlinkOrReparsePoint(nonexistent)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mklink(tb testing.TB, target, link string, isDir bool) {
|
||||
tb.Helper()
|
||||
|
||||
if isDir {
|
||||
assert.NilError(tb, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run())
|
||||
} else {
|
||||
err := os.Symlink(target, link)
|
||||
if err != nil && strings.Contains(err.Error(), "A required privilege is not held by the client") {
|
||||
tb.Log(err)
|
||||
tb.Skip("file symlink support is not enabled without elevation or developer mode")
|
||||
}
|
||||
assert.NilError(tb, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user