blob: b37cae52b366ce91421ee5e7e5c209169c33bd26 [file] [log] [blame]
// 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 }{
// Testcases use word subsitutions:
// * "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)
}
}