99 lines
2.6 KiB
Go
99 lines
2.6 KiB
Go
package promptdef
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/filecatalog"
|
|
)
|
|
|
|
type contentSourceRoot interface {
|
|
readContentFile(sourcePath string, contentFile string) (string, string, error)
|
|
}
|
|
|
|
type osContentSourceRoot struct {
|
|
root string
|
|
sourcePathsRelative bool
|
|
}
|
|
|
|
func (r osContentSourceRoot) readContentFile(sourcePath string, contentFile string) (string, string, error) {
|
|
if strings.TrimSpace(contentFile) == "" {
|
|
return "", "", fmt.Errorf("path is required")
|
|
}
|
|
if filepath.IsAbs(contentFile) {
|
|
return "", "", fmt.Errorf("path %q must be relative", contentFile)
|
|
}
|
|
|
|
root, err := filepath.Abs(r.root)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("resolve source root %q: %w", r.root, err)
|
|
}
|
|
canonicalRoot, err := filepath.EvalSymlinks(root)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("resolve source root %q: %w", r.root, err)
|
|
}
|
|
|
|
promptPath := sourcePath
|
|
if r.sourcePathsRelative && !filepath.IsAbs(promptPath) {
|
|
promptPath = filepath.Join(root, filepath.FromSlash(promptPath))
|
|
} else {
|
|
promptPath, err = filepath.Abs(promptPath)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("resolve prompt source %q: %w", sourcePath, err)
|
|
}
|
|
}
|
|
resolvedPath := filepath.Clean(filepath.Join(filepath.Dir(promptPath), contentFile))
|
|
if !containsOSPath(root, resolvedPath) {
|
|
return "", "", fmt.Errorf("path %q escapes source root %q", contentFile, r.root)
|
|
}
|
|
|
|
canonicalPath, err := filepath.EvalSymlinks(resolvedPath)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if !containsOSPath(canonicalRoot, canonicalPath) {
|
|
return "", "", fmt.Errorf("path %q escapes source root %q", contentFile, r.root)
|
|
}
|
|
|
|
body, err := os.ReadFile(canonicalPath)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return string(body), resolvedPath, nil
|
|
}
|
|
|
|
type fsContentSourceRoot struct {
|
|
fsys fs.FS
|
|
root string
|
|
}
|
|
|
|
func (r fsContentSourceRoot) readContentFile(sourcePath string, contentFile string) (string, string, error) {
|
|
root := filecatalog.CleanFSRoot(r.root)
|
|
cleanSourcePath := path.Clean(sourcePath)
|
|
if cleanSourcePath == root {
|
|
root = path.Dir(root)
|
|
}
|
|
|
|
resolvedPath, _, err := filecatalog.ResolveFSPath(root, path.Dir(cleanSourcePath), contentFile)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
body, err := fs.ReadFile(r.fsys, resolvedPath)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return string(body), resolvedPath, nil
|
|
}
|
|
|
|
func containsOSPath(root string, name string) bool {
|
|
relative, err := filepath.Rel(root, name)
|
|
if err != nil || filepath.IsAbs(relative) {
|
|
return false
|
|
}
|
|
return relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
|
|
}
|