Confine publish archive reads
This commit is contained in:
78
internal/fileops/confined_read.go
Normal file
78
internal/fileops/confined_read.go
Normal file
@@ -0,0 +1,78 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user