}
}
+func TestStatLxSymLink(t *testing.T) {
+ if _, err := exec.LookPath("wsl"); err != nil {
+ t.Skip("skipping: WSL not detected")
+ }
+
+ temp := t.TempDir()
+ chdir(t, temp)
+
+ const target = "target"
+ const link = "link"
+
+ _, err := testenv.Command(t, "wsl", "/bin/mkdir", target).Output()
+ if err != nil {
+ // This normally happens when WSL still doesn't have a distro installed to run on.
+ t.Skipf("skipping: WSL is not correctly installed: %v", err)
+ }
+
+ _, err = testenv.Command(t, "wsl", "/bin/ln", "-s", target, link).Output()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fi, err := os.Lstat(link)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if m := fi.Mode(); m&fs.ModeSymlink != 0 {
+ // This can happen depending on newer WSL versions when running as admin or in developer mode.
+ t.Skip("skipping: WSL created reparse tag IO_REPARSE_TAG_SYMLINK instead of a IO_REPARSE_TAG_LX_SYMLINK")
+ }
+ // Stat'ing a IO_REPARSE_TAG_LX_SYMLINK from outside WSL always return ERROR_CANT_ACCESS_FILE.
+ // We check this condition to validate that os.Stat has tried to follow the link.
+ _, err = os.Stat(link)
+ const ERROR_CANT_ACCESS_FILE = syscall.Errno(1920)
+ if err == nil || !errors.Is(err, ERROR_CANT_ACCESS_FILE) {
+ t.Fatalf("os.Stat(%q): got %v, want ERROR_CANT_ACCESS_FILE", link, err)
+ }
+}
+
func TestStartProcessAttr(t *testing.T) {
t.Parallel()
}
// stat implements both Stat and Lstat of a file.
-func stat(funcname, name string, followSymlinks bool) (FileInfo, error) {
+func stat(funcname, name string, followSurrogates bool) (FileInfo, error) {
if len(name) == 0 {
return nil, &PathError{Op: funcname, Path: name, Err: syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)}
}
}
syscall.FindClose(sh)
if fd.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
- // Not a symlink or mount point. FindFirstFile is good enough.
+ // Not a surrogate for another named entity. FindFirstFile is good enough.
fs := newFileStatFromWin32finddata(&fd)
if err := fs.saveInfoFromPath(name); err != nil {
return nil, err
}
if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
- // The file is definitely not a symlink, because it isn't any kind of reparse point.
+ // Not a surrogate for another named entity, because it isn't any kind of reparse point.
// The information we got from GetFileAttributesEx is good enough for now.
fs := &fileStat{
FileAttributes: fa.FileAttributes,
return fs, nil
}
- // Use CreateFile to determine whether the file is a symlink and, if so,
+ // Use CreateFile to determine whether the file is a name surrogate and, if so,
// save information about the link target.
// Set FILE_FLAG_BACKUP_SEMANTICS so that CreateFile will create the handle
// even if name refers to a directory.
h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil {
// Since CreateFile failed, we can't determine whether name refers to a
- // symlink, or some other kind of reparse point. Since we can't return a
+ // name surrogate, or some other kind of reparse point. Since we can't return a
// FileInfo with a known-accurate Mode, we must return an error.
return nil, &PathError{Op: "CreateFile", Path: name, Err: err}
}
fi, err := statHandle(name, h)
syscall.CloseHandle(h)
- if err == nil && followSymlinks && fi.(*fileStat).isSymlink() {
+ if err == nil && followSurrogates && fi.(*fileStat).isReparseTagNameSurrogate() {
// To obtain information about the link target, we reopen the file without
// FILE_FLAG_OPEN_REPARSE_POINT and examine the resulting handle.
// (See https://devblogs.microsoft.com/oldnewthing/20100212-00/?p=14963.)
// lstatNolog implements Lstat for Windows.
func lstatNolog(name string) (FileInfo, error) {
- followSymlinks := false
+ followSurrogates := false
if name != "" && IsPathSeparator(name[len(name)-1]) {
// We try to implement POSIX semantics for Lstat path resolution
// (per https://pubs.opengroup.org/onlinepubs/9699919799.2013edition/basedefs/V1_chap04.html#tag_04_12):
// symlinks before the last separator in the path must be resolved. Since
// the last separator in this case follows the last path element, we should
// follow symlinks in the last path element.
- followSymlinks = true
+ followSurrogates = true
}
- return stat("Lstat", name, followSymlinks)
+ return stat("Lstat", name, followSurrogates)
}
return fs
}
+// isReparseTagNameSurrogate determines whether a tag's associated
+// reparse point is a surrogate for another named entity (for example, a mounted folder).
+//
+// See https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-isreparsetagnamesurrogate
+// and https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags.
+func (fs *fileStat) isReparseTagNameSurrogate() bool {
+ // True for IO_REPARSE_TAG_SYMLINK and IO_REPARSE_TAG_MOUNT_POINT.
+ return fs.ReparseTag&0x20000000 != 0
+}
+
func (fs *fileStat) isSymlink() bool {
// As of https://go.dev/cl/86556, we treat MOUNT_POINT reparse points as
// symlinks because otherwise certain directory junction tests in the