Files
Cyrene_v2/internal/runtime/exec/file.go
T

100 lines
2.6 KiB
Go

package exec
import (
"fmt"
"log/slog"
"os"
"sync"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// FileSandbox confines file read, write, and list access to a set of allowed
// root directories (DevDocs/design/04-运行时.md §8, design/11 §5). Every path is
// validated by [Resolve] before use, which rejects absolute paths outside the
// roots, relative paths that escape them, and symlinks that resolve outside
// them.
type FileSandbox struct {
mu sync.RWMutex
roots []string
log *slog.Logger
}
// NewFileSandbox builds a FileSandbox confined to roots. Raw roots are made
// absolute and cleaned. The logger is required.
func NewFileSandbox(roots []string, log *slog.Logger) (*FileSandbox, error) {
if log == nil {
return nil, ErrNilLogger
}
return &FileSandbox{
roots: absCleaners(roots),
log: logger.WithSource(log, "runtime/exec"),
}, nil
}
// SetRoots atomically replaces the allowed roots. It enables a live config
// reload without racing in-flight Resolve calls.
func (s *FileSandbox) SetRoots(roots []string) {
s.mu.Lock()
defer s.mu.Unlock()
s.roots = absCleaners(roots)
}
// Resolve validates path against the current allowed roots and returns its
// absolute, symlink-resolved location, or an error if it would escape. A
// relative path is resolved against the first allowed root.
func (s *FileSandbox) Resolve(path string) (string, error) {
roots := s.rootsLocked()
if len(roots) == 0 {
return "", ErrNoAllowedRoot
}
return resolveInsideRoot(path, roots)
}
// ReadFile reads a file confined to the allowed roots.
func (s *FileSandbox) ReadFile(path string) ([]byte, error) {
abs, err := s.Resolve(path)
if err != nil {
return nil, err
}
data, err := os.ReadFile(abs)
if err != nil {
return nil, fmt.Errorf("exec: read %s: %w", abs, err)
}
return data, nil
}
// WriteFile writes data to a path confined to the allowed roots.
func (s *FileSandbox) WriteFile(path string, data []byte, perm os.FileMode) error {
abs, err := s.Resolve(path)
if err != nil {
return err
}
if err := os.WriteFile(abs, data, perm); err != nil {
return fmt.Errorf("exec: write %s: %w", abs, err)
}
return nil
}
// List returns the directory entries of a directory confined to the allowed
// roots.
func (s *FileSandbox) List(dir string) ([]os.DirEntry, error) {
abs, err := s.Resolve(dir)
if err != nil {
return nil, err
}
entries, err := os.ReadDir(abs)
if err != nil {
return nil, fmt.Errorf("exec: list %s: %w", abs, err)
}
return entries, nil
}
func (s *FileSandbox) rootsLocked() []string {
s.mu.RLock()
defer s.mu.RUnlock()
roots := make([]string, len(s.roots))
copy(roots, s.roots)
return roots
}