1// Copyright 2020 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 fs
6
7import (
8	"errors"
9	"path"
10)
11
12// A SubFS is a file system with a Sub method.
13type SubFS interface {
14	FS
15
16	// Sub returns an FS corresponding to the subtree rooted at dir.
17	Sub(dir string) (FS, error)
18}
19
20// Sub returns an [FS] corresponding to the subtree rooted at fsys's dir.
21//
22// If dir is ".", Sub returns fsys unchanged.
23// Otherwise, if fs implements [SubFS], Sub returns fsys.Sub(dir).
24// Otherwise, Sub returns a new [FS] implementation sub that,
25// in effect, implements sub.Open(name) as fsys.Open(path.Join(dir, name)).
26// The implementation also translates calls to ReadDir, ReadFile, and Glob appropriately.
27//
28// Note that Sub(os.DirFS("/"), "prefix") is equivalent to os.DirFS("/prefix")
29// and that neither of them guarantees to avoid operating system
30// accesses outside "/prefix", because the implementation of [os.DirFS]
31// does not check for symbolic links inside "/prefix" that point to
32// other directories. That is, [os.DirFS] is not a general substitute for a
33// chroot-style security mechanism, and Sub does not change that fact.
34func Sub(fsys FS, dir string) (FS, error) {
35	if !ValidPath(dir) {
36		return nil, &PathError{Op: "sub", Path: dir, Err: ErrInvalid}
37	}
38	if dir == "." {
39		return fsys, nil
40	}
41	if fsys, ok := fsys.(SubFS); ok {
42		return fsys.Sub(dir)
43	}
44	return &subFS{fsys, dir}, nil
45}
46
47type subFS struct {
48	fsys FS
49	dir  string
50}
51
52// fullName maps name to the fully-qualified name dir/name.
53func (f *subFS) fullName(op string, name string) (string, error) {
54	if !ValidPath(name) {
55		return "", &PathError{Op: op, Path: name, Err: ErrInvalid}
56	}
57	return path.Join(f.dir, name), nil
58}
59
60// shorten maps name, which should start with f.dir, back to the suffix after f.dir.
61func (f *subFS) shorten(name string) (rel string, ok bool) {
62	if name == f.dir {
63		return ".", true
64	}
65	if len(name) >= len(f.dir)+2 && name[len(f.dir)] == '/' && name[:len(f.dir)] == f.dir {
66		return name[len(f.dir)+1:], true
67	}
68	return "", false
69}
70
71// fixErr shortens any reported names in PathErrors by stripping f.dir.
72func (f *subFS) fixErr(err error) error {
73	if e, ok := err.(*PathError); ok {
74		if short, ok := f.shorten(e.Path); ok {
75			e.Path = short
76		}
77	}
78	return err
79}
80
81func (f *subFS) Open(name string) (File, error) {
82	full, err := f.fullName("open", name)
83	if err != nil {
84		return nil, err
85	}
86	file, err := f.fsys.Open(full)
87	return file, f.fixErr(err)
88}
89
90func (f *subFS) ReadDir(name string) ([]DirEntry, error) {
91	full, err := f.fullName("read", name)
92	if err != nil {
93		return nil, err
94	}
95	dir, err := ReadDir(f.fsys, full)
96	return dir, f.fixErr(err)
97}
98
99func (f *subFS) ReadFile(name string) ([]byte, error) {
100	full, err := f.fullName("read", name)
101	if err != nil {
102		return nil, err
103	}
104	data, err := ReadFile(f.fsys, full)
105	return data, f.fixErr(err)
106}
107
108func (f *subFS) Glob(pattern string) ([]string, error) {
109	// Check pattern is well-formed.
110	if _, err := path.Match(pattern, ""); err != nil {
111		return nil, err
112	}
113	if pattern == "." {
114		return []string{"."}, nil
115	}
116
117	full := f.dir + "/" + pattern
118	list, err := Glob(f.fsys, full)
119	for i, name := range list {
120		name, ok := f.shorten(name)
121		if !ok {
122			return nil, errors.New("invalid result from inner fsys Glob: " + name + " not in " + f.dir) // can't use fmt in this package
123		}
124		list[i] = name
125	}
126	return list, f.fixErr(err)
127}
128
129func (f *subFS) Sub(dir string) (FS, error) {
130	if dir == "." {
131		return f, nil
132	}
133	full, err := f.fullName("sub", dir)
134	if err != nil {
135		return nil, err
136	}
137	return &subFS{f.fsys, full}, nil
138}
139