1package kittygraphics
2
3import (
4 "bytes"
5 "compress/zlib"
6 "fmt"
7 "image/png"
8 "io"
9 "math"
10 "math/big"
11 "strconv"
12
13 "github.com/bnema/vev-vt/graphics"
14)
15
16type placementKey struct {
17 imageID uint64
18 placementID uint64
19}
20
21type placementOrigin struct {
22 x, y uint64
23 set bool
24 // cellW and cellH are the cell size in pixels, or zero when unknown.
25 cellW, cellH int64
26}
27
28type upload struct {
29 controls Controls
30 imageID uint64
31 payload []byte
32 chunks uint64
33 display bool
34 origin placementOrigin
35}
36
37// MutationKind describes a scene mutation performed by a session.
38type MutationKind uint8
39
40const (
41 MutationUpload MutationKind = iota + 1
42 MutationPlacement
43 MutationDeleteImage
44 MutationDeletePlacement
45 MutationClear
46 MutationDeletePlacements
47)
48
49// Mutation records the stable protocol and opaque scene IDs involved in one
50// accepted command.
51type Mutation struct {
52 Kind MutationKind
53 ImageID uint64
54 ChildID uint64
55 AssetID graphics.AssetID
56 PlacementID uint64
57 ScenePlacementID graphics.PlacementID
58}
59
60// Result contains all output from one or more commands in a Feed call.
61type Result struct {
62 Events []Event
63 Responses [][]byte
64 Mutations []Mutation
65}
66
67// Bytes returns response bytes in stable command order. The returned slice is
68// newly allocated and may be modified by the caller.
69func (r Result) Bytes() []byte {
70 var n int
71 for _, response := range r.Responses {
72 n += len(response)
73 }
74 out := make([]byte, 0, n)
75 for _, response := range r.Responses {
76 out = append(out, response...)
77 }
78 return out
79}
80
81// Response is the wire representation of a stable Kitty response.
82type Response struct {
83 ImageID uint64
84 OK bool
85 Code string
86 Bytes []byte
87}
88
89const responsePrefix = "\x1b_G"
90const responseSuffix = "\x1b\\"
91
92// MakeResponse constructs the canonical Kitty response used by Session.
93func MakeResponse(imageID uint64, code string) []byte {
94 if code == "" {
95 code = "OK"
96 }
97 return []byte(responsePrefix + "i=" + strconv.FormatUint(imageID, 10) + ";" + code + responseSuffix)
98}
99
100// Feed parses and applies arbitrary bytes. Ordinary text is returned as
101// EventText; it is never interpreted, discarded, or passed through graphics
102// scene mutation. A malformed command is reported after all complete events
103// in the same input have been handled.
104func (s *Session) Feed(data []byte) (Result, error) {
105 if s == nil {
106 return Result{}, ErrNoScene
107 }
108 s.mu.Lock()
109 defer s.mu.Unlock()
110 var result Result
111 for _, event := range s.parser.Feed(data) {
112 result.Events = append(result.Events, event)
113 if event.Kind == EventError {
114 s.upload = nil
115 continue
116 }
117 if event.Kind != EventCommand {
118 continue
119 }
120 responses, mutation, err := s.apply(event.Command)
121 if len(responses) != 0 {
122 result.Responses = append(result.Responses, responses...)
123 }
124 if mutation != nil {
125 result.Mutations = append(result.Mutations, *mutation)
126 }
127 if err != nil {
128 // Keep the error attached to the event as well as returning it
129 // below. This makes mixed text/graphics feeds inspectable without
130 // requiring callers to reconstruct command order.
131 result.Events[len(result.Events)-1].Kind = EventError
132 result.Events[len(result.Events)-1].Err = err
133 }
134 }
135 var first error
136 for _, event := range result.Events {
137 if event.Kind == EventError && event.Err != nil {
138 first = event.Err
139 break
140 }
141 }
142 return result, first
143}
144
145// Write is an adapter-oriented alias for Feed.
146func (s *Session) Write(data []byte) (Result, error) { return s.Feed(data) }
147
148// Handle is an adapter-oriented alias for Feed.
149func (s *Session) Handle(data []byte) (Result, error) { return s.Feed(data) }
150
151// Finish flushes an incomplete parser sequence. It never mutates the scene.
152func (s *Session) Finish() (Result, error) {
153 if s == nil {
154 return Result{}, ErrNoScene
155 }
156 s.mu.Lock()
157 defer s.mu.Unlock()
158 result := Result{Events: s.parser.Finish()}
159 s.upload = nil
160 if len(result.Events) != 0 {
161 return result, result.Events[0].Err
162 }
163 return result, nil
164}
165
166// Process applies one already parsed command. It is useful for callers that
167// have their own stream framing but want this package's strict adapter.
168//
169// Process never retains command.Payload (or the APC bytes it was parsed from)
170// beyond the call: whatever must outlive it, such as a pending chunked upload
171// or a committed asset, is copied or freshly decoded. Callers may therefore pass
172// a command from ParseAPCBorrowed and reuse or overwrite the underlying bytes
173// as soon as Process returns.
174func (s *Session) Process(command Command) (Result, error) {
175 if s == nil {
176 return Result{}, ErrNoScene
177 }
178 s.mu.Lock()
179 defer s.mu.Unlock()
180 responses, mutation, err := s.apply(command)
181 result := Result{Responses: responses}
182 if mutation != nil {
183 result.Mutations = []Mutation{*mutation}
184 }
185 return result, err
186}
187
188// SetPendingPlacement supplies the cursor-relative pixel origin for the next
189// placement. The origin is adapter context, not a Kitty control field: Kitty
190// X/Y remain pixel offsets within the cursor cell.
191func (s *Session) SetPendingPlacement(x, y uint64) {
192 if s == nil {
193 return
194 }
195 s.mu.Lock()
196 defer s.mu.Unlock()
197 s.origin.x, s.origin.y, s.origin.set = x, y, true
198 if s.upload != nil {
199 s.upload.origin = s.origin
200 }
201}
202
203// SetCellPixels supplies the terminal cell size in pixels. It is used to keep
204// the aspect ratio when a placement gives only one of c/r. Non-positive values
205// mean the size is unknown and a default 1:2 (width:height) cell is assumed.
206func (s *Session) SetCellPixels(width, height int) {
207 if s == nil {
208 return
209 }
210 s.mu.Lock()
211 defer s.mu.Unlock()
212 if width <= 0 || height <= 0 {
213 width, height = 0, 0
214 }
215 s.origin.cellW, s.origin.cellH = int64(width), int64(height)
216 if s.upload != nil {
217 s.upload.origin = s.origin
218 }
219}
220
221// AbortPendingUpload discards an in-flight chunked upload without changing
222// committed images or placements. Screen-level full clears use it to ensure a
223// continuation arriving after the clear cannot publish stale graphics.
224func (s *Session) AbortPendingUpload() {
225 if s == nil {
226 return
227 }
228 s.mu.Lock()
229 defer s.mu.Unlock()
230 s.upload = nil
231}
232
233func (s *Session) apply(command Command) ([][]byte, *Mutation, error) {
234 c := command.Controls
235 action := c.Action
236 if !c.HasAction {
237 action = ActionTransmit
238 }
239 if c.HasCompression && c.Compression != CompressionZlib {
240 return s.failure(c, controlsImageID(c), ErrUnsupported)
241 }
242 if c.HasTransmission && c.Transmission != TransmissionDirect {
243 return s.failure(c, controlsImageID(c), ErrUnsupported)
244 }
245 if action == ActionDelete {
246 s.upload = nil
247 } else if s.upload != nil && action != ActionTransmit && action != ActionTransmitDisplay {
248 return s.failure(c, controlsImageID(c), ErrInterleavedUpload)
249 }
250 switch action {
251 case ActionTransmit, ActionTransmitDisplay:
252 return s.transmit(command, action == ActionTransmitDisplay)
253 case ActionPut:
254 if len(command.Payload) != 0 {
255 return s.failure(c, controlsImageID(c), ErrInvalidCommand)
256 }
257 return s.put(c)
258 case ActionQuery:
259 return s.query(command)
260 case ActionDelete:
261 if len(command.Payload) != 0 {
262 return s.failure(c, controlsImageID(c), ErrInvalidCommand)
263 }
264 return s.delete(c)
265 case ActionFrame, ActionCompose:
266 return s.failure(c, controlsImageID(c), ErrUnsupported)
267 default:
268 return s.failure(c, controlsImageID(c), ErrUnknownAction)
269 }
270}
271
272func (s *Session) transmit(command Command, display bool) ([][]byte, *Mutation, error) {
273 c := command.Controls
274 imageID := c.ImageID
275 if !c.HasImageID || imageID == 0 {
276 imageID = 0
277 if c.HasImageNumber {
278 imageID = s.nextImageID()
279 }
280 }
281 if s.upload != nil {
282 if !continuationControlsAllowed(c, s.upload.controls) {
283 return s.failure(c, responseImageID(c), ErrInterleavedUpload)
284 }
285 if c.HasImageID && c.ImageID != s.upload.imageID {
286 return s.failure(c, c.ImageID, ErrInterleavedUpload)
287 }
288 if c.HasImageNumber && (!s.upload.controls.HasImageNumber || c.ImageNumber != s.upload.controls.ImageNumber) {
289 return s.failure(c, responseImageID(c), ErrInterleavedUpload)
290 }
291 imageID = s.upload.imageID
292 if s.upload.chunks >= s.limits.MaxChunks {
293 return s.abortUpload(c, imageID, ErrTooManyChunks)
294 }
295 if uint64(len(s.upload.payload))+uint64(len(command.Payload)) > s.limits.MaxUploadBytes {
296 return s.abortUpload(c, imageID, ErrPayloadTooLarge)
297 }
298 s.upload.payload = append(s.upload.payload, command.Payload...)
299 s.upload.chunks++
300 s.upload.controls = mergeControls(s.upload.controls, c)
301 if c.HasMore && c.More == 1 {
302 return nil, nil, nil
303 }
304 complete := *s.upload
305 s.upload = nil
306 complete.controls = mergeControls(complete.controls, c)
307 return s.commitUpload(complete, complete.display)
308 }
309
310 if uint64(len(command.Payload)) > s.limits.MaxUploadBytes {
311 return s.failure(c, imageID, ErrPayloadTooLarge)
312 }
313 if c.HasMore && c.More == 1 {
314 s.upload = &upload{
315 controls: c,
316 imageID: imageID,
317 // The session keeps the payload across Process calls, so it must
318 // not alias the caller's (possibly borrowed) bytes.
319 payload: append([]byte(nil), command.Payload...),
320 chunks: 1,
321 display: display,
322 origin: s.origin,
323 }
324 return nil, nil, nil
325 }
326 return s.commitUpload(upload{controls: c, imageID: imageID, payload: command.Payload, chunks: 1, display: display, origin: s.origin}, display)
327}
328
329func (s *Session) commitUpload(value upload, display bool) ([][]byte, *Mutation, error) {
330 if uint64(len(value.payload)) > s.limits.MaxPayloadBytes || uint64(len(value.payload)) > s.limits.MaxUploadBytes {
331 return s.failure(value.controls, value.imageID, ErrPayloadTooLarge)
332 }
333 decoded, err := decodeImagePayload(value.payload, value.controls, s.limits)
334 if err != nil {
335 return s.failure(value.controls, value.imageID, err)
336 }
337 format := value.controls.Format
338 if !value.controls.HasFormat {
339 format = FormatRGBA
340 }
341 width, height, err := imageGeometry(decoded, format, value.controls, s.limits)
342 if err != nil {
343 return s.failure(value.controls, value.imageID, err)
344 }
345 if s.scene == nil {
346 return s.failure(value.controls, value.imageID, ErrNoScene)
347 }
348 persistent := value.controls.HasImageID && value.controls.ImageID != 0 || value.controls.HasImageNumber
349 old, replacing := s.images[value.imageID]
350 if !persistent {
351 replacing = false
352 }
353 if !replacing && s.scene.Usage().Assets >= s.limits.MaxImages {
354 return s.failure(value.controls, value.imageID, graphics.ErrTooManyAssets)
355 }
356 if replacing && display {
357 // Validate the prospective placement before replacing the asset. A
358 // rejected T command must leave both the old asset and its placements
359 // available for subsequent use.
360 if err := s.validateReplacementPlacement(value.controls, old, width, height); err != nil {
361 return s.failure(value.controls, value.imageID, err)
362 }
363 }
364 var assetID graphics.AssetID
365 if replacing {
366 // Scene replacement is copy-on-write: a rejected new asset leaves the
367 // old asset and every placement that references it untouched.
368 assetID, err = s.scene.ReplaceAssetOwned(old, graphics.AssetBlob{
369 Encoded: decoded,
370 Format: assetFormat(format),
371 Width: width,
372 Height: height,
373 })
374 } else {
375 assetID, err = s.scene.AddAssetOwned(graphics.AssetBlob{
376 Encoded: decoded,
377 Format: assetFormat(format),
378 Width: width,
379 Height: height,
380 })
381 }
382 if err != nil {
383 return s.failure(value.controls, value.imageID, err)
384 }
385 if replacing {
386 s.removeMappingsForAsset(old)
387 }
388 if persistent {
389 s.images[value.imageID] = assetID
390 }
391 if value.controls.HasImageNumber {
392 s.imageNumbers[value.controls.ImageNumber] = value.imageID
393 }
394 childID := s.nextChild
395 s.nextChild++
396 s.children[childID] = Child{ID: childID, ImageID: value.imageID, AssetID: assetID}
397 mutation := &Mutation{Kind: MutationUpload, ImageID: value.imageID, ChildID: childID, AssetID: assetID}
398 if display {
399 responses, placementMutation, placeErr := s.place(value.controls, value.imageID, assetID, value.origin)
400 if placementMutation != nil {
401 mutation.Kind = MutationPlacement
402 mutation.PlacementID = placementMutation.PlacementID
403 mutation.ScenePlacementID = placementMutation.ScenePlacementID
404 }
405 if placeErr != nil {
406 // T (transmit and display) is one protocol operation. Do not
407 // leave an uploaded asset behind when its placement is rejected.
408 _ = s.scene.RemoveAssetCascade(assetID)
409 if persistent {
410 delete(s.images, value.imageID)
411 }
412 if value.controls.HasImageNumber && s.imageNumbers[value.controls.ImageNumber] == value.imageID {
413 delete(s.imageNumbers, value.controls.ImageNumber)
414 }
415 delete(s.children, childID)
416 failureResponses, _, failureErr := s.failure(value.controls, value.imageID, placeErr)
417 return failureResponses, nil, failureErr
418 }
419 return s.success(value.controls, value.imageID, responses...), mutation, nil
420 }
421 return s.success(value.controls, value.imageID), mutation, nil
422}
423
424func decodeImagePayload(payload []byte, controls Controls, limits Limits) ([]byte, error) {
425 decoded, err := DecodeBase64(payload)
426 if err != nil {
427 return nil, err
428 }
429 if !controls.HasCompression {
430 return decoded, nil
431 }
432 if controls.Compression != CompressionZlib {
433 return nil, ErrUnsupported
434 }
435 zr, err := zlib.NewReader(bytes.NewReader(decoded))
436 if err != nil {
437 return nil, fmt.Errorf("%w: zlib: %v", ErrInvalidCommand, err)
438 }
439 defer func() { _ = zr.Close() }()
440 limit := limits.MaxDecodedBytes
441 if limit >= math.MaxInt64 {
442 limit = math.MaxInt64 - 1
443 }
444 decompressed, err := io.ReadAll(io.LimitReader(zr, int64(limit)+1))
445 if err != nil {
446 return nil, fmt.Errorf("%w: zlib: %v", ErrInvalidCommand, err)
447 }
448 if uint64(len(decompressed)) > limits.MaxDecodedBytes {
449 return nil, ErrPayloadTooLarge
450 }
451 return decompressed, nil
452}
453
454func assetFormat(format Format) graphics.AssetFormat {
455 switch format {
456 case FormatRGB:
457 return graphics.AssetFormatRGB
458 case FormatRGBA:
459 return graphics.AssetFormatRGBA
460 case FormatPNG:
461 return graphics.AssetFormatPNG
462 default:
463 return graphics.AssetFormatUnknown
464 }
465}
466
467func imageGeometry(data []byte, format Format, c Controls, limits Limits) (int64, int64, error) {
468 if format == FormatPNG {
469 config, err := png.DecodeConfig(bytes.NewReader(data))
470 if err != nil {
471 return 0, 0, fmt.Errorf("%w: %v", ErrInvalidPNG, err)
472 }
473 if config.Width <= 0 || config.Height <= 0 {
474 return 0, 0, ErrInvalidPNG
475 }
476 if uint64(config.Width) > limits.MaxDimension || uint64(config.Height) > limits.MaxDimension {
477 return 0, 0, ErrPayloadTooLarge
478 }
479 pixels, ok := checkedProduct(uint64(config.Width), uint64(config.Height))
480 decodedBytes, bytesOK := checkedProduct(pixels, 4)
481 if !ok || !bytesOK || pixels > limits.MaxDecodedPixels || decodedBytes > limits.MaxDecodedBytes {
482 return 0, 0, ErrPayloadTooLarge
483 }
484 if c.HasWidth && c.Width != uint64(config.Width) || c.HasHeight && c.Height != uint64(config.Height) {
485 return 0, 0, fmt.Errorf("%w: PNG dimensions do not match controls", ErrInvalidPNG)
486 }
487 // Decode, rather than only DecodeConfig, so malformed/truncated IDAT
488 // data and CRC failures are rejected by the standard library.
489 if _, err := png.Decode(bytes.NewReader(data)); err != nil {
490 return 0, 0, fmt.Errorf("%w: %v", ErrInvalidPNG, err)
491 }
492 return int64(config.Width), int64(config.Height), nil
493 }
494 if !format.Valid() {
495 return 0, 0, ErrUnsupported
496 }
497 if !c.HasWidth || !c.HasHeight || c.Width == 0 || c.Height == 0 {
498 return 0, 0, fmt.Errorf("%w: raw format needs s and v", ErrInvalidCommand)
499 }
500 if c.Width > limits.MaxDimension || c.Height > limits.MaxDimension {
501 return 0, 0, ErrPayloadTooLarge
502 }
503 pixels, ok := checkedProduct(c.Width, c.Height)
504 if !ok || pixels > limits.MaxDecodedPixels {
505 return 0, 0, ErrPayloadTooLarge
506 }
507 channels := uint64(3)
508 if format == FormatRGBA {
509 channels = 4
510 }
511 rowBytes, ok := checkedProduct(c.Height, channels)
512 if !ok {
513 return 0, 0, ErrPayloadTooLarge
514 }
515 expected, ok := checkedProduct(c.Width, rowBytes)
516 if !ok || expected > limits.MaxDecodedBytes {
517 return 0, 0, ErrPayloadTooLarge
518 }
519 if uint64(len(data)) != expected {
520 return 0, 0, fmt.Errorf("%w: raw payload has %d bytes, want %d", ErrInvalidCommand, len(data), expected)
521 }
522 return int64(c.Width), int64(c.Height), nil
523}
524
525func (s *Session) put(c Controls) ([][]byte, *Mutation, error) {
526 imageID, assetID, ok := s.resolveImage(c)
527 if !ok {
528 return s.failure(c, responseImageID(c), ErrImageNotFound)
529 }
530 responses, mutation, err := s.place(c, imageID, assetID, s.origin)
531 if err != nil {
532 failureResponses, _, failureErr := s.failure(c, imageID, err)
533 return failureResponses, nil, failureErr
534 }
535 return s.success(c, imageID, responses...), mutation, nil
536}
537
538func (s *Session) validateReplacementPlacement(c Controls, old graphics.AssetID, width, height int64) error {
539 if _, err := placementSpec(c, old, width, height, s.origin); err != nil {
540 return err
541 }
542 snapshot := s.scene.Snapshot()
543 removedScene := uint64(0)
544 for _, placement := range snapshot.Placements() {
545 if placement.AssetID() == old {
546 removedScene++
547 }
548 }
549 removedSession := uint64(0)
550 for _, mapped := range s.placements {
551 placement, ok := snapshot.Placement(mapped)
552 if !ok || placement.AssetID() == old {
553 removedSession++
554 }
555 }
556 projectedSession := uint64(len(s.placements)) - removedSession
557 projectedScene := snapshot.Usage().Placements - removedScene
558 needsSlot := true
559 if c.HasPlacementID && c.PlacementID != 0 {
560 if mapped, ok := s.placements[placementKey{imageID: c.ImageID, placementID: c.PlacementID}]; ok {
561 if placement, ok := snapshot.Placement(mapped); ok && placement.AssetID() != old {
562 needsSlot = false
563 }
564 }
565 }
566 if needsSlot && projectedSession >= s.limits.MaxPlacements {
567 return graphics.ErrTooManyPlacements
568 }
569 if needsSlot && projectedScene >= s.scene.Limits().MaxPlacements {
570 return graphics.ErrTooManyPlacements
571 }
572 if s.scene.Generation() >= ^uint64(0)-1 {
573 return graphics.ErrGenerationOverflow
574 }
575 return nil
576}
577
578func placementSpec(c Controls, assetID graphics.AssetID, width, height int64, origin placementOrigin) (graphics.PlacementSpec, error) {
579 source := graphics.PixelRect{Width: width, Height: height}
580 var err error
581 if source.X, err = checkedCoordinate(c.X, c.HasX); err != nil {
582 return graphics.PlacementSpec{}, err
583 }
584 if source.Y, err = checkedCoordinate(c.Y, c.HasY); err != nil {
585 return graphics.PlacementSpec{}, err
586 }
587 if source.Width, err = checkedDimension(c.SourceWidth, c.HasSourceWidth, width); err != nil {
588 return graphics.PlacementSpec{}, err
589 }
590 if source.Height, err = checkedDimension(c.SourceHeight, c.HasSourceHeight, height); err != nil {
591 return graphics.PlacementSpec{}, err
592 }
593 offsetX, err := checkedCoordinate(c.SourceX, c.HasSourceX)
594 if err != nil {
595 return graphics.PlacementSpec{}, err
596 }
597 offsetY, err := checkedCoordinate(c.SourceY, c.HasSourceY)
598 if err != nil {
599 return graphics.PlacementSpec{}, err
600 }
601 destination := graphics.PixelRect{X: offsetX, Y: offsetY, Width: source.Width, Height: source.Height}
602 if origin.set {
603 if origin.x > math.MaxInt64 || origin.y > math.MaxInt64 || destination.X > math.MaxInt64-int64(origin.x) || destination.Y > math.MaxInt64-int64(origin.y) {
604 return graphics.PlacementSpec{}, ErrIntegerOverflow
605 }
606 destination.X += int64(origin.x)
607 destination.Y += int64(origin.y)
608 }
609 if !source.Valid() || !destination.Valid() {
610 return graphics.PlacementSpec{}, graphics.ErrInvalidRect
611 }
612 assetBounds := graphics.PixelRect{Width: width, Height: height}
613 clipped, ok := assetBounds.Intersect(source)
614 if !ok || clipped != source {
615 return graphics.PlacementSpec{}, graphics.ErrInvalidPlacement
616 }
617 // As in Kitty, c=0 and r=0 mean "unset". When only one is set, the other
618 // is derived to keep the aspect ratio of the displayed source rectangle.
619 var cells graphics.CellRect
620 hasCells := (c.HasColumns && c.Columns != 0) || (c.HasRows && c.Rows != 0)
621 if hasCells {
622 if c.Columns > math.MaxInt64 || c.Rows > math.MaxInt64 {
623 return graphics.PlacementSpec{}, fmt.Errorf("%w: c/r", ErrInvalidCommand)
624 }
625 columns, rows := int64(c.Columns), int64(c.Rows)
626 if !c.HasColumns {
627 columns = 0
628 }
629 if !c.HasRows {
630 rows = 0
631 }
632 // Unknown cell size: assume a 1:2 (width:height) cell, which is only
633 // used for its ratio.
634 cellW, cellH := origin.cellW, origin.cellH
635 if cellW <= 0 || cellH <= 0 {
636 cellW, cellH = 1, 2
637 }
638 var ok bool
639 if columns == 0 {
640 columns, ok = deriveCells(rows, source.Height, source.Width, cellH, cellW)
641 } else if rows == 0 {
642 rows, ok = deriveCells(columns, source.Width, source.Height, cellW, cellH)
643 } else {
644 ok = true
645 }
646 if !ok {
647 return graphics.PlacementSpec{}, ErrIntegerOverflow
648 }
649 cells = graphics.CellRect{X: signedCoordinateInt(c.CellOffsetX, c.HasCellOffsetX), Y: signedCoordinateInt(c.CellOffsetY, c.HasCellOffsetY), Width: columns, Height: rows}
650 if !cells.Valid() {
651 return graphics.PlacementSpec{}, graphics.ErrInvalidRect
652 }
653 }
654 spec := graphics.PlacementSpec{Asset: assetID, Source: source, Destination: destination, Cells: cells, HasCells: hasCells}
655 if c.HasLayer {
656 spec.Layer = c.Layer
657 spec.HasLayer = true
658 }
659 return spec, nil
660}
661
662// deriveCells returns the cell count along the unspecified axis that preserves
663// the source aspect ratio: ceil(given*givenCell*otherSrc / (givenSrc*otherCell)).
664// All inputs are positive, so the result is at least 1. The boolean is false
665// if an input is not positive or the result does not fit in int64.
666func deriveCells(given, givenSrc, otherSrc, givenCell, otherCell int64) (int64, bool) {
667 if given <= 0 || givenSrc <= 0 || otherSrc <= 0 || givenCell <= 0 || otherCell <= 0 {
668 return 0, false
669 }
670 num := new(big.Int).Mul(big.NewInt(given), big.NewInt(givenCell))
671 num.Mul(num, big.NewInt(otherSrc))
672 den := new(big.Int).Mul(big.NewInt(givenSrc), big.NewInt(otherCell))
673 q, r := new(big.Int).QuoRem(num, den, new(big.Int))
674 if r.Sign() != 0 {
675 q.Add(q, big.NewInt(1))
676 }
677 if !q.IsInt64() {
678 return 0, false
679 }
680 return q.Int64(), true
681}
682
683func (s *Session) place(c Controls, imageID uint64, assetID graphics.AssetID, origin placementOrigin) ([][]byte, *Mutation, error) {
684 if c.HasParent || c.HasCellOffsetX || c.HasCellOffsetY {
685 return nil, nil, ErrUnsupported
686 }
687 if s.scene == nil {
688 return nil, nil, ErrNoScene
689 }
690 asset, ok := s.scene.Snapshot().Asset(assetID)
691 if !ok {
692 return nil, nil, ErrImageNotFound
693 }
694 spec, err := placementSpec(c, assetID, asset.Width(), asset.Height(), origin)
695 if err != nil {
696 return nil, nil, err
697 }
698 placementID := c.PlacementID
699 if imageID == 0 || placementID == 0 {
700 var ok bool
701 placementID, ok = s.takePlacementID(imageID)
702 if !ok {
703 return nil, nil, graphics.ErrIdentifierOverflow
704 }
705 }
706 key := placementKey{imageID: imageID, placementID: placementID}
707 if existing, ok := s.placements[key]; ok {
708 if err := s.scene.UpdatePlacement(existing, spec); err != nil {
709 return nil, nil, err
710 }
711 return nil, &Mutation{Kind: MutationPlacement, ImageID: imageID, AssetID: assetID, PlacementID: placementID, ScenePlacementID: existing}, nil
712 }
713 if uint64(len(s.placements)) >= s.limits.MaxPlacements {
714 return nil, nil, graphics.ErrTooManyPlacements
715 }
716 sceneID, err := s.scene.Place(spec)
717 if err != nil {
718 return nil, nil, err
719 }
720 s.placements[key] = sceneID
721 return nil, &Mutation{Kind: MutationPlacement, ImageID: imageID, AssetID: assetID, PlacementID: placementID, ScenePlacementID: sceneID}, nil
722}
723
724func (s *Session) query(command Command) ([][]byte, *Mutation, error) {
725 c := command.Controls
726 if len(command.Payload) == 0 {
727 return s.failure(c, responseImageID(c), ErrInvalidCommand)
728 }
729 decoded, err := decodeImagePayload(command.Payload, c, s.limits)
730 if err != nil {
731 return s.failure(c, responseImageID(c), err)
732 }
733 format := c.Format
734 if !c.HasFormat {
735 format = FormatRGBA
736 }
737 if _, _, err := imageGeometry(decoded, format, c, s.limits); err != nil {
738 return s.failure(c, responseImageID(c), err)
739 }
740 return s.success(c, responseImageID(c)), nil, nil
741}
742
743func (s *Session) delete(c Controls) ([][]byte, *Mutation, error) {
744 target := c.Delete
745 if !c.HasDelete {
746 target = DeleteAll
747 }
748 id, asset, selected := s.resolveImage(c)
749 responseID := responseImageID(c)
750 switch target {
751 case DeleteImage:
752 if !selected || s.scene == nil {
753 return s.failure(c, responseID, ErrImageNotFound)
754 }
755 keys := s.placementKeysForAsset(asset)
756 if c.HasPlacementID && c.PlacementID != 0 {
757 keys = []placementKey{{imageID: id, placementID: c.PlacementID}}
758 }
759 if err := s.removePlacements(keys); err != nil {
760 return s.failure(c, id, err)
761 }
762 return s.success(c, id), &Mutation{Kind: MutationDeletePlacements, ImageID: id, AssetID: asset}, nil
763 case DeleteImageNumber:
764 if !selected {
765 return s.failure(c, responseID, ErrImageNotFound)
766 }
767 if s.scene == nil {
768 return s.failure(c, id, ErrNoScene)
769 }
770 s.removeMappingsForAsset(asset)
771 if err := s.scene.RemoveAssetCascade(asset); err != nil {
772 return s.failure(c, id, err)
773 }
774 return s.success(c, id), &Mutation{Kind: MutationDeleteImage, ImageID: id, AssetID: asset}, nil
775 case DeleteAll:
776 if s.scene == nil {
777 return s.failure(c, responseID, ErrNoScene)
778 }
779 placementIDs := make([]placementKey, 0, len(s.placements))
780 for placementID := range s.placements {
781 placementIDs = append(placementIDs, placementID)
782 }
783 if err := s.removePlacements(placementIDs); err != nil {
784 return s.failure(c, responseID, err)
785 }
786 return s.success(c, responseID), &Mutation{Kind: MutationDeletePlacements}, nil
787 case DeleteCell, DeleteCellAll:
788 // Kitty d=p/P deletes by cell intersection, not by placement ID or
789 // all placements. Those selectors remain unsupported until the scene
790 // tracks terminal cell anchors.
791 return s.failure(c, responseID, ErrUnsupported)
792 case DeleteAllImages:
793 if s.scene == nil {
794 return s.failure(c, responseID, ErrNoScene)
795 }
796 if err := s.scene.Clear(); err != nil {
797 return s.failure(c, responseID, err)
798 }
799 s.images = make(map[uint64]graphics.AssetID)
800 s.imageNumbers = make(map[uint64]uint64)
801 s.placements = make(map[placementKey]graphics.PlacementID)
802 s.children = make(map[uint64]Child)
803 return s.success(c, responseID), &Mutation{Kind: MutationClear}, nil
804 default:
805 return s.failure(c, responseID, ErrUnsupported)
806 }
807}
808
809func (s *Session) placementKeysForAsset(asset graphics.AssetID) []placementKey {
810 if s.scene == nil {
811 return nil
812 }
813 snapshot := s.scene.Snapshot()
814 keys := make([]placementKey, 0)
815 for key, mapped := range s.placements {
816 placement, ok := snapshot.Placement(mapped)
817 if ok && placement.AssetID() == asset {
818 keys = append(keys, key)
819 }
820 }
821 return keys
822}
823
824func (s *Session) removePlacements(ids []placementKey) error {
825 if len(ids) == 0 {
826 return nil
827 }
828 operations := make([]graphics.Operation, 0, len(ids))
829 for _, id := range ids {
830 mapped, ok := s.placements[id]
831 if !ok {
832 continue
833 }
834 operations = append(operations, graphics.Operation{
835 Kind: graphics.OperationRemovePlacement,
836 PlacementID: mapped,
837 })
838 }
839 if len(operations) == 0 {
840 return nil
841 }
842 if err := s.scene.ApplyOperations(operations); err != nil {
843 return err
844 }
845 for _, id := range ids {
846 delete(s.placements, id)
847 }
848 return nil
849}
850
851func (s *Session) removeMappingsForAsset(asset graphics.AssetID) {
852 for imageID, mapped := range s.images {
853 if mapped == asset {
854 delete(s.images, imageID)
855 }
856 }
857 for imageNumber, imageID := range s.imageNumbers {
858 if mapped, ok := s.images[imageID]; !ok || mapped == asset {
859 delete(s.imageNumbers, imageNumber)
860 }
861 }
862 if s.scene != nil {
863 snapshot := s.scene.Snapshot()
864 for placementID, mapped := range s.placements {
865 placement, ok := snapshot.Placement(mapped)
866 if !ok || placement.AssetID() == asset {
867 delete(s.placements, placementID)
868 }
869 }
870 }
871 for childID, child := range s.children {
872 if child.AssetID == asset {
873 delete(s.children, childID)
874 }
875 }
876}
877
878func continuationControlsAllowed(current, initial Controls) bool {
879 if current.HasAction {
880 initialAction := initial.Action
881 if !initial.HasAction {
882 initialAction = ActionTransmit
883 }
884 if current.Action != initialAction {
885 return false
886 }
887 }
888 return !current.HasCompression && !current.HasFormat && !current.HasImageID &&
889 !current.HasImageNumber && !current.HasWidth && !current.HasHeight &&
890 !current.HasColumns && !current.HasRows && !current.HasX && !current.HasY &&
891 !current.HasSourceWidth && !current.HasSourceHeight && !current.HasLayer &&
892 !current.HasPlacementID && !current.HasDelete && !current.HasTransmission &&
893 !current.HasCursor && !current.HasSourceX && !current.HasSourceY &&
894 !current.HasCellOffsetX && !current.HasCellOffsetY && !current.HasParent
895}
896
897func (s *Session) resolveImage(c Controls) (uint64, graphics.AssetID, bool) {
898 if c.HasImageID {
899 asset, ok := s.images[c.ImageID]
900 return c.ImageID, asset, ok
901 }
902 if c.HasImageNumber {
903 imageID, ok := s.imageNumbers[c.ImageNumber]
904 if !ok {
905 return 0, graphics.AssetID{}, false
906 }
907 asset, ok := s.images[imageID]
908 return imageID, asset, ok
909 }
910 return 0, graphics.AssetID{}, false
911}
912
913func responseImageID(c Controls) uint64 {
914 if c.HasImageID {
915 return c.ImageID
916 }
917 return 0
918}
919
920func controlsImageID(c Controls) uint64 { return responseImageID(c) }
921
922func (s *Session) nextImageID() uint64 {
923 for id := uint64(1); ; id++ {
924 if _, ok := s.images[id]; !ok {
925 return id
926 }
927 }
928}
929
930func (s *Session) takePlacementID(imageID uint64) (uint64, bool) {
931 for s.nextPlacement != 0 && s.nextPlacement <= MaxKittyID {
932 id := s.nextPlacement
933 if s.nextPlacement == MaxKittyID {
934 s.nextPlacement = 0
935 } else {
936 s.nextPlacement++
937 }
938 if _, exists := s.placements[placementKey{imageID: imageID, placementID: id}]; !exists {
939 return id, true
940 }
941 }
942 return 0, false
943}
944
945func checkedProduct(left, right uint64) (uint64, bool) {
946 if left != 0 && right > math.MaxUint64/left {
947 return 0, false
948 }
949 return left * right, true
950}
951
952func checkedCoordinate(n uint64, present bool) (int64, error) {
953 if !present {
954 return 0, nil
955 }
956 if n > math.MaxInt64 {
957 return 0, ErrIntegerOverflow
958 }
959 return int64(n), nil
960}
961
962func checkedDimension(n uint64, present bool, fallback int64) (int64, error) {
963 if !present {
964 return fallback, nil
965 }
966 if n > math.MaxInt64 {
967 return 0, ErrIntegerOverflow
968 }
969 return int64(n), nil
970}
971
972func signedCoordinateInt(n int64, present bool) int64 {
973 if !present {
974 return 0
975 }
976 return n
977}
978
979func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte {
980 responses := prior
981 if responseRequested(c) && c.Quiet != QuietSuccess && c.Quiet != QuietAll {
982 response := makeControlResponse(c, imageID, "OK")
983 if uint64(len(response)) <= s.limits.MaxResponseBytes {
984 responses = append(responses, response)
985 }
986 }
987 return responses
988}
989
990// responseRequested matches Kitty's response routing: commands without a
991// non-zero image ID or image number are anonymous and never receive replies.
992func responseRequested(c Controls) bool {
993 return c.HasImageID && c.ImageID != 0 || c.HasImageNumber && c.ImageNumber != 0
994}
995
996func makeControlResponse(c Controls, imageID uint64, code string) []byte {
997 if !c.HasImageNumber {
998 return MakeResponse(imageID, code)
999 }
1000 if code == "" {
1001 code = "OK"
1002 }
1003 return []byte(responsePrefix + "i=" + strconv.FormatUint(imageID, 10) + ",I=" + strconv.FormatUint(c.ImageNumber, 10) + ";" + code + responseSuffix)
1004}
1005
1006func (s *Session) failure(c Controls, imageID uint64, err error) ([][]byte, *Mutation, error) {
1007 if err == nil {
1008 err = ErrInvalidCommand
1009 }
1010 if responseRequested(c) && c.Quiet != QuietAll {
1011 code := errorCode(err)
1012 response := makeControlResponse(c, imageID, code)
1013 if uint64(len(response)) <= s.limits.MaxResponseBytes {
1014 return [][]byte{response}, nil, err
1015 }
1016 }
1017 return nil, nil, err
1018}
1019
1020func (s *Session) abortUpload(c Controls, imageID uint64, err error) ([][]byte, *Mutation, error) {
1021 s.upload = nil
1022 return s.failure(c, imageID, err)
1023}
1024
1025func errorCode(err error) string {
1026 switch {
1027 case err == nil:
1028 return "OK"
1029 case errorsIs(err, ErrImageNotFound), errorsIs(err, ErrPlacementNotFound), errorsIs(err, graphics.ErrAssetNotFound), errorsIs(err, graphics.ErrPlacementNotFound):
1030 return "ENOENT"
1031 case errorsIs(err, ErrPayloadTooLarge), errorsIs(err, ErrAPCTooLarge), errorsIs(err, graphics.ErrEncodedBudget), errorsIs(err, graphics.ErrDecodedPixelBudget):
1032 return "E2BIG"
1033 case errorsIs(err, ErrUnsupported), errorsIs(err, ErrUnknownAction):
1034 return "ENOTSUP"
1035 default:
1036 return "EINVAL"
1037 }
1038}
1039
1040// errorsIs is kept local to make error-code selection explicit and avoid
1041// leaking any protocol error implementation details in the public API.
1042func errorsIs(err, target error) bool {
1043 for err != nil {
1044 if err == target {
1045 return true
1046 }
1047 type unwrapper interface{ Unwrap() error }
1048 u, ok := err.(unwrapper)
1049 if !ok {
1050 return false
1051 }
1052 err = u.Unwrap()
1053 }
1054 return false
1055}
1056
1057func mergeControls(base, override Controls) Controls {
1058 out := base
1059 if override.HasAction {
1060 out.Action, out.HasAction = override.Action, true
1061 }
1062 if override.HasCompression {
1063 out.Compression, out.HasCompression = override.Compression, true
1064 }
1065 if override.HasFormat {
1066 out.Format, out.HasFormat = override.Format, true
1067 }
1068 if override.HasImageID {
1069 out.ImageID, out.HasImageID = override.ImageID, true
1070 }
1071 if override.HasImageNumber {
1072 out.ImageNumber, out.HasImageNumber = override.ImageNumber, true
1073 }
1074 if override.HasMore {
1075 out.More, out.HasMore = override.More, true
1076 }
1077 if override.HasQuiet {
1078 out.Quiet, out.HasQuiet = override.Quiet, true
1079 }
1080 if override.HasWidth {
1081 out.Width, out.HasWidth = override.Width, true
1082 }
1083 if override.HasHeight {
1084 out.Height, out.HasHeight = override.Height, true
1085 }
1086 if override.HasColumns {
1087 out.Columns, out.HasColumns = override.Columns, true
1088 }
1089 if override.HasRows {
1090 out.Rows, out.HasRows = override.Rows, true
1091 }
1092 if override.HasX {
1093 out.X, out.HasX = override.X, true
1094 }
1095 if override.HasY {
1096 out.Y, out.HasY = override.Y, true
1097 }
1098 if override.HasSourceWidth {
1099 out.SourceWidth, out.HasSourceWidth = override.SourceWidth, true
1100 }
1101 if override.HasSourceHeight {
1102 out.SourceHeight, out.HasSourceHeight = override.SourceHeight, true
1103 }
1104 if override.HasLayer {
1105 out.Layer, out.HasLayer = override.Layer, true
1106 }
1107 if override.HasPlacementID {
1108 out.PlacementID, out.HasPlacementID = override.PlacementID, true
1109 }
1110 if override.HasDelete {
1111 out.Delete, out.HasDelete = override.Delete, true
1112 }
1113 if override.HasTransmission {
1114 out.Transmission, out.HasTransmission = override.Transmission, true
1115 }
1116 if override.HasCursor {
1117 out.Cursor, out.HasCursor = override.Cursor, true
1118 }
1119 if override.HasSourceX {
1120 out.SourceX, out.HasSourceX = override.SourceX, true
1121 }
1122 if override.HasSourceY {
1123 out.SourceY, out.HasSourceY = override.SourceY, true
1124 }
1125 if override.HasCellOffsetX {
1126 out.CellOffsetX, out.HasCellOffsetX = override.CellOffsetX, true
1127 }
1128 if override.HasCellOffsetY {
1129 out.CellOffsetY, out.HasCellOffsetY = override.CellOffsetY, true
1130 }
1131 if override.HasParent {
1132 out.Parent, out.HasParent = override.Parent, true
1133 }
1134 return out
1135}