返回 DeepSeek-Reasonix
markdown_image_resolver_test.go
根目录 / desktop / markdown_image_resolver_test.go
1 package main
2
3 import (
4 "bytes"
5 "encoding/base64"
6 "encoding/binary"
7 "hash/crc32"
8 "net/http"
9 "net/http/httptest"
10 "os"
11 "path/filepath"
12 "runtime"
13 "strings"
14 "testing"
15 )
16
17 var markdownImageTestPNG, _ = base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
18
19 func markdownImageTestPNGConfig(width, height uint32) []byte {
20 var out bytes.Buffer
21 out.Write([]byte("\x89PNG\r\n\x1a\n"))
22 _ = binary.Write(&out, binary.BigEndian, uint32(13))
23 chunk := make([]byte, 17)
24 copy(chunk, "IHDR")
25 binary.BigEndian.PutUint32(chunk[4:8], width)
26 binary.BigEndian.PutUint32(chunk[8:12], height)
27 chunk[12] = 8 // bit depth
28 chunk[13] = 6 // RGBA
29 out.Write(chunk)
30 _ = binary.Write(&out, binary.BigEndian, crc32.ChecksumIEEE(chunk))
31 return out.Bytes()
32 }
33
34 func TestResolveMarkdownImageForTabWorkspaceAndRemotePolicy(t *testing.T) {
35 original, _ := os.Getwd()
36 defer os.Chdir(original)
37 workspace := t.TempDir()
38 if err := os.Chdir(workspace); err != nil {
39 t.Fatal(err)
40 }
41 if err := os.MkdirAll("docs", 0o755); err != nil {
42 t.Fatal(err)
43 }
44 if err := os.WriteFile(filepath.Join("docs", "screen shot.png"), markdownImageTestPNG, 0o644); err != nil {
45 t.Fatal(err)
46 }
47
48 app := NewApp()
49 local := app.ResolveMarkdownImageForTab("", "docs/screen%20shot.png")
50 if local.ErrorCode != "" || !strings.HasPrefix(local.URL, "/__reasonix_workspace_media/") || local.Mime != "image/png" {
51 t.Fatalf("local image = %+v", local)
52 }
53 if strings.HasPrefix(local.URL, "file:") || !strings.HasPrefix(local.OpenHref, "file://") {
54 t.Fatalf("local URL/open href boundary = %+v", local)
55 }
56
57 remote := app.ResolveMarkdownImageForTab("", "https://cdn.example.com/a.png#fragment")
58 if remote.ErrorCode != "" || !strings.HasPrefix(remote.URL, remoteMarkdownImagePath+"?url=") || strings.Contains(remote.OpenHref, "#") {
59 t.Fatalf("remote image = %+v", remote)
60 }
61 if blocked := app.ResolveMarkdownImageForTab("", "http://localhost/private.png"); blocked.ErrorCode != "blocked-remote" || blocked.URL != "" {
62 t.Fatalf("localhost image was not blocked: %+v", blocked)
63 }
64 }
65
66 func TestResolveMarkdownImageSelectsPercentAndSpaceNamesExactly(t *testing.T) {
67 original, _ := os.Getwd()
68 defer os.Chdir(original)
69 workspace := t.TempDir()
70 if err := os.Chdir(workspace); err != nil {
71 t.Fatal(err)
72 }
73 percentBody := append(append([]byte(nil), markdownImageTestPNG...), []byte("percent-file")...)
74 spaceBody := append(append([]byte(nil), markdownImageTestPNG...), []byte("space-file")...)
75 if err := os.WriteFile("raw%20name.png", percentBody, 0o644); err != nil {
76 t.Fatal(err)
77 }
78 if err := os.WriteFile("raw name.png", spaceBody, 0o644); err != nil {
79 t.Fatal(err)
80 }
81 app := NewApp()
82 handler := app.workspaceMediaMiddleware()(http.NotFoundHandler())
83 for _, tc := range []struct {
84 source string
85 name string
86 body []byte
87 }{
88 {source: "raw%2520name.png", name: "raw%20name.png", body: percentBody},
89 {source: "raw%20name.png", name: "raw name.png", body: spaceBody},
90 } {
91 view := app.ResolveMarkdownImageForTab("", tc.source)
92 if view.ErrorCode != "" || view.Filename != tc.name {
93 t.Fatalf("resolve %q = %+v", tc.source, view)
94 }
95 recorder := httptest.NewRecorder()
96 handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, view.URL, nil))
97 if recorder.Code != http.StatusOK || !bytes.Equal(recorder.Body.Bytes(), tc.body) {
98 t.Fatalf("GET %q = %d %q, want exact %q", view.URL, recorder.Code, recorder.Body.Bytes(), tc.body)
99 }
100 }
101 }
102
103 func TestResolveMarkdownImageForTabRejectsTraversalAndSymlinkEscape(t *testing.T) {
104 original, _ := os.Getwd()
105 defer os.Chdir(original)
106 parent := t.TempDir()
107 workspace := filepath.Join(parent, "workspace")
108 if err := os.Mkdir(workspace, 0o755); err != nil {
109 t.Fatal(err)
110 }
111 outside := filepath.Join(parent, "outside.png")
112 if err := os.WriteFile(outside, markdownImageTestPNG, 0o644); err != nil {
113 t.Fatal(err)
114 }
115 if err := os.Chdir(workspace); err != nil {
116 t.Fatal(err)
117 }
118 app := NewApp()
119 if view := app.ResolveMarkdownImageForTab("", "../outside.png"); view.ErrorCode != "forbidden" {
120 t.Fatalf("traversal view = %+v", view)
121 }
122
123 link := filepath.Join(workspace, "escape.png")
124 if err := os.Symlink(outside, link); err != nil {
125 if runtime.GOOS == "windows" {
126 t.Skipf("symlink unavailable: %v", err)
127 }
128 t.Fatal(err)
129 }
130 if view := app.ResolveMarkdownImageForTab("", "escape.png"); view.ErrorCode != "forbidden" {
131 t.Fatalf("symlink escape view = %+v", view)
132 }
133 }
134
135 func TestResolveMarkdownDataImageMatrix(t *testing.T) {
136 app := NewApp()
137 png := "data:image/png;base64," + base64.StdEncoding.EncodeToString(markdownImageTestPNG)
138 if view := app.ResolveMarkdownImageForTab("", png); view.ErrorCode != "" || view.URL != png || view.Mime != "image/png" {
139 t.Fatalf("valid data image = %+v", view)
140 }
141 for name, source := range map[string]string{
142 "svg": "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte("<svg/>")),
143 "mime mismatch": "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(markdownImageTestPNG),
144 "malformed": "data:image/png;base64,%%%",
145 } {
146 t.Run(name, func(t *testing.T) {
147 if view := app.ResolveMarkdownImageForTab("", source); view.ErrorCode == "" || view.URL != "" {
148 t.Fatalf("unsafe data image = %+v", view)
149 }
150 })
151 }
152 tooLarge := "data:image/png;base64," + strings.Repeat("A", markdownDataImageMaxEncodedBytes)
153 if view := app.ResolveMarkdownImageForTab("", tooLarge); view.ErrorCode != "too-large" {
154 t.Fatalf("oversized data image = %+v", view)
155 }
156 tooManyPixels := "data:image/png;base64," + base64.StdEncoding.EncodeToString(markdownImageTestPNGConfig(10_000, 4_001))
157 if view := app.ResolveMarkdownImageForTab("", tooManyPixels); view.ErrorCode != "too-large" {
158 t.Fatalf("oversized decoded image = %+v", view)
159 }
160 }
161
162 func TestResolveMarkdownImageForTabRejectsLargeLocalImages(t *testing.T) {
163 original, _ := os.Getwd()
164 defer os.Chdir(original)
165 workspace := t.TempDir()
166 if err := os.Chdir(workspace); err != nil {
167 t.Fatal(err)
168 }
169 app := NewApp()
170
171 largeFile := filepath.Join(workspace, "large.png")
172 f, err := os.Create(largeFile)
173 if err != nil {
174 t.Fatal(err)
175 }
176 if err := f.Truncate(remoteMarkdownImageMaxBytes + 1); err != nil {
177 _ = f.Close()
178 t.Fatal(err)
179 }
180 if err := f.Close(); err != nil {
181 t.Fatal(err)
182 }
183 if view := app.ResolveMarkdownImageForTab("", "large.png"); view.ErrorCode != "too-large" || view.OpenHref == "" {
184 t.Fatalf("large local image = %+v", view)
185 }
186
187 if err := os.WriteFile("wide.png", markdownImageTestPNGConfig(20_000, 2_001), 0o644); err != nil {
188 t.Fatal(err)
189 }
190 if view := app.ResolveMarkdownImageForTab("", "wide.png"); view.ErrorCode != "too-large" || view.OpenHref == "" {
191 t.Fatalf("large decoded local image = %+v", view)
192 }
193 }
194
194 lines GO