Skip to content
14 changes: 7 additions & 7 deletions internal/vfs/internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
)

type Common struct {
RootFor func(root string) fs.FS
Realpath func(path string) string
RootFor func(root string) fs.FS
IsReparsePoint func(path string) bool
}

func RootLength(p string) int {
Expand Down Expand Up @@ -93,12 +93,12 @@ func (vfs *Common) GetAccessibleEntries(path string) (result vfs.Entries) {
continue
}

if entryType&fs.ModeIrregular != 0 && vfs.Realpath != nil {
// Could be a Windows junction. Try Realpath.
// TODO(jakebailey): use syscall.Win32FileAttributeData instead
if entryType&fs.ModeIrregular != 0 && vfs.IsReparsePoint != nil {
// Could be a Windows junction or other reparse point.
// Check using the OS-specific helper.
fullPath := path + "/" + entry.Name()
if realpath := vfs.Realpath(fullPath); fullPath != realpath {
if stat := vfs.Stat(realpath); stat != nil {
if vfs.IsReparsePoint(fullPath) {
if stat := vfs.Stat(fullPath); stat != nil {
addToResult(entry.Name(), stat.Mode())
}
}
Expand Down
27 changes: 27 additions & 0 deletions internal/vfs/osvfs/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package osvfs

import (
"os"
"os/exec"
"runtime"
"strings"
"testing"

"gotest.tools/v3/assert"
)

func mklink(tb testing.TB, target, link string, isDir bool) {
tb.Helper()

if runtime.GOOS == "windows" && isDir {
// Don't use os.Symlink on Windows, as it creates a "real" symlink, not a junction.
assert.NilError(tb, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run())
} else {
err := os.Symlink(target, link)
if err != nil && !isDir && runtime.GOOS == "windows" && 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)
}
}
4 changes: 2 additions & 2 deletions internal/vfs/osvfs/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ func FS() vfs.FS {

var osVFS vfs.FS = &osFS{
common: internal.Common{
RootFor: os.DirFS,
Realpath: osFSRealpath,
RootFor: os.DirFS,
IsReparsePoint: isReparsePoint,
},
}

Expand Down
18 changes: 0 additions & 18 deletions internal/vfs/osvfs/realpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/microsoft/typescript-go/internal/tspath"
Expand Down Expand Up @@ -54,22 +52,6 @@ func setupSymlinks(tb testing.TB) (targetFile, linkFile string) {
return targetFile, linkFile
}

func mklink(tb testing.TB, target, link string, isDir bool) {
tb.Helper()

if runtime.GOOS == "windows" && isDir {
// Don't use os.Symlink on Windows, as it creates a "real" symlink, not a junction.
assert.NilError(tb, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run())
} else {
err := os.Symlink(target, link)
if err != nil && !isDir && runtime.GOOS == "windows" && 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)
}
}

func BenchmarkRealpath(b *testing.B) {
targetFile, linkFile := setupSymlinks(b)

Expand Down
6 changes: 6 additions & 0 deletions internal/vfs/osvfs/reparsepoint_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build !windows

package osvfs

// Only Windows has reparse points; leave this nil for other OSes.
var isReparsePoint func(path string) bool
29 changes: 29 additions & 0 deletions internal/vfs/osvfs/reparsepoint_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package osvfs

import (
"syscall"
"unsafe"
)

func isReparsePoint(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
}
169 changes: 169 additions & 0 deletions internal/vfs/osvfs/reparsepoint_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package osvfs

import (
"os"
"os/exec"
"path/filepath"
"testing"

"gotest.tools/v3/assert"
)

func TestIsReparsePoint(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, isReparsePoint(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, isReparsePoint(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, isReparsePoint(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, isReparsePoint(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, isReparsePoint(link), true)
})

t.Run("nonexistent path", func(t *testing.T) {
t.Parallel()
nonexistent := filepath.Join(tmp, "does-not-exist")
assert.Equal(t, isReparsePoint(nonexistent), false)
})

t.Run("empty path", func(t *testing.T) {
t.Parallel()
assert.Equal(t, isReparsePoint(""), false)
})

t.Run("invalid path with null byte", func(t *testing.T) {
t.Parallel()
assert.Equal(t, isReparsePoint("invalid\x00path"), false)
})
}

func TestIsReparsePointLongPath(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, isReparsePoint(link), true)
}

func TestIsReparsePointNestedInSymlink(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, isReparsePoint(nestedPath), true)
}

func TestIsReparsePointRelativePath(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, isReparsePoint(link), true)
assert.Equal(t, isReparsePoint(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() {
isReparsePoint(regularFile)
}
})

b.Run("junction", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
isReparsePoint(link)
}
})

b.Run("nonexistent", func(b *testing.B) {
b.ReportAllocs()
nonexistent := filepath.Join(tmp, "does-not-exist")
for b.Loop() {
isReparsePoint(nonexistent)
}
})
}