| // Copyright 2025 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| // Contains implementation of oswrapper interfaces using fstest. | |
| package oswrapper | |
| import ( | |
| "errors" | |
| "fmt" | |
| "io" | |
| "io/fs" | |
| "os" | |
| "path" | |
| "path/filepath" | |
| "strings" | |
| "sync/atomic" | |
| "syscall" | |
| "testing/fstest" | |
| "time" | |
| ) | |
| var ( | |
| ErrPwdNotSet = errors.New("PWD not set in test environment") | |
| ErrHomeNotSet = errors.New("HOME not set in test environment") | |
| ) | |
| // --- OSWrapper implementation --- | |
| // FSTestOSWrapper is an in-memory implementation of OSWrapper for testing. | |
| // It uses a map to simulate a filesystem, which can be used to create | |
| // a read-only fstest.MapFS for read operations. Write operations | |
| // manipulate the map directly. | |
| // Caveats/known differences from real os.* functions: | |
| // - Paths in error messages will never have a leading / even if it was in | |
| // the provided path. For example, ReadFile("/root") when /root does not | |
| // exist will have the error message "open root: file does not exist". This | |
| // is a side-effect of fs.FS requiring that Open() reject names that do not | |
| // satisfy fs.ValidPath(), which requires that there is no leading /. | |
| // TODO(crbug.com/436025865): Update the implementation to correctly add | |
| // leading / back in. | |
| // - The current working directory is not taken into account. For example, | |
| // creating /root/subdir/file.txt, changing to /root/subdir, and attempting | |
| // to open file.txt will fail. This cannot currently be easily worked | |
| // around at the moment since the CWD-aware logic lives in EnvironProvider | |
| // while filesystem operations live in FilesystemReaderWriter. | |
| // TODO(crbug.com/436025865): Merge everything into FSTestOSWrapper so that | |
| // the filesystem can be CWD-aware. | |
| type FSTestOSWrapper struct { | |
| FSTestEnvironProvider | |
| FSTestFilesystemReaderWriter | |
| } | |
| // CreateFSTestOSWrapper creates a new FSTestOSWrapper with an empty in-memory filesystem. | |
| func CreateFSTestOSWrapper() FSTestOSWrapper { | |
| return FSTestOSWrapper{ | |
| FSTestEnvironProvider{env: make(map[string]string)}, | |
| FSTestFilesystemReaderWriter{ | |
| FS: make(map[string]*fstest.MapFile), | |
| }, | |
| } | |
| } | |
| // CreateFSTestOSWrapperWithRealEnv creates a new FSTestOSWrapper with an empty in-memory filesystem | |
| // and an environment initialized from the current process's environment. | |
| func CreateFSTestOSWrapperWithRealEnv() FSTestOSWrapper { | |
| envMap := make(map[string]string) | |
| for _, e := range os.Environ() { | |
| pair := strings.SplitN(e, "=", 2) | |
| if len(pair) == 2 { | |
| envMap[pair[0]] = pair[1] | |
| } | |
| } | |
| // os.Getwd() and os.UserHomeDir() are not always from environment variables on all systems, but | |
| // to simplify the test wrapper PWD and HOME will be populated from them. | |
| if _, ok := envMap["PWD"]; !ok { | |
| if wd, err := os.Getwd(); err == nil { | |
| envMap["PWD"] = wd | |
| } | |
| } | |
| if _, ok := envMap["HOME"]; !ok { | |
| if home, err := os.UserHomeDir(); err == nil { | |
| envMap["HOME"] = home | |
| } | |
| } | |
| return FSTestOSWrapper{ | |
| FSTestEnvironProvider: FSTestEnvironProvider{env: envMap}, | |
| FSTestFilesystemReaderWriter: FSTestFilesystemReaderWriter{ | |
| FS: make(map[string]*fstest.MapFile), | |
| }, | |
| } | |
| } | |
| // --- EnvironProvider implementation --- | |
| // FSTestEnvironProvider is a stub implementation of EnvironProvider that uses a map. | |
| type FSTestEnvironProvider struct { | |
| env map[string]string | |
| } | |
| // EnvMap returns the internal environment map. | |
| // This is intended for testing purposes. | |
| func (p FSTestEnvironProvider) EnvMap() map[string]string { | |
| return p.env | |
| } | |
| // Setenv sets an environment variable in the test provider. | |
| func (p FSTestEnvironProvider) Setenv(key, value string) { | |
| p.env[key] = value | |
| } | |
| func (p FSTestEnvironProvider) Environ() []string { | |
| result := make([]string, 0, len(p.env)) | |
| for k, v := range p.env { | |
| result = append(result, k+"="+v) | |
| } | |
| return result | |
| } | |
| func (p FSTestEnvironProvider) Getenv(key string) string { | |
| // os.Getenv returns "" for a missing key, which is the same as a map lookup. | |
| return p.env[key] | |
| } | |
| func (p FSTestEnvironProvider) Getwd() (string, error) { | |
| if wd, ok := p.env["PWD"]; ok { | |
| return wd, nil | |
| } | |
| // The specific error returned by os.Getwd is very system dependent, so just returning a package | |
| // error | |
| return "", ErrPwdNotSet | |
| } | |
| func (p FSTestEnvironProvider) UserHomeDir() (string, error) { | |
| if home, ok := p.env["HOME"]; ok { | |
| return home, nil | |
| } | |
| // The specific error returned by os.UserHomeDir() is very system dependent, so just returning | |
| // a package error | |
| return "", ErrHomeNotSet | |
| } | |
| // --- FilesystemReaderWriter implementation --- | |
| // FSTestFilesystemReaderWriter provides an in-memory implementation of FilesystemReaderWriter. | |
| // It holds the map that represents the filesystem. | |
| type FSTestFilesystemReaderWriter struct { | |
| FS map[string]*fstest.MapFile | |
| } | |
| // CleanPath converts an OS dependent path into a fs.FS compatible path that can be used as a key | |
| // within FSTestOSWrapper | |
| func (w FSTestFilesystemReaderWriter) CleanPath(pathStr string) string { | |
| // Replace all backslashes with forward slashes to handle Windows paths on any OS. | |
| p := strings.ReplaceAll(pathStr, "\\", "/") | |
| // Use the platform-agnostic path package to clean the path. | |
| p = path.Clean(p) | |
| // Normalize the path to be compatible with fs.FS | |
| p = strings.TrimPrefix(p, "/") | |
| if p == "" { | |
| p = "." // The canonical representation of the root in a fs.FS. | |
| } | |
| return p | |
| } | |
| // fs returns a read-only fs.FS view of the underlying map. | |
| func (w FSTestFilesystemReaderWriter) fs() fs.FS { | |
| return legacyBehaviorFS{mapFS: fstest.MapFS(w.FS)} | |
| } | |
| // legacyBehaviorFS is a wrapper around fstest.MapFS that only implements fs.FS. | |
| // This is needed in Go 1.25+ because MapFS implements io/fs.ReadLinkFS, but its resolveSymlinks | |
| // method does not support absolute paths, instead rejecting them with an error. This causes the | |
| // _MatchesReal tests for the fake filesystem to fail since the real filesystem does support | |
| // absolute paths for symlinks. By hiding io/fs.ReadLinkFS, fs.WalkDir falls back to the default | |
| // behavior of not following symlinks. | |
| // | |
| // Additionally, Go 1.25's MapFS.Open auto-resolves symlinks, which causes fs.WalkDir to follow root | |
| // symlinks to directories, violating filepath.Walk behavior. To fix this, Open is overridden to | |
| // return a custom file handle for symlinks found in the map, preventing resolution. | |
| type legacyBehaviorFS struct { | |
| mapFS fstest.MapFS | |
| } | |
| // Open overrides MapFS.Open to prevent symlink resolution for entries in mapFS. | |
| func (f legacyBehaviorFS) Open(name string) (fs.File, error) { | |
| file := f.mapFS[name] | |
| if file != nil && file.Mode&fs.ModeSymlink != 0 { | |
| return noFollowFile{name: name, file: file}, nil | |
| } | |
| return f.mapFS.Open(name) | |
| } | |
| // noFollowFile is a custom fs.File handle for symlinks in legacyBehaviorFS. | |
| // It reports its own file info as a symlink and does not follow the target. | |
| type noFollowFile struct { | |
| name string | |
| file *fstest.MapFile | |
| offset int64 | |
| } | |
| func (f noFollowFile) Stat() (fs.FileInfo, error) { | |
| return &mapFileInfo{path: f.name, file: f.file}, nil | |
| } | |
| func (f noFollowFile) Close() error { return nil } | |
| func (f noFollowFile) Read(b []byte) (int, error) { | |
| if f.offset >= int64(len(f.file.Data)) { | |
| return 0, io.EOF | |
| } | |
| n := copy(b, f.file.Data[f.offset:]) | |
| f.offset += int64(n) | |
| return n, nil | |
| } | |
| // resolvePath resolves symlinks in the given path. | |
| // It assumes pathStr is a cleaned path (as returned by CleanPath). | |
| func (w FSTestFilesystemReaderWriter) resolvePath(pathStr string) (string, error) { | |
| const maxSymlinks = 255 | |
| linksWalked := 0 | |
| parts := strings.Split(pathStr, "/") | |
| resolvedPath := "." | |
| for i := 0; i < len(parts); i++ { | |
| part := parts[i] | |
| // CleanPath guarantees that the initial pathStr doesn't contain ".." components, | |
| // but symlink resolution can introduce them. | |
| if part == ".." { | |
| if resolvedPath != "." { | |
| resolvedPath = path.Dir(resolvedPath) | |
| } | |
| continue | |
| } | |
| currentPath := "" | |
| if resolvedPath == "." { | |
| currentPath = part | |
| } else { | |
| currentPath = resolvedPath + "/" + part | |
| } | |
| entry, exists := w.FS[currentPath] | |
| if exists && entry.Mode&fs.ModeSymlink != 0 { | |
| linksWalked++ | |
| if linksWalked > maxSymlinks { | |
| return "", &os.PathError{Op: "resolve", Path: pathStr, Err: syscall.ELOOP} | |
| } | |
| target := string(entry.Data) | |
| // Clean the target to handle OS specific separators and redundancies. | |
| cleanedTarget := w.CleanPath(target) | |
| // Check for absolute path. | |
| // Note: w.CleanPath removes the leading separator, so check the raw target. | |
| isAbs := filepath.IsAbs(target) || strings.HasPrefix(target, "/") | |
| targetParts := strings.Split(cleanedTarget, "/") | |
| if cleanedTarget == "." { | |
| targetParts = []string{} | |
| } | |
| if isAbs { | |
| resolvedPath = "." | |
| remaining := parts[i+1:] | |
| // Create new slice to be safe | |
| parts = make([]string, 0, len(targetParts)+len(remaining)) | |
| parts = append(parts, targetParts...) | |
| parts = append(parts, remaining...) | |
| i = -1 | |
| } else { | |
| remaining := parts[i+1:] | |
| head := parts[:i] | |
| newParts := make([]string, 0, len(head)+len(targetParts)+len(remaining)) | |
| newParts = append(newParts, head...) | |
| newParts = append(newParts, targetParts...) | |
| newParts = append(newParts, remaining...) | |
| parts = newParts | |
| i-- | |
| } | |
| continue | |
| } | |
| resolvedPath = currentPath | |
| } | |
| return resolvedPath, nil | |
| } | |
| // mapFileInfo wraps a fstest.MapFile to implement the os.FileInfo interface. | |
| // It holds a pointer to the MapFile and the file's full path to derive its base name. | |
| type mapFileInfo struct { | |
| path string | |
| file *fstest.MapFile | |
| } | |
| // Name returns the base name of the file. | |
| func (i *mapFileInfo) Name() string { | |
| return filepath.Base(i.path) | |
| } | |
| // Size returns the length of the file's data in bytes. | |
| func (i *mapFileInfo) Size() int64 { | |
| return int64(len(i.file.Data)) | |
| } | |
| // Mode returns the file mode bits. | |
| func (i *mapFileInfo) Mode() fs.FileMode { | |
| return i.file.Mode | |
| } | |
| // ModTime returns the modification time. | |
| func (i *mapFileInfo) ModTime() time.Time { | |
| return i.file.ModTime | |
| } | |
| // IsDir returns true if the entry is a directory. | |
| func (i *mapFileInfo) IsDir() bool { | |
| return i.file.Mode.IsDir() | |
| } | |
| // Sys returns the underlying data source (can be nil). | |
| func (i *mapFileInfo) Sys() any { | |
| return i.file.Sys | |
| } | |
| // fstestFileHandle implements the oswrapper.File interface for the in-memory fstest map. | |
| type fstestFileHandle struct { | |
| path string | |
| originalPath string | |
| info os.FileInfo | |
| fsMap *map[string]*fstest.MapFile | |
| content []byte | |
| offset int64 | |
| flag int | |
| closed bool | |
| } | |
| // --- FilesystemReader implementation --- | |
| func (w FSTestFilesystemReaderWriter) Open(name string) (File, error) { | |
| return w.OpenFile(name, os.O_RDONLY, 0) | |
| } | |
| func (w FSTestFilesystemReaderWriter) OpenFile(name string, flag int, perm os.FileMode) (File, error) { | |
| path := w.CleanPath(name) | |
| // If O_EXCL is set and the path exists (even as a symlink), fail. | |
| // Check the original path in the map directly. | |
| if _, exists := w.FS[path]; exists { | |
| if flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0 { | |
| return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrExist} | |
| } | |
| } | |
| // Resolve the path to handle symlinks. | |
| // If the file doesn't exist, resolvePath will return the path where it *should* be | |
| // (resolving parent symlinks). | |
| resolvedPath, err := w.resolvePath(path) | |
| if err != nil { | |
| // If resolvePath fails, it might be because of a loop or other error. | |
| // However, if creating a file, it might be okay if the final component doesn't exist. | |
| // But resolvePath returns the path including the non-existent final component if parents resolve. | |
| // So real errors from resolvePath (like ELOOP) should be propagated. | |
| return nil, &os.PathError{Op: "open", Path: name, Err: err} | |
| } | |
| // Work with the resolved path from now on. | |
| mapFile, exists := w.FS[resolvedPath] | |
| // Check if a parent component of the path is a file. | |
| dir := filepath.Dir(resolvedPath) | |
| if dir != "." && dir != "" { | |
| parentInfo, parentExists := w.FS[dir] | |
| if parentExists && !parentInfo.Mode.IsDir() { | |
| return nil, &os.PathError{Op: "open", Path: name, Err: fmt.Errorf("not a directory")} | |
| } | |
| } | |
| if flag&os.O_CREATE != 0 { | |
| if !exists { | |
| parent := filepath.Dir(resolvedPath) | |
| if parent != "." && parent != "" { | |
| parentInfo, parentExists := w.FS[parent] | |
| if !parentExists { | |
| return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} | |
| } | |
| if !parentInfo.Mode.IsDir() { | |
| return nil, &os.PathError{Op: "open", Path: name, Err: fmt.Errorf("not a directory")} | |
| } | |
| } | |
| newFile := &fstest.MapFile{Data: []byte{}, Mode: perm, ModTime: time.Now()} | |
| w.FS[resolvedPath] = newFile | |
| mapFile = newFile | |
| exists = true | |
| } | |
| } | |
| if !exists { | |
| return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} | |
| } | |
| if mapFile.Mode.IsDir() { | |
| if flag&(os.O_WRONLY|os.O_RDWR) != 0 { | |
| return nil, &os.PathError{Op: "open", Path: name, Err: fmt.Errorf("is a directory")} | |
| } | |
| if flag&os.O_TRUNC != 0 { | |
| return nil, &os.PathError{Op: "open", Path: name, Err: fmt.Errorf("is a directory")} | |
| } | |
| } | |
| // Create an os.FileInfo compatible wrapper for the fstest.MapFile. | |
| // Use the original name for the wrapper so that info.Name() matches what the user expects. | |
| info := &renamedFileInfo{FileInfo: &mapFileInfo{path: resolvedPath, file: mapFile}, name: filepath.Base(name)} | |
| handle := &fstestFileHandle{ | |
| path: resolvedPath, | |
| originalPath: name, | |
| info: info, | |
| fsMap: &w.FS, | |
| flag: flag, | |
| content: nil, // Set below | |
| offset: 0, // Set below | |
| } | |
| initialData := mapFile.Data | |
| if flag&os.O_TRUNC != 0 { | |
| initialData = []byte{} | |
| } | |
| handle.content = make([]byte, len(initialData)) | |
| copy(handle.content, initialData) | |
| if flag&os.O_APPEND != 0 { | |
| handle.offset = int64(len(handle.content)) | |
| } | |
| return handle, nil | |
| } | |
| func (w FSTestFilesystemReaderWriter) ReadFile(name string) ([]byte, error) { | |
| p := w.CleanPath(name) | |
| resolvedPath, err := w.resolvePath(p) | |
| if err != nil { | |
| return nil, &os.PathError{Op: "read", Path: name, Err: err} | |
| } | |
| // If the path is the root, it's always a directory. | |
| if resolvedPath == "." { | |
| return nil, &os.PathError{Op: "read", Path: name, Err: fmt.Errorf("is a directory")} | |
| } | |
| // Check for an explicit entry | |
| if mapFile, exists := w.FS[resolvedPath]; exists { | |
| if mapFile.Mode.IsDir() { | |
| return nil, &os.PathError{Op: "read", Path: name, Err: fmt.Errorf("is a directory")} | |
| } | |
| data := make([]byte, len(mapFile.Data)) | |
| copy(data, mapFile.Data) | |
| return data, nil | |
| } | |
| // No check for an implicit entry, since MkdirAll should create all the intermediate | |
| // directories. | |
| return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} | |
| } | |
| func (w FSTestFilesystemReaderWriter) ReadDir(dir string) ([]os.DirEntry, error) { | |
| p := w.CleanPath(dir) | |
| resolvedPath, err := w.resolvePath(p) | |
| if err != nil { | |
| return nil, &os.PathError{Op: "readdir", Path: dir, Err: err} | |
| } | |
| if mapFile, exists := w.FS[resolvedPath]; exists && !mapFile.Mode.IsDir() { | |
| return nil, &os.PathError{Op: "readdir", Path: dir, Err: fmt.Errorf("not a directory")} | |
| } | |
| return fs.ReadDir(w.fs(), resolvedPath) | |
| } | |
| // renamedFileInfo wraps an os.FileInfo to override its Name() method. | |
| type renamedFileInfo struct { | |
| os.FileInfo | |
| name string | |
| } | |
| func (i *renamedFileInfo) Name() string { | |
| return i.name | |
| } | |
| func (w FSTestFilesystemReaderWriter) Stat(name string) (os.FileInfo, error) { | |
| p := w.CleanPath(name) | |
| resolved, err := w.resolvePath(p) | |
| if err != nil { | |
| return nil, err | |
| } | |
| info, err := fs.Stat(w.fs(), resolved) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &renamedFileInfo{FileInfo: info, name: filepath.Base(name)}, nil | |
| } | |
| func (w FSTestFilesystemReaderWriter) Walk(root string, fn filepath.WalkFunc) error { | |
| p := w.CleanPath(root) | |
| // Resolve the parent of the root to handle symlinks in the path leading to the root. | |
| // Do not resolve the root itself, because Walk should not follow the root if it is a symlink. | |
| parent := path.Dir(p) | |
| base := path.Base(p) | |
| resolvedParent, err := w.resolvePath(parent) | |
| if err != nil { | |
| return fn(root, nil, err) | |
| } | |
| // Construct the path to start walking from in the internal map. | |
| fsRoot := base | |
| if resolvedParent != "." { | |
| fsRoot = resolvedParent + "/" + base | |
| } | |
| walkDirFn := func(pathStr string, d fs.DirEntry, err error) error { | |
| // fsRoot is where the walk started in the internal FS. | |
| // pathStr is the current path in the internal FS. | |
| rel, errRel := filepath.Rel(fsRoot, pathStr) | |
| if errRel != nil { | |
| return fn(pathStr, nil, fmt.Errorf("internal error creating relative path: %w", errRel)) | |
| } | |
| var fullPath string | |
| if rel == "." { | |
| fullPath = root | |
| } else { | |
| fullPath = filepath.Join(root, rel) | |
| } | |
| if err != nil { | |
| return fn(fullPath, nil, err) | |
| } | |
| info, errInfo := d.Info() | |
| if errInfo != nil { | |
| return fn(fullPath, nil, errInfo) | |
| } | |
| return fn(fullPath, info, nil) | |
| } | |
| return fs.WalkDir(w.fs(), fsRoot, walkDirFn) | |
| } | |
| // --- FilesystemWriter implementation --- | |
| func (w FSTestFilesystemReaderWriter) Create(name string) (File, error) { | |
| return w.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) | |
| } | |
| func (w FSTestFilesystemReaderWriter) Mkdir(dir string, perm os.FileMode) error { | |
| p := w.CleanPath(dir) | |
| if p == "." { | |
| // The root directory always exists in a fs.FS. | |
| // os.Mkdir returns EEXIST in this case. | |
| return &os.PathError{Op: "mkdir", Path: dir, Err: os.ErrExist} | |
| } | |
| if _, exists := w.FS[p]; exists { | |
| return &os.PathError{Op: "mkdir", Path: dir, Err: os.ErrExist} | |
| } | |
| resolvedPath, err := w.resolvePath(p) | |
| if err != nil { | |
| return &os.PathError{Op: "mkdir", Path: dir, Err: err} | |
| } | |
| if _, exists := w.FS[resolvedPath]; exists { | |
| return &os.PathError{Op: "mkdir", Path: dir, Err: os.ErrExist} | |
| } | |
| parent := filepath.Dir(resolvedPath) | |
| if parent != "." && parent != "" { | |
| parentInfo, parentExists := w.FS[parent] | |
| if !parentExists { | |
| return &os.PathError{Op: "mkdir", Path: dir, Err: os.ErrNotExist} | |
| } | |
| if !parentInfo.Mode.IsDir() { | |
| return &os.PathError{Op: "mkdir", Path: dir, Err: fmt.Errorf("not a directory")} | |
| } | |
| } | |
| w.FS[resolvedPath] = &fstest.MapFile{Mode: os.ModeDir | perm, ModTime: time.Now()} | |
| return nil | |
| } | |
| func (w FSTestFilesystemReaderWriter) MkdirAll(dir string, perm os.FileMode) error { | |
| err := w.Mkdir(dir, perm) | |
| if err == nil { | |
| return nil | |
| } | |
| if os.IsExist(err) { | |
| info, statErr := w.Stat(dir) | |
| if statErr == nil && info.IsDir() { | |
| // p already exists and is a directory, so return success | |
| return nil | |
| } | |
| // p already exists and is not a directory, so propagate the error | |
| return err | |
| } | |
| if !os.IsNotExist(err) && !errors.Is(err, syscall.ELOOP) { | |
| // Unexpected failure, probably indicating a bad path, i.e. parent is a file, so propagate the error | |
| return err | |
| } | |
| // At this point, it is known the problem was the parent didn't exist, so need to recursively create it. | |
| p := w.CleanPath(dir) | |
| parent := filepath.Dir(p) | |
| if parent == "." || parent == p { | |
| // The parent is the root of the filesystem, so Mkdir should have succeeded, so propagating the error. | |
| return err | |
| } | |
| if err := w.MkdirAll(parent, perm); err != nil { | |
| return err | |
| } | |
| // Creating the directory now that the parent exists. | |
| return w.Mkdir(p, perm) | |
| } | |
| // tempCounter is an incrementing value used for generating the 'random' part of temporary file | |
| // names. | |
| var tempCounter atomic.Uint32 | |
| func (w FSTestFilesystemReaderWriter) MkdirTemp(dir, pattern string) (string, error) { | |
| prefix, suffix := pattern, "" | |
| if i := strings.LastIndex(pattern, "*"); i != -1 { | |
| prefix, suffix = pattern[:i], pattern[i+1:] | |
| } | |
| // Note: This is using deterministic values for the 'random' part of the path names. This code | |
| // should only be used for testing purposes, since an attacker might be able to exploit this. | |
| rndStr := fmt.Sprintf("%d", tempCounter.Add(1)) | |
| name := prefix + rndStr + suffix | |
| path := filepath.Join(dir, name) | |
| err := w.Mkdir(path, 0700|os.ModeDir) | |
| if err != nil { | |
| return "", err | |
| } | |
| return path, nil | |
| } | |
| func (w FSTestFilesystemReaderWriter) Remove(name string) error { | |
| p := w.CleanPath(name) | |
| parent := filepath.Dir(p) | |
| base := filepath.Base(p) | |
| resolvedParent := parent | |
| if parent != "." { | |
| var err error | |
| resolvedParent, err = w.resolvePath(parent) | |
| if err != nil { | |
| return &os.PathError{Op: "remove", Path: name, Err: err} | |
| } | |
| } | |
| pathToRemove := base | |
| if resolvedParent != "." { | |
| pathToRemove = resolvedParent + "/" + base | |
| } | |
| entry, exists := w.FS[pathToRemove] | |
| if !exists { | |
| return &os.PathError{Op: "remove", Path: name, Err: os.ErrNotExist} | |
| } | |
| if entry.Mode.IsDir() { | |
| prefix := pathToRemove + "/" | |
| for k := range w.FS { | |
| if strings.HasPrefix(k, prefix) { | |
| return &os.PathError{Op: "remove", Path: name, Err: syscall.ENOTEMPTY} | |
| } | |
| } | |
| } | |
| delete(w.FS, pathToRemove) | |
| return nil | |
| } | |
| func (w FSTestFilesystemReaderWriter) RemoveAll(path string) error { | |
| p := w.CleanPath(path) | |
| parent := filepath.Dir(p) | |
| base := filepath.Base(p) | |
| resolvedParent := parent | |
| if parent != "." { | |
| var err error | |
| resolvedParent, err = w.resolvePath(parent) | |
| if err != nil { | |
| // If the path does not exist, RemoveAll returns nil. | |
| if os.IsNotExist(err) { | |
| return nil | |
| } | |
| return &os.PathError{Op: "removeall", Path: path, Err: err} | |
| } | |
| // If the resolved parent exists but is not a directory, this should return an error. | |
| // However, resolvePath resolves symlinks, so if it returns successfully, | |
| // the resolved path exists. Thus, a directory check is required. | |
| if info, exists := w.FS[resolvedParent]; exists && !info.Mode.IsDir() { | |
| return &os.PathError{Op: "removeall", Path: path, Err: fmt.Errorf("not a directory")} | |
| } | |
| } | |
| pathToRemove := base | |
| if resolvedParent != "." { | |
| pathToRemove = resolvedParent + "/" + base | |
| } | |
| entry, exists := w.FS[pathToRemove] | |
| if !exists { | |
| return nil | |
| } | |
| if entry.Mode.IsDir() { | |
| prefix := pathToRemove + "/" | |
| for key := range w.FS { | |
| if key == pathToRemove || strings.HasPrefix(key, prefix) { | |
| delete(w.FS, key) | |
| } | |
| } | |
| } else { | |
| // It's a file or symlink. | |
| delete(w.FS, pathToRemove) | |
| } | |
| return nil | |
| } | |
| func (w FSTestFilesystemReaderWriter) Symlink(oldname, newname string) error { | |
| p := w.CleanPath(newname) | |
| parent := path.Dir(p) | |
| base := path.Base(p) | |
| resolvedParent := parent | |
| if parent != "." && parent != "" { | |
| var err error | |
| resolvedParent, err = w.resolvePath(parent) | |
| if err != nil { | |
| return &os.PathError{Op: "symlink", Path: newname, Err: err} | |
| } | |
| parentInfo, parentExists := w.FS[resolvedParent] | |
| if !parentExists { | |
| return &os.PathError{Op: "symlink", Path: newname, Err: os.ErrNotExist} | |
| } | |
| if !parentInfo.Mode.IsDir() { | |
| return &os.PathError{Op: "symlink", Path: newname, Err: fmt.Errorf("not a directory")} | |
| } | |
| } | |
| var targetPath string | |
| if resolvedParent == "." { | |
| targetPath = base | |
| } else { | |
| targetPath = resolvedParent + "/" + base | |
| } | |
| if _, exists := w.FS[targetPath]; exists { | |
| return &os.PathError{Op: "symlink", Path: newname, Err: os.ErrExist} | |
| } | |
| // For symlinks, the Data field holds the target path. | |
| // Do NOT clean oldname (the target), as it might be a relative path intended | |
| // to be resolved relative to the link's location, or an absolute path. | |
| // Store it exactly as provided. | |
| w.FS[targetPath] = &fstest.MapFile{ | |
| Data: []byte(oldname), | |
| Mode: fs.ModeSymlink | 0777, | |
| ModTime: time.Now(), | |
| } | |
| return nil | |
| } | |
| func (w FSTestFilesystemReaderWriter) WriteFile(name string, data []byte, perm os.FileMode) error { | |
| p := w.CleanPath(name) | |
| resolvedPath, err := w.resolvePath(p) | |
| if err != nil { | |
| return &os.PathError{Op: "open", Path: name, Err: err} | |
| } | |
| if info, exists := w.FS[resolvedPath]; exists { | |
| if info.Mode.IsDir() { | |
| return &os.PathError{Op: "open", Path: name, Err: fmt.Errorf("is a directory")} | |
| } | |
| // If the file exists, preserve the mode. | |
| perm = info.Mode | |
| } else { | |
| dir := filepath.Dir(resolvedPath) | |
| if dir != "." && dir != "" { | |
| parentInfo, parentExists := w.FS[dir] | |
| if !parentExists { | |
| return &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} | |
| } | |
| if !parentInfo.Mode.IsDir() { | |
| return &os.PathError{Op: "open", Path: name, Err: fmt.Errorf("not a directory")} | |
| } | |
| } | |
| } | |
| w.FS[resolvedPath] = &fstest.MapFile{Data: data, Mode: perm, ModTime: time.Now()} | |
| return nil | |
| } | |
| // --- File implementation --- | |
| func (h *fstestFileHandle) Stat() (os.FileInfo, error) { | |
| if h.closed { | |
| return nil, &os.PathError{Op: "stat", Path: h.originalPath, Err: os.ErrClosed} | |
| } | |
| return h.info, nil | |
| } | |
| func (h *fstestFileHandle) Close() error { | |
| if h.closed { | |
| return &os.PathError{Op: "close", Path: h.originalPath, Err: os.ErrClosed} | |
| } | |
| h.closed = true | |
| // Only flush content back to the map if the file was opened for writing. | |
| if h.flag&(os.O_WRONLY|os.O_RDWR) != 0 { | |
| if mapFile, exists := (*h.fsMap)[h.path]; exists { | |
| mapFile.Data = h.content | |
| mapFile.ModTime = time.Now() | |
| } | |
| } | |
| return nil | |
| } | |
| func (h *fstestFileHandle) Read(p []byte) (int, error) { | |
| if h.closed { | |
| return 0, &os.PathError{Op: "read", Path: h.originalPath, Err: os.ErrClosed} | |
| } | |
| if h.flag&os.O_WRONLY != 0 { | |
| return 0, &os.PathError{Op: "read", Path: h.originalPath, Err: os.ErrInvalid} | |
| } | |
| if h.offset >= int64(len(h.content)) { | |
| return 0, io.EOF | |
| } | |
| n := copy(p, h.content[h.offset:]) | |
| h.offset += int64(n) | |
| return n, nil | |
| } | |
| func (h *fstestFileHandle) Write(p []byte) (int, error) { | |
| if h.closed { | |
| return 0, &os.PathError{Op: "write", Path: h.originalPath, Err: os.ErrClosed} | |
| } | |
| if h.flag&(os.O_WRONLY|os.O_RDWR) == 0 { | |
| return 0, &os.PathError{Op: "write", Path: h.originalPath, Err: os.ErrInvalid} | |
| } | |
| if h.flag&os.O_APPEND != 0 { | |
| h.offset = int64(len(h.content)) | |
| } | |
| n := len(p) | |
| end := h.offset + int64(n) | |
| if end > int64(len(h.content)) { | |
| newContent := make([]byte, end) | |
| copy(newContent, h.content) | |
| h.content = newContent | |
| } | |
| copy(h.content[h.offset:end], p) | |
| h.offset = end | |
| return n, nil | |
| } | |
| func (h *fstestFileHandle) Seek(offset int64, whence int) (int64, error) { | |
| if h.closed { | |
| return 0, &os.PathError{Op: "seek", Path: h.originalPath, Err: os.ErrClosed} | |
| } | |
| var newOffset int64 | |
| switch whence { | |
| case io.SeekStart: | |
| newOffset = offset | |
| case io.SeekCurrent: | |
| newOffset = h.offset + offset | |
| case io.SeekEnd: | |
| newOffset = int64(len(h.content)) + offset | |
| default: | |
| return 0, &os.PathError{Op: "seek", Path: h.originalPath, Err: os.ErrInvalid} | |
| } | |
| if newOffset < 0 { | |
| return 0, &os.PathError{Op: "seek", Path: h.originalPath, Err: os.ErrInvalid} | |
| } | |
| h.offset = newOffset | |
| return h.offset, nil | |
| } |