package server
import (
"bytes"
"fmt"
"html"
"image"
"image/color"
"image/draw"
_ "image/gif"
_ "image/jpeg"
"image/png"
"net/url"
"os"
"path/filepath"
"strings"
)
// artifactLocalPath maps an artifact URI emitted by the worker back into the
// configured artifact directory. It deliberately rejects traversal and files
// outside /artifacts/ so the public preview endpoint cannot become a generic
// file reader.
func artifactLocalPath(root, artifactURI string) (string, error) {
if strings.TrimSpace(root) == "" {
return "", fmt.Errorf("artifact storage disabled")
}
u, err := url.Parse(artifactURI)
if err != nil {
return "", err
}
p := u.Path
const prefix = "/artifacts/"
i := strings.Index(p, prefix)
if i < 0 {
return "", fmt.Errorf("artifact URI outside artifact namespace")
}
rel := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(p[i:], prefix)))
if rel == "." || rel == "" || filepath.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid artifact path")
}
rootAbs, err := filepath.Abs(root)
if err != nil {
return "", err
}
candidate := filepath.Join(rootAbs, rel)
candidateAbs, err := filepath.Abs(candidate)
if err != nil {
return "", err
}
check, err := filepath.Rel(rootAbs, candidateAbs)
if err != nil || check == ".." || strings.HasPrefix(check, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("artifact path escapes storage")
}
return candidateAbs, nil
}
func watermarkPreviewFile(path, label string) ([]byte, string, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, "", err
}
if len(b) > 64<<20 {
return nil, "", fmt.Errorf("artifact too large for preview")
}
ext := strings.ToLower(filepath.Ext(path))
if ext == ".svg" || bytes.Contains(bytes.ToLower(b[:minInt(len(b), 512)]), []byte("