v0.25.0 4492065

compress.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"bytes"
7	"compress/gzip"
8	"crypto/sha256"
9	"encoding/hex"
10	"io"
11	"net/http"
12	"strconv"
13	"strings"
14	"sync"
15)
16
17// minCompress is the size under which a page is sent as it is: the gzip
18// header and the CPU cost are not worth it for a few hundred bytes.
19const minCompress = 1024
20
21// gzipLevel is the level of the pages, compressed on each request: a good
22// ratio at a cost small enough for a render that already takes longer. The
23// assets, compressed once at start, use the best level.
24const gzipLevel = 5
25
26// gzipWriters recycles the writers of the pages: a gzip.Writer holds about
27// 800 KiB of state.
28var gzipWriters = sync.Pool{New: func() any {
29	zw, _ := gzip.NewWriterLevel(nil, gzipLevel) // the level is valid
30	return zw
31}}
32
33// gzipPage compresses b with a pooled writer.
34func gzipPage(b []byte) ([]byte, error) {
35	var out bytes.Buffer
36	zw := gzipWriters.Get().(*gzip.Writer)
37	defer func() {
38		// Drop the reference to out, so a pooled writer does not keep the
39		// last page alive.
40		zw.Reset(io.Discard)
41		gzipWriters.Put(zw)
42	}()
43	zw.Reset(&out)
44	if _, err := zw.Write(b); err != nil {
45		return nil, err
46	}
47	if err := zw.Close(); err != nil {
48		return nil, err
49	}
50	return out.Bytes(), nil
51}
52
53// gzipBest compresses b once, at the best level (for the assets).
54func gzipBest(b []byte) ([]byte, error) {
55	var out bytes.Buffer
56	zw, err := gzip.NewWriterLevel(&out, gzip.BestCompression)
57	if err != nil {
58		return nil, err
59	}
60	if _, err := zw.Write(b); err != nil {
61		return nil, err
62	}
63	if err := zw.Close(); err != nil {
64		return nil, err
65	}
66	return out.Bytes(), nil
67}
68
69// acceptsGzip reports whether the Accept-Encoding of a request allows gzip
70// (RFC 9110 §12.5.3): gzip, or its alias x-gzip, with a quality above 0, or
71// "*" when gzip is not named. A quality of 0 refuses the coding; a malformed
72// quality is a refusal as well.
73func acceptsGzip(h http.Header) bool {
74	star, starSet := false, false
75	for _, line := range h.Values("Accept-Encoding") {
76		for part := range strings.SplitSeq(line, ",") {
77			coding, params, _ := strings.Cut(part, ";")
78			coding = strings.ToLower(strings.TrimSpace(coding))
79			if coding != "gzip" && coding != "x-gzip" && coding != "*" {
80				continue
81			}
82			ok := qualityPositive(params)
83			switch coding {
84			case "*":
85				star, starSet = ok, true
86			default:
87				return ok
88			}
89		}
90	}
91	return starSet && star
92}
93
94// qualityPositive reads the q parameter of a coding (1 when absent) and
95// reports whether it is above 0.
96func qualityPositive(params string) bool {
97	for p := range strings.SplitSeq(params, ";") {
98		k, v, found := strings.Cut(p, "=")
99		if !found || !strings.EqualFold(strings.TrimSpace(k), "q") {
100			continue
101		}
102		q, err := strconv.ParseFloat(strings.TrimSpace(v), 64)
103		return err == nil && q > 0
104	}
105	return true
106}
107
108// addVary adds a field to Vary unless it is already there (or Vary is *).
109func addVary(h http.Header, field string) {
110	for _, v := range h.Values("Vary") {
111		for f := range strings.SplitSeq(v, ",") {
112			if t := strings.TrimSpace(f); t == "*" || strings.EqualFold(t, field) {
113				return
114			}
115		}
116	}
117	h.Add("Vary", field)
118}
119
120// noTransform tells whether Cache-Control forbids changing the body.
121func noTransform(h http.Header) bool {
122	return hasCacheDirective(h, "no-transform")
123}
124
125// hasCacheDirective tells whether Cache-Control holds the directive.
126func hasCacheDirective(h http.Header, name string) bool {
127	for _, v := range h.Values("Cache-Control") {
128		for d := range strings.SplitSeq(v, ",") {
129			if strings.EqualFold(strings.TrimSpace(d), name) {
130				return true
131			}
132		}
133	}
134	return false
135}
136
137// pageETag is the strong validator of a rendered page: a hash of its
138// identity bytes and of the session state, so that the page of the owner
139// never shares a tag with the page of a visitor. The gzip representation
140// has a tag of its own (RFC 9110 §8.8.3).
141func pageETag(body []byte, signedIn, gzipped bool) string {
142	h := sha256.New()
143	if signedIn {
144		h.Write([]byte{1})
145	} else {
146		h.Write([]byte{0})
147	}
148	h.Write(body)
149	tag := hex.EncodeToString(h.Sum(nil)[:16])
150	if gzipped {
151		tag += "-gz"
152	}
153	return `"` + tag + `"`
154}
155
156// etagMatches implements the weak comparison of If-None-Match (RFC 9110
157// §13.1.2): a list of entity tags, each optionally weak, or "*".
158func etagMatches(header, etag string) bool {
159	want := strings.TrimPrefix(etag, "W/")
160	s := header
161	for {
162		s = strings.TrimLeft(s, " \t,")
163		if s == "" {
164			return false
165		}
166		if s[0] == '*' {
167			return true
168		}
169		s = strings.TrimPrefix(s, "W/")
170		if s == "" || s[0] != '"' {
171			return false // malformed: nothing after this can be trusted
172		}
173		end := strings.IndexByte(s[1:], '"')
174		if end < 0 {
175			return false
176		}
177		if s[:end+2] == want {
178			return true
179		}
180		s = s[end+2:]
181	}
182}

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 2

Tags 45

↑↓ moveenter openesc close