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, 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.
    34  func Sub(fsys FS, dir string) (FS, error) {
    35  	if !ValidPath(dir) {
    36  		return nil, &PathError{Op: "sub", Path: dir, Err: errors.New("invalid name")}
    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  
    47  type subFS struct {
    48  	fsys FS
    49  	dir  string
    50  }
    51  
    52  // fullName maps name to the fully-qualified name dir/name.
    53  func (f *subFS) fullName(op string, name string) (string, error) {
    54  	if !ValidPath(name) {
    55  		return "", &PathError{Op: op, Path: name, Err: errors.New("invalid name")}
    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.
    61  func (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.
    72  func (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  
    81  func (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  
    90  func (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  
    99  func (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  
   108  func (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  
   129  func (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  

View as plain text