447 lines
14 KiB
Go
447 lines
14 KiB
Go
package fileops
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// openConfinedParent opens dst's parent without following a symbolic-link
|
|
// component. It starts from the filesystem root, so the resulting handle stays
|
|
// valid even if a pathname ancestor is later renamed or replaced.
|
|
func openConfinedParent(dst string, create bool, mode os.FileMode) (*os.Root, string, error) {
|
|
if strings.TrimSpace(dst) == "" {
|
|
return nil, "", fmt.Errorf("destination path is required")
|
|
}
|
|
abs, err := filepath.Abs(dst)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("resolve destination path: %w", err)
|
|
}
|
|
parts, err := absolutePathParts(abs)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(parts) == 0 {
|
|
return nil, "", fmt.Errorf("destination file name is required")
|
|
}
|
|
|
|
rootPath := filesystemRoot(abs)
|
|
root, err := os.OpenRoot(rootPath)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("open filesystem root %q: %w", rootPath, err)
|
|
}
|
|
for _, part := range parts[:len(parts)-1] {
|
|
child, err := openConfinedChild(root, part, create, mode)
|
|
if err != nil {
|
|
_ = root.Close()
|
|
return nil, "", err
|
|
}
|
|
_ = root.Close()
|
|
root = child
|
|
}
|
|
return root, parts[len(parts)-1], nil
|
|
}
|
|
|
|
func ensureConfinedDirectory(directory string, mode os.FileMode) error {
|
|
abs, err := filepath.Abs(directory)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve workspace directory: %w", err)
|
|
}
|
|
parts, err := absolutePathParts(abs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rootPath := filesystemRoot(abs)
|
|
root, err := os.OpenRoot(rootPath)
|
|
if err != nil {
|
|
return fmt.Errorf("open filesystem root %q: %w", rootPath, err)
|
|
}
|
|
defer func() { _ = root.Close() }()
|
|
|
|
for _, part := range parts {
|
|
child, err := openConfinedChild(root, part, true, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = root.Close()
|
|
root = child
|
|
}
|
|
return setOpenedDirectoryMode(root, mode)
|
|
}
|
|
|
|
func absolutePathParts(path string) ([]string, error) {
|
|
root := filesystemRoot(path)
|
|
relative, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve path below filesystem root: %w", err)
|
|
}
|
|
if relative == "." {
|
|
return nil, nil
|
|
}
|
|
parts := strings.FieldsFunc(relative, func(r rune) bool { return r == filepath.Separator || r == '/' || r == '\\' })
|
|
for _, part := range parts {
|
|
if part == "" || part == "." || part == ".." {
|
|
return nil, fmt.Errorf("unsafe filesystem path component %q", part)
|
|
}
|
|
}
|
|
return parts, nil
|
|
}
|
|
|
|
func filesystemRoot(path string) string {
|
|
volume := filepath.VolumeName(path)
|
|
if volume == "" {
|
|
return string(filepath.Separator)
|
|
}
|
|
return volume + string(filepath.Separator)
|
|
}
|
|
|
|
func openConfinedChild(parent *os.Root, name string, create bool, mode os.FileMode) (*os.Root, error) {
|
|
info, err := parent.Lstat(name)
|
|
created := false
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
if !create {
|
|
return nil, fmt.Errorf("destination ancestor %q does not exist", name)
|
|
}
|
|
if err := parent.Mkdir(name, mode.Perm()); err != nil && !errors.Is(err, os.ErrExist) {
|
|
return nil, fmt.Errorf("create destination ancestor %q: %w", name, err)
|
|
}
|
|
created = true
|
|
info, err = parent.Lstat(name)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inspect destination ancestor %q: %w", name, err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return nil, fmt.Errorf("destination ancestor %q is not a regular directory", name)
|
|
}
|
|
|
|
child, err := parent.OpenRoot(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open destination ancestor %q: %w", name, err)
|
|
}
|
|
opened, err := child.Stat(".")
|
|
if err != nil {
|
|
_ = child.Close()
|
|
return nil, fmt.Errorf("inspect opened destination ancestor %q: %w", name, err)
|
|
}
|
|
current, err := parent.Lstat(name)
|
|
if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.IsDir() || !os.SameFile(opened, current) {
|
|
_ = child.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reinspect destination ancestor %q: %w", name, err)
|
|
}
|
|
return nil, fmt.Errorf("destination ancestor %q changed while being opened", name)
|
|
}
|
|
if created {
|
|
if err := setOpenedDirectoryMode(child, mode); err != nil {
|
|
_ = child.Close()
|
|
return nil, fmt.Errorf("set destination ancestor permissions %q: %w", name, err)
|
|
}
|
|
}
|
|
return child, nil
|
|
}
|
|
|
|
func setOpenedDirectoryMode(root *os.Root, mode os.FileMode) error {
|
|
directory, err := root.Open(".")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = directory.Close() }()
|
|
return directory.Chmod(mode)
|
|
}
|
|
|
|
func createSiblingTemp(parent *os.Root, base string) (*os.File, string, error) {
|
|
for attempt := 0; attempt < 100; attempt++ {
|
|
var randomBytes [16]byte
|
|
if _, err := rand.Read(randomBytes[:]); err != nil {
|
|
return nil, "", fmt.Errorf("generate temporary file name: %w", err)
|
|
}
|
|
name := "." + base + ".tmp-" + hex.EncodeToString(randomBytes[:])
|
|
file, err := parent.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
|
|
if errors.Is(err, os.ErrExist) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return file, name, nil
|
|
}
|
|
return nil, "", fmt.Errorf("create unique sibling temporary file")
|
|
}
|
|
|
|
func syncOpenedDirectory(parent *os.Root) error {
|
|
directory, err := parent.Open(".")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = directory.Close() }()
|
|
return syncDirectoryFile(directory, parent.Name())
|
|
}
|
|
|
|
// OpenFileConfined opens a file after verifying its parent hierarchy without
|
|
// following symbolic links. Existing symbolic-link leaves are rejected.
|
|
func OpenFileConfined(path string, flags int, mode os.FileMode) (*os.File, error) {
|
|
parent, name, err := openConfinedParent(path, false, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = parent.Close() }()
|
|
if info, err := parent.Lstat(name); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
return nil, fmt.Errorf("destination file %q is a symbolic link", name)
|
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return nil, fmt.Errorf("inspect destination file %q: %w", name, err)
|
|
}
|
|
file, err := parent.OpenFile(name, flags, mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opened, err := file.Stat()
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return nil, fmt.Errorf("inspect opened destination file %q: %w", name, err)
|
|
}
|
|
current, err := parent.Lstat(name)
|
|
if err != nil || current.Mode()&os.ModeSymlink != 0 || !os.SameFile(opened, current) {
|
|
_ = file.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reinspect destination file %q: %w", name, err)
|
|
}
|
|
return nil, fmt.Errorf("destination file %q changed while being opened", name)
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
// DownloadedTempFile is a completed, destination-confined temporary file. It
|
|
// keeps its parent directory open until the caller installs or cleans it up.
|
|
type DownloadedTempFile struct {
|
|
parent *os.Root
|
|
name string
|
|
}
|
|
|
|
// DownloadToSiblingTemp writes a remote object into a sibling temporary file.
|
|
// The caller supplies transport behavior through download and retains object
|
|
// identity and validation policy. The destination parent must already exist.
|
|
func DownloadToSiblingTemp(destination string, download func(io.Writer) error) (*DownloadedTempFile, error) {
|
|
if download == nil {
|
|
return nil, fmt.Errorf("download function is required")
|
|
}
|
|
parent, name, err := openConfinedParent(destination, false, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, tempName, err := createSiblingTemp(parent, name)
|
|
if err != nil {
|
|
_ = parent.Close()
|
|
return nil, fmt.Errorf("create sibling temporary file: %w", err)
|
|
}
|
|
if err := download(file); err != nil {
|
|
_ = file.Close()
|
|
_ = parent.Remove(tempName)
|
|
_ = parent.Close()
|
|
return nil, err
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
_ = file.Close()
|
|
_ = parent.Remove(tempName)
|
|
_ = parent.Close()
|
|
return nil, fmt.Errorf("sync downloaded temporary file: %w", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
_ = parent.Remove(tempName)
|
|
_ = parent.Close()
|
|
return nil, fmt.Errorf("close downloaded temporary file: %w", err)
|
|
}
|
|
return &DownloadedTempFile{parent: parent, name: tempName}, nil
|
|
}
|
|
|
|
// Open returns a read handle for validation before installation.
|
|
func (f *DownloadedTempFile) Open() (*os.File, error) {
|
|
if f == nil || f.parent == nil || f.name == "" {
|
|
return nil, fmt.Errorf("downloaded temporary file is unavailable")
|
|
}
|
|
return f.parent.Open(f.name)
|
|
}
|
|
|
|
// Install atomically replaces destinationName in the opened parent directory.
|
|
func (f *DownloadedTempFile) Install(destinationName string, mode os.FileMode) error {
|
|
if f == nil || f.parent == nil || f.name == "" {
|
|
return fmt.Errorf("downloaded temporary file is unavailable")
|
|
}
|
|
if filepath.Base(destinationName) != destinationName || destinationName == "." || destinationName == "" {
|
|
return fmt.Errorf("destination file name is required")
|
|
}
|
|
file, err := f.parent.Open(f.name)
|
|
if err != nil {
|
|
return fmt.Errorf("open downloaded temporary file: %w", err)
|
|
}
|
|
if err := file.Chmod(mode); err != nil {
|
|
_ = file.Close()
|
|
return fmt.Errorf("set downloaded temporary file permissions: %w", err)
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
_ = file.Close()
|
|
return fmt.Errorf("sync downloaded temporary file: %w", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return fmt.Errorf("close downloaded temporary file: %w", err)
|
|
}
|
|
if err := f.parent.Rename(f.name, destinationName); err != nil {
|
|
return fmt.Errorf("install downloaded temporary file: %w", err)
|
|
}
|
|
f.name = ""
|
|
if err := syncOpenedDirectory(f.parent); err != nil {
|
|
return fmt.Errorf("sync destination directory after downloaded-file replacement: %w", err)
|
|
}
|
|
return f.closeParent()
|
|
}
|
|
|
|
// Cleanup removes an uninstalled temporary file. It is safe to call repeatedly.
|
|
func (f *DownloadedTempFile) Cleanup() error {
|
|
if f == nil || f.parent == nil {
|
|
return nil
|
|
}
|
|
var err error
|
|
if f.name != "" {
|
|
err = f.parent.Remove(f.name)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
err = nil
|
|
}
|
|
f.name = ""
|
|
}
|
|
closeErr := f.closeParent()
|
|
if err != nil && closeErr != nil {
|
|
return errors.Join(err, closeErr)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return closeErr
|
|
}
|
|
|
|
func (f *DownloadedTempFile) closeParent() error {
|
|
if f.parent == nil {
|
|
return nil
|
|
}
|
|
err := f.parent.Close()
|
|
f.parent = nil
|
|
return err
|
|
}
|
|
|
|
// DownloadAndInstall obtains remote bytes through a confined sibling temporary
|
|
// file and installs them with the caller-selected final mode.
|
|
func DownloadAndInstall(destination string, mode os.FileMode, download func(io.Writer) error) error {
|
|
temporary, err := DownloadToSiblingTemp(destination, download)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = temporary.Cleanup() }()
|
|
return temporary.Install(filepath.Base(destination), mode)
|
|
}
|
|
|
|
func replaceFileFromReaderConfined(dst string, source io.Reader, options ReplaceFileOptions) (resultErr error) {
|
|
parent, name, err := openConfinedParent(dst, false, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = parent.Close() }()
|
|
temporary, tempName, err := createSiblingTemp(parent, name)
|
|
if err != nil {
|
|
return fmt.Errorf("create temp file: %w", err)
|
|
}
|
|
removeTemp := true
|
|
defer func() {
|
|
if removeTemp {
|
|
if err := parent.Remove(tempName); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
resultErr = errors.Join(resultErr, fmt.Errorf("remove temporary file: %w", err))
|
|
}
|
|
}
|
|
}()
|
|
|
|
if _, err := io.Copy(temporary, source); err != nil {
|
|
return closeTemporaryFileAfterError(temporary, fmt.Errorf("write temp file: %w", err))
|
|
}
|
|
if err := temporary.Chmod(options.Mode); err != nil {
|
|
return closeTemporaryFileAfterError(temporary, fmt.Errorf("set temp file permissions: %w", err))
|
|
}
|
|
if err := temporary.Sync(); err != nil {
|
|
return closeTemporaryFileAfterError(temporary, fmt.Errorf("sync temp file: %w", err))
|
|
}
|
|
if err := temporary.Close(); err != nil {
|
|
return fmt.Errorf("close temp file: %w", err)
|
|
}
|
|
if options.BeforeRename != nil {
|
|
if err := options.BeforeRename(); err != nil {
|
|
return fmt.Errorf("check before file replacement: %w", err)
|
|
}
|
|
}
|
|
if err := parent.Rename(tempName, name); err != nil {
|
|
return fmt.Errorf("install temp file: %w", err)
|
|
}
|
|
removeTemp = false
|
|
if err := syncOpenedDirectory(parent); err != nil {
|
|
return fmt.Errorf("sync destination directory after replacement: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func installDownloadedTempFileConfined(tmpPath, dst string, mode os.FileMode) error {
|
|
if filepath.Clean(filepath.Dir(tmpPath)) != filepath.Clean(filepath.Dir(dst)) {
|
|
source, err := os.Open(tmpPath)
|
|
if err != nil {
|
|
return fmt.Errorf("open downloaded temporary file: %w", err)
|
|
}
|
|
err = replaceFileFromReaderConfined(dst, source, ReplaceFileOptions{Mode: mode})
|
|
closeErr := source.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("install downloaded file: %w", err)
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("close downloaded temporary file: %w", closeErr)
|
|
}
|
|
if err := os.Remove(tmpPath); err != nil {
|
|
return fmt.Errorf("remove installed temporary file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
parent, destinationName, err := openConfinedParent(dst, false, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = parent.Close() }()
|
|
temporaryName := filepath.Base(tmpPath)
|
|
info, err := parent.Lstat(temporaryName)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect downloaded temporary file: %w", err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return fmt.Errorf("downloaded temporary file is not a regular file")
|
|
}
|
|
file, err := parent.Open(temporaryName)
|
|
if err != nil {
|
|
return fmt.Errorf("open downloaded temporary file: %w", err)
|
|
}
|
|
if err := file.Chmod(mode); err != nil {
|
|
_ = file.Close()
|
|
return fmt.Errorf("set temp file permissions: %w", err)
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
_ = file.Close()
|
|
return fmt.Errorf("sync downloaded temp file: %w", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return fmt.Errorf("close downloaded temp file: %w", err)
|
|
}
|
|
if err := parent.Rename(temporaryName, destinationName); err != nil {
|
|
return fmt.Errorf("install downloaded file: %w", err)
|
|
}
|
|
if err := syncOpenedDirectory(parent); err != nil {
|
|
return fmt.Errorf("sync destination directory after downloaded-file replacement: %w", err)
|
|
}
|
|
return nil
|
|
}
|