Source file src/os/root_windows_test.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build windows
     6  
     7  package os_test
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  	"internal/syscall/windows"
    13  	"os"
    14  	"path/filepath"
    15  	"syscall"
    16  	"testing"
    17  	"unsafe"
    18  )
    19  
    20  // Verify that Root.Open rejects Windows reserved names.
    21  func TestRootWindowsDeviceNames(t *testing.T) {
    22  	r, err := os.OpenRoot(t.TempDir())
    23  	if err != nil {
    24  		t.Fatal(err)
    25  	}
    26  	defer r.Close()
    27  	if f, err := r.Open("NUL"); err == nil {
    28  		t.Errorf(`r.Open("NUL") succeeded; want error"`)
    29  		f.Close()
    30  	}
    31  }
    32  
    33  // Verify that Root.Open is case-insensitive.
    34  // (The wrong options to NtOpenFile could make operations case-sensitive,
    35  // so this is worth checking.)
    36  func TestRootWindowsCaseInsensitivity(t *testing.T) {
    37  	dir := t.TempDir()
    38  	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
    39  		t.Fatal(err)
    40  	}
    41  	r, err := os.OpenRoot(dir)
    42  	if err != nil {
    43  		t.Fatal(err)
    44  	}
    45  	defer r.Close()
    46  	f, err := r.Open("FILE")
    47  	if err != nil {
    48  		t.Fatal(err)
    49  	}
    50  	f.Close()
    51  	if err := r.Remove("FILE"); err != nil {
    52  		t.Fatal(err)
    53  	}
    54  	if _, err := os.Stat(filepath.Join(dir, "file")); !errors.Is(err, os.ErrNotExist) {
    55  		t.Fatalf("os.Stat(file) after deletion: %v, want ErrNotFound", err)
    56  	}
    57  }
    58  
    59  // TestRootSymlinkRelativity tests that symlinks created using Root.Symlink have the
    60  // same SYMLINK_FLAG_RELATIVE value as ones creates using os.Symlink.
    61  func TestRootSymlinkRelativity(t *testing.T) {
    62  	dir := t.TempDir()
    63  	root, err := os.OpenRoot(dir)
    64  	if err != nil {
    65  		t.Fatal(err)
    66  	}
    67  	defer root.Close()
    68  
    69  	for i, test := range []struct {
    70  		name   string
    71  		target string
    72  	}{{
    73  		name:   "relative",
    74  		target: `foo`,
    75  	}, {
    76  		name:   "absolute",
    77  		target: `C:\foo`,
    78  	}, {
    79  		name:   "current working directory-relative",
    80  		target: `C:foo`,
    81  	}, {
    82  		name:   "root-relative",
    83  		target: `\foo`,
    84  	}, {
    85  		name:   "question prefix",
    86  		target: `\\?\foo`,
    87  	}, {
    88  		name:   "relative with dot dot",
    89  		target: `a\..\b`, // could be cleaned (but isn't)
    90  	}} {
    91  		t.Run(test.name, func(t *testing.T) {
    92  			name := fmt.Sprintf("symlink_%v", i)
    93  			if err := os.Symlink(test.target, filepath.Join(dir, name)); err != nil {
    94  				t.Fatal(err)
    95  			}
    96  			if err := root.Symlink(test.target, name+"_at"); err != nil {
    97  				t.Fatal(err)
    98  			}
    99  
   100  			osRDB, err := readSymlinkReparseData(filepath.Join(dir, name))
   101  			if err != nil {
   102  				t.Fatal(err)
   103  			}
   104  			rootRDB, err := readSymlinkReparseData(filepath.Join(dir, name+"_at"))
   105  			if err != nil {
   106  				t.Fatal(err)
   107  			}
   108  			if osRDB.Flags != rootRDB.Flags {
   109  				t.Errorf("symlink target %q: Symlink flags = %x, Root.Symlink flags = %x", test.target, osRDB.Flags, rootRDB.Flags)
   110  			}
   111  
   112  			// Compare the link target.
   113  			// os.Symlink converts current working directory-relative links
   114  			// such as c:foo into absolute links.
   115  			osTarget, err := os.Readlink(filepath.Join(dir, name))
   116  			if err != nil {
   117  				t.Fatal(err)
   118  			}
   119  			rootTarget, err := os.Readlink(filepath.Join(dir, name+"_at"))
   120  			if err != nil {
   121  				t.Fatal(err)
   122  			}
   123  			if osTarget != rootTarget {
   124  				t.Errorf("symlink created with target %q: Symlink target = %q, Root.Symlink target = %q", test.target, osTarget, rootTarget)
   125  			}
   126  		})
   127  	}
   128  }
   129  
   130  func readSymlinkReparseData(name string) (*windows.SymbolicLinkReparseBuffer, error) {
   131  	nameu16, err := syscall.UTF16FromString(name)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	h, err := syscall.CreateFile(&nameu16[0], syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING,
   136  		syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	defer syscall.CloseHandle(h)
   141  
   142  	var rdbbuf [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte
   143  	var bytesReturned uint32
   144  	err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
   150  	if rdb.ReparseTag != syscall.IO_REPARSE_TAG_SYMLINK {
   151  		return nil, fmt.Errorf("%q: not a symlink", name)
   152  	}
   153  
   154  	bufoff := unsafe.Offsetof(rdb.DUMMYUNIONNAME)
   155  	symlinkBuf := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdbbuf[bufoff]))
   156  
   157  	return symlinkBuf, nil
   158  }
   159  
   160  // TestRootSymlinkToDirectory tests that Root.Symlink creates directory links
   161  // when the target is a directory contained within the root.
   162  func TestRootSymlinkToDirectory(t *testing.T) {
   163  	dir := t.TempDir()
   164  	root, err := os.OpenRoot(dir)
   165  	if err != nil {
   166  		t.Fatal(err)
   167  	}
   168  	defer root.Close()
   169  
   170  	if err := os.Mkdir(filepath.Join(dir, "dir"), 0777); err != nil {
   171  		t.Fatal(err)
   172  	}
   173  	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
   174  		t.Fatal(err)
   175  	}
   176  
   177  	dir2 := t.TempDir()
   178  
   179  	for i, test := range []struct {
   180  		name    string
   181  		target  string
   182  		wantDir bool
   183  	}{{
   184  		name:    "directory outside root",
   185  		target:  dir2,
   186  		wantDir: false,
   187  	}, {
   188  		name:    "directory inside root",
   189  		target:  "dir",
   190  		wantDir: true,
   191  	}, {
   192  		name:    "file inside root",
   193  		target:  "file",
   194  		wantDir: false,
   195  	}, {
   196  		name:    "nonexistent inside root",
   197  		target:  "nonexistent",
   198  		wantDir: false,
   199  	}} {
   200  		t.Run(test.name, func(t *testing.T) {
   201  			name := fmt.Sprintf("symlink_%v", i)
   202  			if err := root.Symlink(test.target, name); err != nil {
   203  				t.Fatal(err)
   204  			}
   205  
   206  			// Lstat strips the directory mode bit from reparse points,
   207  			// so we need to use GetFileInformationByHandle directly to
   208  			// determine if this is a directory link.
   209  			nameu16, err := syscall.UTF16PtrFromString(filepath.Join(dir, name))
   210  			if err != nil {
   211  				t.Fatal(err)
   212  			}
   213  			h, err := syscall.CreateFile(nameu16, 0, 0, nil, syscall.OPEN_EXISTING,
   214  				syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
   215  			if err != nil {
   216  				t.Fatal(err)
   217  			}
   218  			defer syscall.CloseHandle(h)
   219  			var fi syscall.ByHandleFileInformation
   220  			if err := syscall.GetFileInformationByHandle(h, &fi); err != nil {
   221  				t.Fatal(err)
   222  			}
   223  			gotDir := fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0
   224  
   225  			if got, want := gotDir, test.wantDir; got != want {
   226  				t.Errorf("link target %q: isDir = %v, want %v", test.target, got, want)
   227  			}
   228  		})
   229  	}
   230  }
   231  

View as plain text