github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/filesystem/open.go (about)

     1  package filesystem
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  )
     9  
    10  // ErrUnsupportedOpenType indicates that the filesystem entry at the specified
    11  // path is not supported as a traversal root.
    12  var ErrUnsupportedOpenType = errors.New("unsupported open type")
    13  
    14  // OpenDirectory is a convenience wrapper around Open that requires the result
    15  // to be a directory.
    16  func OpenDirectory(path string, allowSymbolicLinkLeaf bool) (*Directory, *Metadata, error) {
    17  	if d, metadata, err := Open(path, allowSymbolicLinkLeaf); err != nil {
    18  		return nil, nil, err
    19  	} else if (metadata.Mode & ModeTypeMask) != ModeTypeDirectory {
    20  		d.Close()
    21  		return nil, nil, errors.New("path is not a directory")
    22  	} else if directory, ok := d.(*Directory); !ok {
    23  		d.Close()
    24  		panic("invalid directory object returned from open operation")
    25  	} else {
    26  		return directory, metadata, nil
    27  	}
    28  }
    29  
    30  // OpenFile is a convenience wrapper around Open that requires the result to be
    31  // a file.
    32  func OpenFile(path string, allowSymbolicLinkLeaf bool) (io.ReadSeekCloser, *Metadata, error) {
    33  	if f, metadata, err := Open(path, allowSymbolicLinkLeaf); err != nil {
    34  		return nil, nil, err
    35  	} else if (metadata.Mode & ModeTypeMask) != ModeTypeFile {
    36  		f.Close()
    37  		return nil, nil, errors.New("path is not a file")
    38  	} else if file, ok := f.(io.ReadSeekCloser); !ok {
    39  		f.Close()
    40  		panic("invalid file object returned from open operation")
    41  	} else {
    42  		return file, metadata, nil
    43  	}
    44  }
    45  
    46  // Opener is a utility type that wraps a provided root path and provides file
    47  // opening operations on paths relative to that root, guaranteeing that the open
    48  // operations are performed in a race-free manner that can't escape the root via
    49  // a path or symbolic link. It accomplishes this by maintaining an internal
    50  // stack of Directory objects which provide this race-free opening property.
    51  // This implementation means that the Opener operates "fast" if it is used to
    52  // open paths in a sequence that mimics depth-first traversal ordering.
    53  type Opener struct {
    54  	// root is the root path for the opener.
    55  	root string
    56  	// rootDirectory is the Directory object corresponding to the root path. It
    57  	// may be nil if the root directory hasn't been opened.
    58  	rootDirectory *Directory
    59  	// openParentNames is a list of parent directory names representing the
    60  	// stack of currently open directories. It will be empty if rootDirectory is
    61  	// nil.
    62  	openParentNames []string
    63  	// openParentDirectories is a list of parent Directory objects representing
    64  	// the stack of currently open directories. Its length and contents
    65  	// correspond to openParentNames, and likewise it will be empty if
    66  	// rootDirectory is nil.
    67  	openParentDirectories []*Directory
    68  }
    69  
    70  // NewOpener creates a new Opener for the specified root path.
    71  func NewOpener(root string) *Opener {
    72  	return &Opener{root: root}
    73  }
    74  
    75  // OpenFile opens the file at the specified path (relative to the root). On all
    76  // platforms, the path must be provided using a forward slash as the path
    77  // separator, and path components must not be "." or "..". The path may be empty
    78  // to open the root path itself (if it's a file). If any symbolic links or
    79  // non-directory parent components are encountered, or if the target does not
    80  // represent a file, this method will fail.
    81  func (o *Opener) OpenFile(path string) (io.ReadSeekCloser, *Metadata, error) {
    82  	// Handle the special case of a root path. We enforce that it must be a
    83  	// file.
    84  	if path == "" {
    85  		// Verify that the root path hasn't already been opened as a directory.
    86  		// This is primarily just a cheap sanity check. On POSIX systems, the
    87  		// directory we hold open for the root could have been unlinked and
    88  		// replaced with a file, and it's better to catch that here before
    89  		// future Opener operations open files that aren't visible on the
    90  		// filesystem or are somewhere else on the filesystem.
    91  		if o.rootDirectory != nil {
    92  			return nil, nil, errors.New("root already opened as directory")
    93  		}
    94  
    95  		// Attempt to open the file.
    96  		if file, metadata, err := OpenFile(o.root, false); err != nil {
    97  			return nil, nil, fmt.Errorf("unable to open root file: %w", err)
    98  		} else {
    99  			return file, metadata, nil
   100  		}
   101  	}
   102  
   103  	// Split the path and extract the parent components and leaf name.
   104  	components := strings.Split(path, "/")
   105  	parentComponents := components[:len(components)-1]
   106  	leafName := components[len(components)-1]
   107  
   108  	// If it's not already open, open the root directory.
   109  	if o.rootDirectory == nil {
   110  		if directory, _, err := OpenDirectory(o.root, false); err != nil {
   111  			return nil, nil, fmt.Errorf("unable to open root directory: %w", err)
   112  		} else {
   113  			o.rootDirectory = directory
   114  		}
   115  	}
   116  
   117  	// Identify the starting parent directory.
   118  	parent := o.rootDirectory
   119  
   120  	// Walk down parent components and open them.
   121  	for c, component := range parentComponents {
   122  		// See if we can satisfy the component requirement using our stacks. If
   123  		// not, then truncate the stacks beyond this point.
   124  		if c < len(o.openParentNames) {
   125  			if o.openParentNames[c] == component {
   126  				parent = o.openParentDirectories[c]
   127  				continue
   128  			} else {
   129  				for i := c; i < len(o.openParentNames); i++ {
   130  					// Attempt to close the directory.
   131  					if err := o.openParentDirectories[i].Close(); err != nil {
   132  						return nil, nil, fmt.Errorf("unable to close previous parent directory: %w", err)
   133  					}
   134  
   135  					// We nil-out successfully closed directories for two
   136  					// reasons: first, to allow garbage collection, and second,
   137  					// to work as sentinel values for the Close method.
   138  					o.openParentNames[i] = ""
   139  					o.openParentDirectories[i] = nil
   140  				}
   141  				o.openParentNames = o.openParentNames[:c]
   142  				o.openParentDirectories = o.openParentDirectories[:c]
   143  			}
   144  		}
   145  
   146  		// Open the directory ourselves and add it to the parent stacks.
   147  		if directory, err := parent.OpenDirectory(component); err != nil {
   148  			return nil, nil, fmt.Errorf("unable to open parent directory: %w", err)
   149  		} else {
   150  			parent = directory
   151  			o.openParentNames = append(o.openParentNames, component)
   152  			o.openParentDirectories = append(o.openParentDirectories, directory)
   153  		}
   154  	}
   155  
   156  	// Open the leaf name within its parent directory.
   157  	return parent.OpenFile(leafName)
   158  }
   159  
   160  // Close closes any open resources held by the opener. It should only be called
   161  // once. Even on error, there is no benefit in calling it twice.
   162  func (o *Opener) Close() error {
   163  	// Track the first error to arise, if any.
   164  	var firstErr error
   165  
   166  	// Close the root directory, if open.
   167  	if o.rootDirectory != nil {
   168  		firstErr = o.rootDirectory.Close()
   169  	}
   170  
   171  	// Close open directories. If any are nil (which can happen on error
   172  	// conditions in open when truncation doesn't complete successfully), then
   173  	// just skip them.
   174  	for _, directory := range o.openParentDirectories {
   175  		if directory == nil {
   176  			continue
   177  		} else if err := directory.Close(); err != nil && firstErr == nil {
   178  			firstErr = err
   179  		}
   180  	}
   181  
   182  	// Done.
   183  	return firstErr
   184  }