// Copyright 2016 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. package os_test import ( "fmt" "internal/syscall/windows" "internal/testenv" "os" "path/filepath" "strings" "syscall" "testing" ) func TestAddExtendedPrefix(t *testing.T) { // Test addExtendedPrefix instead of fixLongPath so the path manipulation code // is exercised even if long path are supported by the system, else the // function might not be tested at all if/when all test builders support long paths. cwd, err := os.Getwd() if err != nil { t.Fatal("cannot get cwd") } drive := strings.ToLower(filepath.VolumeName(cwd)) cwd = strings.ToLower(cwd[len(drive)+1:]) // Build a very long pathname. Paths in Go are supposed to be arbitrarily long, // so let's make a long path which is comfortably bigger than MAX_PATH on Windows // (256) and thus requires fixLongPath to be correctly interpreted in I/O syscalls. veryLong := "l" + strings.Repeat("o", 500) + "ng" for _, test := range []struct{ in, want string }{ // Test cases use word substitutions: // * "long" is replaced with a very long pathname // * "c:" or "C:" are replaced with the drive of the current directory (preserving case) // * "cwd" is replaced with the current directory // Drive Absolute {`C:\long\foo.txt`, `\\?\C:\long\foo.txt`}, {`C:/long/foo.txt`, `\\?\C:\long\foo.txt`}, {`C:\\\long///foo.txt`, `\\?\C:\long\foo.txt`}, {`C:\long\.\foo.txt`, `\\?\C:\long\foo.txt`}, {`C:\long\..\foo.txt`, `\\?\C:\foo.txt`}, {`C:\long\..\..\foo.txt`, `\\?\C:\foo.txt`}, // Drive Relative {`C:long\foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`C:long/foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`C:long///foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`C:long\.\foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`C:long\..\foo.txt`, `\\?\C:\cwd\foo.txt`}, // Rooted {`\long\foo.txt`, `\\?\C:\long\foo.txt`}, {`/long/foo.txt`, `\\?\C:\long\foo.txt`}, {`\long///foo.txt`, `\\?\C:\long\foo.txt`}, {`\long\.\foo.txt`, `\\?\C:\long\foo.txt`}, {`\long\..\foo.txt`, `\\?\C:\foo.txt`}, // Relative {`long\foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`long/foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`long///foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`long\.\foo.txt`, `\\?\C:\cwd\long\foo.txt`}, {`long\..\foo.txt`, `\\?\C:\cwd\foo.txt`}, {`.\long\foo.txt`, `\\?\C:\cwd\long\foo.txt`}, // UNC Absolute {`\\srv\share\long`, `\\?\UNC\srv\share\long`}, {`//srv/share/long`, `\\?\UNC\srv\share\long`}, {`/\srv/share/long`, `\\?\UNC\srv\share\long`}, {`\\srv\share\long\`, `\\?\UNC\srv\share\long\`}, {`\\srv\share\bar\.\long`, `\\?\UNC\srv\share\bar\long`}, {`\\srv\share\bar\..\long`, `\\?\UNC\srv\share\long`}, {`\\srv\share\bar\..\..\long`, `\\?\UNC\srv\share\long`}, // share name is not removed by ".." // Local Device {`\\.\C:\long\foo.txt`, `\\.\C:\long\foo.txt`}, {`//./C:/long/foo.txt`, `\\.\C:\long\foo.txt`}, {`/\./C:/long/foo.txt`, `\\.\C:\long\foo.txt`}, {`\\.\C:\long///foo.txt`, `\\.\C:\long\foo.txt`}, {`\\.\C:\long\.\foo.txt`, `\\.\C:\long\foo.txt`}, {`\\.\C:\long\..\foo.txt`, `\\.\C:\foo.txt`}, // Misc tests {`C:\short.txt`, `C:\short.txt`}, {`C:\`, `C:\`}, {`C:`, `C:`}, {`\\srv\path`, `\\srv\path`}, {`long.txt`, `\\?\C:\cwd\long.txt`}, {`C:long.txt`, `\\?\C:\cwd\long.txt`}, {`C:\long\.\bar\baz`, `\\?\C:\long\bar\baz`}, {`C:long\.\bar\baz`, `\\?\C:\cwd\long\bar\baz`}, {`C:\long\..\bar\baz`, `\\?\C:\bar\baz`}, {`C:long\..\bar\baz`, `\\?\C:\cwd\bar\baz`}, {`C:\long\foo\\bar\.\baz\\`, `\\?\C:\long\foo\bar\baz\`}, {`C:\long\..`, `\\?\C:\`}, {`C:\.\long\..\.`, `\\?\C:\`}, {`\\?\C:\long\foo.txt`, `\\?\C:\long\foo.txt`}, {`\\?\C:\long/foo.txt`, `\\?\C:\long/foo.txt`}, } { in := strings.ReplaceAll(test.in, "long", veryLong) in = strings.ToLower(in) in = strings.ReplaceAll(in, "c:", drive) want := strings.ReplaceAll(test.want, "long", veryLong) want = strings.ToLower(want) want = strings.ReplaceAll(want, "c:", drive) want = strings.ReplaceAll(want, "cwd", cwd) got := os.AddExtendedPrefix(in) got = strings.ToLower(got) if got != want { in = strings.ReplaceAll(in, veryLong, "long") got = strings.ReplaceAll(got, veryLong, "long") want = strings.ReplaceAll(want, veryLong, "long") t.Errorf("addExtendedPrefix(%#q) = %#q; want %#q", in, got, want) } } } func TestMkdirAllLongPath(t *testing.T) { t.Parallel() tmpDir := t.TempDir() path := tmpDir for i := 0; i < 100; i++ { path += `\another-path-component` } if err := os.MkdirAll(path, 0777); err != nil { t.Fatalf("MkdirAll(%q) failed; %v", path, err) } if err := os.RemoveAll(tmpDir); err != nil { t.Fatalf("RemoveAll(%q) failed; %v", tmpDir, err) } } func TestMkdirAllExtendedLength(t *testing.T) { t.Parallel() tmpDir := t.TempDir() const prefix = `\\?\` if len(tmpDir) < 4 || tmpDir[:4] != prefix { fullPath, err := syscall.FullPath(tmpDir) if err != nil { t.Fatalf("FullPath(%q) fails: %v", tmpDir, err) } tmpDir = prefix + fullPath } path := tmpDir + `\dir\` if err := os.MkdirAll(path, 0777); err != nil { t.Fatalf("MkdirAll(%q) failed: %v", path, err) } path = path + `.\dir2` if err := os.MkdirAll(path, 0777); err == nil { t.Fatalf("MkdirAll(%q) should have failed, but did not", path) } } func TestOpenRootSlash(t *testing.T) { t.Parallel() tests := []string{ `/`, `\`, } for _, test := range tests { dir, err := os.Open(test) if err != nil { t.Fatalf("Open(%q) failed: %v", test, err) } dir.Close() } } func testMkdirAllAtRoot(t *testing.T, root string) { // Create a unique-enough directory name in root. base := fmt.Sprintf("%s-%d", t.Name(), os.Getpid()) path := filepath.Join(root, base) if err := os.MkdirAll(path, 0777); err != nil { t.Fatalf("MkdirAll(%q) failed: %v", path, err) } // Clean up if err := os.RemoveAll(path); err != nil { t.Fatal(err) } } func TestMkdirAllExtendedLengthAtRoot(t *testing.T) { if testenv.Builder() == "" { t.Skipf("skipping non-hermetic test outside of Go builders") } const prefix = `\\?\` vol := filepath.VolumeName(t.TempDir()) + `\` if len(vol) < 4 || vol[:4] != prefix { vol = prefix + vol } testMkdirAllAtRoot(t, vol) } func TestMkdirAllVolumeNameAtRoot(t *testing.T) { if testenv.Builder() == "" { t.Skipf("skipping non-hermetic test outside of Go builders") } vol, err := syscall.UTF16PtrFromString(filepath.VolumeName(t.TempDir()) + `\`) if err != nil { t.Fatal(err) } const maxVolNameLen = 50 var buf [maxVolNameLen]uint16 err = windows.GetVolumeNameForVolumeMountPoint(vol, &buf[0], maxVolNameLen) if err != nil { t.Fatal(err) } volName := syscall.UTF16ToString(buf[:]) testMkdirAllAtRoot(t, volName) } func TestRemoveAllLongPathRelative(t *testing.T) { // Test that RemoveAll doesn't hang with long relative paths. // See go.dev/issue/36375. tmp := t.TempDir() chdir(t, tmp) dir := filepath.Join(tmp, "foo", "bar", strings.Repeat("a", 150), strings.Repeat("b", 150)) err := os.MkdirAll(dir, 0755) if err != nil { t.Fatal(err) } err = os.RemoveAll("foo") if err != nil { t.Fatal(err) } } func testLongPathAbs(t *testing.T, target string) { t.Helper() testWalkFn := func(path string, info os.FileInfo, err error) error { if err != nil { t.Error(err) } return err } if err := os.MkdirAll(target, 0777); err != nil { t.Fatal(err) } // Test that Walk doesn't fail with long paths. // See go.dev/issue/21782. filepath.Walk(target, testWalkFn) // Test that RemoveAll doesn't hang with long paths. // See go.dev/issue/36375. if err := os.RemoveAll(target); err != nil { t.Error(err) } } func TestLongPathAbs(t *testing.T) { t.Parallel() target := t.TempDir() + "\\" + strings.Repeat("a\\", 300) testLongPathAbs(t, target) } func TestLongPathRel(t *testing.T) { chdir(t, t.TempDir()) target := strings.Repeat("b\\", 300) testLongPathAbs(t, target) } func BenchmarkAddExtendedPrefix(b *testing.B) { veryLong := `C:\l` + strings.Repeat("o", 248) + "ng" b.ReportAllocs() for i := 0; i < b.N; i++ { os.AddExtendedPrefix(veryLong) } }