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 5package filepath_test 6 7import ( 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 22func 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 33func 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 99func 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. 148func 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. 215func 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 241func 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 266var 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). 270func 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 304func 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 459func 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 466func 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 480func TestWalkDirectoryJunction(t *testing.T) { 481 testenv.MustHaveSymlink(t) 482 testWalkMklink(t, "J") 483} 484 485func TestWalkDirectorySymlink(t *testing.T) { 486 testenv.MustHaveSymlink(t) 487 testWalkMklink(t, "D") 488} 489 490func 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 532var winsymlink = godebug.New("winsymlink") 533var winreadlinkvolume = godebug.New("winreadlinkvolume") 534 535func 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 567func 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 594func 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 665func 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 691func 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