Skip to content

Commit 1030abf

Browse files
authored
Treat Windows junctions as symlinks in Realpath (#186)
1 parent 01e48fe commit 1030abf

File tree

6 files changed

+154
-1
lines changed

6 files changed

+154
-1
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.3
55
require (
66
github.com/go-json-experiment/json v0.0.0-20241127185351-9802db03f36a
77
github.com/google/go-cmp v0.6.0
8+
golang.org/x/sys v0.27.0
89
golang.org/x/tools v0.27.0
910
gotest.tools/v3 v3.5.1
1011
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
66
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
77
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
88
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
9+
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
10+
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
911
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
1012
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
1113
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=

internal/vfs/os.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (vfs *osFS) Realpath(path string) string {
8282

8383
orig := path
8484
path = filepath.FromSlash(path)
85-
path, err := filepath.EvalSymlinks(path)
85+
path, err := realpath(path)
8686
if err != nil {
8787
return orig
8888
}

internal/vfs/realpath_other.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !windows
2+
3+
package vfs
4+
5+
import (
6+
"path/filepath"
7+
)
8+
9+
func realpath(path string) (string, error) {
10+
return filepath.EvalSymlinks(path)
11+
}

internal/vfs/realpath_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package vfs
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"runtime"
8+
"testing"
9+
10+
"github.com/microsoft/typescript-go/internal/tspath"
11+
"gotest.tools/v3/assert"
12+
"gotest.tools/v3/assert/cmp"
13+
)
14+
15+
func TestSymlinkRealpath(t *testing.T) {
16+
t.Parallel()
17+
18+
tmp := t.TempDir()
19+
20+
target := filepath.Join(tmp, "target")
21+
targetFile := filepath.Join(target, "file")
22+
23+
link := filepath.Join(tmp, "link")
24+
linkFile := filepath.Join(link, "file")
25+
26+
const expectedContents = "hello"
27+
28+
assert.NilError(t, os.MkdirAll(target, 0o777))
29+
assert.NilError(t, os.WriteFile(targetFile, []byte(expectedContents), 0o666))
30+
31+
if runtime.GOOS == "windows" {
32+
// Don't use os.Symlink on Windows, as it creates a "real" symlink, not a junction.
33+
assert.NilError(t, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run())
34+
} else {
35+
assert.NilError(t, os.Symlink(target, link))
36+
}
37+
38+
gotContents, err := os.ReadFile(linkFile)
39+
assert.NilError(t, err)
40+
assert.Equal(t, string(gotContents), expectedContents)
41+
42+
fs := FromOS()
43+
44+
targetRealpath := fs.Realpath(tspath.NormalizePath(targetFile))
45+
linkRealpath := fs.Realpath(tspath.NormalizePath(linkFile))
46+
47+
if !assert.Check(t, cmp.Equal(targetRealpath, linkRealpath)) {
48+
cmd := exec.Command("node", "-e", `console.log({ native: fs.realpathSync.native(process.argv[1]), node: fs.realpathSync(process.argv[1]) })`, linkFile)
49+
out, err := cmd.CombinedOutput()
50+
assert.NilError(t, err)
51+
t.Logf("node: %s", out)
52+
}
53+
}

internal/vfs/realpath_windows.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package vfs
2+
3+
import (
4+
"errors"
5+
"os"
6+
"syscall"
7+
8+
"golang.org/x/sys/windows"
9+
)
10+
11+
// 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
12+
13+
func realpath(path string) (string, error) {
14+
h, err := openMetadata(path)
15+
if err != nil {
16+
return "", err
17+
}
18+
defer windows.CloseHandle(h) //nolint:errcheck
19+
20+
// based on https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/os/file_windows.go#L389
21+
22+
const _VOLUME_NAME_DOS = 0
23+
24+
buf := make([]uint16, 310) // https://github.com/microsoft/go-winio/blob/3c9576c9346a1892dee136329e7e15309e82fb4f/internal/stringbuffer/wstring.go#L13
25+
for {
26+
n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), _VOLUME_NAME_DOS)
27+
if err != nil {
28+
return "", err
29+
}
30+
if n < uint32(len(buf)) {
31+
break
32+
}
33+
buf = make([]uint16, n)
34+
}
35+
36+
s := syscall.UTF16ToString(buf)
37+
if len(s) > 4 && s[:4] == `\\?\` {
38+
s = s[4:]
39+
if len(s) > 3 && s[:3] == `UNC` {
40+
// return path like \\server\share\...
41+
return `\` + s[3:], nil
42+
}
43+
return s, nil
44+
}
45+
46+
return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s)
47+
}
48+
49+
func openMetadata(path string) (windows.Handle, error) {
50+
// based on https://github.com/microsoft/go-winio/blob/3c9576c9346a1892dee136329e7e15309e82fb4f/pkg/fs/resolve.go#L113
51+
52+
pathUTF16, err := windows.UTF16PtrFromString(path)
53+
if err != nil {
54+
return windows.InvalidHandle, err
55+
}
56+
57+
const (
58+
_FILE_ANY_ACCESS = 0
59+
60+
_FILE_SHARE_READ = 0x01
61+
_FILE_SHARE_WRITE = 0x02
62+
_FILE_SHARE_DELETE = 0x04
63+
64+
_OPEN_EXISTING = 0x03
65+
66+
_FILE_FLAG_BACKUP_SEMANTICS = 0x0200_0000
67+
)
68+
69+
h, err := windows.CreateFile(
70+
pathUTF16,
71+
_FILE_ANY_ACCESS,
72+
_FILE_SHARE_READ|_FILE_SHARE_WRITE|_FILE_SHARE_DELETE,
73+
nil,
74+
_OPEN_EXISTING,
75+
_FILE_FLAG_BACKUP_SEMANTICS,
76+
0,
77+
)
78+
if err != nil {
79+
return 0, &os.PathError{
80+
Op: "CreateFile",
81+
Path: path,
82+
Err: err,
83+
}
84+
}
85+
return h, nil
86+
}

0 commit comments

Comments
 (0)