Files
narratio/internal/fileops/confined_read.go

79 lines
2.3 KiB
Go

package fileops
import (
"fmt"
"os"
"path/filepath"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
// OpenConfinedRegularFile opens a declared file beneath root without following
// symlinked ancestors or leaf entries. The returned descriptor remains valid if
// the pathname is later replaced.
func OpenConfinedRegularFile(rootPath, relativePath string) (*os.File, error) {
rootPath = filepath.Clean(strings.TrimSpace(rootPath))
if rootPath == "" || rootPath == "." {
return nil, fmt.Errorf("source root is required")
}
relativePath, err := pathsafe.NormalizeRelativeDestination(relativePath)
if err != nil {
return nil, fmt.Errorf("source path: %w", err)
}
root, err := openConfinedDirectory(rootPath)
if err != nil {
return nil, fmt.Errorf("open source root: %w", err)
}
defer root.Close()
parts := strings.Split(filepath.FromSlash(relativePath), string(filepath.Separator))
parent := root
for _, part := range parts[:len(parts)-1] {
child, err := openConfinedChild(parent, part, false, 0)
if err != nil {
if parent != root {
_ = parent.Close()
}
return nil, fmt.Errorf("open source ancestor %q: %w", part, err)
}
if parent != root {
_ = parent.Close()
}
parent = child
}
if parent != root {
defer parent.Close()
}
name := parts[len(parts)-1]
declared, err := parent.Lstat(name)
if err != nil {
return nil, fmt.Errorf("inspect source %q: %w", relativePath, err)
}
if declared.Mode()&os.ModeSymlink != 0 || !declared.Mode().IsRegular() {
return nil, fmt.Errorf("source %q is not a regular file", relativePath)
}
file, err := parent.Open(name)
if err != nil {
return nil, fmt.Errorf("open source %q: %w", relativePath, err)
}
opened, err := file.Stat()
if err != nil {
_ = file.Close()
return nil, fmt.Errorf("inspect opened source %q: %w", relativePath, err)
}
current, err := parent.Lstat(name)
if err != nil || current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() ||
!opened.Mode().IsRegular() || !os.SameFile(opened, declared) || !os.SameFile(opened, current) {
_ = file.Close()
if err != nil {
return nil, fmt.Errorf("reinspect source %q: %w", relativePath, err)
}
return nil, fmt.Errorf("source %q changed while being opened", relativePath)
}
return file, nil
}