1package favicon
2
3import (
4 "bytes"
5 "encoding/xml"
6 "errors"
7 "fmt"
8 "image"
9 "io"
10
11 appport "github.com/bnema/dumber/internal/application/port"
12 "github.com/srwiley/oksvg"
13 "github.com/srwiley/rasterx"
14)
15
16// svgRasterSize is the square size SVG favicons are rendered at before the
17// normal PNG normalization and resizing steps.
18const svgRasterSize = 128
19
20// rasterizeSVG renders an SVG favicon to a square RGBA image. The parser only
21// reads the given bytes: it never resolves external references.
22func rasterizeSVG(data []byte) (img image.Image, err error) {
23 // The SVG parser handles untrusted page content; a parser panic must
24 // degrade to a favicon miss instead of crashing the browser.
25 defer func() {
26 if r := recover(); r != nil {
27 img = nil
28 err = fmt.Errorf("%w: svg favicon render panic: %v", appport.ErrFaviconMiss, r)
29 }
30 }()
31 trimmed := bytes.TrimSpace(data)
32 if !bytes.Contains(trimmed, []byte("<svg")) {
33 return nil, fmt.Errorf("%w: malformed svg", appport.ErrFaviconMiss)
34 }
35 if validateErr := validateSVGStructure(trimmed); validateErr != nil {
36 return nil, validateErr
37 }
38 icon, err := oksvg.ReadIconStream(bytes.NewReader(trimmed), oksvg.IgnoreErrorMode)
39 if err != nil {
40 return nil, fmt.Errorf("%w: parse svg favicon: %w", appport.ErrFaviconMiss, err)
41 }
42 if icon.ViewBox.W <= 0 || icon.ViewBox.H <= 0 {
43 return nil, fmt.Errorf("%w: svg favicon has no usable size", appport.ErrFaviconMiss)
44 }
45 dst := image.NewRGBA(image.Rect(0, 0, svgRasterSize, svgRasterSize))
46 icon.SetTarget(0, 0, svgRasterSize, svgRasterSize)
47 scanner := rasterx.NewScannerGV(svgRasterSize, svgRasterSize, dst, dst.Bounds())
48 icon.Draw(rasterx.NewDasher(svgRasterSize, svgRasterSize, scanner), 1)
49 if isFullyTransparent(dst) {
50 return nil, fmt.Errorf("%w: svg favicon rendered empty", appport.ErrFaviconMiss)
51 }
52 return dst, nil
53}
54
55const (
56 // maxSVGUseElements bounds <use> expansion work for one favicon.
57 maxSVGUseElements = 64
58 // maxSVGDepth bounds element nesting depth.
59 maxSVGDepth = 64
60)
61
62// validateSVGStructure rejects SVGs that could make the renderer recurse
63// without bound. oksvg expands <use> references recursively with no cycle
64// detection, and a runtime stack overflow cannot be recovered. oksvg only
65// resolves <use> targets from <defs> content, so forbidding <use> inside
66// <defs> limits expansion to one level and rules out cycles. The decoder
67// is strict like oksvg's, so both see the same element tree.
68func validateSVGStructure(data []byte) error {
69 dec := xml.NewDecoder(bytes.NewReader(data))
70 depth, defsDepth, uses := 0, 0, 0
71 for {
72 tok, err := dec.Token()
73 if errors.Is(err, io.EOF) {
74 break
75 }
76 if err != nil {
77 return fmt.Errorf("%w: malformed svg: %w", appport.ErrFaviconMiss, err)
78 }
79 switch t := tok.(type) {
80 case xml.StartElement:
81 depth++
82 if depth > maxSVGDepth {
83 return fmt.Errorf("%w: svg favicon nested too deeply", appport.ErrFaviconMiss)
84 }
85 switch t.Name.Local {
86 case "defs":
87 if defsDepth == 0 {
88 defsDepth = depth
89 }
90 case "use":
91 if defsDepth > 0 {
92 return fmt.Errorf("%w: svg favicon has <use> inside <defs>", appport.ErrFaviconMiss)
93 }
94 uses++
95 if uses > maxSVGUseElements {
96 return fmt.Errorf("%w: svg favicon has too many <use> elements", appport.ErrFaviconMiss)
97 }
98 }
99 case xml.EndElement:
100 if depth == defsDepth {
101 defsDepth = 0
102 }
103 depth--
104 }
105 }
106 return nil
107}
108
109func isFullyTransparent(img *image.RGBA) bool {
110 for i := 3; i < len(img.Pix); i += 4 {
111 if img.Pix[i] != 0 {
112 return false
113 }
114 }
115 return true
116}