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 }