1// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package pathtools
16
17import (
18	"bytes"
19	"fmt"
20	"io"
21	"io/ioutil"
22	"os"
23	"path/filepath"
24	"sort"
25	"strings"
26	"syscall"
27	"time"
28)
29
30// Based on Andrew Gerrand's "10 things you (probably) dont' know about Go"
31
32type ShouldFollowSymlinks bool
33
34const (
35	FollowSymlinks     = ShouldFollowSymlinks(true)
36	DontFollowSymlinks = ShouldFollowSymlinks(false)
37)
38
39var OsFs FileSystem = &osFs{}
40
41func MockFs(files map[string][]byte) FileSystem {
42	fs := &mockFs{
43		files:    make(map[string][]byte, len(files)),
44		dirs:     make(map[string]bool),
45		symlinks: make(map[string]string),
46		all:      []string(nil),
47	}
48
49	for f, b := range files {
50		if tokens := strings.SplitN(f, "->", 2); len(tokens) == 2 {
51			fs.symlinks[strings.TrimSpace(tokens[0])] = strings.TrimSpace(tokens[1])
52			continue
53		}
54
55		fs.files[filepath.Clean(f)] = b
56		dir := filepath.Dir(f)
57		for dir != "." && dir != "/" {
58			fs.dirs[dir] = true
59			dir = filepath.Dir(dir)
60		}
61		fs.dirs[dir] = true
62	}
63
64	fs.dirs["."] = true
65	fs.dirs["/"] = true
66
67	for f := range fs.files {
68		fs.all = append(fs.all, f)
69	}
70
71	for d := range fs.dirs {
72		fs.all = append(fs.all, d)
73	}
74
75	for s := range fs.symlinks {
76		fs.all = append(fs.all, s)
77	}
78
79	sort.Strings(fs.all)
80
81	return fs
82}
83
84type ReaderAtSeekerCloser interface {
85	io.Reader
86	io.ReaderAt
87	io.Seeker
88	io.Closer
89}
90
91type FileSystem interface {
92	// Open opens a file for reading.  Follows symlinks.
93	Open(name string) (ReaderAtSeekerCloser, error)
94
95	// Exists returns whether the file exists and whether it is a directory.  Follows symlinks.
96	Exists(name string) (bool, bool, error)
97
98	Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (matches, dirs []string, err error)
99	glob(pattern string) (matches []string, err error)
100
101	// IsDir returns true if the path points to a directory, false it it points to a file.  Follows symlinks.
102	// Returns os.ErrNotExist if the path does not exist or is a symlink to a path that does not exist.
103	IsDir(name string) (bool, error)
104
105	// IsSymlink returns true if the path points to a symlink, even if that symlink points to a path that does
106	// not exist.  Returns os.ErrNotExist if the path does not exist.
107	IsSymlink(name string) (bool, error)
108
109	// Lstat returns info on a file without following symlinks.
110	Lstat(name string) (os.FileInfo, error)
111
112	// Lstat returns info on a file.
113	Stat(name string) (os.FileInfo, error)
114
115	// ListDirsRecursive returns a list of all the directories in a path, following symlinks if requested.
116	ListDirsRecursive(name string, follow ShouldFollowSymlinks) (dirs []string, err error)
117
118	// ReadDirNames returns a list of everything in a directory.
119	ReadDirNames(name string) ([]string, error)
120
121	// Readlink returns the destination of the named symbolic link.
122	Readlink(name string) (string, error)
123}
124
125// osFs implements FileSystem using the local disk.
126type osFs struct {
127	srcDir string
128}
129
130func NewOsFs(path string) FileSystem {
131	return &osFs{srcDir: path}
132}
133
134func (fs *osFs) toAbs(path string) string {
135	if filepath.IsAbs(path) {
136		return path
137	}
138	return filepath.Join(fs.srcDir, path)
139}
140
141func (fs *osFs) removeSrcDirPrefix(path string) string {
142	if fs.srcDir == "" {
143		return path
144	}
145	rel, err := filepath.Rel(fs.srcDir, path)
146	if err != nil {
147		panic(fmt.Errorf("unexpected failure in removeSrcDirPrefix filepath.Rel(%s, %s): %s",
148			fs.srcDir, path, err))
149	}
150	if strings.HasPrefix(rel, "../") {
151		panic(fmt.Errorf("unexpected relative path outside directory in removeSrcDirPrefix filepath.Rel(%s, %s): %s",
152			fs.srcDir, path, rel))
153	}
154	return rel
155}
156
157func (fs *osFs) removeSrcDirPrefixes(paths []string) []string {
158	if fs.srcDir != "" {
159		for i, path := range paths {
160			paths[i] = fs.removeSrcDirPrefix(path)
161		}
162	}
163	return paths
164}
165
166func (fs *osFs) Open(name string) (ReaderAtSeekerCloser, error) {
167	return os.Open(fs.toAbs(name))
168}
169
170func (fs *osFs) Exists(name string) (bool, bool, error) {
171	stat, err := os.Stat(fs.toAbs(name))
172	if err == nil {
173		return true, stat.IsDir(), nil
174	} else if os.IsNotExist(err) {
175		return false, false, nil
176	} else {
177		return false, false, err
178	}
179}
180
181func (fs *osFs) IsDir(name string) (bool, error) {
182	info, err := os.Stat(fs.toAbs(name))
183	if err != nil {
184		return false, err
185	}
186	return info.IsDir(), nil
187}
188
189func (fs *osFs) IsSymlink(name string) (bool, error) {
190	if info, err := os.Lstat(fs.toAbs(name)); err != nil {
191		return false, err
192	} else {
193		return info.Mode()&os.ModeSymlink != 0, nil
194	}
195}
196
197func (fs *osFs) Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (matches, dirs []string, err error) {
198	return startGlob(fs, pattern, excludes, follow)
199}
200
201func (fs *osFs) glob(pattern string) ([]string, error) {
202	paths, err := filepath.Glob(fs.toAbs(pattern))
203	fs.removeSrcDirPrefixes(paths)
204	return paths, err
205}
206
207func (fs *osFs) Lstat(path string) (stats os.FileInfo, err error) {
208	return os.Lstat(fs.toAbs(path))
209}
210
211func (fs *osFs) Stat(path string) (stats os.FileInfo, err error) {
212	return os.Stat(fs.toAbs(path))
213}
214
215// Returns a list of all directories under dir
216func (fs *osFs) ListDirsRecursive(name string, follow ShouldFollowSymlinks) (dirs []string, err error) {
217	return listDirsRecursive(fs, name, follow)
218}
219
220func (fs *osFs) ReadDirNames(name string) ([]string, error) {
221	dir, err := os.Open(fs.toAbs(name))
222	if err != nil {
223		return nil, err
224	}
225	defer dir.Close()
226
227	contents, err := dir.Readdirnames(-1)
228	if err != nil {
229		return nil, err
230	}
231
232	sort.Strings(contents)
233	return contents, nil
234}
235
236func (fs *osFs) Readlink(name string) (string, error) {
237	return os.Readlink(fs.toAbs(name))
238}
239
240type mockFs struct {
241	files    map[string][]byte
242	dirs     map[string]bool
243	symlinks map[string]string
244	all      []string
245}
246
247func (m *mockFs) followSymlinks(name string) string {
248	dir, file := saneSplit(name)
249	if dir != "." && dir != "/" {
250		dir = m.followSymlinks(dir)
251	}
252	name = filepath.Join(dir, file)
253
254	for i := 0; i < 255; i++ {
255		i++
256		if i > 255 {
257			panic("symlink loop")
258		}
259		to, exists := m.symlinks[name]
260		if !exists {
261			break
262		}
263		if filepath.IsAbs(to) {
264			name = to
265		} else {
266			name = filepath.Join(dir, to)
267		}
268	}
269	return name
270}
271
272func (m *mockFs) Open(name string) (ReaderAtSeekerCloser, error) {
273	name = filepath.Clean(name)
274	name = m.followSymlinks(name)
275	if f, ok := m.files[name]; ok {
276		return struct {
277			io.Closer
278			*bytes.Reader
279		}{
280			ioutil.NopCloser(nil),
281			bytes.NewReader(f),
282		}, nil
283	}
284
285	return nil, &os.PathError{
286		Op:   "open",
287		Path: name,
288		Err:  os.ErrNotExist,
289	}
290}
291
292func (m *mockFs) Exists(name string) (bool, bool, error) {
293	name = filepath.Clean(name)
294	name = m.followSymlinks(name)
295	if _, ok := m.files[name]; ok {
296		return ok, false, nil
297	}
298	if _, ok := m.dirs[name]; ok {
299		return ok, true, nil
300	}
301	return false, false, nil
302}
303
304func (m *mockFs) IsDir(name string) (bool, error) {
305	dir := filepath.Dir(name)
306	if dir != "." && dir != "/" {
307		isDir, err := m.IsDir(dir)
308
309		if serr, ok := err.(*os.SyscallError); ok && serr.Err == syscall.ENOTDIR {
310			isDir = false
311		} else if err != nil {
312			return false, err
313		}
314
315		if !isDir {
316			return false, os.NewSyscallError("stat "+name, syscall.ENOTDIR)
317		}
318	}
319
320	name = filepath.Clean(name)
321	name = m.followSymlinks(name)
322
323	if _, ok := m.dirs[name]; ok {
324		return true, nil
325	}
326	if _, ok := m.files[name]; ok {
327		return false, nil
328	}
329	return false, os.ErrNotExist
330}
331
332func (m *mockFs) IsSymlink(name string) (bool, error) {
333	dir, file := saneSplit(name)
334	dir = m.followSymlinks(dir)
335	name = filepath.Join(dir, file)
336
337	if _, isSymlink := m.symlinks[name]; isSymlink {
338		return true, nil
339	}
340	if _, isDir := m.dirs[name]; isDir {
341		return false, nil
342	}
343	if _, isFile := m.files[name]; isFile {
344		return false, nil
345	}
346	return false, os.ErrNotExist
347}
348
349func (m *mockFs) Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (matches, dirs []string, err error) {
350	return startGlob(m, pattern, excludes, follow)
351}
352
353func unescapeGlob(s string) string {
354	i := 0
355	for i < len(s) {
356		if s[i] == '\\' {
357			s = s[:i] + s[i+1:]
358		} else {
359			i++
360		}
361	}
362	return s
363}
364
365func (m *mockFs) glob(pattern string) ([]string, error) {
366	dir, file := saneSplit(pattern)
367
368	dir = unescapeGlob(dir)
369	toDir := m.followSymlinks(dir)
370
371	var matches []string
372	for _, f := range m.all {
373		fDir, fFile := saneSplit(f)
374		if toDir == fDir {
375			match, err := filepath.Match(file, fFile)
376			if err != nil {
377				return nil, err
378			}
379			if (f == "." || f == "/") && f != pattern {
380				// filepath.Glob won't return "." or "/" unless the pattern was "." or "/"
381				match = false
382			}
383			if match {
384				matches = append(matches, filepath.Join(dir, fFile))
385			}
386		}
387	}
388	return matches, nil
389}
390
391type mockStat struct {
392	name string
393	size int64
394	mode os.FileMode
395}
396
397func (ms *mockStat) Name() string       { return ms.name }
398func (ms *mockStat) IsDir() bool        { return ms.Mode().IsDir() }
399func (ms *mockStat) Size() int64        { return ms.size }
400func (ms *mockStat) Mode() os.FileMode  { return ms.mode }
401func (ms *mockStat) ModTime() time.Time { return time.Time{} }
402func (ms *mockStat) Sys() interface{}   { return nil }
403
404func (m *mockFs) Lstat(name string) (os.FileInfo, error) {
405	dir, file := saneSplit(name)
406	dir = m.followSymlinks(dir)
407	name = filepath.Join(dir, file)
408
409	ms := mockStat{
410		name: file,
411	}
412
413	if symlink, isSymlink := m.symlinks[name]; isSymlink {
414		ms.mode = os.ModeSymlink
415		ms.size = int64(len(symlink))
416	} else if _, isDir := m.dirs[name]; isDir {
417		ms.mode = os.ModeDir
418	} else if _, isFile := m.files[name]; isFile {
419		ms.mode = 0
420		ms.size = int64(len(m.files[name]))
421	} else {
422		return nil, os.ErrNotExist
423	}
424
425	return &ms, nil
426}
427
428func (m *mockFs) Stat(name string) (os.FileInfo, error) {
429	name = filepath.Clean(name)
430	origName := name
431	name = m.followSymlinks(name)
432
433	ms := mockStat{
434		name: filepath.Base(origName),
435		size: int64(len(m.files[name])),
436	}
437
438	if _, isDir := m.dirs[name]; isDir {
439		ms.mode = os.ModeDir
440	} else if _, isFile := m.files[name]; isFile {
441		ms.mode = 0
442		ms.size = int64(len(m.files[name]))
443	} else {
444		return nil, os.ErrNotExist
445	}
446
447	return &ms, nil
448}
449
450func (m *mockFs) ReadDirNames(name string) ([]string, error) {
451	name = filepath.Clean(name)
452	name = m.followSymlinks(name)
453
454	exists, isDir, err := m.Exists(name)
455	if err != nil {
456		return nil, err
457	}
458	if !exists {
459		return nil, os.ErrNotExist
460	}
461	if !isDir {
462		return nil, os.NewSyscallError("readdir", syscall.ENOTDIR)
463	}
464
465	var ret []string
466	for _, f := range m.all {
467		dir, file := saneSplit(f)
468		if dir == name && len(file) > 0 && file[0] != '.' {
469			ret = append(ret, file)
470		}
471	}
472	return ret, nil
473}
474
475func (m *mockFs) ListDirsRecursive(name string, follow ShouldFollowSymlinks) ([]string, error) {
476	return listDirsRecursive(m, name, follow)
477}
478
479func (m *mockFs) Readlink(name string) (string, error) {
480	dir, file := saneSplit(name)
481	dir = m.followSymlinks(dir)
482
483	origName := name
484	name = filepath.Join(dir, file)
485
486	if dest, isSymlink := m.symlinks[name]; isSymlink {
487		return dest, nil
488	}
489
490	if exists, _, err := m.Exists(name); err != nil {
491		return "", err
492	} else if !exists {
493		return "", os.ErrNotExist
494	} else {
495		return "", os.NewSyscallError("readlink: "+origName, syscall.EINVAL)
496	}
497}
498
499func listDirsRecursive(fs FileSystem, name string, follow ShouldFollowSymlinks) ([]string, error) {
500	name = filepath.Clean(name)
501
502	isDir, err := fs.IsDir(name)
503	if err != nil {
504		return nil, err
505	}
506
507	if !isDir {
508		return nil, nil
509	}
510
511	dirs := []string{name}
512
513	subDirs, err := listDirsRecursiveRelative(fs, name, follow, 0)
514	if err != nil {
515		return nil, err
516	}
517
518	for _, d := range subDirs {
519		dirs = append(dirs, filepath.Join(name, d))
520	}
521
522	return dirs, nil
523}
524
525func listDirsRecursiveRelative(fs FileSystem, name string, follow ShouldFollowSymlinks, depth int) ([]string, error) {
526	depth++
527	if depth > 255 {
528		return nil, fmt.Errorf("too many symlinks")
529	}
530	contents, err := fs.ReadDirNames(name)
531	if err != nil {
532		return nil, err
533	}
534
535	var dirs []string
536	for _, f := range contents {
537		if f[0] == '.' {
538			continue
539		}
540		f = filepath.Join(name, f)
541		if isSymlink, _ := fs.IsSymlink(f); isSymlink && follow == DontFollowSymlinks {
542			continue
543		}
544		if isDir, _ := fs.IsDir(f); isDir {
545			dirs = append(dirs, f)
546			subDirs, err := listDirsRecursiveRelative(fs, f, follow, depth)
547			if err != nil {
548				return nil, err
549			}
550			for _, s := range subDirs {
551				dirs = append(dirs, filepath.Join(f, s))
552			}
553		}
554	}
555
556	for i, d := range dirs {
557		rel, err := filepath.Rel(name, d)
558		if err != nil {
559			return nil, err
560		}
561		dirs[i] = rel
562	}
563
564	return dirs, nil
565}
566