Source file src/path/filepath/path_windows_test.go

     1  // Copyright 2013 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  package filepath_test
     6  
     7  import (
     8  	"flag"
     9  	"fmt"
    10  	"internal/godebug"
    11  	"internal/testenv"
    12  	"io/fs"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"reflect"
    17  	"runtime/debug"
    18  	"strings"
    19  	"testing"
    20  )
    21  
    22  func TestWinSplitListTestsAreValid(t *testing.T) {
    23  	comspec := os.Getenv("ComSpec")
    24  	if comspec == "" {
    25  		t.Fatal("%ComSpec% must be set")
    26  	}
    27  
    28  	for ti, tt := range winsplitlisttests {
    29  		testWinSplitListTestIsValid(t, ti, tt, comspec)
    30  	}
    31  }
    32  
    33  func testWinSplitListTestIsValid(t *testing.T, ti int, tt SplitListTest,
    34  	comspec string) {
    35  
    36  	const (
    37  		cmdfile             = `printdir.cmd`
    38  		perm    fs.FileMode = 0700
    39  	)
    40  
    41  	tmp := t.TempDir()
    42  	for i, d := range tt.result {
    43  		if d == "" {
    44  			continue
    45  		}
    46  		if cd := filepath.Clean(d); filepath.VolumeName(cd) != "" ||
    47  			cd[0] == '\\' || cd == ".." || (len(cd) >= 3 && cd[0:3] == `..\`) {
    48  			t.Errorf("%d,%d: %#q refers outside working directory", ti, i, d)
    49  			return
    50  		}
    51  		dd := filepath.Join(tmp, d)
    52  		if _, err := os.Stat(dd); err == nil {
    53  			t.Errorf("%d,%d: %#q already exists", ti, i, d)
    54  			return
    55  		}
    56  		if err := os.MkdirAll(dd, perm); err != nil {
    57  			t.Errorf("%d,%d: MkdirAll(%#q) failed: %v", ti, i, dd, err)
    58  			return
    59  		}
    60  		fn, data := filepath.Join(dd, cmdfile), []byte("@echo "+d+"\r\n")
    61  		if err := os.WriteFile(fn, data, perm); err != nil {
    62  			t.Errorf("%d,%d: WriteFile(%#q) failed: %v", ti, i, fn, err)
    63  			return
    64  		}
    65  	}
    66  
    67  	// on some systems, SystemRoot is required for cmd to work
    68  	systemRoot := os.Getenv("SystemRoot")
    69  
    70  	for i, d := range tt.result {
    71  		if d == "" {
    72  			continue
    73  		}
    74  		exp := []byte(d + "\r\n")
    75  		cmd := &exec.Cmd{
    76  			Path: comspec,
    77  			Args: []string{`/c`, cmdfile},
    78  			Env:  []string{`Path=` + systemRoot + "/System32;" + tt.list, `SystemRoot=` + systemRoot},
    79  			Dir:  tmp,
    80  		}
    81  		out, err := cmd.CombinedOutput()
    82  		switch {
    83  		case err != nil:
    84  			t.Errorf("%d,%d: execution error %v\n%q", ti, i, err, out)
    85  			return
    86  		case !reflect.DeepEqual(out, exp):
    87  			t.Errorf("%d,%d: expected %#q, got %#q", ti, i, exp, out)
    88  			return
    89  		default:
    90  			// unshadow cmdfile in next directory
    91  			err = os.Remove(filepath.Join(tmp, d, cmdfile))
    92  			if err != nil {
    93  				t.Fatalf("Remove test command failed: %v", err)
    94  			}
    95  		}
    96  	}
    97  }
    98  
    99  func TestWindowsEvalSymlinks(t *testing.T) {
   100  	testenv.MustHaveSymlink(t)
   101  
   102  	tmpDir := tempDirCanonical(t)
   103  
   104  	if len(tmpDir) < 3 {
   105  		t.Fatalf("tmpDir path %q is too short", tmpDir)
   106  	}
   107  	if tmpDir[1] != ':' {
   108  		t.Fatalf("tmpDir path %q must have drive letter in it", tmpDir)
   109  	}
   110  	test := EvalSymlinksTest{"test/linkabswin", tmpDir[:3]}
   111  
   112  	// Create the symlink farm using relative paths.
   113  	testdirs := append(EvalSymlinksTestDirs, test)
   114  	for _, d := range testdirs {
   115  		var err error
   116  		path := simpleJoin(tmpDir, d.path)
   117  		if d.dest == "" {
   118  			err = os.Mkdir(path, 0755)
   119  		} else {
   120  			err = os.Symlink(d.dest, path)
   121  		}
   122  		if err != nil {
   123  			t.Fatal(err)
   124  		}
   125  	}
   126  
   127  	path := simpleJoin(tmpDir, test.path)
   128  
   129  	testEvalSymlinks(t, path, test.dest)
   130  
   131  	testEvalSymlinksAfterChdir(t, path, ".", test.dest)
   132  
   133  	testEvalSymlinksAfterChdir(t,
   134  		path,
   135  		filepath.VolumeName(tmpDir)+".",
   136  		test.dest)
   137  
   138  	testEvalSymlinksAfterChdir(t,
   139  		simpleJoin(tmpDir, "test"),
   140  		simpleJoin("..", test.path),
   141  		test.dest)
   142  
   143  	testEvalSymlinksAfterChdir(t, tmpDir, test.path, test.dest)
   144  }
   145  
   146  // TestEvalSymlinksCanonicalNames verify that EvalSymlinks
   147  // returns "canonical" path names on windows.
   148  func TestEvalSymlinksCanonicalNames(t *testing.T) {
   149  	ctmp := tempDirCanonical(t)
   150  	dirs := []string{
   151  		"test",
   152  		"test/dir",
   153  		"testing_long_dir",
   154  		"TEST2",
   155  	}
   156  
   157  	for _, d := range dirs {
   158  		dir := filepath.Join(ctmp, d)
   159  		err := os.Mkdir(dir, 0755)
   160  		if err != nil {
   161  			t.Fatal(err)
   162  		}
   163  		cname, err := filepath.EvalSymlinks(dir)
   164  		if err != nil {
   165  			t.Errorf("EvalSymlinks(%q) error: %v", dir, err)
   166  			continue
   167  		}
   168  		if dir != cname {
   169  			t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", dir, cname, dir)
   170  			continue
   171  		}
   172  		// test non-canonical names
   173  		test := strings.ToUpper(dir)
   174  		p, err := filepath.EvalSymlinks(test)
   175  		if err != nil {
   176  			t.Errorf("EvalSymlinks(%q) error: %v", test, err)
   177  			continue
   178  		}
   179  		if p != cname {
   180  			t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname)
   181  			continue
   182  		}
   183  		// another test
   184  		test = strings.ToLower(dir)
   185  		p, err = filepath.EvalSymlinks(test)
   186  		if err != nil {
   187  			t.Errorf("EvalSymlinks(%q) error: %v", test, err)
   188  			continue
   189  		}
   190  		if p != cname {
   191  			t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname)
   192  			continue
   193  		}
   194  	}
   195  }
   196  
   197  // checkVolume8dot3Setting runs "fsutil 8dot3name query c:" command
   198  // (where c: is vol parameter) to discover "8dot3 name creation state".
   199  // The state is combination of 2 flags. The global flag controls if it
   200  // is per volume or global setting:
   201  //
   202  //	0 - Enable 8dot3 name creation on all volumes on the system
   203  //	1 - Disable 8dot3 name creation on all volumes on the system
   204  //	2 - Set 8dot3 name creation on a per volume basis
   205  //	3 - Disable 8dot3 name creation on all volumes except the system volume
   206  //
   207  // If global flag is set to 2, then per-volume flag needs to be examined:
   208  //
   209  //	0 - Enable 8dot3 name creation on this volume
   210  //	1 - Disable 8dot3 name creation on this volume
   211  //
   212  // checkVolume8dot3Setting verifies that "8dot3 name creation" flags
   213  // are set to 2 and 0, if enabled parameter is true, or 2 and 1, if enabled
   214  // is false. Otherwise checkVolume8dot3Setting returns error.
   215  func checkVolume8dot3Setting(vol string, enabled bool) error {
   216  	// It appears, on some systems "fsutil 8dot3name query ..." command always
   217  	// exits with error. Ignore exit code, and look at fsutil output instead.
   218  	out, _ := exec.Command("fsutil", "8dot3name", "query", vol).CombinedOutput()
   219  	// Check that system has "Volume level setting" set.
   220  	expected := "The registry state of NtfsDisable8dot3NameCreation is 2, the default (Volume level setting)"
   221  	if !strings.Contains(string(out), expected) {
   222  		// Windows 10 version of fsutil has different output message.
   223  		expectedWindow10 := "The registry state is: 2 (Per volume setting - the default)"
   224  		if !strings.Contains(string(out), expectedWindow10) {
   225  			return fmt.Errorf("fsutil output should contain %q, but is %q", expected, string(out))
   226  		}
   227  	}
   228  	// Now check the volume setting.
   229  	expected = "Based on the above two settings, 8dot3 name creation is %s on %s"
   230  	if enabled {
   231  		expected = fmt.Sprintf(expected, "enabled", vol)
   232  	} else {
   233  		expected = fmt.Sprintf(expected, "disabled", vol)
   234  	}
   235  	if !strings.Contains(string(out), expected) {
   236  		return fmt.Errorf("unexpected fsutil output: %q", string(out))
   237  	}
   238  	return nil
   239  }
   240  
   241  func setVolume8dot3Setting(vol string, enabled bool) error {
   242  	cmd := []string{"fsutil", "8dot3name", "set", vol}
   243  	if enabled {
   244  		cmd = append(cmd, "0")
   245  	} else {
   246  		cmd = append(cmd, "1")
   247  	}
   248  	// It appears, on some systems "fsutil 8dot3name set ..." command always
   249  	// exits with error. Ignore exit code, and look at fsutil output instead.
   250  	out, _ := exec.Command(cmd[0], cmd[1:]...).CombinedOutput()
   251  	if string(out) != "\r\nSuccessfully set 8dot3name behavior.\r\n" {
   252  		// Windows 10 version of fsutil has different output message.
   253  		expectedWindow10 := "Successfully %s 8dot3name generation on %s\r\n"
   254  		if enabled {
   255  			expectedWindow10 = fmt.Sprintf(expectedWindow10, "enabled", vol)
   256  		} else {
   257  			expectedWindow10 = fmt.Sprintf(expectedWindow10, "disabled", vol)
   258  		}
   259  		if string(out) != expectedWindow10 {
   260  			return fmt.Errorf("%v command failed: %q", cmd, string(out))
   261  		}
   262  	}
   263  	return nil
   264  }
   265  
   266  var runFSModifyTests = flag.Bool("run_fs_modify_tests", false, "run tests which modify filesystem parameters")
   267  
   268  // This test assumes registry state of NtfsDisable8dot3NameCreation is 2,
   269  // the default (Volume level setting).
   270  func TestEvalSymlinksCanonicalNamesWith8dot3Disabled(t *testing.T) {
   271  	if !*runFSModifyTests {
   272  		t.Skip("skipping test that modifies file system setting; enable with -run_fs_modify_tests")
   273  	}
   274  	tempVol := filepath.VolumeName(os.TempDir())
   275  	if len(tempVol) != 2 {
   276  		t.Fatalf("unexpected temp volume name %q", tempVol)
   277  	}
   278  
   279  	err := checkVolume8dot3Setting(tempVol, true)
   280  	if err != nil {
   281  		t.Fatal(err)
   282  	}
   283  	err = setVolume8dot3Setting(tempVol, false)
   284  	if err != nil {
   285  		t.Fatal(err)
   286  	}
   287  	defer func() {
   288  		err := setVolume8dot3Setting(tempVol, true)
   289  		if err != nil {
   290  			t.Fatal(err)
   291  		}
   292  		err = checkVolume8dot3Setting(tempVol, true)
   293  		if err != nil {
   294  			t.Fatal(err)
   295  		}
   296  	}()
   297  	err = checkVolume8dot3Setting(tempVol, false)
   298  	if err != nil {
   299  		t.Fatal(err)
   300  	}
   301  	TestEvalSymlinksCanonicalNames(t)
   302  }
   303  
   304  func TestToNorm(t *testing.T) {
   305  	stubBase := func(path string) (string, error) {
   306  		vol := filepath.VolumeName(path)
   307  		path = path[len(vol):]
   308  
   309  		if strings.Contains(path, "/") {
   310  			return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
   311  		}
   312  
   313  		if path == "" || path == "." || path == `\` {
   314  			return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
   315  		}
   316  
   317  		i := strings.LastIndexByte(path, filepath.Separator)
   318  		if i == len(path)-1 { // trailing '\' is invalid
   319  			return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
   320  		}
   321  		if i == -1 {
   322  			return strings.ToUpper(path), nil
   323  		}
   324  
   325  		return strings.ToUpper(path[i+1:]), nil
   326  	}
   327  
   328  	// On this test, toNorm should be same as string.ToUpper(filepath.Clean(path)) except empty string.
   329  	tests := []struct {
   330  		arg  string
   331  		want string
   332  	}{
   333  		{"", ""},
   334  		{".", "."},
   335  		{"./foo/bar", `FOO\BAR`},
   336  		{"/", `\`},
   337  		{"/foo/bar", `\FOO\BAR`},
   338  		{"/foo/bar/baz/qux", `\FOO\BAR\BAZ\QUX`},
   339  		{"foo/bar", `FOO\BAR`},
   340  		{"C:/foo/bar", `C:\FOO\BAR`},
   341  		{"C:foo/bar", `C:FOO\BAR`},
   342  		{"c:/foo/bar", `C:\FOO\BAR`},
   343  		{"C:/foo/bar", `C:\FOO\BAR`},
   344  		{"C:/foo/bar/", `C:\FOO\BAR`},
   345  		{`C:\foo\bar`, `C:\FOO\BAR`},
   346  		{`C:\foo/bar\`, `C:\FOO\BAR`},
   347  		{"C:/ふー/バー", `C:\ふー\バー`},
   348  	}
   349  
   350  	for _, test := range tests {
   351  		var path string
   352  		if test.arg != "" {
   353  			path = filepath.Clean(test.arg)
   354  		}
   355  		got, err := filepath.ToNorm(path, stubBase)
   356  		if err != nil {
   357  			t.Errorf("toNorm(%s) failed: %v\n", test.arg, err)
   358  		} else if got != test.want {
   359  			t.Errorf("toNorm(%s) returns %s, but %s expected\n", test.arg, got, test.want)
   360  		}
   361  	}
   362  
   363  	testPath := `{{tmp}}\test\foo\bar`
   364  
   365  	testsDir := []struct {
   366  		wd   string
   367  		arg  string
   368  		want string
   369  	}{
   370  		// test absolute paths
   371  		{".", `{{tmp}}\test\foo\bar`, `{{tmp}}\test\foo\bar`},
   372  		{".", `{{tmp}}\.\test/foo\bar`, `{{tmp}}\test\foo\bar`},
   373  		{".", `{{tmp}}\test\..\test\foo\bar`, `{{tmp}}\test\foo\bar`},
   374  		{".", `{{tmp}}\TEST\FOO\BAR`, `{{tmp}}\test\foo\bar`},
   375  
   376  		// test relative paths begin with drive letter
   377  		{`{{tmp}}\test`, `{{tmpvol}}.`, `{{tmpvol}}.`},
   378  		{`{{tmp}}\test`, `{{tmpvol}}..`, `{{tmpvol}}..`},
   379  		{`{{tmp}}\test`, `{{tmpvol}}foo\bar`, `{{tmpvol}}foo\bar`},
   380  		{`{{tmp}}\test`, `{{tmpvol}}.\foo\bar`, `{{tmpvol}}foo\bar`},
   381  		{`{{tmp}}\test`, `{{tmpvol}}foo\..\foo\bar`, `{{tmpvol}}foo\bar`},
   382  		{`{{tmp}}\test`, `{{tmpvol}}FOO\BAR`, `{{tmpvol}}foo\bar`},
   383  
   384  		// test relative paths begin with '\'
   385  		{"{{tmp}}", `{{tmpnovol}}\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
   386  		{"{{tmp}}", `{{tmpnovol}}\.\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
   387  		{"{{tmp}}", `{{tmpnovol}}\test\..\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
   388  		{"{{tmp}}", `{{tmpnovol}}\TEST\FOO\BAR`, `{{tmpnovol}}\test\foo\bar`},
   389  
   390  		// test relative paths begin without '\'
   391  		{`{{tmp}}\test`, ".", `.`},
   392  		{`{{tmp}}\test`, "..", `..`},
   393  		{`{{tmp}}\test`, `foo\bar`, `foo\bar`},
   394  		{`{{tmp}}\test`, `.\foo\bar`, `foo\bar`},
   395  		{`{{tmp}}\test`, `foo\..\foo\bar`, `foo\bar`},
   396  		{`{{tmp}}\test`, `FOO\BAR`, `foo\bar`},
   397  
   398  		// test UNC paths
   399  		{".", `\\localhost\c$`, `\\localhost\c$`},
   400  	}
   401  
   402  	ctmp := tempDirCanonical(t)
   403  	if err := os.MkdirAll(strings.ReplaceAll(testPath, "{{tmp}}", ctmp), 0777); err != nil {
   404  		t.Fatal(err)
   405  	}
   406  
   407  	cwd, err := os.Getwd()
   408  	if err != nil {
   409  		t.Fatal(err)
   410  	}
   411  	defer func() {
   412  		err := os.Chdir(cwd)
   413  		if err != nil {
   414  			t.Fatal(err)
   415  		}
   416  	}()
   417  
   418  	tmpVol := filepath.VolumeName(ctmp)
   419  	if len(tmpVol) != 2 {
   420  		t.Fatalf("unexpected temp volume name %q", tmpVol)
   421  	}
   422  
   423  	tmpNoVol := ctmp[len(tmpVol):]
   424  
   425  	replacer := strings.NewReplacer("{{tmp}}", ctmp, "{{tmpvol}}", tmpVol, "{{tmpnovol}}", tmpNoVol)
   426  
   427  	for _, test := range testsDir {
   428  		wd := replacer.Replace(test.wd)
   429  		arg := replacer.Replace(test.arg)
   430  		want := replacer.Replace(test.want)
   431  
   432  		if test.wd == "." {
   433  			err := os.Chdir(cwd)
   434  			if err != nil {
   435  				t.Error(err)
   436  
   437  				continue
   438  			}
   439  		} else {
   440  			err := os.Chdir(wd)
   441  			if err != nil {
   442  				t.Error(err)
   443  
   444  				continue
   445  			}
   446  		}
   447  		if arg != "" {
   448  			arg = filepath.Clean(arg)
   449  		}
   450  		got, err := filepath.ToNorm(arg, filepath.NormBase)
   451  		if err != nil {
   452  			t.Errorf("toNorm(%s) failed: %v (wd=%s)\n", arg, err, wd)
   453  		} else if got != want {
   454  			t.Errorf("toNorm(%s) returns %s, but %s expected (wd=%s)\n", arg, got, want, wd)
   455  		}
   456  	}
   457  }
   458  
   459  func TestUNC(t *testing.T) {
   460  	// Test that this doesn't go into an infinite recursion.
   461  	// See golang.org/issue/15879.
   462  	defer debug.SetMaxStack(debug.SetMaxStack(1e6))
   463  	filepath.Glob(`\\?\c:\*`)
   464  }
   465  
   466  func testWalkMklink(t *testing.T, linktype string) {
   467  	output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
   468  	if !strings.Contains(string(output), fmt.Sprintf(" /%s ", linktype)) {
   469  		t.Skipf(`skipping test; mklink does not supports /%s parameter`, linktype)
   470  	}
   471  	testWalkSymlink(t, func(target, link string) error {
   472  		output, err := exec.Command("cmd", "/c", "mklink", "/"+linktype, link, target).CombinedOutput()
   473  		if err != nil {
   474  			return fmt.Errorf(`"mklink /%s %v %v" command failed: %v\n%v`, linktype, link, target, err, string(output))
   475  		}
   476  		return nil
   477  	})
   478  }
   479  
   480  func TestWalkDirectoryJunction(t *testing.T) {
   481  	testenv.MustHaveSymlink(t)
   482  	testWalkMklink(t, "J")
   483  }
   484  
   485  func TestWalkDirectorySymlink(t *testing.T) {
   486  	testenv.MustHaveSymlink(t)
   487  	testWalkMklink(t, "D")
   488  }
   489  
   490  func createMountPartition(t *testing.T, vhd string, args string) []byte {
   491  	testenv.MustHaveExecPath(t, "powershell")
   492  	t.Cleanup(func() {
   493  		cmd := testenv.Command(t, "powershell", "-Command", fmt.Sprintf("Dismount-VHD %q", vhd))
   494  		out, err := cmd.CombinedOutput()
   495  		if err != nil {
   496  			if t.Skipped() {
   497  				// Probably failed to dismount because we never mounted it in
   498  				// the first place. Log the error, but ignore it.
   499  				t.Logf("%v: %v (skipped)\n%s", cmd, err, out)
   500  			} else {
   501  				// Something went wrong, and we don't want to leave dangling VHDs.
   502  				// Better to fail the test than to just log the error and continue.
   503  				t.Errorf("%v: %v\n%s", cmd, err, out)
   504  			}
   505  		}
   506  	})
   507  
   508  	script := filepath.Join(t.TempDir(), "test.ps1")
   509  	cmd := strings.Join([]string{
   510  		"$ErrorActionPreference = \"Stop\"",
   511  		fmt.Sprintf("$vhd = New-VHD -Path %q -SizeBytes 3MB -Fixed", vhd),
   512  		"$vhd | Mount-VHD",
   513  		fmt.Sprintf("$vhd = Get-VHD %q", vhd),
   514  		"$vhd | Get-Disk | Initialize-Disk -PartitionStyle GPT",
   515  		"$part = $vhd | Get-Disk | New-Partition -UseMaximumSize -AssignDriveLetter:$false",
   516  		"$vol = $part | Format-Volume -FileSystem NTFS",
   517  		args,
   518  	}, "\n")
   519  
   520  	err := os.WriteFile(script, []byte(cmd), 0666)
   521  	if err != nil {
   522  		t.Fatal(err)
   523  	}
   524  	output, err := testenv.Command(t, "powershell", "-File", script).CombinedOutput()
   525  	if err != nil {
   526  		// This can happen if Hyper-V is not installed or enabled.
   527  		t.Skip("skipping test because failed to create VHD: ", err, string(output))
   528  	}
   529  	return output
   530  }
   531  
   532  var winsymlink = godebug.New("winsymlink")
   533  var winreadlinkvolume = godebug.New("winreadlinkvolume")
   534  
   535  func TestEvalSymlinksJunctionToVolumeID(t *testing.T) {
   536  	// Test that EvalSymlinks resolves a directory junction which
   537  	// is mapped to volumeID (instead of drive letter). See go.dev/issue/39786.
   538  	if winsymlink.Value() == "0" {
   539  		t.Skip("skipping test because winsymlink is not enabled")
   540  	}
   541  	t.Parallel()
   542  
   543  	output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
   544  	if !strings.Contains(string(output), " /J ") {
   545  		t.Skip("skipping test because mklink command does not support junctions")
   546  	}
   547  
   548  	tmpdir := tempDirCanonical(t)
   549  	vhd := filepath.Join(tmpdir, "Test.vhdx")
   550  	output = createMountPartition(t, vhd, "Write-Host $vol.Path -NoNewline")
   551  	vol := string(output)
   552  
   553  	dirlink := filepath.Join(tmpdir, "dirlink")
   554  	output, err := testenv.Command(t, "cmd", "/c", "mklink", "/J", dirlink, vol).CombinedOutput()
   555  	if err != nil {
   556  		t.Fatalf("failed to run mklink %v %v: %v %q", dirlink, vol, err, output)
   557  	}
   558  	got, err := filepath.EvalSymlinks(dirlink)
   559  	if err != nil {
   560  		t.Fatal(err)
   561  	}
   562  	if got != dirlink {
   563  		t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, dirlink)
   564  	}
   565  }
   566  
   567  func TestEvalSymlinksMountPointRecursion(t *testing.T) {
   568  	// Test that EvalSymlinks doesn't follow recursive mount points.
   569  	// See go.dev/issue/40176.
   570  	if winsymlink.Value() == "0" {
   571  		t.Skip("skipping test because winsymlink is not enabled")
   572  	}
   573  	t.Parallel()
   574  
   575  	tmpdir := tempDirCanonical(t)
   576  	dirlink := filepath.Join(tmpdir, "dirlink")
   577  	err := os.Mkdir(dirlink, 0755)
   578  	if err != nil {
   579  		t.Fatal(err)
   580  	}
   581  
   582  	vhd := filepath.Join(tmpdir, "Test.vhdx")
   583  	createMountPartition(t, vhd, fmt.Sprintf("$part | Add-PartitionAccessPath -AccessPath %q\n", dirlink))
   584  
   585  	got, err := filepath.EvalSymlinks(dirlink)
   586  	if err != nil {
   587  		t.Fatal(err)
   588  	}
   589  	if got != dirlink {
   590  		t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, dirlink)
   591  	}
   592  }
   593  
   594  func TestNTNamespaceSymlink(t *testing.T) {
   595  	output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
   596  	if !strings.Contains(string(output), " /J ") {
   597  		t.Skip("skipping test because mklink command does not support junctions")
   598  	}
   599  
   600  	tmpdir := tempDirCanonical(t)
   601  
   602  	vol := filepath.VolumeName(tmpdir)
   603  	output, err := exec.Command("cmd", "/c", "mountvol", vol, "/L").CombinedOutput()
   604  	if err != nil {
   605  		t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output)
   606  	}
   607  	target := strings.Trim(string(output), " \n\r")
   608  
   609  	dirlink := filepath.Join(tmpdir, "dirlink")
   610  	output, err = exec.Command("cmd", "/c", "mklink", "/J", dirlink, target).CombinedOutput()
   611  	if err != nil {
   612  		t.Fatalf("failed to run mklink %v %v: %v %q", dirlink, target, err, output)
   613  	}
   614  
   615  	got, err := filepath.EvalSymlinks(dirlink)
   616  	if err != nil {
   617  		t.Fatal(err)
   618  	}
   619  	var want string
   620  	if winsymlink.Value() == "0" {
   621  		if winreadlinkvolume.Value() == "0" {
   622  			want = vol + `\`
   623  		} else {
   624  			want = target
   625  		}
   626  	} else {
   627  		want = dirlink
   628  	}
   629  	if got != want {
   630  		t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, want)
   631  	}
   632  
   633  	// Make sure we have sufficient privilege to run mklink command.
   634  	testenv.MustHaveSymlink(t)
   635  
   636  	file := filepath.Join(tmpdir, "file")
   637  	err = os.WriteFile(file, []byte(""), 0666)
   638  	if err != nil {
   639  		t.Fatal(err)
   640  	}
   641  
   642  	target = filepath.Join(target, file[len(filepath.VolumeName(file)):])
   643  
   644  	filelink := filepath.Join(tmpdir, "filelink")
   645  	output, err = exec.Command("cmd", "/c", "mklink", filelink, target).CombinedOutput()
   646  	if err != nil {
   647  		t.Fatalf("failed to run mklink %v %v: %v %q", filelink, target, err, output)
   648  	}
   649  
   650  	got, err = filepath.EvalSymlinks(filelink)
   651  	if err != nil {
   652  		t.Fatal(err)
   653  	}
   654  
   655  	if winreadlinkvolume.Value() == "0" {
   656  		want = file
   657  	} else {
   658  		want = target
   659  	}
   660  	if got != want {
   661  		t.Errorf(`EvalSymlinks(%q): got %q, want %q`, filelink, got, want)
   662  	}
   663  }
   664  
   665  func TestIssue52476(t *testing.T) {
   666  	tests := []struct {
   667  		lhs, rhs string
   668  		want     string
   669  	}{
   670  		{`..\.`, `C:`, `..\C:`},
   671  		{`..`, `C:`, `..\C:`},
   672  		{`.`, `:`, `.\:`},
   673  		{`.`, `C:`, `.\C:`},
   674  		{`.`, `C:/a/b/../c`, `.\C:\a\c`},
   675  		{`.`, `\C:`, `.\C:`},
   676  		{`C:\`, `.`, `C:\`},
   677  		{`C:\`, `C:\`, `C:\C:`},
   678  		{`C`, `:`, `C\:`},
   679  		{`\.`, `C:`, `\C:`},
   680  		{`\`, `C:`, `\C:`},
   681  	}
   682  
   683  	for _, test := range tests {
   684  		got := filepath.Join(test.lhs, test.rhs)
   685  		if got != test.want {
   686  			t.Errorf(`Join(%q, %q): got %q, want %q`, test.lhs, test.rhs, got, test.want)
   687  		}
   688  	}
   689  }
   690  
   691  func TestAbsWindows(t *testing.T) {
   692  	for _, test := range []struct {
   693  		path string
   694  		want string
   695  	}{
   696  		{`C:\foo`, `C:\foo`},
   697  		{`\\host\share\foo`, `\\host\share\foo`},
   698  		{`\\host`, `\\host`},
   699  		{`\\.\NUL`, `\\.\NUL`},
   700  		{`NUL`, `\\.\NUL`},
   701  		{`COM1`, `\\.\COM1`},
   702  		{`a/NUL`, `\\.\NUL`},
   703  	} {
   704  		got, err := filepath.Abs(test.path)
   705  		if err != nil || got != test.want {
   706  			t.Errorf("Abs(%q) = %q, %v; want %q, nil", test.path, got, err, test.want)
   707  		}
   708  	}
   709  }
   710  

View as plain text