v0.4.3 2a19df1

clipboard_image.go RawHistoryFolder

1package tui
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"image"
8	_ "image/gif"
9	_ "image/jpeg"
10	"image/png"
11	"io"
12	"os"
13
14	_ "golang.org/x/image/tiff"
15	_ "golang.org/x/image/webp"
16)
17
18const (
19	maxClipboardImageBytes      = 32 << 20
20	maxClipboardImagePixels     = 25_000_000
21	maxNormalizedImageBytes     = 64 << 20
22	maxClipboardMetadataBytes   = 1 << 20
23	maxClipboardDiagnosticBytes = 16 << 10
24)
25
26var errClipboardImageTooLarge = errors.New("clipboard image is too large")
27
28// normalizeClipboardImagePNG validates an image and converts it to PNG. The
29// limits keep an untrusted clipboard owner from making zot allocate without
30// bound while probing the clipboard.
31func normalizeClipboardImagePNG(data []byte) ([]byte, error) {
32	return normalizeClipboardImagePNGWithLimits(data, maxClipboardImageBytes, maxClipboardImagePixels, maxNormalizedImageBytes)
33}
34
35func normalizeClipboardImagePNGWithLimits(data []byte, maxInput, maxPixels, maxOutput int) ([]byte, error) {
36	if len(data) == 0 {
37		return nil, errors.New("clipboard image is empty")
38	}
39	if len(data) > maxInput {
40		return nil, fmt.Errorf("%w (maximum input is %d bytes)", errClipboardImageTooLarge, maxInput)
41	}
42
43	cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
44	if err != nil {
45		return nil, fmt.Errorf("decode clipboard image metadata: %w", err)
46	}
47	if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width > maxPixels/cfg.Height {
48		return nil, fmt.Errorf("%w (maximum is %d pixels)", errClipboardImageTooLarge, maxPixels)
49	}
50
51	img, _, err := image.Decode(bytes.NewReader(data))
52	if err != nil {
53		return nil, fmt.Errorf("decode clipboard image: %w", err)
54	}
55	var out cappedBuffer
56	out.max = maxOutput
57	if err := png.Encode(&out, img); err != nil {
58		return nil, fmt.Errorf("encode clipboard image as PNG: %w", err)
59	}
60	if out.overflow {
61		return nil, fmt.Errorf("%w (maximum normalized size is %d bytes)", errClipboardImageTooLarge, maxOutput)
62	}
63	return append([]byte(nil), out.Bytes()...), nil
64}
65
66// cappedBuffer retains at most max bytes while reporting successful writes.
67// Reporting the full write prevents child processes and encoders from failing
68// with an unrelated short-write error after the configured limit is reached.
69type cappedBuffer struct {
70	buf      bytes.Buffer
71	max      int
72	overflow bool
73}
74
75func (b *cappedBuffer) Write(p []byte) (int, error) {
76	n := len(p)
77	remaining := b.max - b.buf.Len()
78	if remaining > 0 {
79		if remaining > len(p) {
80			remaining = len(p)
81		}
82		_, _ = b.buf.Write(p[:remaining])
83	}
84	if remaining < len(p) {
85		b.overflow = true
86	}
87	return n, nil
88}
89
90func (b *cappedBuffer) Bytes() []byte  { return b.buf.Bytes() }
91func (b *cappedBuffer) String() string { return b.buf.String() }
92
93func readClipboardImageFile(path string) ([]byte, error) {
94	f, err := os.Open(path)
95	if err != nil {
96		return nil, err
97	}
98	defer f.Close()
99	data, err := io.ReadAll(io.LimitReader(f, maxClipboardImageBytes+1))
100	if err != nil {
101		return nil, err
102	}
103	if len(data) > maxClipboardImageBytes {
104		return nil, fmt.Errorf("%w (maximum input is %d bytes)", errClipboardImageTooLarge, maxClipboardImageBytes)
105	}
106	return data, nil
107}
108
109func writeClipboardPNGFile(path string, data []byte) error {
110	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
111	if err != nil {
112		return err
113	}
114	if _, err := f.Write(data); err != nil {
115		_ = f.Close()
116		_ = os.Remove(path)
117		return err
118	}
119	if err := f.Close(); err != nil {
120		_ = os.Remove(path)
121		return err
122	}
123	return nil
124}

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 1

Tags 476

↑↓ moveenter openesc close