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