| // 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) |
| } |
| }) |
| } |
| } |