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)) }