@@ -0,0 +1,213 @@
|
||||
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("<svg")) {
|
||||
out, err := watermarkSVG(b, label)
|
||||
return out, "image/svg+xml; charset=utf-8", err
|
||||
}
|
||||
img, _, err := image.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("decode preview image: %w", err)
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() < 1 || bounds.Dy() < 1 {
|
||||
return nil, "", fmt.Errorf("empty artifact image")
|
||||
}
|
||||
dst := image.NewNRGBA(bounds)
|
||||
draw.Draw(dst, bounds, img, bounds.Min, draw.Src)
|
||||
drawRasterWatermark(dst, label)
|
||||
var out bytes.Buffer
|
||||
if err := png.Encode(&out, dst); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return out.Bytes(), "image/png", nil
|
||||
}
|
||||
|
||||
func watermarkSVG(src []byte, label string) ([]byte, error) {
|
||||
s := string(src)
|
||||
i := strings.LastIndex(strings.ToLower(s), "</svg>")
|
||||
if i < 0 {
|
||||
return nil, fmt.Errorf("invalid svg artifact")
|
||||
}
|
||||
label = html.EscapeString(strings.ToUpper(strings.TrimSpace(label)))
|
||||
if label == "" {
|
||||
label = "NEURAL HUNT PREVIEW"
|
||||
}
|
||||
overlay := fmt.Sprintf(`<defs><pattern id="nh-preview-watermark" width="420" height="190" patternUnits="userSpaceOnUse" patternTransform="rotate(-24)"><text x="18" y="96" fill="white" fill-opacity="0.16" font-family="system-ui,sans-serif" font-size="28" font-weight="800" letter-spacing="5">%s</text></pattern></defs><rect x="0" y="0" width="100%%" height="100%%" fill="url(#nh-preview-watermark)" pointer-events="none"/><rect x="2%%" y="92%%" width="96%%" height="5%%" rx="12" fill="black" fill-opacity="0.30"/><text x="50%%" y="95.5%%" text-anchor="middle" fill="white" fill-opacity="0.72" font-family="system-ui,sans-serif" font-size="18" font-weight="800" letter-spacing="4">WATERMARKED LEADERBOARD PREVIEW</text>`, label)
|
||||
return []byte(s[:i] + overlay + s[i:]), nil
|
||||
}
|
||||
|
||||
var pixelFont = map[rune][7]string{
|
||||
'A': {"01110", "10001", "10001", "11111", "10001", "10001", "10001"},
|
||||
'E': {"11111", "10000", "10000", "11110", "10000", "10000", "11111"},
|
||||
'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
|
||||
'I': {"11111", "00100", "00100", "00100", "00100", "00100", "11111"},
|
||||
'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"},
|
||||
'N': {"10001", "11001", "10101", "10101", "10011", "10001", "10001"},
|
||||
'P': {"11110", "10001", "10001", "11110", "10000", "10000", "10000"},
|
||||
'R': {"11110", "10001", "10001", "11110", "10100", "10010", "10001"},
|
||||
'T': {"11111", "00100", "00100", "00100", "00100", "00100", "00100"},
|
||||
'U': {"10001", "10001", "10001", "10001", "10001", "10001", "01110"},
|
||||
'V': {"10001", "10001", "10001", "10001", "10001", "01010", "00100"},
|
||||
'W': {"10001", "10001", "10001", "10101", "10101", "10101", "01010"},
|
||||
}
|
||||
|
||||
func drawRasterWatermark(img *image.NRGBA, label string) {
|
||||
b := img.Bounds()
|
||||
w, h := b.Dx(), b.Dy()
|
||||
if w < 1 || h < 1 {
|
||||
return
|
||||
}
|
||||
label = strings.ToUpper(strings.TrimSpace(label))
|
||||
if label == "" {
|
||||
label = "NEURAL HUNT PREVIEW"
|
||||
}
|
||||
// Keep the bitmap watermark large enough to survive thumbnail scaling.
|
||||
scale := minInt(w, h) / 360
|
||||
if scale < 1 {
|
||||
scale = 1
|
||||
}
|
||||
if scale > 6 {
|
||||
scale = 6
|
||||
}
|
||||
textW := pixelTextWidth(label, scale)
|
||||
rowStep := 72 * scale
|
||||
colStep := textW + 54*scale
|
||||
for y, row := b.Min.Y+18*scale, 0; y < b.Max.Y; y, row = y+rowStep, row+1 {
|
||||
offset := 0
|
||||
if row%2 == 1 {
|
||||
offset = -(colStep / 2)
|
||||
}
|
||||
for x := b.Min.X + offset; x < b.Max.X; x += colStep {
|
||||
drawPixelText(img, x+scale, y+scale, label, scale, color.NRGBA{0, 0, 0, 72})
|
||||
drawPixelText(img, x, y, label, scale, color.NRGBA{255, 255, 255, 48})
|
||||
}
|
||||
}
|
||||
// Strong lower preview band so cropped screenshots still visibly carry a
|
||||
// watermark. It contains a repeated NH glyph rather than metadata.
|
||||
bandH := maxInt(16*scale, h/18)
|
||||
bandY := b.Max.Y - bandH
|
||||
draw.Draw(img, image.Rect(b.Min.X, bandY, b.Max.X, b.Max.Y), &image.Uniform{C: color.NRGBA{0, 0, 0, 92}}, image.Point{}, draw.Over)
|
||||
for x := b.Min.X + 8*scale; x < b.Max.X; x += 40 * scale {
|
||||
drawPixelText(img, x, bandY+4*scale, "NH", scale, color.NRGBA{255, 255, 255, 118})
|
||||
}
|
||||
}
|
||||
|
||||
func pixelTextWidth(text string, scale int) int {
|
||||
if scale < 1 {
|
||||
scale = 1
|
||||
}
|
||||
w := 0
|
||||
for _, r := range text {
|
||||
if r == ' ' {
|
||||
w += 4 * scale
|
||||
} else {
|
||||
w += 6 * scale
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func drawPixelText(dst draw.Image, x, y int, text string, scale int, col color.Color) {
|
||||
if scale < 1 {
|
||||
scale = 1
|
||||
}
|
||||
cx := x
|
||||
for _, r := range text {
|
||||
if r == ' ' {
|
||||
cx += 4 * scale
|
||||
continue
|
||||
}
|
||||
glyph, ok := pixelFont[r]
|
||||
if !ok {
|
||||
cx += 6 * scale
|
||||
continue
|
||||
}
|
||||
for gy, row := range glyph {
|
||||
for gx, bit := range row {
|
||||
if bit != '1' {
|
||||
continue
|
||||
}
|
||||
rect := image.Rect(cx+gx*scale, y+gy*scale, cx+(gx+1)*scale, y+(gy+1)*scale)
|
||||
draw.Draw(dst, rect, &image.Uniform{C: col}, image.Point{}, draw.Over)
|
||||
}
|
||||
}
|
||||
cx += 6 * scale
|
||||
}
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user