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/strconv"
    13  	"internal/syscall/windows"
    14  	"os"
    15  	"path/filepath"
    16  	"syscall"
    17  	"testing"
    18  	"unsafe"
    19  )
    20  
    21  // Verify that Root.Open rejects Windows reserved names.
    22  func TestRootWindowsDeviceNames(t *testing.T) {
    23  	r, err := os.OpenRoot(t.TempDir())
    24  	if err != nil {
    25  		t.Fatal(err)
    26  	}
    27  	defer r.Close()
    28  	if f, err := r.Open("NUL"); err == nil {
    29  		t.Errorf(`r.Open("NUL") succeeded; want error"`)
    30  		f.Close()
    31  	}
    32  }
    33  
    34  // Verify that Root.Open is case-insensitive.
    35  // (The wrong options to NtOpenFile could make operations case-sensitive,
    36  // so this is worth checking.)
    37  func TestRootWindowsCaseInsensitivity(t *testing.T) {
    38  	dir := t.TempDir()
    39  	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
    40  		t.Fatal(err)
    41  	}
    42  	r, err := os.OpenRoot(dir)
    43  	if err != nil {
    44  		t.Fatal(err)
    45  	}
    46  	defer r.Close()
    47  	f, err := r.Open("FILE")
    48  	if err != nil {
    49  		t.Fatal(err)
    50  	}
    51  	f.Close()
    52  	if err := r.Remove("FILE"); err != nil {
    53  		t.Fatal(err)
    54  	}
    55  	if _, err := os.Stat(filepath.Join(dir, "file")); !errors.Is(err, os.ErrNotExist) {
    56  		t.Fatalf("os.Stat(file) after deletion: %v, want ErrNotFound", err)
    57  	}
    58  }
    59  
    60  // TestRootSymlinkRelativity tests that symlinks created using Root.Symlink have the
    61  // same SYMLINK_FLAG_RELATIVE value as ones creates using os.Symlink.
    62  func TestRootSymlinkRelativity(t *testing.T) {
    63  	dir := t.TempDir()
    64  	root, err := os.OpenRoot(dir)
    65  	if err != nil {
    66  		t.Fatal(err)
    67  	}
    68  	defer root.Close()
    69  
    70  	for i, test := range []struct {
    71  		name   string
    72  		target string
    73  	}{{
    74  		name:   "relative",
    75  		target: `foo`,
    76  	}, {
    77  		name:   "absolute",
    78  		target: `C:\foo`,
    79  	}, {
    80  		name:   "current working directory-relative",
    81  		target: `C:foo`,
    82  	}, {
    83  		name:   "root-relative",
    84  		target: `\foo`,
    85  	}, {
    86  		name:   "question prefix",
    87  		target: `\\?\foo`,
    88  	}, {
    89  		name:   "relative with dot dot",
    90  		target: `a\..\b`, // could be cleaned (but isn't)
    91  	}} {
    92  		t.Run(test.name, func(t *testing.T) {
    93  			name := fmt.Sprintf("symlink_%v", i)
    94  			if err := os.Symlink(test.target, filepath.Join(dir, name)); err != nil {
    95  				t.Fatal(err)
    96  			}
    97  			if err := root.Symlink(test.target, name+"_at"); err != nil {
    98  				t.Fatal(err)
    99  			}
   100  
   101  			osRDB, err := readSymlinkReparseData(filepath.Join(dir, name))
   102  			if err != nil {
   103  				t.Fatal(err)
   104  			}
   105  			rootRDB, err := readSymlinkReparseData(filepath.Join(dir, name+"_at"))
   106  			if err != nil {
   107  				t.Fatal(err)
   108  			}
   109  			if osRDB.Flags != rootRDB.Flags {
   110  				t.Errorf("symlink target %q: Symlink flags = %x, Root.Symlink flags = %x", test.target, osRDB.Flags, rootRDB.Flags)
   111  			}
   112  
   113  			// Compare the link target.
   114  			// os.Symlink converts current working directory-relative links
   115  			// such as c:foo into absolute links.
   116  			osTarget, err := os.Readlink(filepath.Join(dir, name))
   117  			if err != nil {
   118  				t.Fatal(err)
   119  			}
   120  			rootTarget, err := os.Readlink(filepath.Join(dir, name+"_at"))
   121  			if err != nil {
   122  				t.Fatal(err)
   123  			}
   124  			if osTarget != rootTarget {
   125  				t.Errorf("symlink created with target %q: Symlink target = %q, Root.Symlink target = %q", test.target, osTarget, rootTarget)
   126  			}
   127  		})
   128  	}
   129  }
   130  
   131  func readSymlinkReparseData(name string) (*windows.SymbolicLinkReparseBuffer, error) {
   132  	nameu16, err := syscall.UTF16FromString(name)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	h, err := syscall.CreateFile(&nameu16[0], syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING,
   137  		syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	defer syscall.CloseHandle(h)
   142  
   143  	var rdbbuf [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte
   144  	var bytesReturned uint32
   145  	err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
   151  	if rdb.ReparseTag != syscall.IO_REPARSE_TAG_SYMLINK {
   152  		return nil, fmt.Errorf("%q: not a symlink", name)
   153  	}
   154  
   155  	bufoff := unsafe.Offsetof(rdb.DUMMYUNIONNAME)
   156  	symlinkBuf := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdbbuf[bufoff]))
   157  
   158  	return symlinkBuf, nil
   159  }
   160  
   161  // TestRootSymlinkToDirectory tests that Root.Symlink creates directory links
   162  // when the target is a directory contained within the root.
   163  func TestRootSymlinkToDirectory(t *testing.T) {
   164  	dir := t.TempDir()
   165  	root, err := os.OpenRoot(dir)
   166  	if err != nil {
   167  		t.Fatal(err)
   168  	}
   169  	defer root.Close()
   170  
   171  	if err := os.Mkdir(filepath.Join(dir, "dir"), 0777); err != nil {
   172  		t.Fatal(err)
   173  	}
   174  	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
   175  		t.Fatal(err)
   176  	}
   177  
   178  	dir2 := t.TempDir()
   179  
   180  	for i, test := range []struct {
   181  		name    string
   182  		target  string
   183  		wantDir bool
   184  	}{{
   185  		name:    "directory outside root",
   186  		target:  dir2,
   187  		wantDir: false,
   188  	}, {
   189  		name:    "directory inside root",
   190  		target:  "dir",
   191  		wantDir: true,
   192  	}, {
   193  		name:    "file inside root",
   194  		target:  "file",
   195  		wantDir: false,
   196  	}, {
   197  		name:    "nonexistent inside root",
   198  		target:  "nonexistent",
   199  		wantDir: false,
   200  	}} {
   201  		t.Run(test.name, func(t *testing.T) {
   202  			name := fmt.Sprintf("symlink_%v", i)
   203  			if err := root.Symlink(test.target, name); err != nil {
   204  				t.Fatal(err)
   205  			}
   206  
   207  			// Lstat strips the directory mode bit from reparse points,
   208  			// so we need to use GetFileInformationByHandle directly to
   209  			// determine if this is a directory link.
   210  			nameu16, err := syscall.UTF16PtrFromString(filepath.Join(dir, name))
   211  			if err != nil {
   212  				t.Fatal(err)
   213  			}
   214  			h, err := syscall.CreateFile(nameu16, 0, 0, nil, syscall.OPEN_EXISTING,
   215  				syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
   216  			if err != nil {
   217  				t.Fatal(err)
   218  			}
   219  			defer syscall.CloseHandle(h)
   220  			var fi syscall.ByHandleFileInformation
   221  			if err := syscall.GetFileInformationByHandle(h, &fi); err != nil {
   222  				t.Fatal(err)
   223  			}
   224  			gotDir := fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0
   225  
   226  			if got, want := gotDir, test.wantDir; got != want {
   227  				t.Errorf("link target %q: isDir = %v, want %v", test.target, got, want)
   228  			}
   229  		})
   230  	}
   231  }
   232  
   233  func TestRootOpenFileTruncateNamedPipe(t *testing.T) {
   234  	t.Parallel()
   235  	name := pipeName()
   236  	pipe := newBytePipe(t, name, false)
   237  	defer pipe.Close()
   238  
   239  	root, err := os.OpenRoot(filepath.Dir(name))
   240  	if err != nil {
   241  		t.Fatal(err)
   242  	}
   243  	defer root.Close()
   244  
   245  	f, err := root.OpenFile(filepath.Base(name), os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0666)
   246  	if err != nil {
   247  		t.Fatal(err)
   248  	}
   249  	f.Close()
   250  }
   251  
   252  func TestRootOpenFileFlags(t *testing.T) {
   253  	t.Parallel()
   254  
   255  	dir := t.TempDir()
   256  	root, err := os.OpenRoot(dir)
   257  	if err != nil {
   258  		t.Fatal(err)
   259  	}
   260  	defer root.Close()
   261  
   262  	// The only way to retrieve some of the flags passed in CreateFile
   263  	// is using NtQueryInformationFile, which returns the file flags
   264  	// NT equivalent. Note that FILE_SYNCHRONOUS_IO_NONALERT is always
   265  	// set when FILE_FLAG_OVERLAPPED is not passed.
   266  	// The flags that can't be retrieved using NtQueryInformationFile won't
   267  	// be tested in here, but we at least know that the logic to handle them is correct.
   268  	tests := []struct {
   269  		flag     uint32
   270  		wantMode uint32
   271  	}{
   272  		{0, windows.FILE_SYNCHRONOUS_IO_NONALERT},
   273  		{windows.O_FILE_FLAG_OVERLAPPED, 0},
   274  		{windows.O_FILE_FLAG_NO_BUFFERING, windows.FILE_NO_INTERMEDIATE_BUFFERING | windows.FILE_SYNCHRONOUS_IO_NONALERT},
   275  		{windows.O_FILE_FLAG_NO_BUFFERING | windows.O_FILE_FLAG_OVERLAPPED, windows.FILE_NO_INTERMEDIATE_BUFFERING},
   276  		{windows.O_FILE_FLAG_SEQUENTIAL_SCAN, windows.FILE_SEQUENTIAL_ONLY | windows.FILE_SYNCHRONOUS_IO_NONALERT},
   277  		{windows.O_FILE_FLAG_WRITE_THROUGH, windows.FILE_WRITE_THROUGH | windows.FILE_SYNCHRONOUS_IO_NONALERT},
   278  	}
   279  	for i, tt := range tests {
   280  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   281  			f, err := root.OpenFile(strconv.Itoa(i)+".txt", syscall.O_RDWR|syscall.O_CREAT|int(tt.flag), 0666)
   282  			if err != nil {
   283  				t.Fatal(err)
   284  			}
   285  			defer f.Close()
   286  			var info windows.FILE_MODE_INFORMATION
   287  			if err := windows.NtQueryInformationFile(syscall.Handle(f.Fd()), &windows.IO_STATUS_BLOCK{},
   288  				unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), windows.FileModeInformation); err != nil {
   289  				t.Fatal(err)
   290  			}
   291  			if info.Mode != tt.wantMode {
   292  				t.Errorf("file mode = 0x%x; want 0x%x", info.Mode, tt.wantMode)
   293  			}
   294  		})
   295  	}
   296  }
   297  
   298  func TestRootOpenFileDeleteOnClose(t *testing.T) {
   299  	t.Parallel()
   300  	dir := t.TempDir()
   301  	root, err := os.OpenRoot(dir)
   302  	if err != nil {
   303  		t.Fatal(err)
   304  	}
   305  	defer root.Close()
   306  	const name = "test.txt"
   307  	f, err := root.OpenFile(name, syscall.O_RDWR|syscall.O_CREAT|windows.O_FILE_FLAG_DELETE_ON_CLOSE, 0666)
   308  	if err != nil {
   309  		t.Fatal(err)
   310  	}
   311  	if err := f.Close(); err != nil {
   312  		t.Fatal(err)
   313  	}
   314  	// The file should be deleted after closing.
   315  	if _, err := os.Stat(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) {
   316  		t.Errorf("expected file to be deleted, got %v", err)
   317  	}
   318  }
   319  
   320  func TestRootOpenFileFlagInvalid(t *testing.T) {
   321  	t.Parallel()
   322  	dir := t.TempDir()
   323  	root, err := os.OpenRoot(dir)
   324  	if err != nil {
   325  		t.Fatal(err)
   326  	}
   327  	defer root.Close()
   328  	// invalidFileFlag is the only value in the file flag range that is not supported,
   329  	// as it is not defined in the Windows API.
   330  	const invalidFileFlag = 0x00400000
   331  	f, err := root.OpenFile("test.txt", syscall.O_RDWR|syscall.O_CREAT|invalidFileFlag, 0666)
   332  	if !errors.Is(err, os.ErrInvalid) {
   333  		t.Fatalf("expected os.ErrInvalid, got %v", err)
   334  	}
   335  	f.Close()
   336  }
   337  

View as plain text