// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. package errorfs import ( "fmt" "io" "os" "strings" "sync/atomic" "github.com/cockroachdb/errors" "github.com/cockroachdb/errors/oserror" "github.com/cockroachdb/pebble/internal/dsl" "github.com/cockroachdb/pebble/vfs" ) // ErrInjected is an error artificially injected for testing fs error paths. var ErrInjected = LabelledError{ error: errors.New("injected error"), Label: "ErrInjected", } // Op describes a filesystem operation. type Op struct { // Kind describes the particular kind of operation being performed. Kind OpKind // Path is the path of the file of the file being operated on. Path string // Offset is the offset of an operation. It's set for OpFileReadAt and // OpFileWriteAt operations. Offset int64 } // OpKind is an enum describing the type of operation. type OpKind int const ( // OpCreate describes a create file operation. OpCreate OpKind = iota // OpLink describes a hardlink operation. OpLink // OpOpen describes a file open operation. OpOpen // OpOpenDir describes a directory open operation. OpOpenDir // OpRemove describes a remove file operation. OpRemove // OpRemoveAll describes a recursive remove operation. OpRemoveAll // OpRename describes a rename operation. OpRename // OpReuseForWrite describes a reuse for write operation. OpReuseForWrite // OpMkdirAll describes a make directory including parents operation. OpMkdirAll // OpLock describes a lock file operation. OpLock // OpList describes a list directory operation. OpList // OpFilePreallocate describes a file preallocate operation. OpFilePreallocate // OpStat describes a path-based stat operation. OpStat // OpGetDiskUsage describes a disk usage operation. OpGetDiskUsage // OpFileClose describes a close file operation. OpFileClose // OpFileRead describes a file read operation. OpFileRead // OpFileReadAt describes a file seek read operation. OpFileReadAt // OpFileWrite describes a file write operation. OpFileWrite // OpFileWriteAt describes a file seek write operation. OpFileWriteAt // OpFileStat describes a file stat operation. OpFileStat // OpFileSync describes a file sync operation. OpFileSync // OpFileFlush describes a file flush operation. OpFileFlush ) // ReadOrWrite returns the operation's kind. func (o OpKind) ReadOrWrite() OpReadWrite { switch o { case OpOpen, OpOpenDir, OpList, OpStat, OpGetDiskUsage, OpFileRead, OpFileReadAt, OpFileStat: return OpIsRead case OpCreate, OpLink, OpRemove, OpRemoveAll, OpRename, OpReuseForWrite, OpMkdirAll, OpLock, OpFileClose, OpFileWrite, OpFileWriteAt, OpFileSync, OpFileFlush, OpFilePreallocate: return OpIsWrite default: panic(fmt.Sprintf("unrecognized op %v\n", o)) } } // OpReadWrite is an enum describing whether an operation is a read or write // operation. type OpReadWrite int const ( // OpIsRead describes read operations. OpIsRead OpReadWrite = iota // OpIsWrite describes write operations. OpIsWrite ) // String implements fmt.Stringer. func (kind OpReadWrite) String() string { switch kind { case OpIsRead: return "Reads" case OpIsWrite: return "Writes" default: panic(fmt.Sprintf("unrecognized OpKind %d", kind)) } } // OnIndex is a convenience function for constructing a dsl.OnIndex for use with // an error-injecting filesystem. func OnIndex(index int32) *InjectIndex { return &InjectIndex{dsl.OnIndex[Op](index)} } // InjectIndex implements Injector, injecting an error at a specific index. type InjectIndex struct { *dsl.Index[Op] } // MaybeError implements the Injector interface. // // TODO(jackson): Remove this implementation and update callers to compose it // with other injectors. func (ii *InjectIndex) MaybeError(op Op) error { if !ii.Evaluate(op) { return nil } return ErrInjected } // InjectorFunc implements the Injector interface for a function with // MaybeError's signature. type InjectorFunc func(Op) error // String implements fmt.Stringer. func (f InjectorFunc) String() string { return "" } // MaybeError implements the Injector interface. func (f InjectorFunc) MaybeError(op Op) error { return f(op) } // Injector injects errors into FS operations. type Injector interface { fmt.Stringer // MaybeError is invoked by an errorfs before an operation is executed. It // is passed an enum indicating the type of operation and a path of the // subject file or directory. If the operation takes two paths (eg, // Rename, Link), the original source path is provided. MaybeError(op Op) error } // Any returns an injector that injects an error if any of the provided // injectors inject an error. The error returned by the first injector to return // an error is used. func Any(injectors ...Injector) Injector { return anyInjector(injectors) } type anyInjector []Injector func (a anyInjector) String() string { var sb strings.Builder sb.WriteString("(Any") for _, inj := range a { sb.WriteString(" ") sb.WriteString(inj.String()) } sb.WriteString(")") return sb.String() } func (a anyInjector) MaybeError(op Op) error { for _, inj := range a { if err := inj.MaybeError(op); err != nil { return err } } return nil } // Counter wraps an Injector, counting the number of errors injected. It may be // used in tests to ensure that when an error is injected, the error is // surfaced through the user interface. type Counter struct { Injector atomic.Uint64 } // String implements fmt.Stringer. func (c *Counter) String() string { return c.Injector.String() } // MaybeError implements Injector. func (c *Counter) MaybeError(op Op) error { err := c.Injector.MaybeError(op) if err != nil { c.Uint64.Add(1) } return err } // Toggle wraps an Injector. By default, Toggle injects nothing. When toggled on // through its On method, it begins injecting errors when the contained injector // injects them. It may be returned to its original state through Off. type Toggle struct { Injector on atomic.Bool } // String implements fmt.Stringer. func (t *Toggle) String() string { return t.Injector.String() } // MaybeError implements Injector. func (t *Toggle) MaybeError(op Op) error { if !t.on.Load() { return nil } return t.Injector.MaybeError(op) } // On enables error injection. func (t *Toggle) On() { t.on.Store(true) } // Off disables error injection. func (t *Toggle) Off() { t.on.Store(false) } // FS implements vfs.FS, injecting errors into // its operations. type FS struct { fs vfs.FS inj Injector } // Wrap wraps an existing vfs.FS implementation, returning a new // vfs.FS implementation that shadows operations to the provided FS. // It uses the provided Injector for deciding when to inject errors. // If an error is injected, FS propagates the error instead of // shadowing the operation. func Wrap(fs vfs.FS, inj Injector) *FS { return &FS{ fs: fs, inj: inj, } } // WrapFile wraps an existing vfs.File, returning a new vfs.File that shadows // operations to the provided vfs.File. It uses the provided Injector for // deciding when to inject errors. If an error is injected, the file // propagates the error instead of shadowing the operation. func WrapFile(f vfs.File, inj Injector) vfs.File { return &errorFile{file: f, inj: inj} } // Unwrap returns the FS implementation underlying fs. // See pebble/vfs.Root. func (fs *FS) Unwrap() vfs.FS { return fs.fs } // Create implements FS.Create. func (fs *FS) Create(name string) (vfs.File, error) { if err := fs.inj.MaybeError(Op{Kind: OpCreate, Path: name}); err != nil { return nil, err } f, err := fs.fs.Create(name) if err != nil { return nil, err } return &errorFile{name, f, fs.inj}, nil } // Link implements FS.Link. func (fs *FS) Link(oldname, newname string) error { if err := fs.inj.MaybeError(Op{Kind: OpLink, Path: oldname}); err != nil { return err } return fs.fs.Link(oldname, newname) } // Open implements FS.Open. func (fs *FS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) { if err := fs.inj.MaybeError(Op{Kind: OpOpen, Path: name}); err != nil { return nil, err } f, err := fs.fs.Open(name) if err != nil { return nil, err } ef := &errorFile{name, f, fs.inj} for _, opt := range opts { opt.Apply(ef) } return ef, nil } // OpenReadWrite implements FS.OpenReadWrite. func (fs *FS) OpenReadWrite(name string, opts ...vfs.OpenOption) (vfs.File, error) { if err := fs.inj.MaybeError(Op{Kind: OpOpen, Path: name}); err != nil { return nil, err } f, err := fs.fs.OpenReadWrite(name) if err != nil { return nil, err } ef := &errorFile{name, f, fs.inj} for _, opt := range opts { opt.Apply(ef) } return ef, nil } // OpenDir implements FS.OpenDir. func (fs *FS) OpenDir(name string) (vfs.File, error) { if err := fs.inj.MaybeError(Op{Kind: OpOpenDir, Path: name}); err != nil { return nil, err } f, err := fs.fs.OpenDir(name) if err != nil { return nil, err } return &errorFile{name, f, fs.inj}, nil } // GetDiskUsage implements FS.GetDiskUsage. func (fs *FS) GetDiskUsage(path string) (vfs.DiskUsage, error) { if err := fs.inj.MaybeError(Op{Kind: OpGetDiskUsage, Path: path}); err != nil { return vfs.DiskUsage{}, err } return fs.fs.GetDiskUsage(path) } // PathBase implements FS.PathBase. func (fs *FS) PathBase(p string) string { return fs.fs.PathBase(p) } // PathDir implements FS.PathDir. func (fs *FS) PathDir(p string) string { return fs.fs.PathDir(p) } // PathJoin implements FS.PathJoin. func (fs *FS) PathJoin(elem ...string) string { return fs.fs.PathJoin(elem...) } // Remove implements FS.Remove. func (fs *FS) Remove(name string) error { if _, err := fs.fs.Stat(name); oserror.IsNotExist(err) { return nil } if err := fs.inj.MaybeError(Op{Kind: OpRemove, Path: name}); err != nil { return err } return fs.fs.Remove(name) } // RemoveAll implements FS.RemoveAll. func (fs *FS) RemoveAll(fullname string) error { if err := fs.inj.MaybeError(Op{Kind: OpRemoveAll, Path: fullname}); err != nil { return err } return fs.fs.RemoveAll(fullname) } // Rename implements FS.Rename. func (fs *FS) Rename(oldname, newname string) error { if err := fs.inj.MaybeError(Op{Kind: OpRename, Path: oldname}); err != nil { return err } return fs.fs.Rename(oldname, newname) } // ReuseForWrite implements FS.ReuseForWrite. func (fs *FS) ReuseForWrite(oldname, newname string) (vfs.File, error) { if err := fs.inj.MaybeError(Op{Kind: OpReuseForWrite, Path: oldname}); err != nil { return nil, err } return fs.fs.ReuseForWrite(oldname, newname) } // MkdirAll implements FS.MkdirAll. func (fs *FS) MkdirAll(dir string, perm os.FileMode) error { if err := fs.inj.MaybeError(Op{Kind: OpMkdirAll, Path: dir}); err != nil { return err } return fs.fs.MkdirAll(dir, perm) } // Lock implements FS.Lock. func (fs *FS) Lock(name string) (io.Closer, error) { if err := fs.inj.MaybeError(Op{Kind: OpLock, Path: name}); err != nil { return nil, err } return fs.fs.Lock(name) } // List implements FS.List. func (fs *FS) List(dir string) ([]string, error) { if err := fs.inj.MaybeError(Op{Kind: OpList, Path: dir}); err != nil { return nil, err } return fs.fs.List(dir) } // Stat implements FS.Stat. func (fs *FS) Stat(name string) (os.FileInfo, error) { if err := fs.inj.MaybeError(Op{Kind: OpStat, Path: name}); err != nil { return nil, err } return fs.fs.Stat(name) } // errorFile implements vfs.File. The interface is implemented on the pointer // type to allow pointer equality comparisons. type errorFile struct { path string file vfs.File inj Injector } func (f *errorFile) Close() error { // We don't inject errors during close as those calls should never fail in // practice. return f.file.Close() } func (f *errorFile) Read(p []byte) (int, error) { if err := f.inj.MaybeError(Op{Kind: OpFileRead, Path: f.path}); err != nil { return 0, err } return f.file.Read(p) } func (f *errorFile) ReadAt(p []byte, off int64) (int, error) { if err := f.inj.MaybeError(Op{ Kind: OpFileReadAt, Path: f.path, Offset: off, }); err != nil { return 0, err } return f.file.ReadAt(p, off) } func (f *errorFile) Write(p []byte) (int, error) { if err := f.inj.MaybeError(Op{Kind: OpFileWrite, Path: f.path}); err != nil { return 0, err } return f.file.Write(p) } func (f *errorFile) WriteAt(p []byte, off int64) (int, error) { if err := f.inj.MaybeError(Op{ Kind: OpFileWriteAt, Path: f.path, Offset: off, }); err != nil { return 0, err } return f.file.WriteAt(p, off) } func (f *errorFile) Stat() (os.FileInfo, error) { if err := f.inj.MaybeError(Op{Kind: OpFileStat, Path: f.path}); err != nil { return nil, err } return f.file.Stat() } func (f *errorFile) Prefetch(offset, length int64) error { // TODO(radu): Consider error injection. return f.file.Prefetch(offset, length) } func (f *errorFile) Preallocate(offset, length int64) error { if err := f.inj.MaybeError(Op{Kind: OpFilePreallocate, Path: f.path}); err != nil { return err } return f.file.Preallocate(offset, length) } func (f *errorFile) Sync() error { if err := f.inj.MaybeError(Op{Kind: OpFileSync, Path: f.path}); err != nil { return err } return f.file.Sync() } func (f *errorFile) SyncData() error { // TODO(jackson): Consider error injection. return f.file.SyncData() } func (f *errorFile) SyncTo(length int64) (fullSync bool, err error) { // TODO(jackson): Consider error injection. return f.file.SyncTo(length) } func (f *errorFile) Fd() uintptr { return f.file.Fd() }