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