package ctxbilly import ( "context" "errors" "fmt" "os" "path/filepath" "strings" securejoin "github.com/cyphar/filepath-securejoin" "github.com/iceber/iouring-go" ) func NewURingFS() (*UringFS, error) { ur, err := iouring.New(64, iouring.WithAsync()) if err != nil { return nil, err } return &UringFS{ ur: ur, }, nil } var _ Filesystem = (*UringFS)(nil) const ( defaultDirectoryMode = 0o755 defaultCreateMode = 0o666 ) // UringFS is a fs implementation based on the OS filesystem which is bound to // a base dir. // Prefer this fs implementation over ChrootOS. // // Behaviours of note: // 1. Read and write operations can only be directed to files which descends // from the base dir. // 2. Symlinks don't have their targets modified, and therefore can point // to locations outside the base dir or to non-existent paths. // 3. Readlink and Lstat ensures that the link file is located within the base // dir, evaluating any symlinks that file or base dir may contain. type UringFS struct { ur *iouring.IOURing baseDir string } func newBoundOS(d string) *UringFS { return &UringFS{baseDir: d} } func (fs *UringFS) Create(ctx context.Context, filename string) (File, error) { return fs.OpenFile(ctx, filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode) } func (fs *UringFS) OpenFile(ctx context.Context, filename string, flag int, perm os.FileMode) (File, error) { fn, err := fs.abs(filename) if err != nil { return nil, err } f, err := os.OpenFile(fn, flag, perm) if err != nil { return nil, err } return newFile(fs.ur, f) } func (fs *UringFS) ReadDir(ctx context.Context, path string) ([]os.FileInfo, error) { dir, err := fs.abs(path) if err != nil { return nil, err } entries, err := os.ReadDir(dir) if err != nil { return nil, err } infos := make([]os.FileInfo, 0, len(entries)) for _, v := range entries { info, err := v.Info() if err != nil { return nil, err } infos = append(infos, info) } return infos, nil } func (fs *UringFS) Rename(ctx context.Context, from, to string) error { f, err := fs.abs(from) if err != nil { return err } t, err := fs.abs(to) if err != nil { return err } // MkdirAll for target name. if err := fs.createDir(t); err != nil { return err } return os.Rename(f, t) } func (fs *UringFS) MkdirAll(ctx context.Context, path string, perm os.FileMode) error { dir, err := fs.abs(path) if err != nil { return err } return os.MkdirAll(dir, perm) } func (fs *UringFS) Open(ctx context.Context, filename string) (File, error) { return fs.OpenFile(ctx, filename, os.O_RDONLY, 0) } func (fs *UringFS) Stat(ctx context.Context, filename string) (os.FileInfo, error) { filename, err := fs.abs(filename) if err != nil { return nil, err } return os.Stat(filename) } func (fs *UringFS) Remove(ctx context.Context, filename string) error { fn, err := fs.abs(filename) if err != nil { return err } return os.Remove(fn) } func (fs *UringFS) Join(elem ...string) string { return filepath.Join(elem...) } func (fs *UringFS) RemoveAll(path string) error { dir, err := fs.abs(path) if err != nil { return err } return os.RemoveAll(dir) } func (fs *UringFS) Symlink(ctx context.Context, target, link string) error { ln, err := fs.abs(link) if err != nil { return err } // MkdirAll for containing dir. if err := fs.createDir(ln); err != nil { return err } return os.Symlink(target, ln) } func (fs *UringFS) Lstat(ctx context.Context, filename string) (os.FileInfo, error) { filename = filepath.Clean(filename) if !filepath.IsAbs(filename) { filename = filepath.Join(fs.baseDir, filename) } if ok, err := fs.insideBaseDirEval(filename); !ok { return nil, err } return os.Lstat(filename) } func (fs *UringFS) Readlink(ctx context.Context, link string) (string, error) { if !filepath.IsAbs(link) { link = filepath.Clean(filepath.Join(fs.baseDir, link)) } if ok, err := fs.insideBaseDirEval(link); !ok { return "", err } return os.Readlink(link) } // Chroot returns a new OS filesystem, with the base dir set to the // result of joining the provided path with the underlying base dir. // func (fs *UringFS) Chroot(path string) (Filesystem, error) { // joined, err := securejoin.SecureJoin(fs.baseDir, path) // if err != nil { // return nil, err // } // return newBoundOS(joined), nil // } // Root returns the current base dir of the billy.Filesystem. // This is required in order for this implementation to be a drop-in // replacement for other upstream implementations (e.g. memory and osfs). func (fs *UringFS) Root() string { return fs.baseDir } func (fs *UringFS) createDir(fullpath string) error { dir := filepath.Dir(fullpath) if dir != "." { if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil { return err } } return nil } // abs transforms filename to an absolute path, taking into account the base dir. // Relative paths won't be allowed to ascend the base dir, so `../file` will become // `/working-dir/file`. // // Note that if filename is a symlink, the returned address will be the target of the // symlink. func (fs *UringFS) abs(filename string) (string, error) { if filename == fs.baseDir { filename = string(filepath.Separator) } path, err := securejoin.SecureJoin(fs.baseDir, filename) if err != nil { return "", nil } return path, nil } // insideBaseDirEval checks whether filename is contained within // a dir that is within the fs.baseDir, by first evaluating any symlinks // that either filename or fs.baseDir may contain. func (fs *UringFS) insideBaseDirEval(filename string) (bool, error) { dir, err := filepath.EvalSymlinks(filepath.Dir(filename)) if dir == "" || os.IsNotExist(err) { dir = filepath.Dir(filename) } wd, err := filepath.EvalSymlinks(fs.baseDir) if wd == "" || os.IsNotExist(err) { wd = fs.baseDir } if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) { return false, fmt.Errorf("path outside base dir") } return true, nil } func newFile(fsur *iouring.IOURing, f *os.File) (*URingFile, error) { ur, err := iouring.New(64, iouring.WithAttachWQ(fsur)) if err != nil { return nil, err } return &URingFile{ ur: ur, f: f, }, nil } type URingFile struct { ur *iouring.IOURing f *os.File } // Close implements File. func (o *URingFile) Close(ctx context.Context) error { return errors.Join(o.ur.UnregisterFile(o.f), o.Close(ctx)) } // Name implements File. func (o *URingFile) Name() string { return o.f.Name() } // Read implements File. func (o *URingFile) Read(ctx context.Context, p []byte) (n int, err error) { req, err := o.ur.Read(o.f, p, nil) if err != nil { return 0, err } defer req.Cancel() select { case <-req.Done(): return req.GetRes() case <-ctx.Done(): req.Cancel() <-req.Done() return req.GetRes() } } // ReadAt implements File. func (o *URingFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) { req, err := o.ur.Pread(o.f, p, uint64(off), nil) if err != nil { return 0, err } defer req.Cancel() select { case <-req.Done(): return req.GetRes() case <-ctx.Done(): req.Cancel() <-req.Done() return req.GetRes() } } // Write implements File. func (o *URingFile) Write(ctx context.Context, p []byte) (n int, err error) { req, err := o.ur.Write(o.f, p, nil) if err != nil { return 0, err } defer req.Cancel() select { case <-req.Done(): return req.GetRes() case <-ctx.Done(): req.Cancel() <-req.Done() return req.GetRes() } } // WriteAt implements File. func (o *URingFile) WriteAt(ctx context.Context, p []byte, off int64) (n int, err error) { req, err := o.ur.Pwrite(o.f, p, uint64(off), nil) if err != nil { return 0, err } defer req.Cancel() select { case <-req.Done(): return req.GetRes() case <-ctx.Done(): req.Cancel() <-req.Done() return req.GetRes() } } // Seek implements File. func (o *URingFile) Seek(offset int64, whence int) (int64, error) { return o.f.Seek(offset, whence) } // Truncate implements File. func (o *URingFile) Truncate(ctx context.Context, size int64) error { return o.f.Truncate(size) } var _ File = (*URingFile)(nil)