1// Copyright 2014 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 webdav
6
7import (
8	"context"
9	"encoding/xml"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"os"
14	"path"
15	"path/filepath"
16	"reflect"
17	"runtime"
18	"sort"
19	"strconv"
20	"strings"
21	"testing"
22)
23
24func TestSlashClean(t *testing.T) {
25	testCases := []string{
26		"",
27		".",
28		"/",
29		"/./",
30		"//",
31		"//.",
32		"//a",
33		"/a",
34		"/a/b/c",
35		"/a//b/./../c/d/",
36		"a",
37		"a/b/c",
38	}
39	for _, tc := range testCases {
40		got := slashClean(tc)
41		want := path.Clean("/" + tc)
42		if got != want {
43			t.Errorf("tc=%q: got %q, want %q", tc, got, want)
44		}
45	}
46}
47
48func TestDirResolve(t *testing.T) {
49	testCases := []struct {
50		dir, name, want string
51	}{
52		{"/", "", "/"},
53		{"/", "/", "/"},
54		{"/", ".", "/"},
55		{"/", "./a", "/a"},
56		{"/", "..", "/"},
57		{"/", "..", "/"},
58		{"/", "../", "/"},
59		{"/", "../.", "/"},
60		{"/", "../a", "/a"},
61		{"/", "../..", "/"},
62		{"/", "../bar/a", "/bar/a"},
63		{"/", "../baz/a", "/baz/a"},
64		{"/", "...", "/..."},
65		{"/", ".../a", "/.../a"},
66		{"/", ".../..", "/"},
67		{"/", "a", "/a"},
68		{"/", "a/./b", "/a/b"},
69		{"/", "a/../../b", "/b"},
70		{"/", "a/../b", "/b"},
71		{"/", "a/b", "/a/b"},
72		{"/", "a/b/c/../../d", "/a/d"},
73		{"/", "a/b/c/../../../d", "/d"},
74		{"/", "a/b/c/../../../../d", "/d"},
75		{"/", "a/b/c/d", "/a/b/c/d"},
76
77		{"/foo/bar", "", "/foo/bar"},
78		{"/foo/bar", "/", "/foo/bar"},
79		{"/foo/bar", ".", "/foo/bar"},
80		{"/foo/bar", "./a", "/foo/bar/a"},
81		{"/foo/bar", "..", "/foo/bar"},
82		{"/foo/bar", "../", "/foo/bar"},
83		{"/foo/bar", "../.", "/foo/bar"},
84		{"/foo/bar", "../a", "/foo/bar/a"},
85		{"/foo/bar", "../..", "/foo/bar"},
86		{"/foo/bar", "../bar/a", "/foo/bar/bar/a"},
87		{"/foo/bar", "../baz/a", "/foo/bar/baz/a"},
88		{"/foo/bar", "...", "/foo/bar/..."},
89		{"/foo/bar", ".../a", "/foo/bar/.../a"},
90		{"/foo/bar", ".../..", "/foo/bar"},
91		{"/foo/bar", "a", "/foo/bar/a"},
92		{"/foo/bar", "a/./b", "/foo/bar/a/b"},
93		{"/foo/bar", "a/../../b", "/foo/bar/b"},
94		{"/foo/bar", "a/../b", "/foo/bar/b"},
95		{"/foo/bar", "a/b", "/foo/bar/a/b"},
96		{"/foo/bar", "a/b/c/../../d", "/foo/bar/a/d"},
97		{"/foo/bar", "a/b/c/../../../d", "/foo/bar/d"},
98		{"/foo/bar", "a/b/c/../../../../d", "/foo/bar/d"},
99		{"/foo/bar", "a/b/c/d", "/foo/bar/a/b/c/d"},
100
101		{"/foo/bar/", "", "/foo/bar"},
102		{"/foo/bar/", "/", "/foo/bar"},
103		{"/foo/bar/", ".", "/foo/bar"},
104		{"/foo/bar/", "./a", "/foo/bar/a"},
105		{"/foo/bar/", "..", "/foo/bar"},
106
107		{"/foo//bar///", "", "/foo/bar"},
108		{"/foo//bar///", "/", "/foo/bar"},
109		{"/foo//bar///", ".", "/foo/bar"},
110		{"/foo//bar///", "./a", "/foo/bar/a"},
111		{"/foo//bar///", "..", "/foo/bar"},
112
113		{"/x/y/z", "ab/c\x00d/ef", ""},
114
115		{".", "", "."},
116		{".", "/", "."},
117		{".", ".", "."},
118		{".", "./a", "a"},
119		{".", "..", "."},
120		{".", "..", "."},
121		{".", "../", "."},
122		{".", "../.", "."},
123		{".", "../a", "a"},
124		{".", "../..", "."},
125		{".", "../bar/a", "bar/a"},
126		{".", "../baz/a", "baz/a"},
127		{".", "...", "..."},
128		{".", ".../a", ".../a"},
129		{".", ".../..", "."},
130		{".", "a", "a"},
131		{".", "a/./b", "a/b"},
132		{".", "a/../../b", "b"},
133		{".", "a/../b", "b"},
134		{".", "a/b", "a/b"},
135		{".", "a/b/c/../../d", "a/d"},
136		{".", "a/b/c/../../../d", "d"},
137		{".", "a/b/c/../../../../d", "d"},
138		{".", "a/b/c/d", "a/b/c/d"},
139
140		{"", "", "."},
141		{"", "/", "."},
142		{"", ".", "."},
143		{"", "./a", "a"},
144		{"", "..", "."},
145	}
146
147	for _, tc := range testCases {
148		d := Dir(filepath.FromSlash(tc.dir))
149		if got := filepath.ToSlash(d.resolve(tc.name)); got != tc.want {
150			t.Errorf("dir=%q, name=%q: got %q, want %q", tc.dir, tc.name, got, tc.want)
151		}
152	}
153}
154
155func TestWalk(t *testing.T) {
156	type walkStep struct {
157		name, frag string
158		final      bool
159	}
160
161	testCases := []struct {
162		dir  string
163		want []walkStep
164	}{
165		{"", []walkStep{
166			{"", "", true},
167		}},
168		{"/", []walkStep{
169			{"", "", true},
170		}},
171		{"/a", []walkStep{
172			{"", "a", true},
173		}},
174		{"/a/", []walkStep{
175			{"", "a", true},
176		}},
177		{"/a/b", []walkStep{
178			{"", "a", false},
179			{"a", "b", true},
180		}},
181		{"/a/b/", []walkStep{
182			{"", "a", false},
183			{"a", "b", true},
184		}},
185		{"/a/b/c", []walkStep{
186			{"", "a", false},
187			{"a", "b", false},
188			{"b", "c", true},
189		}},
190		// The following test case is the one mentioned explicitly
191		// in the method description.
192		{"/foo/bar/x", []walkStep{
193			{"", "foo", false},
194			{"foo", "bar", false},
195			{"bar", "x", true},
196		}},
197	}
198
199	ctx := context.Background()
200
201	for _, tc := range testCases {
202		fs := NewMemFS().(*memFS)
203
204		parts := strings.Split(tc.dir, "/")
205		for p := 2; p < len(parts); p++ {
206			d := strings.Join(parts[:p], "/")
207			if err := fs.Mkdir(ctx, d, 0666); err != nil {
208				t.Errorf("tc.dir=%q: mkdir: %q: %v", tc.dir, d, err)
209			}
210		}
211
212		i, prevFrag := 0, ""
213		err := fs.walk("test", tc.dir, func(dir *memFSNode, frag string, final bool) error {
214			got := walkStep{
215				name:  prevFrag,
216				frag:  frag,
217				final: final,
218			}
219			want := tc.want[i]
220
221			if got != want {
222				return fmt.Errorf("got %+v, want %+v", got, want)
223			}
224			i, prevFrag = i+1, frag
225			return nil
226		})
227		if err != nil {
228			t.Errorf("tc.dir=%q: %v", tc.dir, err)
229		}
230	}
231}
232
233// find appends to ss the names of the named file and its children. It is
234// analogous to the Unix find command.
235//
236// The returned strings are not guaranteed to be in any particular order.
237func find(ctx context.Context, ss []string, fs FileSystem, name string) ([]string, error) {
238	stat, err := fs.Stat(ctx, name)
239	if err != nil {
240		return nil, err
241	}
242	ss = append(ss, name)
243	if stat.IsDir() {
244		f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
245		if err != nil {
246			return nil, err
247		}
248		defer f.Close()
249		children, err := f.Readdir(-1)
250		if err != nil {
251			return nil, err
252		}
253		for _, c := range children {
254			ss, err = find(ctx, ss, fs, path.Join(name, c.Name()))
255			if err != nil {
256				return nil, err
257			}
258		}
259	}
260	return ss, nil
261}
262
263func testFS(t *testing.T, fs FileSystem) {
264	errStr := func(err error) string {
265		switch {
266		case os.IsExist(err):
267			return "errExist"
268		case os.IsNotExist(err):
269			return "errNotExist"
270		case err != nil:
271			return "err"
272		}
273		return "ok"
274	}
275
276	// The non-"find" non-"stat" test cases should change the file system state. The
277	// indentation of the "find"s and "stat"s helps distinguish such test cases.
278	testCases := []string{
279		"  stat / want dir",
280		"  stat /a want errNotExist",
281		"  stat /d want errNotExist",
282		"  stat /d/e want errNotExist",
283		"create /a A want ok",
284		"  stat /a want 1",
285		"create /d/e EEE want errNotExist",
286		"mk-dir /a want errExist",
287		"mk-dir /d/m want errNotExist",
288		"mk-dir /d want ok",
289		"  stat /d want dir",
290		"create /d/e EEE want ok",
291		"  stat /d/e want 3",
292		"  find / /a /d /d/e",
293		"create /d/f FFFF want ok",
294		"create /d/g GGGGGGG want ok",
295		"mk-dir /d/m want ok",
296		"mk-dir /d/m want errExist",
297		"create /d/m/p PPPPP want ok",
298		"  stat /d/e want 3",
299		"  stat /d/f want 4",
300		"  stat /d/g want 7",
301		"  stat /d/h want errNotExist",
302		"  stat /d/m want dir",
303		"  stat /d/m/p want 5",
304		"  find / /a /d /d/e /d/f /d/g /d/m /d/m/p",
305		"rm-all /d want ok",
306		"  stat /a want 1",
307		"  stat /d want errNotExist",
308		"  stat /d/e want errNotExist",
309		"  stat /d/f want errNotExist",
310		"  stat /d/g want errNotExist",
311		"  stat /d/m want errNotExist",
312		"  stat /d/m/p want errNotExist",
313		"  find / /a",
314		"mk-dir /d/m want errNotExist",
315		"mk-dir /d want ok",
316		"create /d/f FFFF want ok",
317		"rm-all /d/f want ok",
318		"mk-dir /d/m want ok",
319		"rm-all /z want ok",
320		"rm-all / want err",
321		"create /b BB want ok",
322		"  stat / want dir",
323		"  stat /a want 1",
324		"  stat /b want 2",
325		"  stat /c want errNotExist",
326		"  stat /d want dir",
327		"  stat /d/m want dir",
328		"  find / /a /b /d /d/m",
329		"move__ o=F /b /c want ok",
330		"  stat /b want errNotExist",
331		"  stat /c want 2",
332		"  stat /d/m want dir",
333		"  stat /d/n want errNotExist",
334		"  find / /a /c /d /d/m",
335		"move__ o=F /d/m /d/n want ok",
336		"create /d/n/q QQQQ want ok",
337		"  stat /d/m want errNotExist",
338		"  stat /d/n want dir",
339		"  stat /d/n/q want 4",
340		"move__ o=F /d /d/n/z want err",
341		"move__ o=T /c /d/n/q want ok",
342		"  stat /c want errNotExist",
343		"  stat /d/n/q want 2",
344		"  find / /a /d /d/n /d/n/q",
345		"create /d/n/r RRRRR want ok",
346		"mk-dir /u want ok",
347		"mk-dir /u/v want ok",
348		"move__ o=F /d/n /u want errExist",
349		"create /t TTTTTT want ok",
350		"move__ o=F /d/n /t want errExist",
351		"rm-all /t want ok",
352		"move__ o=F /d/n /t want ok",
353		"  stat /d want dir",
354		"  stat /d/n want errNotExist",
355		"  stat /d/n/r want errNotExist",
356		"  stat /t want dir",
357		"  stat /t/q want 2",
358		"  stat /t/r want 5",
359		"  find / /a /d /t /t/q /t/r /u /u/v",
360		"move__ o=F /t / want errExist",
361		"move__ o=T /t /u/v want ok",
362		"  stat /u/v/r want 5",
363		"move__ o=F / /z want err",
364		"  find / /a /d /u /u/v /u/v/q /u/v/r",
365		"  stat /a want 1",
366		"  stat /b want errNotExist",
367		"  stat /c want errNotExist",
368		"  stat /u/v/r want 5",
369		"copy__ o=F d=0 /a /b want ok",
370		"copy__ o=T d=0 /a /c want ok",
371		"  stat /a want 1",
372		"  stat /b want 1",
373		"  stat /c want 1",
374		"  stat /u/v/r want 5",
375		"copy__ o=F d=0 /u/v/r /b want errExist",
376		"  stat /b want 1",
377		"copy__ o=T d=0 /u/v/r /b want ok",
378		"  stat /a want 1",
379		"  stat /b want 5",
380		"  stat /u/v/r want 5",
381		"rm-all /a want ok",
382		"rm-all /b want ok",
383		"mk-dir /u/v/w want ok",
384		"create /u/v/w/s SSSSSSSS want ok",
385		"  stat /d want dir",
386		"  stat /d/x want errNotExist",
387		"  stat /d/y want errNotExist",
388		"  stat /u/v/r want 5",
389		"  stat /u/v/w/s want 8",
390		"  find / /c /d /u /u/v /u/v/q /u/v/r /u/v/w /u/v/w/s",
391		"copy__ o=T d=0 /u/v /d/x want ok",
392		"copy__ o=T d=∞ /u/v /d/y want ok",
393		"rm-all /u want ok",
394		"  stat /d/x want dir",
395		"  stat /d/x/q want errNotExist",
396		"  stat /d/x/r want errNotExist",
397		"  stat /d/x/w want errNotExist",
398		"  stat /d/x/w/s want errNotExist",
399		"  stat /d/y want dir",
400		"  stat /d/y/q want 2",
401		"  stat /d/y/r want 5",
402		"  stat /d/y/w want dir",
403		"  stat /d/y/w/s want 8",
404		"  stat /u want errNotExist",
405		"  find / /c /d /d/x /d/y /d/y/q /d/y/r /d/y/w /d/y/w/s",
406		"copy__ o=F d=∞ /d/y /d/x want errExist",
407	}
408
409	ctx := context.Background()
410
411	for i, tc := range testCases {
412		tc = strings.TrimSpace(tc)
413		j := strings.IndexByte(tc, ' ')
414		if j < 0 {
415			t.Fatalf("test case #%d %q: invalid command", i, tc)
416		}
417		op, arg := tc[:j], tc[j+1:]
418
419		switch op {
420		default:
421			t.Fatalf("test case #%d %q: invalid operation %q", i, tc, op)
422
423		case "create":
424			parts := strings.Split(arg, " ")
425			if len(parts) != 4 || parts[2] != "want" {
426				t.Fatalf("test case #%d %q: invalid write", i, tc)
427			}
428			f, opErr := fs.OpenFile(ctx, parts[0], os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
429			if got := errStr(opErr); got != parts[3] {
430				t.Fatalf("test case #%d %q: OpenFile: got %q (%v), want %q", i, tc, got, opErr, parts[3])
431			}
432			if f != nil {
433				if _, err := f.Write([]byte(parts[1])); err != nil {
434					t.Fatalf("test case #%d %q: Write: %v", i, tc, err)
435				}
436				if err := f.Close(); err != nil {
437					t.Fatalf("test case #%d %q: Close: %v", i, tc, err)
438				}
439			}
440
441		case "find":
442			got, err := find(ctx, nil, fs, "/")
443			if err != nil {
444				t.Fatalf("test case #%d %q: find: %v", i, tc, err)
445			}
446			sort.Strings(got)
447			want := strings.Split(arg, " ")
448			if !reflect.DeepEqual(got, want) {
449				t.Fatalf("test case #%d %q:\ngot  %s\nwant %s", i, tc, got, want)
450			}
451
452		case "copy__", "mk-dir", "move__", "rm-all", "stat":
453			nParts := 3
454			switch op {
455			case "copy__":
456				nParts = 6
457			case "move__":
458				nParts = 5
459			}
460			parts := strings.Split(arg, " ")
461			if len(parts) != nParts {
462				t.Fatalf("test case #%d %q: invalid %s", i, tc, op)
463			}
464
465			got, opErr := "", error(nil)
466			switch op {
467			case "copy__":
468				depth := 0
469				if parts[1] == "d=∞" {
470					depth = infiniteDepth
471				}
472				_, opErr = copyFiles(ctx, fs, parts[2], parts[3], parts[0] == "o=T", depth, 0)
473			case "mk-dir":
474				opErr = fs.Mkdir(ctx, parts[0], 0777)
475			case "move__":
476				_, opErr = moveFiles(ctx, fs, parts[1], parts[2], parts[0] == "o=T")
477			case "rm-all":
478				opErr = fs.RemoveAll(ctx, parts[0])
479			case "stat":
480				var stat os.FileInfo
481				fileName := parts[0]
482				if stat, opErr = fs.Stat(ctx, fileName); opErr == nil {
483					if stat.IsDir() {
484						got = "dir"
485					} else {
486						got = strconv.Itoa(int(stat.Size()))
487					}
488
489					if fileName == "/" {
490						// For a Dir FileSystem, the virtual file system root maps to a
491						// real file system name like "/tmp/webdav-test012345", which does
492						// not end with "/". We skip such cases.
493					} else if statName := stat.Name(); path.Base(fileName) != statName {
494						t.Fatalf("test case #%d %q: file name %q inconsistent with stat name %q",
495							i, tc, fileName, statName)
496					}
497				}
498			}
499			if got == "" {
500				got = errStr(opErr)
501			}
502
503			if parts[len(parts)-2] != "want" {
504				t.Fatalf("test case #%d %q: invalid %s", i, tc, op)
505			}
506			if want := parts[len(parts)-1]; got != want {
507				t.Fatalf("test case #%d %q: got %q (%v), want %q", i, tc, got, opErr, want)
508			}
509		}
510	}
511}
512
513func TestDir(t *testing.T) {
514	switch runtime.GOOS {
515	case "nacl":
516		t.Skip("see golang.org/issue/12004")
517	case "plan9":
518		t.Skip("see golang.org/issue/11453")
519	}
520
521	td, err := ioutil.TempDir("", "webdav-test")
522	if err != nil {
523		t.Fatal(err)
524	}
525	defer os.RemoveAll(td)
526	testFS(t, Dir(td))
527}
528
529func TestMemFS(t *testing.T) {
530	testFS(t, NewMemFS())
531}
532
533func TestMemFSRoot(t *testing.T) {
534	ctx := context.Background()
535	fs := NewMemFS()
536	for i := 0; i < 5; i++ {
537		stat, err := fs.Stat(ctx, "/")
538		if err != nil {
539			t.Fatalf("i=%d: Stat: %v", i, err)
540		}
541		if !stat.IsDir() {
542			t.Fatalf("i=%d: Stat.IsDir is false, want true", i)
543		}
544
545		f, err := fs.OpenFile(ctx, "/", os.O_RDONLY, 0)
546		if err != nil {
547			t.Fatalf("i=%d: OpenFile: %v", i, err)
548		}
549		defer f.Close()
550		children, err := f.Readdir(-1)
551		if err != nil {
552			t.Fatalf("i=%d: Readdir: %v", i, err)
553		}
554		if len(children) != i {
555			t.Fatalf("i=%d: got %d children, want %d", i, len(children), i)
556		}
557
558		if _, err := f.Write(make([]byte, 1)); err == nil {
559			t.Fatalf("i=%d: Write: got nil error, want non-nil", i)
560		}
561
562		if err := fs.Mkdir(ctx, fmt.Sprintf("/dir%d", i), 0777); err != nil {
563			t.Fatalf("i=%d: Mkdir: %v", i, err)
564		}
565	}
566}
567
568func TestMemFileReaddir(t *testing.T) {
569	ctx := context.Background()
570	fs := NewMemFS()
571	if err := fs.Mkdir(ctx, "/foo", 0777); err != nil {
572		t.Fatalf("Mkdir: %v", err)
573	}
574	readdir := func(count int) ([]os.FileInfo, error) {
575		f, err := fs.OpenFile(ctx, "/foo", os.O_RDONLY, 0)
576		if err != nil {
577			t.Fatalf("OpenFile: %v", err)
578		}
579		defer f.Close()
580		return f.Readdir(count)
581	}
582	if got, err := readdir(-1); len(got) != 0 || err != nil {
583		t.Fatalf("readdir(-1): got %d fileInfos with err=%v, want 0, <nil>", len(got), err)
584	}
585	if got, err := readdir(+1); len(got) != 0 || err != io.EOF {
586		t.Fatalf("readdir(+1): got %d fileInfos with err=%v, want 0, EOF", len(got), err)
587	}
588}
589
590func TestMemFile(t *testing.T) {
591	testCases := []string{
592		"wantData ",
593		"wantSize 0",
594		"write abc",
595		"wantData abc",
596		"write de",
597		"wantData abcde",
598		"wantSize 5",
599		"write 5*x",
600		"write 4*y+2*z",
601		"write 3*st",
602		"wantData abcdexxxxxyyyyzzststst",
603		"wantSize 22",
604		"seek set 4 want 4",
605		"write EFG",
606		"wantData abcdEFGxxxyyyyzzststst",
607		"wantSize 22",
608		"seek set 2 want 2",
609		"read cdEF",
610		"read Gx",
611		"seek cur 0 want 8",
612		"seek cur 2 want 10",
613		"seek cur -1 want 9",
614		"write J",
615		"wantData abcdEFGxxJyyyyzzststst",
616		"wantSize 22",
617		"seek cur -4 want 6",
618		"write ghijk",
619		"wantData abcdEFghijkyyyzzststst",
620		"wantSize 22",
621		"read yyyz",
622		"seek cur 0 want 15",
623		"write ",
624		"seek cur 0 want 15",
625		"read ",
626		"seek cur 0 want 15",
627		"seek end -3 want 19",
628		"write ZZ",
629		"wantData abcdEFghijkyyyzzstsZZt",
630		"wantSize 22",
631		"write 4*A",
632		"wantData abcdEFghijkyyyzzstsZZAAAA",
633		"wantSize 25",
634		"seek end 0 want 25",
635		"seek end -5 want 20",
636		"read Z+4*A",
637		"write 5*B",
638		"wantData abcdEFghijkyyyzzstsZZAAAABBBBB",
639		"wantSize 30",
640		"seek end 10 want 40",
641		"write C",
642		"wantData abcdEFghijkyyyzzstsZZAAAABBBBB..........C",
643		"wantSize 41",
644		"write D",
645		"wantData abcdEFghijkyyyzzstsZZAAAABBBBB..........CD",
646		"wantSize 42",
647		"seek set 43 want 43",
648		"write E",
649		"wantData abcdEFghijkyyyzzstsZZAAAABBBBB..........CD.E",
650		"wantSize 44",
651		"seek set 0 want 0",
652		"write 5*123456789_",
653		"wantData 123456789_123456789_123456789_123456789_123456789_",
654		"wantSize 50",
655		"seek cur 0 want 50",
656		"seek cur -99 want err",
657	}
658
659	ctx := context.Background()
660
661	const filename = "/foo"
662	fs := NewMemFS()
663	f, err := fs.OpenFile(ctx, filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
664	if err != nil {
665		t.Fatalf("OpenFile: %v", err)
666	}
667	defer f.Close()
668
669	for i, tc := range testCases {
670		j := strings.IndexByte(tc, ' ')
671		if j < 0 {
672			t.Fatalf("test case #%d %q: invalid command", i, tc)
673		}
674		op, arg := tc[:j], tc[j+1:]
675
676		// Expand an arg like "3*a+2*b" to "aaabb".
677		parts := strings.Split(arg, "+")
678		for j, part := range parts {
679			if k := strings.IndexByte(part, '*'); k >= 0 {
680				repeatCount, repeatStr := part[:k], part[k+1:]
681				n, err := strconv.Atoi(repeatCount)
682				if err != nil {
683					t.Fatalf("test case #%d %q: invalid repeat count %q", i, tc, repeatCount)
684				}
685				parts[j] = strings.Repeat(repeatStr, n)
686			}
687		}
688		arg = strings.Join(parts, "")
689
690		switch op {
691		default:
692			t.Fatalf("test case #%d %q: invalid operation %q", i, tc, op)
693
694		case "read":
695			buf := make([]byte, len(arg))
696			if _, err := io.ReadFull(f, buf); err != nil {
697				t.Fatalf("test case #%d %q: ReadFull: %v", i, tc, err)
698			}
699			if got := string(buf); got != arg {
700				t.Fatalf("test case #%d %q:\ngot  %q\nwant %q", i, tc, got, arg)
701			}
702
703		case "seek":
704			parts := strings.Split(arg, " ")
705			if len(parts) != 4 {
706				t.Fatalf("test case #%d %q: invalid seek", i, tc)
707			}
708
709			whence := 0
710			switch parts[0] {
711			default:
712				t.Fatalf("test case #%d %q: invalid seek whence", i, tc)
713			case "set":
714				whence = os.SEEK_SET
715			case "cur":
716				whence = os.SEEK_CUR
717			case "end":
718				whence = os.SEEK_END
719			}
720			offset, err := strconv.Atoi(parts[1])
721			if err != nil {
722				t.Fatalf("test case #%d %q: invalid offset %q", i, tc, parts[1])
723			}
724
725			if parts[2] != "want" {
726				t.Fatalf("test case #%d %q: invalid seek", i, tc)
727			}
728			if parts[3] == "err" {
729				_, err := f.Seek(int64(offset), whence)
730				if err == nil {
731					t.Fatalf("test case #%d %q: Seek returned nil error, want non-nil", i, tc)
732				}
733			} else {
734				got, err := f.Seek(int64(offset), whence)
735				if err != nil {
736					t.Fatalf("test case #%d %q: Seek: %v", i, tc, err)
737				}
738				want, err := strconv.Atoi(parts[3])
739				if err != nil {
740					t.Fatalf("test case #%d %q: invalid want %q", i, tc, parts[3])
741				}
742				if got != int64(want) {
743					t.Fatalf("test case #%d %q: got %d, want %d", i, tc, got, want)
744				}
745			}
746
747		case "write":
748			n, err := f.Write([]byte(arg))
749			if err != nil {
750				t.Fatalf("test case #%d %q: write: %v", i, tc, err)
751			}
752			if n != len(arg) {
753				t.Fatalf("test case #%d %q: write returned %d bytes, want %d", i, tc, n, len(arg))
754			}
755
756		case "wantData":
757			g, err := fs.OpenFile(ctx, filename, os.O_RDONLY, 0666)
758			if err != nil {
759				t.Fatalf("test case #%d %q: OpenFile: %v", i, tc, err)
760			}
761			gotBytes, err := ioutil.ReadAll(g)
762			if err != nil {
763				t.Fatalf("test case #%d %q: ReadAll: %v", i, tc, err)
764			}
765			for i, c := range gotBytes {
766				if c == '\x00' {
767					gotBytes[i] = '.'
768				}
769			}
770			got := string(gotBytes)
771			if got != arg {
772				t.Fatalf("test case #%d %q:\ngot  %q\nwant %q", i, tc, got, arg)
773			}
774			if err := g.Close(); err != nil {
775				t.Fatalf("test case #%d %q: Close: %v", i, tc, err)
776			}
777
778		case "wantSize":
779			n, err := strconv.Atoi(arg)
780			if err != nil {
781				t.Fatalf("test case #%d %q: invalid size %q", i, tc, arg)
782			}
783			fi, err := fs.Stat(ctx, filename)
784			if err != nil {
785				t.Fatalf("test case #%d %q: Stat: %v", i, tc, err)
786			}
787			if got, want := fi.Size(), int64(n); got != want {
788				t.Fatalf("test case #%d %q: got %d, want %d", i, tc, got, want)
789			}
790		}
791	}
792}
793
794// TestMemFileWriteAllocs tests that writing N consecutive 1KiB chunks to a
795// memFile doesn't allocate a new buffer for each of those N times. Otherwise,
796// calling io.Copy(aMemFile, src) is likely to have quadratic complexity.
797func TestMemFileWriteAllocs(t *testing.T) {
798	if runtime.Compiler == "gccgo" {
799		t.Skip("gccgo allocates here")
800	}
801	ctx := context.Background()
802	fs := NewMemFS()
803	f, err := fs.OpenFile(ctx, "/xxx", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
804	if err != nil {
805		t.Fatalf("OpenFile: %v", err)
806	}
807	defer f.Close()
808
809	xxx := make([]byte, 1024)
810	for i := range xxx {
811		xxx[i] = 'x'
812	}
813
814	a := testing.AllocsPerRun(100, func() {
815		f.Write(xxx)
816	})
817	// AllocsPerRun returns an integral value, so we compare the rounded-down
818	// number to zero.
819	if a > 0 {
820		t.Fatalf("%v allocs per run, want 0", a)
821	}
822}
823
824func BenchmarkMemFileWrite(b *testing.B) {
825	ctx := context.Background()
826	fs := NewMemFS()
827	xxx := make([]byte, 1024)
828	for i := range xxx {
829		xxx[i] = 'x'
830	}
831
832	b.ResetTimer()
833	for i := 0; i < b.N; i++ {
834		f, err := fs.OpenFile(ctx, "/xxx", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
835		if err != nil {
836			b.Fatalf("OpenFile: %v", err)
837		}
838		for j := 0; j < 100; j++ {
839			f.Write(xxx)
840		}
841		if err := f.Close(); err != nil {
842			b.Fatalf("Close: %v", err)
843		}
844		if err := fs.RemoveAll(ctx, "/xxx"); err != nil {
845			b.Fatalf("RemoveAll: %v", err)
846		}
847	}
848}
849
850func TestCopyMoveProps(t *testing.T) {
851	ctx := context.Background()
852	fs := NewMemFS()
853	create := func(name string) error {
854		f, err := fs.OpenFile(ctx, name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
855		if err != nil {
856			return err
857		}
858		_, wErr := f.Write([]byte("contents"))
859		cErr := f.Close()
860		if wErr != nil {
861			return wErr
862		}
863		return cErr
864	}
865	patch := func(name string, patches ...Proppatch) error {
866		f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0666)
867		if err != nil {
868			return err
869		}
870		_, pErr := f.(DeadPropsHolder).Patch(patches)
871		cErr := f.Close()
872		if pErr != nil {
873			return pErr
874		}
875		return cErr
876	}
877	props := func(name string) (map[xml.Name]Property, error) {
878		f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0666)
879		if err != nil {
880			return nil, err
881		}
882		m, pErr := f.(DeadPropsHolder).DeadProps()
883		cErr := f.Close()
884		if pErr != nil {
885			return nil, pErr
886		}
887		if cErr != nil {
888			return nil, cErr
889		}
890		return m, nil
891	}
892
893	p0 := Property{
894		XMLName:  xml.Name{Space: "x:", Local: "boat"},
895		InnerXML: []byte("pea-green"),
896	}
897	p1 := Property{
898		XMLName:  xml.Name{Space: "x:", Local: "ring"},
899		InnerXML: []byte("1 shilling"),
900	}
901	p2 := Property{
902		XMLName:  xml.Name{Space: "x:", Local: "spoon"},
903		InnerXML: []byte("runcible"),
904	}
905	p3 := Property{
906		XMLName:  xml.Name{Space: "x:", Local: "moon"},
907		InnerXML: []byte("light"),
908	}
909
910	if err := create("/src"); err != nil {
911		t.Fatalf("create /src: %v", err)
912	}
913	if err := patch("/src", Proppatch{Props: []Property{p0, p1}}); err != nil {
914		t.Fatalf("patch /src +p0 +p1: %v", err)
915	}
916	if _, err := copyFiles(ctx, fs, "/src", "/tmp", true, infiniteDepth, 0); err != nil {
917		t.Fatalf("copyFiles /src /tmp: %v", err)
918	}
919	if _, err := moveFiles(ctx, fs, "/tmp", "/dst", true); err != nil {
920		t.Fatalf("moveFiles /tmp /dst: %v", err)
921	}
922	if err := patch("/src", Proppatch{Props: []Property{p0}, Remove: true}); err != nil {
923		t.Fatalf("patch /src -p0: %v", err)
924	}
925	if err := patch("/src", Proppatch{Props: []Property{p2}}); err != nil {
926		t.Fatalf("patch /src +p2: %v", err)
927	}
928	if err := patch("/dst", Proppatch{Props: []Property{p1}, Remove: true}); err != nil {
929		t.Fatalf("patch /dst -p1: %v", err)
930	}
931	if err := patch("/dst", Proppatch{Props: []Property{p3}}); err != nil {
932		t.Fatalf("patch /dst +p3: %v", err)
933	}
934
935	gotSrc, err := props("/src")
936	if err != nil {
937		t.Fatalf("props /src: %v", err)
938	}
939	wantSrc := map[xml.Name]Property{
940		p1.XMLName: p1,
941		p2.XMLName: p2,
942	}
943	if !reflect.DeepEqual(gotSrc, wantSrc) {
944		t.Fatalf("props /src:\ngot  %v\nwant %v", gotSrc, wantSrc)
945	}
946
947	gotDst, err := props("/dst")
948	if err != nil {
949		t.Fatalf("props /dst: %v", err)
950	}
951	wantDst := map[xml.Name]Property{
952		p0.XMLName: p0,
953		p3.XMLName: p3,
954	}
955	if !reflect.DeepEqual(gotDst, wantDst) {
956		t.Fatalf("props /dst:\ngot  %v\nwant %v", gotDst, wantDst)
957	}
958}
959
960func TestWalkFS(t *testing.T) {
961	testCases := []struct {
962		desc    string
963		buildfs []string
964		startAt string
965		depth   int
966		walkFn  filepath.WalkFunc
967		want    []string
968	}{{
969		"just root",
970		[]string{},
971		"/",
972		infiniteDepth,
973		nil,
974		[]string{
975			"/",
976		},
977	}, {
978		"infinite walk from root",
979		[]string{
980			"mkdir /a",
981			"mkdir /a/b",
982			"touch /a/b/c",
983			"mkdir /a/d",
984			"mkdir /e",
985			"touch /f",
986		},
987		"/",
988		infiniteDepth,
989		nil,
990		[]string{
991			"/",
992			"/a",
993			"/a/b",
994			"/a/b/c",
995			"/a/d",
996			"/e",
997			"/f",
998		},
999	}, {
1000		"infinite walk from subdir",
1001		[]string{
1002			"mkdir /a",
1003			"mkdir /a/b",
1004			"touch /a/b/c",
1005			"mkdir /a/d",
1006			"mkdir /e",
1007			"touch /f",
1008		},
1009		"/a",
1010		infiniteDepth,
1011		nil,
1012		[]string{
1013			"/a",
1014			"/a/b",
1015			"/a/b/c",
1016			"/a/d",
1017		},
1018	}, {
1019		"depth 1 walk from root",
1020		[]string{
1021			"mkdir /a",
1022			"mkdir /a/b",
1023			"touch /a/b/c",
1024			"mkdir /a/d",
1025			"mkdir /e",
1026			"touch /f",
1027		},
1028		"/",
1029		1,
1030		nil,
1031		[]string{
1032			"/",
1033			"/a",
1034			"/e",
1035			"/f",
1036		},
1037	}, {
1038		"depth 1 walk from subdir",
1039		[]string{
1040			"mkdir /a",
1041			"mkdir /a/b",
1042			"touch /a/b/c",
1043			"mkdir /a/b/g",
1044			"mkdir /a/b/g/h",
1045			"touch /a/b/g/i",
1046			"touch /a/b/g/h/j",
1047		},
1048		"/a/b",
1049		1,
1050		nil,
1051		[]string{
1052			"/a/b",
1053			"/a/b/c",
1054			"/a/b/g",
1055		},
1056	}, {
1057		"depth 0 walk from subdir",
1058		[]string{
1059			"mkdir /a",
1060			"mkdir /a/b",
1061			"touch /a/b/c",
1062			"mkdir /a/b/g",
1063			"mkdir /a/b/g/h",
1064			"touch /a/b/g/i",
1065			"touch /a/b/g/h/j",
1066		},
1067		"/a/b",
1068		0,
1069		nil,
1070		[]string{
1071			"/a/b",
1072		},
1073	}, {
1074		"infinite walk from file",
1075		[]string{
1076			"mkdir /a",
1077			"touch /a/b",
1078			"touch /a/c",
1079		},
1080		"/a/b",
1081		0,
1082		nil,
1083		[]string{
1084			"/a/b",
1085		},
1086	}, {
1087		"infinite walk with skipped subdir",
1088		[]string{
1089			"mkdir /a",
1090			"mkdir /a/b",
1091			"touch /a/b/c",
1092			"mkdir /a/b/g",
1093			"mkdir /a/b/g/h",
1094			"touch /a/b/g/i",
1095			"touch /a/b/g/h/j",
1096			"touch /a/b/z",
1097		},
1098		"/",
1099		infiniteDepth,
1100		func(path string, info os.FileInfo, err error) error {
1101			if path == "/a/b/g" {
1102				return filepath.SkipDir
1103			}
1104			return nil
1105		},
1106		[]string{
1107			"/",
1108			"/a",
1109			"/a/b",
1110			"/a/b/c",
1111			"/a/b/z",
1112		},
1113	}}
1114	ctx := context.Background()
1115	for _, tc := range testCases {
1116		fs, err := buildTestFS(tc.buildfs)
1117		if err != nil {
1118			t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err)
1119		}
1120		var got []string
1121		traceFn := func(path string, info os.FileInfo, err error) error {
1122			if tc.walkFn != nil {
1123				err = tc.walkFn(path, info, err)
1124				if err != nil {
1125					return err
1126				}
1127			}
1128			got = append(got, path)
1129			return nil
1130		}
1131		fi, err := fs.Stat(ctx, tc.startAt)
1132		if err != nil {
1133			t.Fatalf("%s: cannot stat: %v", tc.desc, err)
1134		}
1135		err = walkFS(ctx, fs, tc.depth, tc.startAt, fi, traceFn)
1136		if err != nil {
1137			t.Errorf("%s:\ngot error %v, want nil", tc.desc, err)
1138			continue
1139		}
1140		sort.Strings(got)
1141		sort.Strings(tc.want)
1142		if !reflect.DeepEqual(got, tc.want) {
1143			t.Errorf("%s:\ngot  %q\nwant %q", tc.desc, got, tc.want)
1144			continue
1145		}
1146	}
1147}
1148
1149func buildTestFS(buildfs []string) (FileSystem, error) {
1150	// TODO: Could this be merged with the build logic in TestFS?
1151
1152	ctx := context.Background()
1153	fs := NewMemFS()
1154	for _, b := range buildfs {
1155		op := strings.Split(b, " ")
1156		switch op[0] {
1157		case "mkdir":
1158			err := fs.Mkdir(ctx, op[1], os.ModeDir|0777)
1159			if err != nil {
1160				return nil, err
1161			}
1162		case "touch":
1163			f, err := fs.OpenFile(ctx, op[1], os.O_RDWR|os.O_CREATE, 0666)
1164			if err != nil {
1165				return nil, err
1166			}
1167			f.Close()
1168		case "write":
1169			f, err := fs.OpenFile(ctx, op[1], os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
1170			if err != nil {
1171				return nil, err
1172			}
1173			_, err = f.Write([]byte(op[2]))
1174			f.Close()
1175			if err != nil {
1176				return nil, err
1177			}
1178		default:
1179			return nil, fmt.Errorf("unknown file operation %q", op[0])
1180		}
1181	}
1182	return fs, nil
1183}
1184