• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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	for f := range fs.files {
65		fs.all = append(fs.all, f)
66	}
67
68	for d := range fs.dirs {
69		fs.all = append(fs.all, d)
70	}
71
72	for s := range fs.symlinks {
73		fs.all = append(fs.all, s)
74	}
75
76	sort.Strings(fs.all)
77
78	return fs
79}
80
81type ReaderAtSeekerCloser interface {
82	io.Reader
83	io.ReaderAt
84	io.Seeker
85	io.Closer
86}
87
88type FileSystem interface {
89	// Open opens a file for reading.  Follows symlinks.
90	Open(name string) (ReaderAtSeekerCloser, error)
91
92	// Exists returns whether the file exists and whether it is a directory.  Follows symlinks.
93	Exists(name string) (bool, bool, error)
94
95	Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (matches, dirs []string, err error)
96	glob(pattern string) (matches []string, err error)
97
98	// IsDir returns true if the path points to a directory, false it it points to a file.  Follows symlinks.
99	// Returns os.ErrNotExist if the path does not exist or is a symlink to a path that does not exist.
100	IsDir(name string) (bool, error)
101
102	// IsSymlink returns true if the path points to a symlink, even if that symlink points to a path that does
103	// not exist.  Returns os.ErrNotExist if the path does not exist.
104	IsSymlink(name string) (bool, error)
105
106	// Lstat returns info on a file without following symlinks.
107	Lstat(name string) (os.FileInfo, error)
108
109	// Lstat returns info on a file.
110	Stat(name string) (os.FileInfo, error)
111
112	// ListDirsRecursive returns a list of all the directories in a path, following symlinks if requested.
113	ListDirsRecursive(name string, follow ShouldFollowSymlinks) (dirs []string, err error)
114
115	// ReadDirNames returns a list of everything in a directory.
116	ReadDirNames(name string) ([]string, error)
117
118	// Readlink returns the destination of the named symbolic link.
119	Readlink(name string) (string, error)
120}
121
122// osFs implements FileSystem using the local disk.
123type osFs struct{}
124
125func (osFs) Open(name string) (ReaderAtSeekerCloser, error) { return os.Open(name) }
126func (osFs) Exists(name string) (bool, bool, error) {
127	stat, err := os.Stat(name)
128	if err == nil {
129		return true, stat.IsDir(), nil
130	} else if os.IsNotExist(err) {
131		return false, false, nil
132	} else {
133		return false, false, err
134	}
135}
136
137func (osFs) IsDir(name string) (bool, error) {
138	info, err := os.Stat(name)
139	if err != nil {
140		return false, err
141	}
142	return info.IsDir(), nil
143}
144
145func (osFs) IsSymlink(name string) (bool, error) {
146	if info, err := os.Lstat(name); err != nil {
147		return false, err
148	} else {
149		return info.Mode()&os.ModeSymlink != 0, nil
150	}
151}
152
153func (fs osFs) Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (matches, dirs []string, err error) {
154	return startGlob(fs, pattern, excludes, follow)
155}
156
157func (osFs) glob(pattern string) ([]string, error) {
158	return filepath.Glob(pattern)
159}
160
161func (osFs) Lstat(path string) (stats os.FileInfo, err error) {
162	return os.Lstat(path)
163}
164
165func (osFs) Stat(path string) (stats os.FileInfo, err error) {
166	return os.Stat(path)
167}
168
169// Returns a list of all directories under dir
170func (osFs) ListDirsRecursive(name string, follow ShouldFollowSymlinks) (dirs []string, err error) {
171	return listDirsRecursive(OsFs, name, follow)
172}
173
174func (osFs) ReadDirNames(name string) ([]string, error) {
175	dir, err := os.Open(name)
176	if err != nil {
177		return nil, err
178	}
179	defer dir.Close()
180
181	contents, err := dir.Readdirnames(-1)
182	if err != nil {
183		return nil, err
184	}
185
186	sort.Strings(contents)
187	return contents, nil
188}
189
190func (osFs) Readlink(name string) (string, error) {
191	return os.Readlink(name)
192}
193
194type mockFs struct {
195	files    map[string][]byte
196	dirs     map[string]bool
197	symlinks map[string]string
198	all      []string
199}
200
201func (m *mockFs) followSymlinks(name string) string {
202	dir, file := saneSplit(name)
203	if dir != "." && dir != "/" {
204		dir = m.followSymlinks(dir)
205	}
206	name = filepath.Join(dir, file)
207
208	for i := 0; i < 255; i++ {
209		i++
210		if i > 255 {
211			panic("symlink loop")
212		}
213		to, exists := m.symlinks[name]
214		if !exists {
215			break
216		}
217		if filepath.IsAbs(to) {
218			name = to
219		} else {
220			name = filepath.Join(dir, to)
221		}
222	}
223	return name
224}
225
226func (m *mockFs) Open(name string) (ReaderAtSeekerCloser, error) {
227	name = filepath.Clean(name)
228	name = m.followSymlinks(name)
229	if f, ok := m.files[name]; ok {
230		return struct {
231			io.Closer
232			*bytes.Reader
233		}{
234			ioutil.NopCloser(nil),
235			bytes.NewReader(f),
236		}, nil
237	}
238
239	return nil, &os.PathError{
240		Op:   "open",
241		Path: name,
242		Err:  os.ErrNotExist,
243	}
244}
245
246func (m *mockFs) Exists(name string) (bool, bool, error) {
247	name = filepath.Clean(name)
248	name = m.followSymlinks(name)
249	if _, ok := m.files[name]; ok {
250		return ok, false, nil
251	}
252	if _, ok := m.dirs[name]; ok {
253		return ok, true, nil
254	}
255	return false, false, nil
256}
257
258func (m *mockFs) IsDir(name string) (bool, error) {
259	dir := filepath.Dir(name)
260	if dir != "." && dir != "/" {
261		isDir, err := m.IsDir(dir)
262
263		if serr, ok := err.(*os.SyscallError); ok && serr.Err == syscall.ENOTDIR {
264			isDir = false
265		} else if err != nil {
266			return false, err
267		}
268
269		if !isDir {
270			return false, os.NewSyscallError("stat "+name, syscall.ENOTDIR)
271		}
272	}
273
274	name = filepath.Clean(name)
275	name = m.followSymlinks(name)
276
277	if _, ok := m.dirs[name]; ok {
278		return true, nil
279	}
280	if _, ok := m.files[name]; ok {
281		return false, nil
282	}
283	return false, os.ErrNotExist
284}
285
286func (m *mockFs) IsSymlink(name string) (bool, error) {
287	dir, file := saneSplit(name)
288	dir = m.followSymlinks(dir)
289	name = filepath.Join(dir, file)
290
291	if _, isSymlink := m.symlinks[name]; isSymlink {
292		return true, nil
293	}
294	if _, isDir := m.dirs[name]; isDir {
295		return false, nil
296	}
297	if _, isFile := m.files[name]; isFile {
298		return false, nil
299	}
300	return false, os.ErrNotExist
301}
302
303func (m *mockFs) Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (matches, dirs []string, err error) {
304	return startGlob(m, pattern, excludes, follow)
305}
306
307func unescapeGlob(s string) string {
308	i := 0
309	for i < len(s) {
310		if s[i] == '\\' {
311			s = s[:i] + s[i+1:]
312		} else {
313			i++
314		}
315	}
316	return s
317}
318
319func (m *mockFs) glob(pattern string) ([]string, error) {
320	dir, file := saneSplit(pattern)
321
322	dir = unescapeGlob(dir)
323	toDir := m.followSymlinks(dir)
324
325	var matches []string
326	for _, f := range m.all {
327		fDir, fFile := saneSplit(f)
328		if toDir == fDir {
329			match, err := filepath.Match(file, fFile)
330			if err != nil {
331				return nil, err
332			}
333			if f == "." && f != pattern {
334				// filepath.Glob won't return "." unless the pattern was "."
335				match = false
336			}
337			if match {
338				matches = append(matches, filepath.Join(dir, fFile))
339			}
340		}
341	}
342	return matches, nil
343}
344
345type mockStat struct {
346	name string
347	size int64
348	mode os.FileMode
349}
350
351func (ms *mockStat) Name() string       { return ms.name }
352func (ms *mockStat) IsDir() bool        { return ms.Mode().IsDir() }
353func (ms *mockStat) Size() int64        { return ms.size }
354func (ms *mockStat) Mode() os.FileMode  { return ms.mode }
355func (ms *mockStat) ModTime() time.Time { return time.Time{} }
356func (ms *mockStat) Sys() interface{}   { return nil }
357
358func (m *mockFs) Lstat(name string) (os.FileInfo, error) {
359	dir, file := saneSplit(name)
360	dir = m.followSymlinks(dir)
361	name = filepath.Join(dir, file)
362
363	ms := mockStat{
364		name: file,
365	}
366
367	if symlink, isSymlink := m.symlinks[name]; isSymlink {
368		ms.mode = os.ModeSymlink
369		ms.size = int64(len(symlink))
370	} else if _, isDir := m.dirs[name]; isDir {
371		ms.mode = os.ModeDir
372	} else if _, isFile := m.files[name]; isFile {
373		ms.mode = 0
374		ms.size = int64(len(m.files[name]))
375	} else {
376		return nil, os.ErrNotExist
377	}
378
379	return &ms, nil
380}
381
382func (m *mockFs) Stat(name string) (os.FileInfo, error) {
383	name = filepath.Clean(name)
384	origName := name
385	name = m.followSymlinks(name)
386
387	ms := mockStat{
388		name: filepath.Base(origName),
389		size: int64(len(m.files[name])),
390	}
391
392	if _, isDir := m.dirs[name]; isDir {
393		ms.mode = os.ModeDir
394	} else if _, isFile := m.files[name]; isFile {
395		ms.mode = 0
396		ms.size = int64(len(m.files[name]))
397	} else {
398		return nil, os.ErrNotExist
399	}
400
401	return &ms, nil
402}
403
404func (m *mockFs) ReadDirNames(name string) ([]string, error) {
405	name = filepath.Clean(name)
406	name = m.followSymlinks(name)
407
408	exists, isDir, err := m.Exists(name)
409	if err != nil {
410		return nil, err
411	}
412	if !exists {
413		return nil, os.ErrNotExist
414	}
415	if !isDir {
416		return nil, os.NewSyscallError("readdir", syscall.ENOTDIR)
417	}
418
419	var ret []string
420	for _, f := range m.all {
421		dir, file := saneSplit(f)
422		if dir == name && len(file) > 0 && file[0] != '.' {
423			ret = append(ret, file)
424		}
425	}
426	return ret, nil
427}
428
429func (m *mockFs) ListDirsRecursive(name string, follow ShouldFollowSymlinks) ([]string, error) {
430	return listDirsRecursive(m, name, follow)
431}
432
433func (m *mockFs) Readlink(name string) (string, error) {
434	dir, file := saneSplit(name)
435	dir = m.followSymlinks(dir)
436
437	origName := name
438	name = filepath.Join(dir, file)
439
440	if dest, isSymlink := m.symlinks[name]; isSymlink {
441		return dest, nil
442	}
443
444	if exists, _, err := m.Exists(name); err != nil {
445		return "", err
446	} else if !exists {
447		return "", os.ErrNotExist
448	} else {
449		return "", os.NewSyscallError("readlink: "+origName, syscall.EINVAL)
450	}
451}
452
453func listDirsRecursive(fs FileSystem, name string, follow ShouldFollowSymlinks) ([]string, error) {
454	name = filepath.Clean(name)
455
456	isDir, err := fs.IsDir(name)
457	if err != nil {
458		return nil, err
459	}
460
461	if !isDir {
462		return nil, nil
463	}
464
465	dirs := []string{name}
466
467	subDirs, err := listDirsRecursiveRelative(fs, name, follow, 0)
468	if err != nil {
469		return nil, err
470	}
471
472	for _, d := range subDirs {
473		dirs = append(dirs, filepath.Join(name, d))
474	}
475
476	return dirs, nil
477}
478
479func listDirsRecursiveRelative(fs FileSystem, name string, follow ShouldFollowSymlinks, depth int) ([]string, error) {
480	depth++
481	if depth > 255 {
482		return nil, fmt.Errorf("too many symlinks")
483	}
484	contents, err := fs.ReadDirNames(name)
485	if err != nil {
486		return nil, err
487	}
488
489	var dirs []string
490	for _, f := range contents {
491		if f[0] == '.' {
492			continue
493		}
494		f = filepath.Join(name, f)
495		if isSymlink, _ := fs.IsSymlink(f); isSymlink && follow == DontFollowSymlinks {
496			continue
497		}
498		if isDir, _ := fs.IsDir(f); isDir {
499			dirs = append(dirs, f)
500			subDirs, err := listDirsRecursiveRelative(fs, f, follow, depth)
501			if err != nil {
502				return nil, err
503			}
504			for _, s := range subDirs {
505				dirs = append(dirs, filepath.Join(f, s))
506			}
507		}
508	}
509
510	for i, d := range dirs {
511		rel, err := filepath.Rel(name, d)
512		if err != nil {
513			return nil, err
514		}
515		dirs[i] = rel
516	}
517
518	return dirs, nil
519}
520