server(filesystem): rebuild everything imaginable
This wonderfully large commit replaces basically everything under the `server/filesystem` package, re-implementing essentially everything. This is related to https://github.com/pterodactyl/wings/security/advisories/GHSA-494h-9924-xww9 If any vulnerabilities related to symlinks persist after this commit, I will be very upset. Signed-off-by: Matthew Penner <me@matthewp.io>
This commit is contained in:
21
internal/ufs/LICENSE
Normal file
21
internal/ufs/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Matthew Penner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
17
internal/ufs/README.md
Normal file
17
internal/ufs/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Filesystem
|
||||
|
||||
## Licensing
|
||||
|
||||
Most code in this package is licensed under `MIT` with some exceptions.
|
||||
|
||||
The following files are licensed under `BSD-3-Clause` due to them being copied
|
||||
verbatim or derived from [Go](https://go.dev)'s source code.
|
||||
|
||||
- [`file_posix.go`](./file_posix.go)
|
||||
- [`mkdir_unix.go`](./mkdir_unix.go)
|
||||
- [`path_unix.go`](./path_unix.go)
|
||||
- [`removeall_unix.go`](./removeall_unix.go)
|
||||
- [`stat_unix.go`](./stat_unix.go)
|
||||
- [`walk.go`](./walk.go)
|
||||
|
||||
These changes are not associated with nor endorsed by The Go Authors.
|
||||
12
internal/ufs/doc.go
Normal file
12
internal/ufs/doc.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
// Package ufs provides an abstraction layer for performing I/O on filesystems.
|
||||
// This package is designed to be used in-place of standard `os` package I/O
|
||||
// calls, and is not designed to be used as a generic filesystem abstraction
|
||||
// like the `io/fs` package.
|
||||
//
|
||||
// The primary use-case of this package was to provide a "chroot-like" `os`
|
||||
// wrapper, so we can safely sandbox I/O operations within a directory and
|
||||
// use untrusted arbitrary paths.
|
||||
package ufs
|
||||
120
internal/ufs/error.go
Normal file
120
internal/ufs/error.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
iofs "io/fs"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrIsDirectory is an error for when an operation that operates only on
|
||||
// files is given a path to a directory.
|
||||
ErrIsDirectory = errors.New("is a directory")
|
||||
// ErrNotDirectory is an error for when an operation that operates only on
|
||||
// directories is given a path to a file.
|
||||
ErrNotDirectory = errors.New("not a directory")
|
||||
// ErrBadPathResolution is an error for when a sand-boxed filesystem
|
||||
// resolves a given path to a forbidden location.
|
||||
ErrBadPathResolution = errors.New("bad path resolution")
|
||||
// ErrNotRegular is an error for when an operation that operates only on
|
||||
// regular files is passed something other than a regular file.
|
||||
ErrNotRegular = errors.New("not a regular file")
|
||||
|
||||
// ErrClosed is an error for when an entry was accessed after being closed.
|
||||
ErrClosed = iofs.ErrClosed
|
||||
// ErrInvalid is an error for when an invalid argument was used.
|
||||
ErrInvalid = iofs.ErrInvalid
|
||||
// ErrExist is an error for when an entry already exists.
|
||||
ErrExist = iofs.ErrExist
|
||||
// ErrNotExist is an error for when an entry does not exist.
|
||||
ErrNotExist = iofs.ErrNotExist
|
||||
// ErrPermission is an error for when the required permissions to perform an
|
||||
// operation are missing.
|
||||
ErrPermission = iofs.ErrPermission
|
||||
)
|
||||
|
||||
// LinkError records an error during a link or symlink or rename
|
||||
// system call and the paths that caused it.
|
||||
type LinkError = os.LinkError
|
||||
|
||||
// PathError records an error and the operation and file path that caused it.
|
||||
type PathError = iofs.PathError
|
||||
|
||||
// SyscallError records an error from a specific system call.
|
||||
type SyscallError = os.SyscallError
|
||||
|
||||
// NewSyscallError returns, as an error, a new SyscallError
|
||||
// with the given system call name and error details.
|
||||
// As a convenience, if err is nil, NewSyscallError returns nil.
|
||||
func NewSyscallError(syscall string, err error) error {
|
||||
return os.NewSyscallError(syscall, err)
|
||||
}
|
||||
|
||||
// convertErrorType converts errors into our custom errors to ensure consistent
|
||||
// error values.
|
||||
func convertErrorType(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var pErr *PathError
|
||||
switch {
|
||||
case errors.As(err, &pErr):
|
||||
switch {
|
||||
// File exists
|
||||
case errors.Is(pErr.Err, unix.EEXIST):
|
||||
return &PathError{
|
||||
Op: pErr.Op,
|
||||
Path: pErr.Path,
|
||||
Err: ErrExist,
|
||||
}
|
||||
// Is a directory
|
||||
case errors.Is(pErr.Err, unix.EISDIR):
|
||||
return &PathError{
|
||||
Op: pErr.Op,
|
||||
Path: pErr.Path,
|
||||
Err: ErrIsDirectory,
|
||||
}
|
||||
// Not a directory
|
||||
case errors.Is(pErr.Err, unix.ENOTDIR):
|
||||
return &PathError{
|
||||
Op: pErr.Op,
|
||||
Path: pErr.Path,
|
||||
Err: ErrNotDirectory,
|
||||
}
|
||||
// No such file or directory
|
||||
case errors.Is(pErr.Err, unix.ENOENT):
|
||||
return &PathError{
|
||||
Op: pErr.Op,
|
||||
Path: pErr.Path,
|
||||
Err: ErrNotExist,
|
||||
}
|
||||
// Operation not permitted
|
||||
case errors.Is(pErr.Err, unix.EPERM):
|
||||
return &PathError{
|
||||
Op: pErr.Op,
|
||||
Path: pErr.Path,
|
||||
Err: ErrPermission,
|
||||
}
|
||||
// Invalid cross-device link
|
||||
case errors.Is(pErr.Err, unix.EXDEV):
|
||||
return &PathError{
|
||||
Op: pErr.Op,
|
||||
Path: pErr.Path,
|
||||
Err: ErrBadPathResolution,
|
||||
}
|
||||
// Too many levels of symbolic links
|
||||
case errors.Is(pErr.Err, unix.ELOOP):
|
||||
return &PathError{
|
||||
Op: pErr.Op,
|
||||
Path: pErr.Path,
|
||||
Err: ErrBadPathResolution,
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
179
internal/ufs/file.go
Normal file
179
internal/ufs/file.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"io"
|
||||
iofs "io/fs"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// DirEntry is an entry read from a directory.
|
||||
type DirEntry = iofs.DirEntry
|
||||
|
||||
// File describes readable and/or writable file from a Filesystem.
|
||||
type File interface {
|
||||
// Name returns the base name of the file.
|
||||
Name() string
|
||||
|
||||
// Stat returns the FileInfo structure describing the file.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Stat() (FileInfo, error)
|
||||
|
||||
// ReadDir reads the contents of the directory associated with the file f
|
||||
// and returns a slice of DirEntry values in directory order.
|
||||
// Subsequent calls on the same file will yield later DirEntry records in the directory.
|
||||
//
|
||||
// If n > 0, ReadDir returns at most n DirEntry records.
|
||||
// In this case, if ReadDir returns an empty slice, it will return an error explaining why.
|
||||
// At the end of a directory, the error is io.EOF.
|
||||
//
|
||||
// If n <= 0, ReadDir returns all the DirEntry records remaining in the directory.
|
||||
// When it succeeds, it returns a nil error (not io.EOF).
|
||||
ReadDir(n int) ([]DirEntry, error)
|
||||
|
||||
// Readdirnames reads the contents of the directory associated with file
|
||||
// and returns a slice of up to n names of files in the directory,
|
||||
// in directory order. Subsequent calls on the same file will yield
|
||||
// further names.
|
||||
//
|
||||
// If n > 0, Readdirnames returns at most n names. In this case, if
|
||||
// Readdirnames returns an empty slice, it will return a non-nil error
|
||||
// explaining why. At the end of a directory, the error is io.EOF.
|
||||
//
|
||||
// If n <= 0, Readdirnames returns all the names from the directory in
|
||||
// a single slice. In this case, if Readdirnames succeeds (reads all
|
||||
// the way to the end of the directory), it returns the slice and a
|
||||
// nil error. If it encounters an error before the end of the
|
||||
// directory, Readdirnames returns the names read until that point and
|
||||
// a non-nil error.
|
||||
Readdirnames(n int) (names []string, err error)
|
||||
|
||||
// Fd returns the integer Unix file descriptor referencing the open file.
|
||||
// If f is closed, the file descriptor becomes invalid.
|
||||
// If f is garbage collected, a finalizer may close the file descriptor,
|
||||
// making it invalid; see runtime.SetFinalizer for more information on when
|
||||
// a finalizer might be run. On Unix systems this will cause the SetDeadline
|
||||
// methods to stop working.
|
||||
// Because file descriptors can be reused, the returned file descriptor may
|
||||
// only be closed through the Close method of f, or by its finalizer during
|
||||
// garbage collection. Otherwise, during garbage collection the finalizer
|
||||
// may close an unrelated file descriptor with the same (reused) number.
|
||||
//
|
||||
// As an alternative, see the f.SyscallConn method.
|
||||
Fd() uintptr
|
||||
|
||||
// Truncate changes the size of the file.
|
||||
// It does not change the I/O offset.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Truncate(size int64) error
|
||||
|
||||
io.Closer
|
||||
|
||||
io.Reader
|
||||
io.ReaderAt
|
||||
io.ReaderFrom
|
||||
|
||||
io.Writer
|
||||
io.WriterAt
|
||||
|
||||
io.Seeker
|
||||
}
|
||||
|
||||
// FileInfo describes a file and is returned by Stat and Lstat.
|
||||
type FileInfo = iofs.FileInfo
|
||||
|
||||
// FileMode represents a file's mode and permission bits.
|
||||
// The bits have the same definition on all systems, so that
|
||||
// information about files can be moved from one system
|
||||
// to another portably. Not all bits apply to all systems.
|
||||
// The only required bit is ModeDir for directories.
|
||||
type FileMode = iofs.FileMode
|
||||
|
||||
// The defined file mode bits are the most significant bits of the FileMode.
|
||||
// The nine least-significant bits are the standard Unix rwxrwxrwx permissions.
|
||||
// The values of these bits should be considered part of the public API and
|
||||
// may be used in wire protocols or disk representations: they must not be
|
||||
// changed, although new bits might be added.
|
||||
const (
|
||||
// ModeDir represents a directory.
|
||||
// d: is a directory
|
||||
ModeDir = iofs.ModeDir
|
||||
// ModeAppend represents an append-only file.
|
||||
// a: append-only
|
||||
ModeAppend = iofs.ModeAppend
|
||||
// ModeExclusive represents an exclusive file.
|
||||
// l: exclusive use
|
||||
ModeExclusive = iofs.ModeExclusive
|
||||
// ModeTemporary .
|
||||
// T: temporary file; Plan 9 only.
|
||||
ModeTemporary = iofs.ModeTemporary
|
||||
// ModeSymlink .
|
||||
// L: symbolic link.
|
||||
ModeSymlink = iofs.ModeSymlink
|
||||
// ModeDevice .
|
||||
// D: device file.
|
||||
ModeDevice = iofs.ModeDevice
|
||||
// ModeNamedPipe .
|
||||
// p: named pipe (FIFO)
|
||||
ModeNamedPipe = iofs.ModeNamedPipe
|
||||
// ModeSocket .
|
||||
// S: Unix domain socket.
|
||||
ModeSocket = iofs.ModeSocket
|
||||
// ModeSetuid .
|
||||
// u: setuid
|
||||
ModeSetuid = iofs.ModeSetuid
|
||||
// ModeSetgid .
|
||||
// g: setgid
|
||||
ModeSetgid = iofs.ModeSetgid
|
||||
// ModeCharDevice .
|
||||
// c: Unix character device, when ModeDevice is set
|
||||
ModeCharDevice = iofs.ModeCharDevice
|
||||
// ModeSticky .
|
||||
// t: sticky
|
||||
ModeSticky = iofs.ModeSticky
|
||||
// ModeIrregular .
|
||||
// ?: non-regular file; nothing else is known about this file.
|
||||
ModeIrregular = iofs.ModeIrregular
|
||||
|
||||
// ModeType .
|
||||
ModeType = iofs.ModeType
|
||||
|
||||
// ModePerm .
|
||||
// Unix permission bits, 0o777.
|
||||
ModePerm = iofs.ModePerm
|
||||
)
|
||||
|
||||
const (
|
||||
// O_RDONLY opens the file read-only.
|
||||
O_RDONLY = unix.O_RDONLY
|
||||
// O_WRONLY opens the file write-only.
|
||||
O_WRONLY = unix.O_WRONLY
|
||||
// O_RDWR opens the file read-write.
|
||||
O_RDWR = unix.O_RDWR
|
||||
// O_APPEND appends data to the file when writing.
|
||||
O_APPEND = unix.O_APPEND
|
||||
// O_CREATE creates a new file if it doesn't exist.
|
||||
O_CREATE = unix.O_CREAT
|
||||
// O_EXCL is used with O_CREATE, file must not exist.
|
||||
O_EXCL = unix.O_EXCL
|
||||
// O_SYNC open for synchronous I/O.
|
||||
O_SYNC = unix.O_SYNC
|
||||
// O_TRUNC truncates regular writable file when opened.
|
||||
O_TRUNC = unix.O_TRUNC
|
||||
// O_DIRECTORY opens a directory only. If the entry is not a directory an
|
||||
// error will be returned.
|
||||
O_DIRECTORY = unix.O_DIRECTORY
|
||||
// O_NOFOLLOW opens the exact path given without following symlinks.
|
||||
O_NOFOLLOW = unix.O_NOFOLLOW
|
||||
O_CLOEXEC = unix.O_CLOEXEC
|
||||
O_LARGEFILE = unix.O_LARGEFILE
|
||||
)
|
||||
|
||||
const (
|
||||
AT_SYMLINK_NOFOLLOW = unix.AT_SYMLINK_NOFOLLOW
|
||||
AT_REMOVEDIR = unix.AT_REMOVEDIR
|
||||
AT_EMPTY_PATH = unix.AT_EMPTY_PATH
|
||||
)
|
||||
49
internal/ufs/file_posix.go
Normal file
49
internal/ufs/file_posix.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code in this file was copied from `go/src/os/file_posix.go`.
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the `go.LICENSE` file.
|
||||
|
||||
//go:build unix || (js && wasm) || wasip1 || windows
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ignoringEINTR makes a function call and repeats it if it returns an
|
||||
// EINTR error. This appears to be required even though we install all
|
||||
// signal handlers with SA_RESTART: see https://go.dev/issue/22838,
|
||||
// https://go.dev/issue/38033, https://go.dev/issue/38836,
|
||||
// https://go.dev/issue/40846. Also, https://go.dev/issue/20400 and
|
||||
// https://go.dev/issue/36644 are issues in which a signal handler is
|
||||
// installed without setting SA_RESTART. None of these are the common case,
|
||||
// but there are enough of them that it seems that we can't avoid
|
||||
// an EINTR loop.
|
||||
func ignoringEINTR(fn func() error) error {
|
||||
for {
|
||||
err := fn()
|
||||
if err != unix.EINTR {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syscallMode returns the syscall-specific mode bits from Go's portable mode bits.
|
||||
func syscallMode(i FileMode) (o FileMode) {
|
||||
o |= i.Perm()
|
||||
if i&ModeSetuid != 0 {
|
||||
o |= unix.S_ISUID
|
||||
}
|
||||
if i&ModeSetgid != 0 {
|
||||
o |= unix.S_ISGID
|
||||
}
|
||||
if i&ModeSticky != 0 {
|
||||
o |= unix.S_ISVTX
|
||||
}
|
||||
// No mapping for Go's ModeTemporary (plan9 only).
|
||||
return
|
||||
}
|
||||
168
internal/ufs/filesystem.go
Normal file
168
internal/ufs/filesystem.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Filesystem represents a filesystem capable of performing I/O operations.
|
||||
type Filesystem interface {
|
||||
// Chmod changes the mode of the named file to mode.
|
||||
//
|
||||
// If the file is a symbolic link, it changes the mode of the link's target.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
//
|
||||
// A different subset of the mode bits are used, depending on the
|
||||
// operating system.
|
||||
//
|
||||
// On Unix, the mode's permission bits, ModeSetuid, ModeSetgid, and
|
||||
// ModeSticky are used.
|
||||
//
|
||||
// On Windows, only the 0200 bit (owner writable) of mode is used; it
|
||||
// controls whether the file's read-only attribute is set or cleared.
|
||||
// The other bits are currently unused. For compatibility with Go 1.12
|
||||
// and earlier, use a non-zero mode. Use mode 0400 for a read-only
|
||||
// file and 0600 for a readable+writable file.
|
||||
//
|
||||
// On Plan 9, the mode's permission bits, ModeAppend, ModeExclusive,
|
||||
// and ModeTemporary are used.
|
||||
Chmod(name string, mode FileMode) error
|
||||
|
||||
// Chown changes the numeric uid and gid of the named file.
|
||||
//
|
||||
// If the file is a symbolic link, it changes the uid and gid of the link's target.
|
||||
// A uid or gid of -1 means to not change that value.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
//
|
||||
// On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or
|
||||
// EPLAN9 error, wrapped in *PathError.
|
||||
Chown(name string, uid, gid int) error
|
||||
|
||||
// Lchown changes the numeric uid and gid of the named file.
|
||||
//
|
||||
// If the file is a symbolic link, it changes the uid and gid of the link itself.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
//
|
||||
// On Windows, it always returns the syscall.EWINDOWS error, wrapped
|
||||
// in *PathError.
|
||||
Lchown(name string, uid, gid int) error
|
||||
|
||||
// Chtimes changes the access and modification times of the named
|
||||
// file, similar to the Unix utime() or utimes() functions.
|
||||
//
|
||||
// The underlying filesystem may truncate or round the values to a
|
||||
// less precise time unit.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Chtimes(name string, atime, mtime time.Time) error
|
||||
|
||||
// Create creates or truncates the named file. If the file already exists,
|
||||
// it is truncated.
|
||||
//
|
||||
// If the file does not exist, it is created with mode 0666
|
||||
// (before umask). If successful, methods on the returned File can
|
||||
// be used for I/O; the associated file descriptor has mode O_RDWR.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Create(name string) (File, error)
|
||||
|
||||
// Mkdir creates a new directory with the specified name and permission
|
||||
// bits (before umask).
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Mkdir(name string, perm FileMode) error
|
||||
|
||||
// MkdirAll creates a directory named path, along with any necessary
|
||||
// parents, and returns nil, or else returns an error.
|
||||
//
|
||||
// The permission bits perm (before umask) are used for all
|
||||
// directories that MkdirAll creates.
|
||||
// If path is already a directory, MkdirAll does nothing
|
||||
// and returns nil.
|
||||
MkdirAll(path string, perm FileMode) error
|
||||
|
||||
// Open opens the named file for reading.
|
||||
//
|
||||
// If successful, methods on the returned file can be used for reading; the
|
||||
// associated file descriptor has mode O_RDONLY.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Open(name string) (File, error)
|
||||
|
||||
// OpenFile is the generalized open call; most users will use Open
|
||||
// or Create instead. It opens the named file with specified flag
|
||||
// (O_RDONLY etc.).
|
||||
//
|
||||
// If the file does not exist, and the O_CREATE flag
|
||||
// is passed, it is created with mode perm (before umask). If successful,
|
||||
// methods on the returned File can be used for I/O.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
OpenFile(name string, flag int, perm FileMode) (File, error)
|
||||
|
||||
// ReadDir reads the named directory,
|
||||
//
|
||||
// returning all its directory entries sorted by filename.
|
||||
// If an error occurs reading the directory, ReadDir returns the entries it
|
||||
// was able to read before the error, along with the error.
|
||||
ReadDir(name string) ([]DirEntry, error)
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Remove(name string) error
|
||||
|
||||
// RemoveAll removes path and any children it contains.
|
||||
//
|
||||
// It removes everything it can but returns the first error
|
||||
// it encounters. If the path does not exist, RemoveAll
|
||||
// returns nil (no error).
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
RemoveAll(path string) error
|
||||
|
||||
// Rename renames (moves) oldpath to newpath.
|
||||
//
|
||||
// If newpath already exists and is not a directory, Rename replaces it.
|
||||
// OS-specific restrictions may apply when oldpath and newpath are in different directories.
|
||||
// Even within the same directory, on non-Unix platforms Rename is not an atomic operation.
|
||||
//
|
||||
// If there is an error, it will be of type *LinkError.
|
||||
Rename(oldname, newname string) error
|
||||
|
||||
// Stat returns a FileInfo describing the named file.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Stat(name string) (FileInfo, error)
|
||||
|
||||
// Lstat returns a FileInfo describing the named file.
|
||||
//
|
||||
// If the file is a symbolic link, the returned FileInfo
|
||||
// describes the symbolic link. Lstat makes no attempt to follow the link.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
Lstat(name string) (FileInfo, error)
|
||||
|
||||
// Symlink creates newname as a symbolic link to oldname.
|
||||
//
|
||||
// On Windows, a symlink to a non-existent oldname creates a file symlink;
|
||||
// if oldname is later created as a directory the symlink will not work.
|
||||
//
|
||||
// If there is an error, it will be of type *LinkError.
|
||||
Symlink(oldname, newname string) error
|
||||
|
||||
// WalkDir walks the file tree rooted at root, calling fn for each file or
|
||||
// directory in the tree, including root.
|
||||
//
|
||||
// All errors that arise visiting files and directories are filtered by fn:
|
||||
// see the [WalkDirFunc] documentation for details.
|
||||
//
|
||||
// The files are walked in lexical order, which makes the output deterministic
|
||||
// but requires WalkDir to read an entire directory into memory before proceeding
|
||||
// to walk that directory.
|
||||
//
|
||||
// WalkDir does not follow symbolic links found in directories,
|
||||
// but if root itself is a symbolic link, its target will be walked.
|
||||
WalkDir(root string, fn WalkDirFunc) error
|
||||
}
|
||||
159
internal/ufs/fs_quota.go
Normal file
159
internal/ufs/fs_quota.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Quota struct {
|
||||
// fs is the underlying filesystem that runs the actual I/O operations.
|
||||
*UnixFS
|
||||
|
||||
// limit is the size limit of the filesystem.
|
||||
//
|
||||
// limit is atomic to allow the limit to be safely changed after the
|
||||
// filesystem was created.
|
||||
//
|
||||
// A limit of `-1` disables any write operation from being performed.
|
||||
// A limit of `0` disables any limit checking.
|
||||
limit atomic.Int64
|
||||
|
||||
// usage is the current usage of the filesystem.
|
||||
//
|
||||
// If usage is set to `-1`, it hasn't been calculated yet.
|
||||
usage atomic.Int64
|
||||
}
|
||||
|
||||
func NewQuota(fs *UnixFS, limit int64) *Quota {
|
||||
qfs := Quota{UnixFS: fs}
|
||||
qfs.limit.Store(limit)
|
||||
return &qfs
|
||||
}
|
||||
|
||||
// Close closes the filesystem.
|
||||
func (fs *Quota) Close() (err error) {
|
||||
err = fs.UnixFS.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Limit returns the limit of the filesystem.
|
||||
func (fs *Quota) Limit() int64 {
|
||||
return fs.limit.Load()
|
||||
}
|
||||
|
||||
// SetLimit returns the limit of the filesystem.
|
||||
func (fs *Quota) SetLimit(newLimit int64) int64 {
|
||||
return fs.limit.Swap(newLimit)
|
||||
}
|
||||
|
||||
// Usage returns the current usage of the filesystem.
|
||||
func (fs *Quota) Usage() int64 {
|
||||
return fs.usage.Load()
|
||||
}
|
||||
|
||||
// SetUsage updates the total usage of the filesystem.
|
||||
func (fs *Quota) SetUsage(newUsage int64) int64 {
|
||||
return fs.usage.Swap(newUsage)
|
||||
}
|
||||
|
||||
// Add adds `i` to the tracked usage total.
|
||||
func (fs *Quota) Add(i int64) int64 {
|
||||
usage := fs.Usage()
|
||||
|
||||
// If adding `i` to the usage will put us below 0, cap it. (`i` can be negative)
|
||||
if usage+i < 0 {
|
||||
fs.usage.Store(0)
|
||||
return 0
|
||||
}
|
||||
return fs.usage.Add(i)
|
||||
}
|
||||
|
||||
// CanFit checks if the given size can fit in the filesystem without exceeding
|
||||
// the limit of the filesystem.
|
||||
func (fs *Quota) CanFit(size int64) bool {
|
||||
// Get the size limit of the filesystem.
|
||||
limit := fs.Limit()
|
||||
switch limit {
|
||||
case -1:
|
||||
// A limit of -1 means no write operations are allowed.
|
||||
return false
|
||||
case 0:
|
||||
// A limit of 0 means unlimited.
|
||||
return true
|
||||
}
|
||||
|
||||
// Any other limit is a value we need to check.
|
||||
usage := fs.Usage()
|
||||
if usage == -1 {
|
||||
// We don't know what the current usage is yet.
|
||||
return true
|
||||
}
|
||||
|
||||
// If the current usage + the requested size are under the limit of the
|
||||
// filesystem, allow it.
|
||||
if usage+size <= limit {
|
||||
return true
|
||||
}
|
||||
|
||||
// Welp, the size would exceed the limit of the filesystem, deny it.
|
||||
return false
|
||||
}
|
||||
|
||||
func (fs *Quota) Remove(name string) error {
|
||||
// For information on why this interface is used here, check its
|
||||
// documentation.
|
||||
s, err := fs.RemoveStat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't reduce the quota's usage as `name` is not a regular file.
|
||||
if !s.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the size of the deleted file from the quota usage.
|
||||
fs.Add(-s.Size())
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAll removes path and any children it contains.
|
||||
//
|
||||
// It removes everything it can but returns the first error
|
||||
// it encounters. If the path does not exist, RemoveAll
|
||||
// returns nil (no error).
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *Quota) RemoveAll(name string) error {
|
||||
name, err := fs.unsafePath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// While removeAll internally checks this, I want to make sure we check it
|
||||
// and return the proper error so our tests can ensure that this will never
|
||||
// be a possibility.
|
||||
if name == "." {
|
||||
return &PathError{
|
||||
Op: "removeall",
|
||||
Path: name,
|
||||
Err: ErrBadPathResolution,
|
||||
}
|
||||
}
|
||||
return fs.removeAll(name)
|
||||
}
|
||||
|
||||
func (fs *Quota) removeAll(path string) error {
|
||||
return removeAll(fs, path)
|
||||
}
|
||||
|
||||
func (fs *Quota) unlinkat(dirfd int, name string, flags int) error {
|
||||
if flags == 0 {
|
||||
s, err := fs.Lstatat(dirfd, name)
|
||||
if err == nil && s.Mode().IsRegular() {
|
||||
fs.Add(-s.Size())
|
||||
}
|
||||
}
|
||||
return fs.UnixFS.unlinkat(dirfd, name, flags)
|
||||
}
|
||||
825
internal/ufs/fs_unix.go
Normal file
825
internal/ufs/fs_unix.go
Normal file
@@ -0,0 +1,825 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// UnixFS is a filesystem that uses the unix package to make io calls.
|
||||
//
|
||||
// This is used for proper sand-boxing and full control over the exact syscalls
|
||||
// being performed.
|
||||
type UnixFS struct {
|
||||
// basePath is the base path for file operations to take place in.
|
||||
basePath string
|
||||
|
||||
// dirfd holds the file descriptor of BasePath and is used to ensure
|
||||
// operations are restricted into descendants of BasePath.
|
||||
dirfd atomic.Int64
|
||||
|
||||
// useOpenat2 controls whether the `openat2` syscall is used instead of the
|
||||
// older `openat` syscall.
|
||||
useOpenat2 bool
|
||||
}
|
||||
|
||||
// NewUnixFS creates a new sandboxed unix filesystem. BasePath is used as the
|
||||
// sandbox path, operations on BasePath itself are not allowed, but any
|
||||
// operations on its descendants are. Symlinks pointing outside BasePath are
|
||||
// checked and prevented from enabling an escape in a non-raceable manor.
|
||||
func NewUnixFS(basePath string, useOpenat2 bool) (*UnixFS, error) {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
// We don't need Openat2, if we are given a basePath that is already unsafe
|
||||
// I give up on trying to sandbox it.
|
||||
dirfd, err := unix.Openat(AT_EMPTY_PATH, basePath, O_DIRECTORY|O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, convertErrorType(err)
|
||||
}
|
||||
|
||||
fs := &UnixFS{
|
||||
basePath: basePath,
|
||||
useOpenat2: useOpenat2,
|
||||
}
|
||||
fs.dirfd.Store(int64(dirfd))
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// BasePath returns the base path of the UnixFS sandbox, file operations
|
||||
// pointing outside this path are prohibited and will be blocked by all
|
||||
// operations implemented by UnixFS.
|
||||
func (fs *UnixFS) BasePath() string {
|
||||
return fs.basePath
|
||||
}
|
||||
|
||||
// Close releases the file descriptor used to sandbox operations within the
|
||||
// base path of the filesystem.
|
||||
func (fs *UnixFS) Close() error {
|
||||
// Once closed, change dirfd to something invalid to detect when it has been
|
||||
// closed.
|
||||
defer func() {
|
||||
fs.dirfd.Store(-1)
|
||||
}()
|
||||
return unix.Close(int(fs.dirfd.Load()))
|
||||
}
|
||||
|
||||
// Chmod changes the mode of the named file to mode.
|
||||
//
|
||||
// If the file is a symbolic link, it changes the mode of the link's target.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
//
|
||||
// A different subset of the mode bits are used, depending on the
|
||||
// operating system.
|
||||
//
|
||||
// On Unix, the mode's permission bits, ModeSetuid, ModeSetgid, and
|
||||
// ModeSticky are used.
|
||||
//
|
||||
// On Windows, only the 0200 bit (owner writable) of mode is used; it
|
||||
// controls whether the file's read-only attribute is set or cleared.
|
||||
// The other bits are currently unused. For compatibility with Go 1.12
|
||||
// and earlier, use a non-zero mode. Use mode 0400 for a read-only
|
||||
// file and 0600 for a readable+writable file.
|
||||
//
|
||||
// On Plan 9, the mode's permission bits, ModeAppend, ModeExclusive,
|
||||
// and ModeTemporary are used.
|
||||
func (fs *UnixFS) Chmod(name string, mode FileMode) error {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convertErrorType(unix.Fchmodat(dirfd, name, uint32(mode), 0))
|
||||
}
|
||||
|
||||
// Chown changes the numeric uid and gid of the named file.
|
||||
//
|
||||
// If the file is a symbolic link, it changes the uid and gid of the link's target.
|
||||
// A uid or gid of -1 means to not change that value.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
//
|
||||
// On Windows or Plan 9, Chown always returns the syscall.EWINDOWS or
|
||||
// EPLAN9 error, wrapped in *PathError.
|
||||
func (fs *UnixFS) Chown(name string, uid, gid int) error {
|
||||
return fs.fchown(name, uid, gid, 0)
|
||||
}
|
||||
|
||||
// Lchown changes the numeric uid and gid of the named file.
|
||||
//
|
||||
// If the file is a symbolic link, it changes the uid and gid of the link itself.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
//
|
||||
// On Windows, it always returns the syscall.EWINDOWS error, wrapped
|
||||
// in *PathError.
|
||||
func (fs *UnixFS) Lchown(name string, uid, gid int) error {
|
||||
// With AT_SYMLINK_NOFOLLOW, Fchownat acts like Lchown but allows us to
|
||||
// pass a dirfd.
|
||||
return fs.fchown(name, uid, gid, AT_SYMLINK_NOFOLLOW)
|
||||
}
|
||||
|
||||
// fchown is a re-usable Fchownat syscall used by Chown and Lchown.
|
||||
func (fs *UnixFS) fchown(name string, uid, gid, flags int) error {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, flags))
|
||||
}
|
||||
|
||||
// Chownat is like Chown but allows passing an existing directory file
|
||||
// descriptor rather than needing to resolve one.
|
||||
func (fs *UnixFS) Chownat(dirfd int, name string, uid, gid int) error {
|
||||
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, 0))
|
||||
}
|
||||
|
||||
// Lchownat is like Lchown but allows passing an existing directory file
|
||||
// descriptor rather than needing to resolve one.
|
||||
func (fs *UnixFS) Lchownat(dirfd int, name string, uid, gid int) error {
|
||||
return convertErrorType(unix.Fchownat(dirfd, name, uid, gid, AT_SYMLINK_NOFOLLOW))
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named
|
||||
// file, similar to the Unix utime() or utimes() functions.
|
||||
//
|
||||
// The underlying filesystem may truncate or round the values to a
|
||||
// less precise time unit.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) Chtimes(name string, atime, mtime time.Time) error {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.Chtimesat(dirfd, name, atime, mtime)
|
||||
}
|
||||
|
||||
// Chtimesat is like Chtimes but allows passing an existing directory file
|
||||
// descriptor rather than needing to resolve one.
|
||||
func (fs *UnixFS) Chtimesat(dirfd int, name string, atime, mtime time.Time) error {
|
||||
var utimes [2]unix.Timespec
|
||||
set := func(i int, t time.Time) {
|
||||
if t.IsZero() {
|
||||
utimes[i] = unix.Timespec{Sec: unix.UTIME_OMIT, Nsec: unix.UTIME_OMIT}
|
||||
} else {
|
||||
utimes[i] = unix.NsecToTimespec(t.UnixNano())
|
||||
}
|
||||
}
|
||||
set(0, atime)
|
||||
set(1, mtime)
|
||||
// This does support `AT_SYMLINK_NOFOLLOW` as well if needed.
|
||||
if err := unix.UtimesNanoAt(dirfd, name, utimes[0:], 0); err != nil {
|
||||
return convertErrorType(&PathError{Op: "chtimes", Path: name, Err: err})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create creates or truncates the named file. If the file already exists,
|
||||
// it is truncated.
|
||||
//
|
||||
// If the file does not exist, it is created with mode 0666
|
||||
// (before umask). If successful, methods on the returned File can
|
||||
// be used for I/O; the associated file descriptor has mode O_RDWR.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) Create(name string) (File, error) {
|
||||
return fs.OpenFile(name, O_CREATE|O_WRONLY|O_TRUNC, 0o644)
|
||||
}
|
||||
|
||||
// Mkdir creates a new directory with the specified name and permission
|
||||
// bits (before umask).
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) Mkdir(name string, mode FileMode) error {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.Mkdirat(dirfd, name, mode)
|
||||
}
|
||||
|
||||
func (fs *UnixFS) Mkdirat(dirfd int, name string, mode FileMode) error {
|
||||
return convertErrorType(unix.Mkdirat(dirfd, name, uint32(mode)))
|
||||
}
|
||||
|
||||
// MkdirAll creates a directory named path, along with any necessary
|
||||
// parents, and returns nil, or else returns an error.
|
||||
//
|
||||
// The permission bits perm (before umask) are used for all
|
||||
// directories that MkdirAll creates.
|
||||
// If path is already a directory, MkdirAll does nothing
|
||||
// and returns nil.
|
||||
func (fs *UnixFS) MkdirAll(name string, mode FileMode) error {
|
||||
// Ensure name is somewhat clean before continuing.
|
||||
name, err := fs.unsafePath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.mkdirAll(name, mode)
|
||||
}
|
||||
|
||||
// Open opens the named file for reading.
|
||||
//
|
||||
// If successful, methods on the returned file can be used for reading; the
|
||||
// associated file descriptor has mode O_RDONLY.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) Open(name string) (File, error) {
|
||||
return fs.OpenFile(name, O_RDONLY, 0)
|
||||
}
|
||||
|
||||
// OpenFile is the generalized open call; most users will use Open
|
||||
// or Create instead. It opens the named file with specified flag
|
||||
// (O_RDONLY etc.).
|
||||
//
|
||||
// If the file does not exist, and the O_CREATE flag
|
||||
// is passed, it is created with mode perm (before umask). If successful,
|
||||
// methods on the returned File can be used for I/O.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) OpenFile(name string, flag int, mode FileMode) (File, error) {
|
||||
fd, err := fs.openFile(name, flag, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Do not close `fd` here, it is passed to a file that needs the fd, the
|
||||
// caller of this function is responsible for calling Close() on the File
|
||||
// to release the file descriptor.
|
||||
return os.NewFile(uintptr(fd), name), nil
|
||||
}
|
||||
|
||||
func (fs *UnixFS) openFile(name string, flag int, mode FileMode) (int, error) {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return fs.openat(dirfd, name, flag, mode)
|
||||
}
|
||||
|
||||
func (fs *UnixFS) OpenFileat(dirfd int, name string, flag int, mode FileMode) (File, error) {
|
||||
fd, err := fs.openat(dirfd, name, flag, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Do not close `fd` here, it is passed to a file that needs the fd, the
|
||||
// caller of this function is responsible for calling Close() on the File
|
||||
// to release the file descriptor.
|
||||
return os.NewFile(uintptr(fd), name), nil
|
||||
}
|
||||
|
||||
// ReadDir reads the named directory,
|
||||
//
|
||||
// returning all its directory entries sorted by filename.
|
||||
// If an error occurs reading the directory, ReadDir returns the entries it
|
||||
// was able to read before the error, along with the error.
|
||||
func (fs *UnixFS) ReadDir(path string) ([]DirEntry, error) {
|
||||
dirfd, name, closeFd, err := fs.safePath(path)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := fs.openat(dirfd, name, O_DIRECTORY|O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
return fs.readDir(fd, name, nil)
|
||||
}
|
||||
|
||||
// RemoveStat is a combination of Stat and Remove, it is used to more
|
||||
// efficiently remove a file when the caller needs to stat it before
|
||||
// removing it.
|
||||
//
|
||||
// This optimized function exists for our QuotaFS abstraction, which needs
|
||||
// to track writes to a filesystem. When removing a file, the QuotaFS needs
|
||||
// to know if the entry is a file and if so, how large it is. Because we
|
||||
// need to Stat a file in order to get its mode and size, we will already
|
||||
// know if the entry needs to be removed by using Unlink or Rmdir. The
|
||||
// standard `Remove` method just tries both Unlink and Rmdir (in that order)
|
||||
// as it ends up usually being faster and more efficient than calling Stat +
|
||||
// the proper operation in the first place.
|
||||
func (fs *UnixFS) RemoveStat(name string) (FileInfo, error) {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Lstat name, we use Lstat as Unlink doesn't care about symlinks.
|
||||
s, err := fs.Lstatat(dirfd, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.IsDir() {
|
||||
err = fs.unlinkat(dirfd, name, AT_REMOVEDIR) // Rmdir
|
||||
} else {
|
||||
err = fs.unlinkat(dirfd, name, 0)
|
||||
}
|
||||
if err != nil {
|
||||
return s, convertErrorType(&PathError{Op: "remove", Path: name, Err: err})
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) Remove(name string) error {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prevent trying to Remove the base directory.
|
||||
if name == "." {
|
||||
return &PathError{
|
||||
Op: "remove",
|
||||
Path: name,
|
||||
Err: ErrBadPathResolution,
|
||||
}
|
||||
}
|
||||
|
||||
// System call interface forces us to know
|
||||
// whether name is a file or directory.
|
||||
// Try both: it is cheaper on average than
|
||||
// doing a Stat plus the right one.
|
||||
err = fs.unlinkat(dirfd, name, 0)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err1 := fs.unlinkat(dirfd, name, AT_REMOVEDIR) // Rmdir
|
||||
if err1 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Both failed: figure out which error to return.
|
||||
// OS X and Linux differ on whether unlink(dir)
|
||||
// returns EISDIR, so can't use that. However,
|
||||
// both agree that rmdir(file) returns ENOTDIR,
|
||||
// so we can use that to decide which error is real.
|
||||
// Rmdir might also return ENOTDIR if given a bad
|
||||
// file path, like /etc/passwd/foo, but in that case,
|
||||
// both errors will be ENOTDIR, so it's okay to
|
||||
// use the error from unlink.
|
||||
if err1 != unix.ENOTDIR {
|
||||
err = err1
|
||||
}
|
||||
return convertErrorType(&PathError{Op: "remove", Path: name, Err: err})
|
||||
}
|
||||
|
||||
// RemoveAll removes path and any children it contains.
|
||||
//
|
||||
// It removes everything it can but returns the first error
|
||||
// it encounters. If the path does not exist, RemoveAll
|
||||
// returns nil (no error).
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) RemoveAll(name string) error {
|
||||
name, err := fs.unsafePath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// While removeAll internally checks this, I want to make sure we check it
|
||||
// and return the proper error so our tests can ensure that this will never
|
||||
// be a possibility.
|
||||
if name == "." {
|
||||
return &PathError{
|
||||
Op: "removeall",
|
||||
Path: name,
|
||||
Err: ErrBadPathResolution,
|
||||
}
|
||||
}
|
||||
return fs.removeAll(name)
|
||||
}
|
||||
|
||||
func (fs *UnixFS) unlinkat(dirfd int, name string, flags int) error {
|
||||
return ignoringEINTR(func() error {
|
||||
return unix.Unlinkat(dirfd, name, flags)
|
||||
})
|
||||
}
|
||||
|
||||
// Rename renames (moves) oldpath to newpath.
|
||||
//
|
||||
// If newpath already exists and is not a directory, Rename replaces it.
|
||||
// OS-specific restrictions may apply when oldpath and newpath are in different directories.
|
||||
// Even within the same directory, on non-Unix platforms Rename is not an atomic operation.
|
||||
//
|
||||
// If there is an error, it will be of type *LinkError.
|
||||
func (fs *UnixFS) Rename(oldpath, newpath string) error {
|
||||
// Simple case: both paths are the same.
|
||||
if oldpath == newpath {
|
||||
return nil
|
||||
}
|
||||
|
||||
olddirfd, oldname, closeFd, err := fs.safePath(oldpath)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure that we are not trying to rename the base directory itself.
|
||||
// While unix.Renameat ends up throwing a "device or resource busy" error,
|
||||
// that doesn't mean we are protecting the system properly.
|
||||
if oldname == "." {
|
||||
return convertErrorType(&PathError{
|
||||
Op: "rename",
|
||||
Path: oldname,
|
||||
Err: ErrBadPathResolution,
|
||||
})
|
||||
}
|
||||
// Stat the old target to return proper errors.
|
||||
if _, err := fs.Lstatat(olddirfd, oldname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newdirfd, newname, closeFd2, err := fs.safePath(newpath)
|
||||
if err != nil {
|
||||
closeFd2()
|
||||
if !errors.Is(err, ErrNotExist) {
|
||||
return convertErrorType(err)
|
||||
}
|
||||
var pathErr *PathError
|
||||
if !errors.As(err, &pathErr) {
|
||||
return convertErrorType(err)
|
||||
}
|
||||
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
newdirfd, newname, closeFd2, err = fs.safePath(newpath)
|
||||
defer closeFd2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
defer closeFd2()
|
||||
}
|
||||
|
||||
// Ensure that we are not trying to rename the base directory itself.
|
||||
// While unix.Renameat ends up throwing a "device or resource busy" error,
|
||||
// that doesn't mean we are protecting the system properly.
|
||||
if newname == "." {
|
||||
return convertErrorType(&PathError{
|
||||
Op: "rename",
|
||||
Path: newname,
|
||||
Err: ErrBadPathResolution,
|
||||
})
|
||||
}
|
||||
// Stat the new target to return proper errors.
|
||||
_, err = fs.Lstatat(newdirfd, newname)
|
||||
switch {
|
||||
case err == nil:
|
||||
return convertErrorType(&PathError{
|
||||
Op: "rename",
|
||||
Path: newname,
|
||||
Err: ErrExist,
|
||||
})
|
||||
case !errors.Is(err, ErrNotExist):
|
||||
return err
|
||||
}
|
||||
return unix.Renameat(olddirfd, oldname, newdirfd, newname)
|
||||
}
|
||||
|
||||
// Stat returns a FileInfo describing the named file.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) Stat(name string) (FileInfo, error) {
|
||||
return fs.fstat(name, 0)
|
||||
}
|
||||
|
||||
// Statat is like Stat but allows passing an existing directory file
|
||||
// descriptor rather than needing to resolve one.
|
||||
func (fs *UnixFS) Statat(dirfd int, name string) (FileInfo, error) {
|
||||
return fs.fstatat(dirfd, name, 0)
|
||||
}
|
||||
|
||||
// Lstat returns a FileInfo describing the named file.
|
||||
//
|
||||
// If the file is a symbolic link, the returned FileInfo
|
||||
// describes the symbolic link. Lstat makes no attempt to follow the link.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func (fs *UnixFS) Lstat(name string) (FileInfo, error) {
|
||||
return fs.fstat(name, AT_SYMLINK_NOFOLLOW)
|
||||
}
|
||||
|
||||
// Lstatat is like Lstat but allows passing an existing directory file
|
||||
// descriptor rather than needing to resolve one.
|
||||
func (fs *UnixFS) Lstatat(dirfd int, name string) (FileInfo, error) {
|
||||
return fs.fstatat(dirfd, name, AT_SYMLINK_NOFOLLOW)
|
||||
}
|
||||
|
||||
func (fs *UnixFS) fstat(name string, flags int) (FileInfo, error) {
|
||||
dirfd, name, closeFd, err := fs.safePath(name)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.fstatat(dirfd, name, flags)
|
||||
}
|
||||
|
||||
func (fs *UnixFS) fstatat(dirfd int, name string, flags int) (FileInfo, error) {
|
||||
var s fileStat
|
||||
if err := ignoringEINTR(func() error {
|
||||
return unix.Fstatat(dirfd, name, &s.sys, flags)
|
||||
}); err != nil {
|
||||
return nil, &PathError{Op: "stat", Path: name, Err: err}
|
||||
}
|
||||
fillFileStatFromSys(&s, name)
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Symlink creates newname as a symbolic link to oldname.
|
||||
//
|
||||
// On Windows, a symlink to a non-existent oldname creates a file symlink;
|
||||
// if oldname is later created as a directory the symlink will not work.
|
||||
//
|
||||
// If there is an error, it will be of type *LinkError.
|
||||
func (fs *UnixFS) Symlink(oldpath, newpath string) error {
|
||||
dirfd, newpath, closeFd, err := fs.safePath(newpath)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ignoringEINTR(func() error {
|
||||
// We aren't concerned with oldpath here as a symlink can point anywhere
|
||||
// it wants.
|
||||
return unix.Symlinkat(oldpath, dirfd, newpath)
|
||||
}); err != nil {
|
||||
return &LinkError{Op: "symlink", Old: oldpath, New: newpath, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Touch will attempt to open a file for reading and/or writing. If the file
|
||||
// does not exist it will be created, and any missing parent directories will
|
||||
// also be created. The opened file may be truncated, only if `flag` has
|
||||
// O_TRUNC set.
|
||||
func (fs *UnixFS) Touch(path string, flag int, mode FileMode) (File, error) {
|
||||
if flag&O_CREATE == 0 {
|
||||
flag |= O_CREATE
|
||||
}
|
||||
dirfd, name, closeFd, err := fs.safePath(path)
|
||||
defer closeFd()
|
||||
if err == nil {
|
||||
return fs.OpenFileat(dirfd, name, flag, mode)
|
||||
}
|
||||
if !errors.Is(err, ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
var pathErr *PathError
|
||||
if !errors.As(err, &pathErr) {
|
||||
return nil, err
|
||||
}
|
||||
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Try to open the file one more time after creating its parent directories.
|
||||
return fs.OpenFile(path, flag, mode)
|
||||
}
|
||||
|
||||
// WalkDir walks the file tree rooted at root, calling fn for each file or
|
||||
// directory in the tree, including root.
|
||||
//
|
||||
// All errors that arise visiting files and directories are filtered by fn:
|
||||
// see the [WalkDirFunc] documentation for details.
|
||||
//
|
||||
// The files are walked in lexical order, which makes the output deterministic
|
||||
// but requires WalkDir to read an entire directory into memory before proceeding
|
||||
// to walk that directory.
|
||||
//
|
||||
// WalkDir does not follow symbolic links found in directories,
|
||||
// but if root itself is a symbolic link, its target will be walked.
|
||||
func (fs *UnixFS) WalkDir(root string, fn WalkDirFunc) error {
|
||||
return WalkDir(fs, root, fn)
|
||||
}
|
||||
|
||||
// openat is a wrapper around both unix.Openat and unix.Openat2. If the UnixFS
|
||||
// was configured to enable openat2 support, unix.Openat2 will be used instead
|
||||
// of unix.Openat due to having better security properties for our use-case.
|
||||
func (fs *UnixFS) openat(dirfd int, name string, flag int, mode FileMode) (int, error) {
|
||||
if flag&O_NOFOLLOW == 0 {
|
||||
flag |= O_NOFOLLOW
|
||||
}
|
||||
|
||||
var fd int
|
||||
for {
|
||||
var err error
|
||||
if fs.useOpenat2 {
|
||||
fd, err = fs._openat2(dirfd, name, uint64(flag), uint64(syscallMode(mode)))
|
||||
} else {
|
||||
fd, err = fs._openat(dirfd, name, flag, uint32(syscallMode(mode)))
|
||||
}
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
// We have to check EINTR here, per issues https://go.dev/issue/11180 and https://go.dev/issue/39237.
|
||||
if err == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
return 0, convertErrorType(err)
|
||||
}
|
||||
|
||||
// If we are not using openat2, do additional path checking. This assumes
|
||||
// that openat2 is using `RESOLVE_BENEATH` to avoid the same security
|
||||
// issue.
|
||||
if !fs.useOpenat2 {
|
||||
var finalPath string
|
||||
finalPath, err := filepath.EvalSymlinks(filepath.Join("/proc/self/fd/", strconv.Itoa(dirfd)))
|
||||
if err != nil {
|
||||
return fd, convertErrorType(err)
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrNotExist) {
|
||||
return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
|
||||
}
|
||||
|
||||
// The target of one of the symlinks (EvalSymlinks is recursive)
|
||||
// does not exist. So get the path that does not exist and use
|
||||
// that for further validation instead.
|
||||
var pErr *PathError
|
||||
if ok := errors.As(err, &pErr); !ok {
|
||||
return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
|
||||
}
|
||||
finalPath = pErr.Path
|
||||
}
|
||||
|
||||
// Check if the path is within our root.
|
||||
if !fs.unsafeIsPathInsideOfBase(finalPath) {
|
||||
return fd, convertErrorType(&PathError{
|
||||
Op: "openat",
|
||||
Path: name,
|
||||
Err: ErrBadPathResolution,
|
||||
})
|
||||
}
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// _openat is a wrapper around unix.Openat. This method should never be directly
|
||||
// called, use `openat` instead.
|
||||
func (fs *UnixFS) _openat(dirfd int, name string, flag int, mode uint32) (int, error) {
|
||||
// Ensure the O_CLOEXEC flag is set.
|
||||
// Go sets this in the os package, but since we are directly using unix
|
||||
// we need to set it ourselves.
|
||||
if flag&O_CLOEXEC == 0 {
|
||||
flag |= O_CLOEXEC
|
||||
}
|
||||
// O_LARGEFILE is set by Openat for us automatically.
|
||||
fd, err := unix.Openat(dirfd, name, flag, mode)
|
||||
switch {
|
||||
case err == nil:
|
||||
return fd, nil
|
||||
case err == unix.EINTR:
|
||||
return 0, err
|
||||
case err == unix.EAGAIN:
|
||||
return 0, err
|
||||
default:
|
||||
return 0, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// _openat2 is a wonderful syscall that supersedes the `openat` syscall. It has
|
||||
// improved validation and security characteristics that weren't available or
|
||||
// considered when `openat` was originally implemented. As such, it is only
|
||||
// present in Kernel 5.6 and above.
|
||||
//
|
||||
// This method should never be directly called, use `openat` instead.
|
||||
func (fs *UnixFS) _openat2(dirfd int, name string, flag uint64, mode uint64) (int, error) {
|
||||
// Ensure the O_CLOEXEC flag is set.
|
||||
// Go sets this when using the os package, but since we are directly using
|
||||
// the unix package we need to set it ourselves.
|
||||
if flag&O_CLOEXEC == 0 {
|
||||
flag |= O_CLOEXEC
|
||||
}
|
||||
// Ensure the O_LARGEFILE flag is set.
|
||||
// Go sets this for unix.Open, unix.Openat, but not unix.Openat2.
|
||||
if flag&O_LARGEFILE == 0 {
|
||||
flag |= O_LARGEFILE
|
||||
}
|
||||
fd, err := unix.Openat2(dirfd, name, &unix.OpenHow{
|
||||
Flags: flag,
|
||||
Mode: mode,
|
||||
// This is the bread and butter of preventing a symlink escape, without
|
||||
// this option, we have to handle path validation fully on our own.
|
||||
//
|
||||
// This is why using Openat2 over Openat is preferred if available.
|
||||
Resolve: unix.RESOLVE_BENEATH,
|
||||
})
|
||||
switch {
|
||||
case err == nil:
|
||||
return fd, nil
|
||||
case err == unix.EINTR:
|
||||
return 0, err
|
||||
case err == unix.EAGAIN:
|
||||
return 0, err
|
||||
default:
|
||||
return 0, &PathError{Op: "openat2", Path: name, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *UnixFS) SafePath(path string) (int, string, func(), error) {
|
||||
return fs.safePath(path)
|
||||
}
|
||||
|
||||
func (fs *UnixFS) safePath(path string) (dirfd int, file string, closeFd func(), err error) {
|
||||
// Default closeFd to a NO-OP.
|
||||
closeFd = func() {}
|
||||
|
||||
// Use unsafePath to clean the path and strip BasePath if path is absolute.
|
||||
var name string
|
||||
name, err = fs.unsafePath(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if dirfd was closed, this will happen if (*UnixFS).Close()
|
||||
// was called.
|
||||
fsDirfd := int(fs.dirfd.Load())
|
||||
if fsDirfd == -1 {
|
||||
err = ErrClosed
|
||||
return
|
||||
}
|
||||
|
||||
// Split the parent from the last element in the path, this gives us the
|
||||
// "file name" and the full path to its parent.
|
||||
var dir string
|
||||
dir, file = filepath.Split(name)
|
||||
// If dir is empty then name is not nested.
|
||||
if dir == "" {
|
||||
// We don't need to set closeFd here as it will default to a NO-OP and
|
||||
// `fs.dirfd` is re-used until the filesystem is no-longer needed.
|
||||
dirfd = fsDirfd
|
||||
|
||||
// Return dirfd, name, an empty closeFd func, and no error
|
||||
return
|
||||
}
|
||||
|
||||
// Dir will usually contain a trailing slash as filepath.Split doesn't
|
||||
// trim slashes.
|
||||
dir = strings.TrimSuffix(dir, "/")
|
||||
dirfd, err = fs.openat(fsDirfd, dir, O_DIRECTORY|O_RDONLY, 0)
|
||||
if dirfd != 0 {
|
||||
// Set closeFd to close the newly opened directory file descriptor.
|
||||
closeFd = func() { _ = unix.Close(dirfd) }
|
||||
}
|
||||
|
||||
// Return dirfd, name, the closeFd func, and err
|
||||
return
|
||||
}
|
||||
|
||||
// unsafePath prefixes the given path and prefixes it with the filesystem's
|
||||
// base path, cleaning the result. The path returned by this function may not
|
||||
// be inside the filesystem's base path, additional checks are required to
|
||||
// safely use paths returned by this function.
|
||||
func (fs *UnixFS) unsafePath(path string) (string, error) {
|
||||
// Calling filepath.Clean on the joined directory will resolve it to the
|
||||
// absolute path, removing any ../ type of resolution arguments, and leaving
|
||||
// us with a direct path link.
|
||||
//
|
||||
// This will also trim the existing root path off the beginning of the path
|
||||
// passed to the function since that can get a bit messy.
|
||||
r := filepath.Clean(filepath.Join(fs.basePath, strings.TrimPrefix(path, fs.basePath)))
|
||||
|
||||
if fs.unsafeIsPathInsideOfBase(r) {
|
||||
// This is kinda ironic isn't it.
|
||||
// We do this as we are operating with dirfds and `*at` syscalls which
|
||||
// behave differently if given an absolute path.
|
||||
//
|
||||
// First trim the BasePath, then trim any leading slashes.
|
||||
r = strings.TrimPrefix(strings.TrimPrefix(r, fs.basePath), "/")
|
||||
// If the path is empty then return "." as the path is pointing to the
|
||||
// root.
|
||||
if r == "" {
|
||||
return ".", nil
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
return "", &PathError{
|
||||
Op: "safePath",
|
||||
Path: path,
|
||||
Err: ErrBadPathResolution,
|
||||
}
|
||||
}
|
||||
|
||||
// unsafeIsPathInsideOfBase checks if the given path is inside the filesystem's
|
||||
// base path.
|
||||
func (fs *UnixFS) unsafeIsPathInsideOfBase(path string) bool {
|
||||
return strings.HasPrefix(
|
||||
strings.TrimSuffix(path, "/")+"/",
|
||||
fs.basePath+"/",
|
||||
)
|
||||
}
|
||||
255
internal/ufs/fs_unix_test.go
Normal file
255
internal/ufs/fs_unix_test.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ufs_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pterodactyl/wings/internal/ufs"
|
||||
)
|
||||
|
||||
type testUnixFS struct {
|
||||
*ufs.UnixFS
|
||||
|
||||
TmpDir string
|
||||
Root string
|
||||
}
|
||||
|
||||
func (fs *testUnixFS) Cleanup() {
|
||||
_ = fs.Close()
|
||||
_ = os.RemoveAll(fs.TmpDir)
|
||||
}
|
||||
|
||||
func newTestUnixFS() (*testUnixFS, error) {
|
||||
tmpDir, err := os.MkdirTemp(os.TempDir(), "ufs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root := filepath.Join(tmpDir, "root")
|
||||
if err := os.Mkdir(root, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: test both disabled and enabled.
|
||||
fs, err := ufs.NewUnixFS(root, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tfs := &testUnixFS{
|
||||
UnixFS: fs,
|
||||
TmpDir: tmpDir,
|
||||
Root: root,
|
||||
}
|
||||
return tfs, nil
|
||||
}
|
||||
|
||||
func TestUnixFS_Remove(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs, err := newTestUnixFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
defer fs.Cleanup()
|
||||
|
||||
t.Run("base directory", func(t *testing.T) {
|
||||
// Try to remove the base directory.
|
||||
if err := fs.Remove(""); !errors.Is(err, ufs.ErrBadPathResolution) {
|
||||
t.Errorf("expected an a bad path resolution error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path traversal", func(t *testing.T) {
|
||||
// Try to remove the base directory.
|
||||
if err := fs.RemoveAll("../root"); !errors.Is(err, ufs.ErrBadPathResolution) {
|
||||
t.Errorf("expected an a bad path resolution error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnixFS_RemoveAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs, err := newTestUnixFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
defer fs.Cleanup()
|
||||
|
||||
t.Run("base directory", func(t *testing.T) {
|
||||
// Try to remove the base directory.
|
||||
if err := fs.RemoveAll(""); !errors.Is(err, ufs.ErrBadPathResolution) {
|
||||
t.Errorf("expected an a bad path resolution error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path traversal", func(t *testing.T) {
|
||||
// Try to remove the base directory.
|
||||
if err := fs.RemoveAll("../root"); !errors.Is(err, ufs.ErrBadPathResolution) {
|
||||
t.Errorf("expected an a bad path resolution error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnixFS_Rename(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs, err := newTestUnixFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
defer fs.Cleanup()
|
||||
|
||||
t.Run("rename base directory", func(t *testing.T) {
|
||||
// Try to rename the base directory.
|
||||
if err := fs.Rename("", "yeet"); !errors.Is(err, ufs.ErrBadPathResolution) {
|
||||
t.Errorf("expected an a bad path resolution error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rename over base directory", func(t *testing.T) {
|
||||
// Create a directory that we are going to try and move over top of the
|
||||
// existing base directory.
|
||||
if err := fs.Mkdir("overwrite_dir", 0o755); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to rename over the base directory.
|
||||
if err := fs.Rename("overwrite_dir", ""); !errors.Is(err, ufs.ErrBadPathResolution) {
|
||||
t.Errorf("expected an a bad path resolution error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory rename", func(t *testing.T) {
|
||||
// Create a directory to rename to something else.
|
||||
if err := fs.Mkdir("test_directory", 0o755); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to rename "test_directory" to "directory".
|
||||
if err := fs.Rename("test_directory", "directory"); err != nil {
|
||||
t.Errorf("expected no error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
if _, err := os.Lstat(filepath.Join(fs.Root, "directory")); err != nil {
|
||||
t.Errorf("Lstat errored when performing sanity check: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file rename", func(t *testing.T) {
|
||||
// Create a directory to rename to something else.
|
||||
if f, err := fs.Create("test_file"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else {
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
// Try to rename "test_file" to "file".
|
||||
if err := fs.Rename("test_file", "file"); err != nil {
|
||||
t.Errorf("expected no error, but got: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
if _, err := os.Lstat(filepath.Join(fs.Root, "file")); err != nil {
|
||||
t.Errorf("Lstat errored when performing sanity check: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnixFS_Touch(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs, err := newTestUnixFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
defer fs.Cleanup()
|
||||
|
||||
t.Run("base directory", func(t *testing.T) {
|
||||
path := "i_touched_a_file"
|
||||
f, err := fs.Touch(path, ufs.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
// Sanity check
|
||||
if _, err := os.Lstat(filepath.Join(fs.Root, path)); err != nil {
|
||||
t.Errorf("Lstat errored when performing sanity check: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("existing parent directory", func(t *testing.T) {
|
||||
dir := "some_parent_directory"
|
||||
if err := fs.Mkdir(dir, 0o755); err != nil {
|
||||
t.Errorf("error creating parent directory: %v", err)
|
||||
return
|
||||
}
|
||||
path := filepath.Join(dir, "i_touched_a_file")
|
||||
f, err := fs.Touch(path, ufs.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
t.Errorf("error touching file: %v", err)
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
// Sanity check
|
||||
if _, err := os.Lstat(filepath.Join(fs.Root, path)); err != nil {
|
||||
t.Errorf("Lstat errored when performing sanity check: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existent parent directory", func(t *testing.T) {
|
||||
path := "some_other_directory/i_touched_a_file"
|
||||
f, err := fs.Touch(path, ufs.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
t.Errorf("error touching file: %v", err)
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
// Sanity check
|
||||
if _, err := os.Lstat(filepath.Join(fs.Root, path)); err != nil {
|
||||
t.Errorf("Lstat errored when performing sanity check: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existent parent directories", func(t *testing.T) {
|
||||
path := "some_other_directory/some_directory/i_touched_a_file"
|
||||
f, err := fs.Touch(path, ufs.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
t.Errorf("error touching file: %v", err)
|
||||
return
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
// Sanity check
|
||||
if _, err := os.Lstat(filepath.Join(fs.Root, path)); err != nil {
|
||||
t.Errorf("Lstat errored when performing sanity check: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
27
internal/ufs/go.LICENSE
Normal file
27
internal/ufs/go.LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* 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.
|
||||
* Neither the name of Google Inc. 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
|
||||
OWNER 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.
|
||||
67
internal/ufs/mkdir_unix.go
Normal file
67
internal/ufs/mkdir_unix.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code in this file was derived from `go/src/os/path.go`.
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the `go.LICENSE` file.
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// mkdirAll is a recursive Mkdir implementation that properly handles symlinks.
|
||||
func (fs *UnixFS) mkdirAll(name string, mode FileMode) error {
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := fs.Lstat(name)
|
||||
if err == nil {
|
||||
if dir.Mode()&ModeSymlink != 0 {
|
||||
// If the final path is a symlink, resolve its target and use that
|
||||
// to check instead.
|
||||
dir, err = fs.Stat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return convertErrorType(&PathError{Op: "mkdir", Path: name, Err: unix.ENOTDIR})
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(name)
|
||||
for i > 0 && name[i-1] == '/' { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && name[j-1] != '/' { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent.
|
||||
err = fs.mkdirAll(name[:j-1], mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
err = fs.Mkdir(name, mode)
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := fs.Lstat(name)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
80
internal/ufs/path_unix.go
Normal file
80
internal/ufs/path_unix.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code in this file was copied from `go/src/os/path.go`
|
||||
// and `go/src/os/path_unix.go`.
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the `go.LICENSE` file.
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// basename removes trailing slashes and the leading directory name from path name.
|
||||
func basename(name string) string {
|
||||
i := len(name) - 1
|
||||
// Remove trailing slashes
|
||||
for ; i > 0 && name[i] == '/'; i-- {
|
||||
name = name[:i]
|
||||
}
|
||||
// Remove leading directory name
|
||||
for i--; i >= 0; i-- {
|
||||
if name[i] == '/' {
|
||||
name = name[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// endsWithDot reports whether the final component of path is ".".
|
||||
func endsWithDot(path string) bool {
|
||||
if path == "." {
|
||||
return true
|
||||
}
|
||||
if len(path) >= 2 && path[len(path)-1] == '.' && os.IsPathSeparator(path[len(path)-2]) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// splitPath returns the base name and parent directory.
|
||||
func splitPath(path string) (string, string) {
|
||||
// if no better parent is found, the path is relative from "here"
|
||||
dirname := "."
|
||||
|
||||
// Remove all but one leading slash.
|
||||
for len(path) > 1 && path[0] == '/' && path[1] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
i := len(path) - 1
|
||||
|
||||
// Remove trailing slashes.
|
||||
for ; i > 0 && path[i] == '/'; i-- {
|
||||
path = path[:i]
|
||||
}
|
||||
|
||||
// if no slashes in path, base is path
|
||||
basename := path
|
||||
|
||||
// Remove leading directory path
|
||||
for i--; i >= 0; i-- {
|
||||
if path[i] == '/' {
|
||||
if i == 0 {
|
||||
dirname = path[:1]
|
||||
} else {
|
||||
dirname = path[:i]
|
||||
}
|
||||
basename = path[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return dirname, basename
|
||||
}
|
||||
117
internal/ufs/quota_writer.go
Normal file
117
internal/ufs/quota_writer.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// CountedWriter is a writer that counts the amount of data written to the
|
||||
// underlying writer.
|
||||
type CountedWriter struct {
|
||||
File
|
||||
|
||||
counter atomic.Int64
|
||||
err error
|
||||
}
|
||||
|
||||
// NewCountedWriter returns a new countedWriter that counts the amount of bytes
|
||||
// written to the underlying writer.
|
||||
func NewCountedWriter(f File) *CountedWriter {
|
||||
return &CountedWriter{File: f}
|
||||
}
|
||||
|
||||
// BytesWritten returns the amount of bytes that have been written to the
|
||||
// underlying writer.
|
||||
func (w *CountedWriter) BytesWritten() int64 {
|
||||
return w.counter.Load()
|
||||
}
|
||||
|
||||
// Error returns the error from the writer if any. If the error is an EOF, nil
|
||||
// will be returned.
|
||||
func (w *CountedWriter) Error() error {
|
||||
if errors.Is(w.err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return w.err
|
||||
}
|
||||
|
||||
// Write writes bytes to the underlying writer while tracking the total amount
|
||||
// of bytes written.
|
||||
func (w *CountedWriter) Write(p []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// Write is a very simple operation for us to handle.
|
||||
n, err := w.File.Write(p)
|
||||
w.counter.Add(int64(n))
|
||||
w.err = err
|
||||
|
||||
// TODO: is this how we actually want to handle errors with this?
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
} else {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *CountedWriter) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
cr := NewCountedReader(r)
|
||||
n, err = w.File.ReadFrom(cr)
|
||||
w.counter.Add(n)
|
||||
return
|
||||
}
|
||||
|
||||
// CountedReader is a reader that counts the amount of data read from the
|
||||
// underlying reader.
|
||||
type CountedReader struct {
|
||||
reader io.Reader
|
||||
|
||||
counter atomic.Int64
|
||||
err error
|
||||
}
|
||||
|
||||
var _ io.Reader = (*CountedReader)(nil)
|
||||
|
||||
// NewCountedReader returns a new countedReader that counts the amount of bytes
|
||||
// read from the underlying reader.
|
||||
func NewCountedReader(r io.Reader) *CountedReader {
|
||||
return &CountedReader{reader: r}
|
||||
}
|
||||
|
||||
// BytesRead returns the amount of bytes that have been read from the underlying
|
||||
// reader.
|
||||
func (r *CountedReader) BytesRead() int64 {
|
||||
return r.counter.Load()
|
||||
}
|
||||
|
||||
// Error returns the error from the reader if any. If the error is an EOF, nil
|
||||
// will be returned.
|
||||
func (r *CountedReader) Error() error {
|
||||
if errors.Is(r.err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return r.err
|
||||
}
|
||||
|
||||
// Read reads bytes from the underlying reader while tracking the total amount
|
||||
// of bytes read.
|
||||
func (r *CountedReader) Read(p []byte) (int, error) {
|
||||
if r.err != nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
n, err := r.reader.Read(p)
|
||||
r.counter.Add(int64(n))
|
||||
r.err = err
|
||||
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
} else {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
207
internal/ufs/removeall_unix.go
Normal file
207
internal/ufs/removeall_unix.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code in this file was derived from `go/src/os/removeall_at.go`.
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the `go.LICENSE` file.
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type unixFS interface {
|
||||
Open(name string) (File, error)
|
||||
Remove(name string) error
|
||||
unlinkat(dirfd int, path string, flags int) error
|
||||
}
|
||||
|
||||
func (fs *UnixFS) removeAll(path string) error {
|
||||
return removeAll(fs, path)
|
||||
}
|
||||
|
||||
func removeAll(fs unixFS, path string) error {
|
||||
if path == "" {
|
||||
// fail silently to retain compatibility with previous behavior
|
||||
// of RemoveAll. See issue https://go.dev/issue/28830.
|
||||
return nil
|
||||
}
|
||||
|
||||
// The rmdir system call does not permit removing ".",
|
||||
// so we don't permit it either.
|
||||
if endsWithDot(path) {
|
||||
return &PathError{Op: "removeall", Path: path, Err: unix.EINVAL}
|
||||
}
|
||||
|
||||
// Simple case: if Remove works, we're done.
|
||||
err := fs.Remove(path)
|
||||
if err == nil || errors.Is(err, ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAll recurses by deleting the path base from
|
||||
// its parent directory
|
||||
parentDir, base := splitPath(path)
|
||||
|
||||
parent, err := fs.Open(parentDir)
|
||||
if errors.Is(err, ErrNotExist) {
|
||||
// If parent does not exist, base cannot exist. Fail silently
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer parent.Close()
|
||||
|
||||
if err := removeAllFrom(fs, parent, base); err != nil {
|
||||
if pathErr, ok := err.(*PathError); ok {
|
||||
pathErr.Path = parentDir + string(os.PathSeparator) + pathErr.Path
|
||||
err = pathErr
|
||||
}
|
||||
return convertErrorType(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAllFrom(fs unixFS, parent File, base string) error {
|
||||
parentFd := int(parent.Fd())
|
||||
// Simple case: if Unlink (aka remove) works, we're done.
|
||||
err := fs.unlinkat(parentFd, base, 0)
|
||||
if err == nil || errors.Is(err, ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EISDIR means that we have a directory, and we need to
|
||||
// remove its contents.
|
||||
// EPERM or EACCES means that we don't have write permission on
|
||||
// the parent directory, but this entry might still be a directory
|
||||
// whose contents need to be removed.
|
||||
// Otherwise, just return the error.
|
||||
if err != unix.EISDIR && err != unix.EPERM && err != unix.EACCES {
|
||||
return &PathError{Op: "unlinkat", Path: base, Err: err}
|
||||
}
|
||||
|
||||
// Is this a directory we need to recurse into?
|
||||
var statInfo unix.Stat_t
|
||||
statErr := ignoringEINTR(func() error {
|
||||
return unix.Fstatat(parentFd, base, &statInfo, AT_SYMLINK_NOFOLLOW)
|
||||
})
|
||||
if statErr != nil {
|
||||
if errors.Is(statErr, ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return &PathError{Op: "fstatat", Path: base, Err: statErr}
|
||||
}
|
||||
if statInfo.Mode&unix.S_IFMT != unix.S_IFDIR {
|
||||
// Not a directory; return the error from the unix.Unlinkat.
|
||||
return &PathError{Op: "unlinkat", Path: base, Err: err}
|
||||
}
|
||||
|
||||
// Remove the directory's entries.
|
||||
var recurseErr error
|
||||
for {
|
||||
const reqSize = 1024
|
||||
var respSize int
|
||||
|
||||
// Open the directory to recurse into
|
||||
file, err := openFdAt(parentFd, base)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
recurseErr = &PathError{Op: "openfdat", Path: base, Err: err}
|
||||
break
|
||||
}
|
||||
|
||||
for {
|
||||
numErr := 0
|
||||
|
||||
names, readErr := file.Readdirnames(reqSize)
|
||||
// Errors other than EOF should stop us from continuing.
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
_ = file.Close()
|
||||
if errors.Is(readErr, ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return &PathError{Op: "readdirnames", Path: base, Err: readErr}
|
||||
}
|
||||
|
||||
respSize = len(names)
|
||||
for _, name := range names {
|
||||
err := removeAllFrom(fs, file, name)
|
||||
if err != nil {
|
||||
if pathErr, ok := err.(*PathError); ok {
|
||||
pathErr.Path = base + string(os.PathSeparator) + pathErr.Path
|
||||
}
|
||||
numErr++
|
||||
if recurseErr == nil {
|
||||
recurseErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we can delete any entry, break to start new iteration.
|
||||
// Otherwise, we discard current names, get next entries and try deleting them.
|
||||
if numErr != reqSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Removing files from the directory may have caused
|
||||
// the OS to reshuffle it. Simply calling Readdirnames
|
||||
// again may skip some entries. The only reliable way
|
||||
// to avoid this is to close and re-open the
|
||||
// directory. See issue https://go.dev/issue/20841.
|
||||
_ = file.Close()
|
||||
|
||||
// Finish when the end of the directory is reached
|
||||
if respSize < reqSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the directory itself.
|
||||
unlinkErr := fs.unlinkat(parentFd, base, AT_REMOVEDIR)
|
||||
if unlinkErr == nil || errors.Is(unlinkErr, ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if recurseErr != nil {
|
||||
return recurseErr
|
||||
}
|
||||
return &PathError{Op: "unlinkat", Path: base, Err: unlinkErr}
|
||||
}
|
||||
|
||||
// openFdAt opens path relative to the directory in fd.
|
||||
// Other than that this should act like openFileNolog.
|
||||
// This acts like openFileNolog rather than OpenFile because
|
||||
// we are going to (try to) remove the file.
|
||||
// The contents of this file are not relevant for test caching.
|
||||
func openFdAt(dirfd int, name string) (File, error) {
|
||||
var fd int
|
||||
for {
|
||||
var err error
|
||||
fd, err = unix.Openat(dirfd, name, O_RDONLY|O_CLOEXEC|O_NOFOLLOW, 0)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// See comment in openFileNolog.
|
||||
if err == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
// This is stupid, os.NewFile immediately casts `fd` to an `int`, but wants
|
||||
// it to be passed as a `uintptr`.
|
||||
return os.NewFile(uintptr(fd), name), nil
|
||||
}
|
||||
67
internal/ufs/stat_unix.go
Normal file
67
internal/ufs/stat_unix.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code in this file was copied from `go/src/os/stat_linux.go`
|
||||
// and `go/src/os/types_unix.go`.
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the `go.LICENSE` file.
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type fileStat struct {
|
||||
name string
|
||||
size int64
|
||||
mode FileMode
|
||||
modTime time.Time
|
||||
sys unix.Stat_t
|
||||
}
|
||||
|
||||
var _ FileInfo = (*fileStat)(nil)
|
||||
|
||||
func (fs *fileStat) Size() int64 { return fs.size }
|
||||
func (fs *fileStat) Mode() FileMode { return fs.mode }
|
||||
func (fs *fileStat) ModTime() time.Time { return fs.modTime }
|
||||
func (fs *fileStat) Sys() any { return &fs.sys }
|
||||
func (fs *fileStat) Name() string { return fs.name }
|
||||
func (fs *fileStat) IsDir() bool { return fs.Mode().IsDir() }
|
||||
|
||||
func fillFileStatFromSys(fs *fileStat, name string) {
|
||||
fs.name = basename(name)
|
||||
fs.size = fs.sys.Size
|
||||
fs.modTime = time.Unix(fs.sys.Mtim.Unix())
|
||||
fs.mode = FileMode(fs.sys.Mode & 0o777)
|
||||
switch fs.sys.Mode & unix.S_IFMT {
|
||||
case unix.S_IFBLK:
|
||||
fs.mode |= ModeDevice
|
||||
case unix.S_IFCHR:
|
||||
fs.mode |= ModeDevice | ModeCharDevice
|
||||
case unix.S_IFDIR:
|
||||
fs.mode |= ModeDir
|
||||
case unix.S_IFIFO:
|
||||
fs.mode |= ModeNamedPipe
|
||||
case unix.S_IFLNK:
|
||||
fs.mode |= ModeSymlink
|
||||
case unix.S_IFREG:
|
||||
// nothing to do
|
||||
case unix.S_IFSOCK:
|
||||
fs.mode |= ModeSocket
|
||||
}
|
||||
if fs.sys.Mode&unix.S_ISGID != 0 {
|
||||
fs.mode |= ModeSetgid
|
||||
}
|
||||
if fs.sys.Mode&unix.S_ISUID != 0 {
|
||||
fs.mode |= ModeSetuid
|
||||
}
|
||||
if fs.sys.Mode&unix.S_ISVTX != 0 {
|
||||
fs.mode |= ModeSticky
|
||||
}
|
||||
}
|
||||
123
internal/ufs/walk.go
Normal file
123
internal/ufs/walk.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code in this file was derived from `go/src/io/fs/walk.go`.
|
||||
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the `go.LICENSE` file.
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
iofs "io/fs"
|
||||
"path"
|
||||
)
|
||||
|
||||
// SkipDir is used as a return value from [WalkDirFunc] to indicate that
|
||||
// the directory named in the call is to be skipped. It is not returned
|
||||
// as an error by any function.
|
||||
var SkipDir = iofs.SkipDir
|
||||
|
||||
// SkipAll is used as a return value from [WalkDirFunc] to indicate that
|
||||
// all remaining files and directories are to be skipped. It is not returned
|
||||
// as an error by any function.
|
||||
var SkipAll = iofs.SkipAll
|
||||
|
||||
// WalkDirFunc is the type of the function called by [WalkDir] to visit
|
||||
// each file or directory.
|
||||
//
|
||||
// The path argument contains the argument to [WalkDir] as a prefix.
|
||||
// That is, if WalkDir is called with root argument "dir" and finds a file
|
||||
// named "a" in that directory, the walk function will be called with
|
||||
// argument "dir/a".
|
||||
//
|
||||
// The d argument is the [DirEntry] for the named path.
|
||||
//
|
||||
// The error result returned by the function controls how [WalkDir]
|
||||
// continues. If the function returns the special value [SkipDir], WalkDir
|
||||
// skips the current directory (path if d.IsDir() is true, otherwise
|
||||
// path's parent directory). If the function returns the special value
|
||||
// [SkipAll], WalkDir skips all remaining files and directories. Otherwise,
|
||||
// if the function returns a non-nil error, WalkDir stops entirely and
|
||||
// returns that error.
|
||||
//
|
||||
// The err argument reports an error related to path, signaling that
|
||||
// [WalkDir] will not walk into that directory. The function can decide how
|
||||
// to handle that error; as described earlier, returning the error will
|
||||
// cause WalkDir to stop walking the entire tree.
|
||||
//
|
||||
// [WalkDir] calls the function with a non-nil err argument in two cases.
|
||||
//
|
||||
// First, if the initial [Stat] on the root directory fails, WalkDir
|
||||
// calls the function with path set to root, d set to nil, and err set to
|
||||
// the error from [fs.Stat].
|
||||
//
|
||||
// Second, if a directory's ReadDir method (see [ReadDirFile]) fails, WalkDir calls the
|
||||
// function with path set to the directory's path, d set to an
|
||||
// [DirEntry] describing the directory, and err set to the error from
|
||||
// ReadDir. In this second case, the function is called twice with the
|
||||
// path of the directory: the first call is before the directory read is
|
||||
// attempted and has err set to nil, giving the function a chance to
|
||||
// return [SkipDir] or [SkipAll] and avoid the ReadDir entirely. The second call
|
||||
// is after a failed ReadDir and reports the error from ReadDir.
|
||||
// (If ReadDir succeeds, there is no second call.)
|
||||
type WalkDirFunc func(path string, d DirEntry, err error) error
|
||||
|
||||
// WalkDir walks the file tree rooted at root, calling fn for each file or
|
||||
// directory in the tree, including root.
|
||||
//
|
||||
// All errors that arise visiting files and directories are filtered by fn:
|
||||
// see the [WalkDirFunc] documentation for details.
|
||||
//
|
||||
// The files are walked in lexical order, which makes the output deterministic
|
||||
// but requires WalkDir to read an entire directory into memory before proceeding
|
||||
// to walk that directory.
|
||||
//
|
||||
// WalkDir does not follow symbolic links found in directories,
|
||||
// but if root itself is a symbolic link, its target will be walked.
|
||||
func WalkDir(fs Filesystem, root string, fn WalkDirFunc) error {
|
||||
info, err := fs.Stat(root)
|
||||
if err != nil {
|
||||
err = fn(root, nil, err)
|
||||
} else {
|
||||
err = walkDir(fs, root, iofs.FileInfoToDirEntry(info), fn)
|
||||
}
|
||||
if err == SkipDir || err == SkipAll {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// walkDir recursively descends path, calling walkDirFn.
|
||||
func walkDir(fs Filesystem, name string, d DirEntry, walkDirFn WalkDirFunc) error {
|
||||
if err := walkDirFn(name, d, nil); err != nil || !d.IsDir() {
|
||||
if err == SkipDir && d.IsDir() {
|
||||
// Successfully skipped directory.
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
dirs, err := fs.ReadDir(name)
|
||||
if err != nil {
|
||||
// Second call, to report ReadDir error.
|
||||
err = walkDirFn(name, d, err)
|
||||
if err != nil {
|
||||
if err == SkipDir && d.IsDir() {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, d1 := range dirs {
|
||||
name1 := path.Join(name, d1.Name())
|
||||
if err := walkDir(fs, name1, d1, walkDirFn); err != nil {
|
||||
if err == SkipDir {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
291
internal/ufs/walk_unix.go
Normal file
291
internal/ufs/walk_unix.go
Normal file
@@ -0,0 +1,291 @@
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// Some code in this file was derived from https://github.com/karrick/godirwalk.
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ufs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
iofs "io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type WalkDiratFunc func(dirfd int, name, relative string, d DirEntry, err error) error
|
||||
|
||||
func (fs *UnixFS) WalkDirat(dirfd int, name string, fn WalkDiratFunc) error {
|
||||
if dirfd == 0 {
|
||||
// TODO: proper validation, ideally a dedicated function.
|
||||
dirfd = int(fs.dirfd.Load())
|
||||
}
|
||||
info, err := fs.Lstatat(dirfd, name)
|
||||
if err != nil {
|
||||
err = fn(dirfd, name, name, nil, err)
|
||||
} else {
|
||||
b := newScratchBuffer()
|
||||
err = fs.walkDir(b, dirfd, name, name, iofs.FileInfoToDirEntry(info), fn)
|
||||
}
|
||||
if err == SkipDir || err == SkipAll {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *UnixFS) walkDir(b []byte, parentfd int, name, relative string, d DirEntry, walkDirFn WalkDiratFunc) error {
|
||||
if err := walkDirFn(parentfd, name, relative, d, nil); err != nil || !d.IsDir() {
|
||||
if err == SkipDir && d.IsDir() {
|
||||
// Successfully skipped directory.
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
dirfd, err := fs.openat(parentfd, name, O_DIRECTORY|O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unix.Close(dirfd)
|
||||
|
||||
dirs, err := fs.readDir(dirfd, name, b)
|
||||
if err != nil {
|
||||
// Second call, to report ReadDir error.
|
||||
err = walkDirFn(dirfd, name, relative, d, err)
|
||||
if err != nil {
|
||||
if err == SkipDir && d.IsDir() {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, d1 := range dirs {
|
||||
if err := fs.walkDir(b, dirfd, d1.Name(), path.Join(relative, d1.Name()), d1, walkDirFn); err != nil {
|
||||
if err == SkipDir {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDirMap .
|
||||
// TODO: document
|
||||
func ReadDirMap[T any](fs *UnixFS, path string, fn func(DirEntry) (T, error)) ([]T, error) {
|
||||
dirfd, name, closeFd, err := fs.safePath(path)
|
||||
defer closeFd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := fs.openat(dirfd, name, O_DIRECTORY|O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
entries, err := fs.readDir(fd, ".", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]T, len(entries))
|
||||
for i, e := range entries {
|
||||
idx := i
|
||||
e := e
|
||||
v, err := fn(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[idx] = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// nameOffset is a compile time constant
|
||||
const nameOffset = int(unsafe.Offsetof(unix.Dirent{}.Name))
|
||||
|
||||
func nameFromDirent(de *unix.Dirent) (name []byte) {
|
||||
// Because this GOOS' syscall.Dirent does not provide a field that specifies
|
||||
// the name length, this function must first calculate the max possible name
|
||||
// length, and then search for the NULL byte.
|
||||
ml := int(de.Reclen) - nameOffset
|
||||
|
||||
// Convert syscall.Dirent.Name, which is array of int8, to []byte, by
|
||||
// overwriting Cap, Len, and Data slice header fields to the max possible
|
||||
// name length computed above, and finding the terminating NULL byte.
|
||||
//
|
||||
// TODO: is there an alternative to the deprecated SliceHeader?
|
||||
// SliceHeader was mainly deprecated due to it being misused for avoiding
|
||||
// allocations when converting a byte slice to a string, ref;
|
||||
// https://go.dev/issue/53003
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&name))
|
||||
sh.Cap = ml
|
||||
sh.Len = ml
|
||||
sh.Data = uintptr(unsafe.Pointer(&de.Name[0]))
|
||||
|
||||
if index := bytes.IndexByte(name, 0); index >= 0 {
|
||||
// Found NULL byte; set slice's cap and len accordingly.
|
||||
sh.Cap = index
|
||||
sh.Len = index
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: This branch is not expected, but included for defensive
|
||||
// programming, and provides a hard stop on the name based on the structure
|
||||
// field array size.
|
||||
sh.Cap = len(de.Name)
|
||||
sh.Len = sh.Cap
|
||||
return
|
||||
}
|
||||
|
||||
// modeTypeFromDirent converts a syscall defined constant, which is in purview
|
||||
// of OS, to a constant defined by Go, assumed by this project to be stable.
|
||||
//
|
||||
// When the syscall constant is not recognized, this function falls back to a
|
||||
// Stat on the file system.
|
||||
func (fs *UnixFS) modeTypeFromDirent(fd int, de *unix.Dirent, osDirname, osBasename string) (FileMode, error) {
|
||||
switch de.Type {
|
||||
case unix.DT_REG:
|
||||
return 0, nil
|
||||
case unix.DT_DIR:
|
||||
return ModeDir, nil
|
||||
case unix.DT_LNK:
|
||||
return ModeSymlink, nil
|
||||
case unix.DT_CHR:
|
||||
return ModeDevice | ModeCharDevice, nil
|
||||
case unix.DT_BLK:
|
||||
return ModeDevice, nil
|
||||
case unix.DT_FIFO:
|
||||
return ModeNamedPipe, nil
|
||||
case unix.DT_SOCK:
|
||||
return ModeSocket, nil
|
||||
default:
|
||||
// If syscall returned unknown type (e.g., DT_UNKNOWN, DT_WHT), then
|
||||
// resolve actual mode by reading file information.
|
||||
return fs.modeType(fd, filepath.Join(osDirname, osBasename))
|
||||
}
|
||||
}
|
||||
|
||||
// modeType returns the mode type of the file system entry identified by
|
||||
// osPathname by calling os.LStat function, to intentionally not follow symbolic
|
||||
// links.
|
||||
//
|
||||
// Even though os.LStat provides all file mode bits, we want to ensure same
|
||||
// values returned to caller regardless of whether we obtained file mode bits
|
||||
// from syscall or stat call. Therefore, mask out the additional file mode bits
|
||||
// that are provided by stat but not by the syscall, so users can rely on their
|
||||
// values.
|
||||
func (fs *UnixFS) modeType(dirfd int, name string) (os.FileMode, error) {
|
||||
fi, err := fs.Lstatat(dirfd, name)
|
||||
if err == nil {
|
||||
return fi.Mode() & ModeType, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var minimumScratchBufferSize = os.Getpagesize()
|
||||
|
||||
func newScratchBuffer() []byte {
|
||||
return make([]byte, minimumScratchBufferSize)
|
||||
}
|
||||
|
||||
func (fs *UnixFS) readDir(fd int, name string, b []byte) ([]DirEntry, error) {
|
||||
scratchBuffer := b
|
||||
if scratchBuffer == nil || len(scratchBuffer) < minimumScratchBufferSize {
|
||||
scratchBuffer = newScratchBuffer()
|
||||
}
|
||||
|
||||
var entries []DirEntry
|
||||
var workBuffer []byte
|
||||
|
||||
var sde unix.Dirent
|
||||
for {
|
||||
if len(workBuffer) == 0 {
|
||||
n, err := unix.Getdents(fd, scratchBuffer)
|
||||
if err != nil {
|
||||
if err == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
return nil, convertErrorType(err)
|
||||
}
|
||||
if n <= 0 {
|
||||
// end of directory: normal exit
|
||||
return entries, nil
|
||||
}
|
||||
workBuffer = scratchBuffer[:n] // trim work buffer to number of bytes read
|
||||
}
|
||||
|
||||
// "Go is like C, except that you just put `unsafe` all over the place".
|
||||
copy((*[unsafe.Sizeof(unix.Dirent{})]byte)(unsafe.Pointer(&sde))[:], workBuffer)
|
||||
workBuffer = workBuffer[sde.Reclen:] // advance buffer for next iteration through loop
|
||||
|
||||
if sde.Ino == 0 {
|
||||
continue // inode set to 0 indicates an entry that was marked as deleted
|
||||
}
|
||||
|
||||
nameSlice := nameFromDirent(&sde)
|
||||
nameLength := len(nameSlice)
|
||||
|
||||
if nameLength == 0 || (nameSlice[0] == '.' && (nameLength == 1 || (nameLength == 2 && nameSlice[1] == '.'))) {
|
||||
continue
|
||||
}
|
||||
|
||||
childName := string(nameSlice)
|
||||
mt, err := fs.modeTypeFromDirent(fd, &sde, name, childName)
|
||||
if err != nil {
|
||||
return nil, convertErrorType(err)
|
||||
}
|
||||
entries = append(entries, &dirent{name: childName, path: name, modeType: mt, dirfd: fd, fs: fs})
|
||||
}
|
||||
}
|
||||
|
||||
// dirent stores the name and file system mode type of discovered file system
|
||||
// entries.
|
||||
type dirent struct {
|
||||
name string
|
||||
path string
|
||||
modeType FileMode
|
||||
|
||||
dirfd int
|
||||
fs *UnixFS
|
||||
}
|
||||
|
||||
func (de dirent) Name() string {
|
||||
return de.name
|
||||
}
|
||||
|
||||
func (de dirent) IsDir() bool {
|
||||
return de.modeType&ModeDir != 0
|
||||
}
|
||||
|
||||
func (de dirent) Type() FileMode {
|
||||
return de.modeType
|
||||
}
|
||||
|
||||
func (de dirent) Info() (FileInfo, error) {
|
||||
if de.fs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return de.fs.Lstatat(de.dirfd, de.name)
|
||||
}
|
||||
|
||||
func (de dirent) Open() (File, error) {
|
||||
if de.fs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return de.fs.OpenFileat(de.dirfd, de.name, O_RDONLY, 0)
|
||||
}
|
||||
|
||||
// reset releases memory held by entry err and name, and resets mode type to 0.
|
||||
func (de *dirent) reset() {
|
||||
de.name = ""
|
||||
de.path = ""
|
||||
de.modeType = 0
|
||||
}
|
||||
Reference in New Issue
Block a user