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 }