1// Copyright 2016 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 os_test
6
7import (
8	"fmt"
9	"internal/syscall/windows"
10	"internal/testenv"
11	"os"
12	"path/filepath"
13	"strings"
14	"syscall"
15	"testing"
16)
17
18func TestAddExtendedPrefix(t *testing.T) {
19	// Test addExtendedPrefix instead of fixLongPath so the path manipulation code
20	// is exercised even if long path are supported by the system, else the
21	// function might not be tested at all if/when all test builders support long paths.
22	cwd, err := os.Getwd()
23	if err != nil {
24		t.Fatal("cannot get cwd")
25	}
26	drive := strings.ToLower(filepath.VolumeName(cwd))
27	cwd = strings.ToLower(cwd[len(drive)+1:])
28	// Build a very long pathname. Paths in Go are supposed to be arbitrarily long,
29	// so let's make a long path which is comfortably bigger than MAX_PATH on Windows
30	// (256) and thus requires fixLongPath to be correctly interpreted in I/O syscalls.
31	veryLong := "l" + strings.Repeat("o", 500) + "ng"
32	for _, test := range []struct{ in, want string }{
33		// Test cases use word substitutions:
34		//   * "long" is replaced with a very long pathname
35		//   * "c:" or "C:" are replaced with the drive of the current directory (preserving case)
36		//   * "cwd" is replaced with the current directory
37
38		// Drive Absolute
39		{`C:\long\foo.txt`, `\\?\C:\long\foo.txt`},
40		{`C:/long/foo.txt`, `\\?\C:\long\foo.txt`},
41		{`C:\\\long///foo.txt`, `\\?\C:\long\foo.txt`},
42		{`C:\long\.\foo.txt`, `\\?\C:\long\foo.txt`},
43		{`C:\long\..\foo.txt`, `\\?\C:\foo.txt`},
44		{`C:\long\..\..\foo.txt`, `\\?\C:\foo.txt`},
45
46		// Drive Relative
47		{`C:long\foo.txt`, `\\?\C:\cwd\long\foo.txt`},
48		{`C:long/foo.txt`, `\\?\C:\cwd\long\foo.txt`},
49		{`C:long///foo.txt`, `\\?\C:\cwd\long\foo.txt`},
50		{`C:long\.\foo.txt`, `\\?\C:\cwd\long\foo.txt`},
51		{`C:long\..\foo.txt`, `\\?\C:\cwd\foo.txt`},
52
53		// Rooted
54		{`\long\foo.txt`, `\\?\C:\long\foo.txt`},
55		{`/long/foo.txt`, `\\?\C:\long\foo.txt`},
56		{`\long///foo.txt`, `\\?\C:\long\foo.txt`},
57		{`\long\.\foo.txt`, `\\?\C:\long\foo.txt`},
58		{`\long\..\foo.txt`, `\\?\C:\foo.txt`},
59
60		// Relative
61		{`long\foo.txt`, `\\?\C:\cwd\long\foo.txt`},
62		{`long/foo.txt`, `\\?\C:\cwd\long\foo.txt`},
63		{`long///foo.txt`, `\\?\C:\cwd\long\foo.txt`},
64		{`long\.\foo.txt`, `\\?\C:\cwd\long\foo.txt`},
65		{`long\..\foo.txt`, `\\?\C:\cwd\foo.txt`},
66		{`.\long\foo.txt`, `\\?\C:\cwd\long\foo.txt`},
67
68		// UNC Absolute
69		{`\\srv\share\long`, `\\?\UNC\srv\share\long`},
70		{`//srv/share/long`, `\\?\UNC\srv\share\long`},
71		{`/\srv/share/long`, `\\?\UNC\srv\share\long`},
72		{`\\srv\share\long\`, `\\?\UNC\srv\share\long\`},
73		{`\\srv\share\bar\.\long`, `\\?\UNC\srv\share\bar\long`},
74		{`\\srv\share\bar\..\long`, `\\?\UNC\srv\share\long`},
75		{`\\srv\share\bar\..\..\long`, `\\?\UNC\srv\share\long`}, // share name is not removed by ".."
76
77		// Local Device
78		{`\\.\C:\long\foo.txt`, `\\.\C:\long\foo.txt`},
79		{`//./C:/long/foo.txt`, `\\.\C:\long\foo.txt`},
80		{`/\./C:/long/foo.txt`, `\\.\C:\long\foo.txt`},
81		{`\\.\C:\long///foo.txt`, `\\.\C:\long\foo.txt`},
82		{`\\.\C:\long\.\foo.txt`, `\\.\C:\long\foo.txt`},
83		{`\\.\C:\long\..\foo.txt`, `\\.\C:\foo.txt`},
84
85		// Misc tests
86		{`C:\short.txt`, `C:\short.txt`},
87		{`C:\`, `C:\`},
88		{`C:`, `C:`},
89		{`\\srv\path`, `\\srv\path`},
90		{`long.txt`, `\\?\C:\cwd\long.txt`},
91		{`C:long.txt`, `\\?\C:\cwd\long.txt`},
92		{`C:\long\.\bar\baz`, `\\?\C:\long\bar\baz`},
93		{`C:long\.\bar\baz`, `\\?\C:\cwd\long\bar\baz`},
94		{`C:\long\..\bar\baz`, `\\?\C:\bar\baz`},
95		{`C:long\..\bar\baz`, `\\?\C:\cwd\bar\baz`},
96		{`C:\long\foo\\bar\.\baz\\`, `\\?\C:\long\foo\bar\baz\`},
97		{`C:\long\..`, `\\?\C:\`},
98		{`C:\.\long\..\.`, `\\?\C:\`},
99		{`\\?\C:\long\foo.txt`, `\\?\C:\long\foo.txt`},
100		{`\\?\C:\long/foo.txt`, `\\?\C:\long/foo.txt`},
101	} {
102		in := strings.ReplaceAll(test.in, "long", veryLong)
103		in = strings.ToLower(in)
104		in = strings.ReplaceAll(in, "c:", drive)
105
106		want := strings.ReplaceAll(test.want, "long", veryLong)
107		want = strings.ToLower(want)
108		want = strings.ReplaceAll(want, "c:", drive)
109		want = strings.ReplaceAll(want, "cwd", cwd)
110
111		got := os.AddExtendedPrefix(in)
112		got = strings.ToLower(got)
113		if got != want {
114			in = strings.ReplaceAll(in, veryLong, "long")
115			got = strings.ReplaceAll(got, veryLong, "long")
116			want = strings.ReplaceAll(want, veryLong, "long")
117			t.Errorf("addExtendedPrefix(%#q) = %#q; want %#q", in, got, want)
118		}
119	}
120}
121
122func TestMkdirAllLongPath(t *testing.T) {
123	t.Parallel()
124
125	tmpDir := t.TempDir()
126	path := tmpDir
127	for i := 0; i < 100; i++ {
128		path += `\another-path-component`
129	}
130	if err := os.MkdirAll(path, 0777); err != nil {
131		t.Fatalf("MkdirAll(%q) failed; %v", path, err)
132	}
133	if err := os.RemoveAll(tmpDir); err != nil {
134		t.Fatalf("RemoveAll(%q) failed; %v", tmpDir, err)
135	}
136}
137
138func TestMkdirAllExtendedLength(t *testing.T) {
139	t.Parallel()
140	tmpDir := t.TempDir()
141
142	const prefix = `\\?\`
143	if len(tmpDir) < 4 || tmpDir[:4] != prefix {
144		fullPath, err := syscall.FullPath(tmpDir)
145		if err != nil {
146			t.Fatalf("FullPath(%q) fails: %v", tmpDir, err)
147		}
148		tmpDir = prefix + fullPath
149	}
150	path := tmpDir + `\dir\`
151	if err := os.MkdirAll(path, 0777); err != nil {
152		t.Fatalf("MkdirAll(%q) failed: %v", path, err)
153	}
154
155	path = path + `.\dir2`
156	if err := os.MkdirAll(path, 0777); err == nil {
157		t.Fatalf("MkdirAll(%q) should have failed, but did not", path)
158	}
159}
160
161func TestOpenRootSlash(t *testing.T) {
162	t.Parallel()
163
164	tests := []string{
165		`/`,
166		`\`,
167	}
168
169	for _, test := range tests {
170		dir, err := os.Open(test)
171		if err != nil {
172			t.Fatalf("Open(%q) failed: %v", test, err)
173		}
174		dir.Close()
175	}
176}
177
178func testMkdirAllAtRoot(t *testing.T, root string) {
179	// Create a unique-enough directory name in root.
180	base := fmt.Sprintf("%s-%d", t.Name(), os.Getpid())
181	path := filepath.Join(root, base)
182	if err := os.MkdirAll(path, 0777); err != nil {
183		t.Fatalf("MkdirAll(%q) failed: %v", path, err)
184	}
185	// Clean up
186	if err := os.RemoveAll(path); err != nil {
187		t.Fatal(err)
188	}
189}
190
191func TestMkdirAllExtendedLengthAtRoot(t *testing.T) {
192	if testenv.Builder() == "" {
193		t.Skipf("skipping non-hermetic test outside of Go builders")
194	}
195
196	const prefix = `\\?\`
197	vol := filepath.VolumeName(t.TempDir()) + `\`
198	if len(vol) < 4 || vol[:4] != prefix {
199		vol = prefix + vol
200	}
201	testMkdirAllAtRoot(t, vol)
202}
203
204func TestMkdirAllVolumeNameAtRoot(t *testing.T) {
205	if testenv.Builder() == "" {
206		t.Skipf("skipping non-hermetic test outside of Go builders")
207	}
208
209	vol, err := syscall.UTF16PtrFromString(filepath.VolumeName(t.TempDir()) + `\`)
210	if err != nil {
211		t.Fatal(err)
212	}
213	const maxVolNameLen = 50
214	var buf [maxVolNameLen]uint16
215	err = windows.GetVolumeNameForVolumeMountPoint(vol, &buf[0], maxVolNameLen)
216	if err != nil {
217		t.Fatal(err)
218	}
219	volName := syscall.UTF16ToString(buf[:])
220	testMkdirAllAtRoot(t, volName)
221}
222
223func TestRemoveAllLongPathRelative(t *testing.T) {
224	// Test that RemoveAll doesn't hang with long relative paths.
225	// See go.dev/issue/36375.
226	tmp := t.TempDir()
227	chdir(t, tmp)
228	dir := filepath.Join(tmp, "foo", "bar", strings.Repeat("a", 150), strings.Repeat("b", 150))
229	err := os.MkdirAll(dir, 0755)
230	if err != nil {
231		t.Fatal(err)
232	}
233	err = os.RemoveAll("foo")
234	if err != nil {
235		t.Fatal(err)
236	}
237}
238
239func testLongPathAbs(t *testing.T, target string) {
240	t.Helper()
241	testWalkFn := func(path string, info os.FileInfo, err error) error {
242		if err != nil {
243			t.Error(err)
244		}
245		return err
246	}
247	if err := os.MkdirAll(target, 0777); err != nil {
248		t.Fatal(err)
249	}
250	// Test that Walk doesn't fail with long paths.
251	// See go.dev/issue/21782.
252	filepath.Walk(target, testWalkFn)
253	// Test that RemoveAll doesn't hang with long paths.
254	// See go.dev/issue/36375.
255	if err := os.RemoveAll(target); err != nil {
256		t.Error(err)
257	}
258}
259
260func TestLongPathAbs(t *testing.T) {
261	t.Parallel()
262
263	target := t.TempDir() + "\\" + strings.Repeat("a\\", 300)
264	testLongPathAbs(t, target)
265}
266
267func TestLongPathRel(t *testing.T) {
268	chdir(t, t.TempDir())
269
270	target := strings.Repeat("b\\", 300)
271	testLongPathAbs(t, target)
272}
273
274func BenchmarkAddExtendedPrefix(b *testing.B) {
275	veryLong := `C:\l` + strings.Repeat("o", 248) + "ng"
276	b.ReportAllocs()
277	for i := 0; i < b.N; i++ {
278		os.AddExtendedPrefix(veryLong)
279	}
280}
281