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}