79 lines
2.3 KiB
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
|
|
}
|