blob: 8ae6f0c9d34d7451d3a43cd1914a6ab0cb097f97 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package os_test
import (
"errors"
"fmt"
"internal/syscall/windows"
"os"
"path/filepath"
"syscall"
"testing"
"unsafe"
)
// Verify that Root.Open rejects Windows reserved names.
func TestRootWindowsDeviceNames(t *testing.T) {
r, err := os.OpenRoot(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer r.Close()
if f, err := r.Open("NUL"); err == nil {
t.Errorf(`r.Open("NUL") succeeded; want error"`)
f.Close()
}
}
// Verify that Root.Open is case-insensitive.
// (The wrong options to NtOpenFile could make operations case-sensitive,
// so this is worth checking.)
func TestRootWindowsCaseInsensitivity(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
t.Fatal(err)
}
r, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer r.Close()
f, err := r.Open("FILE")
if err != nil {
t.Fatal(err)
}
f.Close()
if err := r.Remove("FILE"); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dir, "file")); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("os.Stat(file) after deletion: %v, want ErrNotFound", err)
}
}
// TestRootSymlinkRelativity tests that symlinks created using Root.Symlink have the
// same SYMLINK_FLAG_RELATIVE value as ones creates using os.Symlink.
func TestRootSymlinkRelativity(t *testing.T) {
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
for i, test := range []struct {
name string
target string
}{{
name: "relative",
target: `foo`,
}, {
name: "absolute",
target: `C:\foo`,
}, {
name: "current working directory-relative",
target: `C:foo`,
}, {
name: "root-relative",
target: `\foo`,
}, {
name: "question prefix",
target: `\\?\foo`,
}, {
name: "relative with dot dot",
target: `a\..\b`, // could be cleaned (but isn't)
}} {
t.Run(test.name, func(t *testing.T) {
name := fmt.Sprintf("symlink_%v", i)
if err := os.Symlink(test.target, filepath.Join(dir, name)); err != nil {
t.Fatal(err)
}
if err := root.Symlink(test.target, name+"_at"); err != nil {
t.Fatal(err)
}
osRDB, err := readSymlinkReparseData(filepath.Join(dir, name))
if err != nil {
t.Fatal(err)
}
rootRDB, err := readSymlinkReparseData(filepath.Join(dir, name+"_at"))
if err != nil {
t.Fatal(err)
}
if osRDB.Flags != rootRDB.Flags {
t.Errorf("symlink target %q: Symlink flags = %x, Root.Symlink flags = %x", test.target, osRDB.Flags, rootRDB.Flags)
}
// Compare the link target.
// os.Symlink converts current working directory-relative links
// such as c:foo into absolute links.
osTarget, err := os.Readlink(filepath.Join(dir, name))
if err != nil {
t.Fatal(err)
}
rootTarget, err := os.Readlink(filepath.Join(dir, name+"_at"))
if err != nil {
t.Fatal(err)
}
if osTarget != rootTarget {
t.Errorf("symlink created with target %q: Symlink target = %q, Root.Symlink target = %q", test.target, osTarget, rootTarget)
}
})
}
}
func readSymlinkReparseData(name string) (*windows.SymbolicLinkReparseBuffer, error) {
nameu16, err := syscall.UTF16FromString(name)
if err != nil {
return nil, err
}
h, err := syscall.CreateFile(&nameu16[0], syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING,
syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
return nil, err
}
defer syscall.CloseHandle(h)
var rdbbuf [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte
var bytesReturned uint32
err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
if err != nil {
return nil, err
}
rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
if rdb.ReparseTag != syscall.IO_REPARSE_TAG_SYMLINK {
return nil, fmt.Errorf("%q: not a symlink", name)
}
bufoff := unsafe.Offsetof(rdb.DUMMYUNIONNAME)
symlinkBuf := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdbbuf[bufoff]))
return symlinkBuf, nil
}
// TestRootSymlinkToDirectory tests that Root.Symlink creates directory links
// when the target is a directory contained within the root.
func TestRootSymlinkToDirectory(t *testing.T) {
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
if err := os.Mkdir(filepath.Join(dir, "dir"), 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
t.Fatal(err)
}
dir2 := t.TempDir()
for i, test := range []struct {
name string
target string
wantDir bool
}{{
name: "directory outside root",
target: dir2,
wantDir: false,
}, {
name: "directory inside root",
target: "dir",
wantDir: true,
}, {
name: "file inside root",
target: "file",
wantDir: false,
}, {
name: "nonexistent inside root",
target: "nonexistent",
wantDir: false,
}} {
t.Run(test.name, func(t *testing.T) {
name := fmt.Sprintf("symlink_%v", i)
if err := root.Symlink(test.target, name); err != nil {
t.Fatal(err)
}
// Lstat strips the directory mode bit from reparse points,
// so we need to use GetFileInformationByHandle directly to
// determine if this is a directory link.
nameu16, err := syscall.UTF16PtrFromString(filepath.Join(dir, name))
if err != nil {
t.Fatal(err)
}
h, err := syscall.CreateFile(nameu16, 0, 0, nil, syscall.OPEN_EXISTING,
syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
t.Fatal(err)
}
defer syscall.CloseHandle(h)
var fi syscall.ByHandleFileInformation
if err := syscall.GetFileInformationByHandle(h, &fi); err != nil {
t.Fatal(err)
}
gotDir := fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0
if got, want := gotDir, test.wantDir; got != want {
t.Errorf("link target %q: isDir = %v, want %v", test.target, got, want)
}
})
}
}