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