1package kittygraphics
2
3import (
4 "bytes"
5 "compress/zlib"
6 "encoding/base64"
7 "errors"
8 "image"
9 "image/color"
10 "image/png"
11 "math/rand"
12 "os"
13 "strconv"
14 "testing"
15
16 "github.com/bnema/vev-vt/graphics"
17)
18
19func apc(header, payload string) []byte {
20 return append([]byte("\x1b_G"+header+";"+payload+"\x1b\\"), []byte{}...)
21}
22
23func pngBytes(t *testing.T, width, height int) []byte {
24 t.Helper()
25 img := image.NewRGBA(image.Rect(0, 0, width, height))
26 img.SetRGBA(0, 0, color.RGBA{R: 10, G: 20, B: 30, A: 255})
27 var out bytes.Buffer
28 if err := png.Encode(&out, img); err != nil {
29 t.Fatal(err)
30 }
31 return out.Bytes()
32}
33
34func TestParserRecognizesSplitPrefixAndPreservesText(t *testing.T) {
35 p := NewParser()
36 var events []Event
37 for _, b := range []byte("before\x1b_Ga=q,i=7;\x1b\\after") {
38 events = append(events, p.Feed([]byte{b})...)
39 }
40 var textBefore, textAfter []byte
41 var command Event
42 for _, event := range events {
43 if event.Kind == EventText && command.Kind == 0 {
44 textBefore = append(textBefore, event.Text...)
45 }
46 if event.Kind == EventCommand {
47 command = event
48 }
49 if event.Kind == EventText && command.Kind != 0 {
50 textAfter = append(textAfter, event.Text...)
51 }
52 }
53 if string(textBefore) != "before" || command.Kind != EventCommand || string(textAfter) != "after" {
54 t.Fatalf("events = %#v", events)
55 }
56 if command.Command.Controls.Action != ActionQuery || command.Command.Controls.ImageID != 7 {
57 t.Fatalf("command = %#v", command.Command)
58 }
59}
60
61func TestSessionCapabilityQueryValidatesWithoutPersisting(t *testing.T) {
62 scene := graphics.NewScene(graphics.Limits{})
63 session := NewSession(scene)
64
65 result, err := session.Feed(apc("a=q,i=31,t=d,f=24,s=1,v=1", "AAAA"))
66 if err != nil {
67 t.Fatal(err)
68 }
69 if got := string(result.Bytes()); got != "\x1b_Gi=31;OK\x1b\\" {
70 t.Fatalf("response = %q", got)
71 }
72 if got := scene.Usage(); got != (graphics.Usage{}) {
73 t.Fatalf("query persisted graphics state: %#v", got)
74 }
75}
76
77func TestSessionAcceptsKittenZlibCompressedRGB(t *testing.T) {
78 raw := []byte{
79 255, 0, 0, 0, 255, 0,
80 0, 0, 255, 255, 255, 255,
81 }
82 var compressed bytes.Buffer
83 zw := zlib.NewWriter(&compressed)
84 if _, err := zw.Write(raw); err != nil {
85 t.Fatal(err)
86 }
87 if err := zw.Close(); err != nil {
88 t.Fatal(err)
89 }
90
91 scene := graphics.NewScene(graphics.Limits{})
92 session := NewSession(scene)
93 payload := base64.RawStdEncoding.EncodeToString(compressed.Bytes())
94 split := len(payload) / 2
95 split -= split % 4
96 first, err := session.Feed(apc("a=T,q=2,f=24,o=z,m=1,s=2,v=2", payload[:split]))
97 if err != nil {
98 t.Fatal(err)
99 }
100 if len(first.Mutations) != 0 {
101 t.Fatalf("first chunk mutations = %#v", first.Mutations)
102 }
103 result, err := session.Feed(apc("m=0", payload[split:]))
104 if err != nil {
105 t.Fatal(err)
106 }
107 if len(result.Mutations) != 1 || result.Mutations[0].Kind != MutationPlacement {
108 t.Fatalf("mutations = %#v", result.Mutations)
109 }
110 assets := scene.Snapshot().Assets()
111 if len(assets) != 1 || assets[0].Format() != graphics.AssetFormatRGB || !bytes.Equal(assets[0].Encoded(), raw) {
112 t.Fatalf("assets = %#v", assets)
113 }
114}
115
116func TestSessionAcceptsZlibCompressedCapabilityQuery(t *testing.T) {
117 raw := []byte{1, 2, 3}
118 var compressed bytes.Buffer
119 zw := zlib.NewWriter(&compressed)
120 _, _ = zw.Write(raw)
121 _ = zw.Close()
122
123 session := NewSession(graphics.NewScene(graphics.Limits{}))
124 payload := base64.RawStdEncoding.EncodeToString(compressed.Bytes())
125 result, err := session.Feed(apc("a=q,i=31,f=24,o=z,s=1,v=1", payload))
126 if err != nil {
127 t.Fatal(err)
128 }
129 if got := string(result.Bytes()); got != "\x1b_Gi=31;OK\x1b\\" {
130 t.Fatalf("response = %q", got)
131 }
132}
133
134func TestSessionBoundsZlibDecompression(t *testing.T) {
135 raw := bytes.Repeat([]byte{1}, 64)
136 var compressed bytes.Buffer
137 zw := zlib.NewWriter(&compressed)
138 _, _ = zw.Write(raw)
139 _ = zw.Close()
140
141 session := NewSession(graphics.NewScene(graphics.Limits{}), Limits{MaxDecodedBytes: 32})
142 payload := base64.RawStdEncoding.EncodeToString(compressed.Bytes())
143 _, err := session.Feed(apc("a=T,f=24,o=z,s=1,v=1", payload))
144 if !errors.Is(err, ErrPayloadTooLarge) {
145 t.Fatalf("error = %v, want ErrPayloadTooLarge", err)
146 }
147}
148
149func TestSessionUsesCurrentKittenChunkFixture(t *testing.T) {
150 data, err := os.ReadFile("testdata/kitten-icat-stream-chunk.bin")
151 if err != nil {
152 t.Fatal(err)
153 }
154 scene := graphics.NewScene(graphics.Limits{})
155 session := NewSession(scene)
156 // Feed split boundaries deliberately unrelated to APC boundaries. This
157 // models the short writes observed from the current kitten/icat fixture.
158 rng := rand.New(rand.NewSource(7))
159 var result Result
160 for len(data) != 0 {
161 n := 1 + rng.Intn(4096)
162 if n > len(data) {
163 n = len(data)
164 }
165 part, err := session.Feed(data[:n])
166 if err != nil {
167 t.Fatal(err)
168 }
169 result.Events = append(result.Events, part.Events...)
170 result.Responses = append(result.Responses, part.Responses...)
171 result.Mutations = append(result.Mutations, part.Mutations...)
172 data = data[n:]
173 }
174 if len(result.Mutations) != 1 || result.Mutations[0].Kind != MutationPlacement {
175 t.Fatalf("mutations = %#v", result.Mutations)
176 }
177 if got := scene.Usage(); got.Assets != 1 || got.Placements != 1 || got.DecodedPixels != 40000 {
178 t.Fatalf("usage = %#v", got)
179 }
180 if len(result.Responses) != 0 {
181 t.Fatalf("quiet fixture response = %q", result.Bytes())
182 }
183 if _, ok := session.Image(0); ok {
184 t.Fatal("unidentified fixture image was retained as a reusable mapping")
185 }
186}
187
188func TestDecodeBase64AcceptsStandardAndRaw(t *testing.T) {
189 want := []byte{0, 1, 2, 253, 254, 255}
190 standard := base64.StdEncoding.EncodeToString(want)
191 raw := base64.RawStdEncoding.EncodeToString(want)
192 for _, encoded := range []string{standard, raw} {
193 got, err := DecodeBase64([]byte(encoded))
194 if err != nil || !bytes.Equal(got, want) {
195 t.Fatalf("DecodeBase64(%q) = %x, %v", encoded, got, err)
196 }
197 }
198 if _, err := DecodeBase64([]byte("not+base64?")); !errors.Is(err, ErrInvalidBase64) {
199 t.Fatalf("invalid base64 error = %v", err)
200 }
201}
202
203func TestSessionAcceptsRawBase64AndMapsPlacement(t *testing.T) {
204 scene := graphics.NewScene(graphics.Limits{})
205 session := NewSession(scene)
206 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
207 result, err := session.Feed(apc("a=t,i=9,f=32,s=1,v=1,q=2", payload))
208 if err != nil {
209 t.Fatal(err)
210 }
211 if len(result.Responses) != 0 || len(result.Mutations) != 1 {
212 t.Fatalf("result = %#v", result)
213 }
214 asset, ok := session.Image(9)
215 if !ok {
216 t.Fatal("image mapping missing")
217 }
218 if _, err := session.Feed(apc("a=p,i=9,p=4,X=5,Y=6,c=1,r=1", "")); err != nil {
219 t.Fatal(err)
220 }
221 placement, ok := session.Placement(9, 4)
222 if !ok {
223 t.Fatal("placement mapping missing")
224 }
225 view, ok := scene.Snapshot().Placement(placement)
226 if !ok || view.AssetID() != asset || view.Destination() != (graphics.PixelRect{X: 5, Y: 6, Width: 1, Height: 1}) {
227 t.Fatalf("placement = %#v, ok=%v", view, ok)
228 }
229}
230
231func TestPlacementIDsAreScopedToTheirImage(t *testing.T) {
232 scene := graphics.NewScene(graphics.Limits{})
233 session := NewSession(scene)
234 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
235 for _, imageID := range []uint64{1, 2} {
236 if _, err := session.Feed(apc("a=t,i="+strconv.FormatUint(imageID, 10)+",f=32,s=1,v=1,q=2", payload)); err != nil {
237 t.Fatal(err)
238 }
239 if _, err := session.Feed(apc("a=p,i="+strconv.FormatUint(imageID, 10)+",p=7,q=2", "")); err != nil {
240 t.Fatal(err)
241 }
242 }
243 first, ok := session.Placement(1, 7)
244 if !ok {
245 t.Fatal("first placement missing")
246 }
247 second, ok := session.Placement(2, 7)
248 if !ok || first == second {
249 t.Fatalf("placement mapping collision: first=%v second=%v ok=%v", first, second, ok)
250 }
251 if got := scene.Usage().Placements; got != 2 {
252 t.Fatalf("placements = %d, want 2", got)
253 }
254}
255
256func TestParseAPCAcceptsPayloadFreeCommands(t *testing.T) {
257 for _, raw := range []string{"\x1b_Ga=d,d=a\x1b\\", "\x1b_Ga=p,i=1\x1b\\"} {
258 command, err := ParseAPC([]byte(raw))
259 if err != nil {
260 t.Fatalf("ParseAPC(%q) error = %v", raw, err)
261 }
262 if len(command.Payload) != 0 {
263 t.Fatalf("ParseAPC(%q) payload = %q", raw, command.Payload)
264 }
265 }
266}
267
268func TestMalformedTruncatedInterleavedAndOversizeAreBounded(t *testing.T) {
269 if _, err := ParseCommand([]byte("a=q,a=t;")); !errors.Is(err, ErrDuplicateControl) {
270 t.Fatalf("duplicate control error = %v", err)
271 }
272 if _, err := ParseCommand([]byte("a=q,i=18446744073709551616;")); !errors.Is(err, ErrIntegerOverflow) {
273 t.Fatalf("integer overflow error = %v", err)
274 }
275 if _, err := ParseCommand([]byte("a=q,q=256;")); !errors.Is(err, ErrInvalidCommand) {
276 t.Fatalf("out-of-range quiet error = %v", err)
277 }
278 if _, err := ParseCommand([]byte("a=q,i=1,I=2;")); !errors.Is(err, ErrInvalidCommand) {
279 t.Fatalf("mixed image selector error = %v", err)
280 }
281
282 p := NewParser(Limits{MaxAPCBytes: 8})
283 events := p.Feed(apc("a=q", "0123456789"))
284 if len(events) != 1 || !errors.Is(events[0].Err, ErrAPCTooLarge) {
285 t.Fatalf("oversize events = %#v", events)
286 }
287 p = NewParser()
288 if got := p.Feed([]byte("\x1b_Ga=q;payload")); len(got) != 0 {
289 t.Fatalf("truncated feed events = %#v", got)
290 }
291 got := p.Finish()
292 if len(got) != 1 || !errors.Is(got[0].Err, ErrAPCTruncated) {
293 t.Fatalf("truncated finish events = %#v", got)
294 }
295
296 session := NewSession(graphics.NewScene(graphics.Limits{}))
297 first := apc("a=t,i=1,f=32,s=1,v=1,m=1,q=2", "AQ")
298 if _, err := session.Feed(first); err != nil {
299 t.Fatal(err)
300 }
301 if _, err := session.Feed(apc("a=t,i=2,m=0,q=2", "ID")); !errors.Is(err, ErrInterleavedUpload) {
302 t.Fatalf("interleaved error = %v", err)
303 }
304 if _, err := session.Feed(apc("a=t,m=0,q=2", "IDBA")); err != nil {
305 t.Fatal(err)
306 }
307}
308
309func TestSessionTruncationAndDeleteAbortPendingUpload(t *testing.T) {
310 for _, action := range []string{"finish", "delete"} {
311 t.Run(action, func(t *testing.T) {
312 scene := graphics.NewScene(graphics.Limits{})
313 session := NewSession(scene)
314 if _, err := session.Feed(apc("a=T,i=1,f=32,s=1,v=1,m=1,q=2", "AQ")); err != nil {
315 t.Fatal(err)
316 }
317 if action == "finish" {
318 if _, err := session.Finish(); err != nil {
319 t.Fatalf("Finish error = %v", err)
320 }
321 } else if _, err := session.Feed(apc("a=d,q=2", "")); err != nil {
322 t.Fatal(err)
323 }
324 if _, err := session.Feed(apc("m=0,q=2", "IDBA")); err == nil {
325 t.Fatal("stale continuation unexpectedly succeeded")
326 }
327 if got := scene.Usage(); got != (graphics.Usage{}) {
328 t.Fatalf("stale continuation mutated scene: %#v", got)
329 }
330 })
331 }
332}
333
334func TestMalformedPNGDoesNotMutateScene(t *testing.T) {
335 good := pngBytes(t, 1, 1)
336 scene := graphics.NewScene(graphics.Limits{})
337 session := NewSession(scene)
338 if _, err := session.Feed(apc("a=t,i=1,f=100,q=2", base64.RawStdEncoding.EncodeToString(good[:len(good)-2]))); !errors.Is(err, ErrInvalidPNG) {
339 t.Fatalf("malformed PNG error = %v", err)
340 }
341 if got := scene.Usage(); got.Assets != 0 || got.Placements != 0 {
342 t.Fatalf("malformed PNG mutated scene: %#v", got)
343 }
344}
345
346func TestImageGeometryRejectsOverflowBeforeByteArithmetic(t *testing.T) {
347 controls := Controls{Width: ^uint64(0), HasWidth: true, Height: ^uint64(0), HasHeight: true}
348 limits := Limits{MaxDimension: ^uint64(0), MaxDecodedPixels: ^uint64(0)}
349 if _, _, err := imageGeometry(nil, FormatRGBA, controls, limits); !errors.Is(err, ErrPayloadTooLarge) {
350 t.Fatalf("overflowing geometry error = %v", err)
351 }
352}
353
354func TestChunkedTransmitAndDisplayRetainsDisplayAction(t *testing.T) {
355 scene := graphics.NewScene(graphics.Limits{})
356 session := NewSession(scene)
357 first := apc("a=T,i=1,f=32,s=1,v=1,m=1,q=2", "AQ")
358 if _, err := session.Feed(first); err != nil {
359 t.Fatal(err)
360 }
361 last := apc("m=0,q=2", "IDBA")
362 result, err := session.Feed(last)
363 if err != nil {
364 t.Fatal(err)
365 }
366 if len(result.Mutations) != 1 || result.Mutations[0].Kind != MutationPlacement {
367 t.Fatalf("mutations = %#v", result.Mutations)
368 }
369 if got := scene.Usage(); got.Assets != 1 || got.Placements != 1 {
370 t.Fatalf("usage = %#v", got)
371 }
372}
373
374func TestControlsIgnoreExtensionsAndBoundIntegerFields(t *testing.T) {
375 controls, err := ParseControls([]byte("a=p,i=1,N=1,S=2,O=3"))
376 if err != nil || controls.ImageID != 1 {
377 t.Fatalf("extension controls = %#v, %v", controls, err)
378 }
379 if _, err := ParseControls([]byte("a=p,s=4294967296")); !errors.Is(err, ErrIntegerOverflow) {
380 t.Fatalf("out-of-range uint32 control error = %v", err)
381 }
382 if _, err := ParseControls([]byte("a=p,z=2147483648")); !errors.Is(err, ErrIntegerOverflow) {
383 t.Fatalf("out-of-range int32 control error = %v", err)
384 }
385}
386
387func TestImplicitPlacementIDsStopAtKittyLimit(t *testing.T) {
388 session := NewSession(nil)
389 session.nextPlacement = MaxKittyID + 1
390 if _, ok := session.takePlacementID(1); ok {
391 t.Fatal("implicit placement ID exceeded Kitty's uint32 namespace")
392 }
393}
394
395func TestUnsupportedRelativeAndCellDeleteControlsFailClosed(t *testing.T) {
396 session := NewSession(graphics.NewScene(graphics.Limits{}))
397 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
398 if _, err := session.Feed(apc("a=t,i=1,f=32,s=1,v=1,q=2", payload)); err != nil {
399 t.Fatal(err)
400 }
401 for _, header := range []string{"a=p,i=1,P=2", "a=p,i=1,H=1", "a=d,d=p,x=1,y=1", "a=d,d=P,x=1,y=1"} {
402 if _, err := session.Feed(apc(header, "")); !errors.Is(err, ErrUnsupported) {
403 t.Fatalf("%q error = %v, want ErrUnsupported", header, err)
404 }
405 }
406}
407
408func TestImageGeometryRejectsRawDecodedByteLimit(t *testing.T) {
409 controls := Controls{Width: 2, HasWidth: true, Height: 2, HasHeight: true}
410 limits := normalizeLimits(Limits{MaxDecodedBytes: 11})
411 if _, _, err := imageGeometry(make([]byte, 12), FormatRGB, controls, limits); !errors.Is(err, ErrPayloadTooLarge) {
412 t.Fatalf("error = %v, want ErrPayloadTooLarge", err)
413 }
414}
415
416func TestPNGDecodedByteBudgetAndAssetFormat(t *testing.T) {
417 data := pngBytes(t, 2, 2)
418 if _, _, err := imageGeometry(data, FormatPNG, Controls{}, normalizeLimits(Limits{MaxDecodedBytes: 15})); !errors.Is(err, ErrPayloadTooLarge) {
419 t.Fatalf("PNG byte-budget error = %v", err)
420 }
421 scene := graphics.NewScene(graphics.Limits{})
422 session := NewSession(scene)
423 payload := base64.RawStdEncoding.EncodeToString(data)
424 if _, err := session.Feed(apc("a=t,i=1,f=100,q=2", payload)); err != nil {
425 t.Fatal(err)
426 }
427 assetID, ok := session.Image(1)
428 if !ok {
429 t.Fatal("image mapping missing")
430 }
431 asset, ok := scene.Snapshot().Asset(assetID)
432 if !ok || asset.Format() != graphics.AssetFormatPNG || asset.Blob().Format != graphics.AssetFormatPNG {
433 t.Fatalf("asset format = %#v, ok=%v", asset, ok)
434 }
435}
436
437func TestPlacementFailureEmitsStableProtocolError(t *testing.T) {
438 scene := graphics.NewScene(graphics.Limits{})
439 session := NewSession(scene)
440 if _, err := session.Feed(apc("a=t,i=1,f=32,s=1,v=1,q=2", "AQIDBA")); err != nil {
441 t.Fatal(err)
442 }
443 result, err := session.Feed(apc("a=p,i=1,p=7,w=2,q=0", ""))
444 if !errors.Is(err, graphics.ErrInvalidPlacement) {
445 t.Fatalf("placement error = %v", err)
446 }
447 if got := result.Bytes(); string(got) != "\x1b_Gi=1;EINVAL\x1b\\" {
448 t.Fatalf("placement response = %q", got)
449 }
450 if len(result.Mutations) != 0 || scene.Usage().Placements != 0 {
451 t.Fatalf("failed placement mutated state: result=%#v usage=%#v", result, scene.Usage())
452 }
453}
454
455func TestFailedImageReplacementPreservesAssetAndPlacements(t *testing.T) {
456 scene := graphics.NewScene(graphics.Limits{MaxEncodedBytes: 4, MaxDecodedPixels: 4})
457 session := NewSession(scene)
458 if _, err := session.Feed(apc("a=t,i=1,f=32,s=1,v=1,q=2", "AQIDBA")); err != nil {
459 t.Fatal(err)
460 }
461 if _, err := session.Feed(apc("a=p,i=1,p=7,q=2", "")); err != nil {
462 t.Fatal(err)
463 }
464 assetID, ok := session.Image(1)
465 if !ok {
466 t.Fatal("original image mapping missing")
467 }
468 placementID, ok := session.Placement(1, 7)
469 if !ok {
470 t.Fatal("original placement mapping missing")
471 }
472 before := scene.Snapshot()
473 result, err := session.Feed(apc("a=t,i=1,f=32,s=2,v=1,q=0", "AQIDBAUGBwg"))
474 if !errors.Is(err, graphics.ErrEncodedBudget) {
475 t.Fatalf("replacement error = %v", err)
476 }
477 if got := result.Bytes(); string(got) != "\x1b_Gi=1;E2BIG\x1b\\" {
478 t.Fatalf("replacement response = %q", got)
479 }
480 if got, ok := session.Image(1); !ok || got != assetID {
481 t.Fatalf("replacement changed image mapping: id=%v ok=%v want=%v", got, ok, assetID)
482 }
483 if got, ok := session.Placement(1, 7); !ok || got != placementID {
484 t.Fatalf("replacement changed placement mapping: id=%v ok=%v want=%v", got, ok, placementID)
485 }
486 if scene.Snapshot().Generation() != before.Generation() || scene.Usage() != before.Usage() {
487 t.Fatalf("failed replacement changed scene: before=%#v after=%#v", before.Usage(), scene.Usage())
488 }
489 if _, ok := scene.Snapshot().Placement(placementID); !ok {
490 t.Fatal("failed replacement removed original placement")
491 }
492}
493
494func TestFailedDisplayedImageReplacementPreservesOldPlacement(t *testing.T) {
495 scene := graphics.NewScene(graphics.Limits{})
496 session := NewSession(scene)
497 if _, err := session.Feed(apc("a=T,i=1,f=32,s=1,v=1,q=2", "AQIDBA")); err != nil {
498 t.Fatal(err)
499 }
500 assetID, ok := session.Image(1)
501 if !ok {
502 t.Fatal("original image mapping missing")
503 }
504 placementID, ok := session.Placement(1, 1)
505 if !ok {
506 t.Fatal("original placement mapping missing")
507 }
508
509 result, err := session.Feed(apc("a=T,i=1,f=32,s=1,v=1,w=2,q=0", "AQIDBA"))
510 if !errors.Is(err, graphics.ErrInvalidPlacement) {
511 t.Fatalf("replacement placement error = %v", err)
512 }
513 if got := result.Bytes(); string(got) != "\x1b_Gi=1;EINVAL\x1b\\" {
514 t.Fatalf("replacement response = %q", got)
515 }
516 if got, ok := session.Image(1); !ok || got != assetID {
517 t.Fatalf("failed displayed replacement changed image: id=%v ok=%v want=%v", got, ok, assetID)
518 }
519 if got, ok := session.Placement(1, 1); !ok || got != placementID {
520 t.Fatalf("failed displayed replacement changed placement: id=%v ok=%v want=%v", got, ok, placementID)
521 }
522 if got := scene.Usage(); got.Assets != 1 || got.Placements != 1 {
523 t.Fatalf("failed displayed replacement usage = %#v", got)
524 }
525}
526
527func TestDeleteSelectedImagePlacementsAndImageNumberNamespace(t *testing.T) {
528 scene := graphics.NewScene(graphics.Limits{})
529 session := NewSession(scene)
530 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
531 for range 2 {
532 if _, err := session.Feed(apc("a=t,I=7,f=32,s=1,v=1,q=2", payload)); err != nil {
533 t.Fatal(err)
534 }
535 }
536 if got := scene.Usage(); got.Assets != 2 || got.Placements != 0 {
537 t.Fatalf("numbered uploads collapsed namespaces: %#v", got)
538 }
539 if _, err := session.Feed(apc("a=p,I=7,p=2,q=2", "")); err != nil {
540 t.Fatal(err)
541 }
542 if _, err := session.Feed(apc("a=p,i=1,p=1,q=2", "")); err != nil {
543 t.Fatal(err)
544 }
545 if got := scene.Usage(); got.Placements != 2 {
546 t.Fatalf("placements = %#v", got)
547 }
548 if _, err := session.Feed(apc("a=d,d=i,i=1,q=2", "")); err != nil {
549 t.Fatal(err)
550 }
551 if got := scene.Usage(); got.Assets != 2 || got.Placements != 1 {
552 t.Fatalf("selected delete affected unrelated scene state: %#v", got)
553 }
554 if _, err := session.Feed(apc("a=d,d=I,I=7,q=2", "")); err != nil {
555 t.Fatal(err)
556 }
557 if got := scene.Usage(); got.Assets != 1 || got.Placements != 0 {
558 t.Fatalf("image-number delete = %#v", got)
559 }
560 if _, ok := session.Image(1); !ok {
561 t.Fatal("image ID namespace was removed by image-number delete")
562 }
563}
564
565func FuzzParserNeverPanics(f *testing.F) {
566 f.Add([]byte("text\x1b_Ga=q;\x1b\\"))
567 f.Add([]byte("\x1b_Ga=T,m=1;AAAA\x1b\\\x1b_Gm=0;\x1b\\"))
568 f.Fuzz(func(t *testing.T, data []byte) {
569 p := NewParser(Limits{MaxAPCBytes: 1024, MaxPayloadBytes: 512, MaxUploadBytes: 512})
570 _ = p.Feed(data)
571 _ = p.Finish()
572 })
573}
574
575func FuzzSessionNeverPanics(f *testing.F) {
576 f.Add([]byte("\x1b_Ga=t,i=1,f=32,s=1,v=1;AQIDBA==\x1b\\"))
577 f.Add([]byte("\x1b_Ga=T,f=100,m=1;AAAA\x1b\\\x1b_Gm=0;AAAA\x1b\\"))
578 f.Fuzz(func(t *testing.T, data []byte) {
579 s := NewSession(graphics.NewScene(graphics.Limits{}), Limits{
580 MaxAPCBytes: 4096, MaxPayloadBytes: 2048, MaxUploadBytes: 2048,
581 MaxDimension: 128, MaxDecodedPixels: 4096,
582 })
583 _, _ = s.Feed(data)
584 _, _ = s.Finish()
585 })
586}
587
588func placeWithControls(t *testing.T, cellW, cellH int, imgW, imgH int, header string, chunked bool) graphics.PlacementView {
589 t.Helper()
590 scene := graphics.NewScene(graphics.Limits{})
591 session := NewSession(scene)
592 session.SetCellPixels(cellW, cellH)
593 payload := base64.StdEncoding.EncodeToString(pngBytes(t, imgW, imgH))
594 if chunked {
595 half := len(payload) / 2 / 4 * 4
596 if _, err := session.Feed(apc(header+",m=1", payload[:half])); err != nil {
597 t.Fatal(err)
598 }
599 if _, err := session.Feed(apc("m=0", payload[half:])); err != nil {
600 t.Fatal(err)
601 }
602 } else if _, err := session.Feed(apc(header, payload)); err != nil {
603 t.Fatal(err)
604 }
605 placements := scene.Snapshot().Placements()
606 if len(placements) != 1 || scene.Snapshot().Usage().Assets != 1 {
607 t.Fatalf("usage = %#v", scene.Snapshot().Usage())
608 }
609 return placements[0]
610}
611
612func TestPlacementSingleColumnOrRowDerivesOther(t *testing.T) {
613 tests := []struct {
614 name string
615 cellW, cellH int
616 imgW, imgH int
617 header string
618 chunked bool
619 cols, rows int64
620 }{
621 {"columns only", 10, 20, 4, 4, "a=T,f=100,C=1,c=5", false, 5, 3},
622 {"rows only", 10, 20, 4, 4, "a=T,f=100,C=1,r=3", false, 6, 3},
623 {"both", 10, 20, 4, 4, "a=T,f=100,C=1,c=5,r=3", false, 5, 3},
624 {"wide image columns only", 10, 20, 40, 10, "a=T,f=100,C=1,c=8", false, 8, 1},
625 {"crop columns only", 10, 20, 8, 8, "a=T,f=100,C=1,c=4,w=8,h=4", false, 4, 1},
626 {"crop rows only", 10, 20, 8, 8, "a=T,f=100,C=1,r=2,w=8,h=4", false, 8, 2},
627 {"unknown cell size assumes 1:2", 0, 0, 4, 4, "a=T,f=100,C=1,c=4", false, 4, 2},
628 {"unknown cell size rows only", 0, 0, 4, 4, "a=T,f=100,C=1,r=2", false, 4, 2},
629 {"minimum one", 10, 20, 100, 1, "a=T,f=100,C=1,c=2", false, 2, 1},
630 {"zero means unset", 10, 20, 4, 4, "a=T,f=100,C=1,c=5,r=0", false, 5, 3},
631 {"chunked rows only", 10, 20, 4, 4, "a=T,f=100,C=1,r=3", true, 6, 3},
632 }
633 for _, tt := range tests {
634 t.Run(tt.name, func(t *testing.T) {
635 p := placeWithControls(t, tt.cellW, tt.cellH, tt.imgW, tt.imgH, tt.header, tt.chunked)
636 cells := p.Cells()
637 if cells.Width != tt.cols || cells.Height != tt.rows {
638 t.Fatalf("cells = %dx%d, want %dx%d", cells.Width, cells.Height, tt.cols, tt.rows)
639 }
640 })
641 }
642}
643
644func TestPlacementZeroColumnsAndRowsMeansAuto(t *testing.T) {
645 p := placeWithControls(t, 10, 20, 4, 4, "a=T,f=100,C=1,c=0,r=0", false)
646 if !p.Cells().Empty() {
647 t.Fatalf("cells = %#v, want none", p.Cells())
648 }
649}
650
651func TestDeriveCells(t *testing.T) {
652 const maxInt64 = int64(^uint64(0) >> 1)
653 tests := []struct {
654 name string
655 given, givenSrc, otherSrc, givenCell, otherCell int64
656 want int64
657 wantOK bool
658 }{
659 {"exact division", 4, 4, 4, 10, 20, 2, true},
660 {"rounds up", 5, 4, 4, 10, 20, 3, true},
661 {"tiny result is at least one", 1, 1000, 1, 1, 1, 1, true},
662 {"overflow", maxInt64, 1, 1000, 1000, 1, 0, false},
663 {"zero source", 5, 0, 4, 10, 20, 0, false},
664 }
665 for _, tt := range tests {
666 t.Run(tt.name, func(t *testing.T) {
667 got, ok := deriveCells(tt.given, tt.givenSrc, tt.otherSrc, tt.givenCell, tt.otherCell)
668 if got != tt.want || ok != tt.wantOK {
669 t.Fatalf("deriveCells = %d, %v; want %d, %v", got, ok, tt.want, tt.wantOK)
670 }
671 })
672 }
673}