main 5041353

svg.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close