Source file src/io/fs/sub.go

     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  
     5  package fs
     6  
     7  import (
     8  	"errors"
     9  	"path"
    10  )
    11  
    12  // A SubFS is a file system with a Sub method.
    13  type 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,
    27  // ReadLink, Lstat, and Glob appropriately.
    28  //
    29  // Note that Sub(os.DirFS("/"), "prefix") is equivalent to os.DirFS("/prefix")
    30  // and that neither of them guarantees to avoid operating system
    31  // accesses outside "/prefix", because the implementation of [os.DirFS]
    32  // does not check for symbolic links inside "/prefix" that point to
    33  // other directories. That is, [os.DirFS] is not a general substitute for a
    34  // chroot-style security mechanism, and Sub does not change that fact.
    35  func Sub(fsys FS, dir string) (FS, error) {
    36  	if !ValidPath(dir) {
    37  		return nil, &PathError{Op: "sub", Path: dir, Err: ErrInvalid}
    38  	}
    39  	if dir == "." {
    40  		return fsys, nil
    41  	}
    42  	if fsys, ok := fsys.(SubFS); ok {
    43  		return fsys.Sub(dir)
    44  	}
    45  	return &subFS{fsys, dir}, nil
    46  }
    47  
    48  var _ FS = (*subFS)(nil)
    49  var _ ReadDirFS = (*subFS)(nil)
    50  var _ ReadFileFS = (*subFS)(nil)
    51  var _ ReadLinkFS = (*subFS)(nil)
    52  var _ GlobFS = (*subFS)(nil)
    53  
    54  type subFS struct {
    55  	fsys FS
    56  	dir  string
    57  }
    58  
    59  // fullName maps name to the fully-qualified name dir/name.
    60  func (f *subFS) fullName(op string, name string) (string, error) {
    61  	if !ValidPath(name) {
    62  		return "", &PathError{Op: op, Path: name, Err: ErrInvalid}
    63  	}
    64  	return path.Join(f.dir, name), nil
    65  }
    66  
    67  // shorten maps name, which should start with f.dir, back to the suffix after f.dir.
    68  func (f *subFS) shorten(name string) (rel string, ok bool) {
    69  	if name == f.dir {
    70  		return ".", true
    71  	}
    72  	if len(name) >= len(f.dir)+2 && name[len(f.dir)] == '/' && name[:len(f.dir)] == f.dir {
    73  		return name[len(f.dir)+1:], true
    74  	}
    75  	return "", false
    76  }
    77  
    78  // fixErr shortens any reported names in PathErrors by stripping f.dir.
    79  func (f *subFS) fixErr(err error) error {
    80  	if e, ok := err.(*PathError); ok {
    81  		if short, ok := f.shorten(e.Path); ok {
    82  			e.Path = short
    83  		}
    84  	}
    85  	return err
    86  }
    87  
    88  func (f *subFS) Open(name string) (File, error) {
    89  	full, err := f.fullName("open", name)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	file, err := f.fsys.Open(full)
    94  	return file, f.fixErr(err)
    95  }
    96  
    97  func (f *subFS) ReadDir(name string) ([]DirEntry, error) {
    98  	full, err := f.fullName("read", name)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	dir, err := ReadDir(f.fsys, full)
   103  	return dir, f.fixErr(err)
   104  }
   105  
   106  func (f *subFS) ReadFile(name string) ([]byte, error) {
   107  	full, err := f.fullName("read", name)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	data, err := ReadFile(f.fsys, full)
   112  	return data, f.fixErr(err)
   113  }
   114  
   115  func (f *subFS) ReadLink(name string) (string, error) {
   116  	full, err := f.fullName("readlink", name)
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  	target, err := ReadLink(f.fsys, full)
   121  	if err != nil {
   122  		return "", f.fixErr(err)
   123  	}
   124  	return target, nil
   125  }
   126  
   127  func (f *subFS) Lstat(name string) (FileInfo, error) {
   128  	full, err := f.fullName("lstat", name)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	info, err := Lstat(f.fsys, full)
   133  	if err != nil {
   134  		return nil, f.fixErr(err)
   135  	}
   136  	return info, nil
   137  }
   138  
   139  func (f *subFS) Glob(pattern string) ([]string, error) {
   140  	// Check pattern is well-formed.
   141  	if _, err := path.Match(pattern, ""); err != nil {
   142  		return nil, err
   143  	}
   144  	if pattern == "." {
   145  		return []string{"."}, nil
   146  	}
   147  
   148  	full := f.dir + "/" + pattern
   149  	list, err := Glob(f.fsys, full)
   150  	for i, name := range list {
   151  		name, ok := f.shorten(name)
   152  		if !ok {
   153  			return nil, errors.New("invalid result from inner fsys Glob: " + name + " not in " + f.dir) // can't use fmt in this package
   154  		}
   155  		list[i] = name
   156  	}
   157  	return list, f.fixErr(err)
   158  }
   159  
   160  func (f *subFS) Sub(dir string) (FS, error) {
   161  	if dir == "." {
   162  		return f, nil
   163  	}
   164  	full, err := f.fullName("sub", dir)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	return &subFS{f.fsys, full}, nil
   169  }
   170  

View as plain text