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