| 1 | package attachment |
| 2 | |
| 3 | import "reasonix/internal/sessioncontent" |
| 4 | |
| 5 | const ( |
| 6 | RefVersion = 1 |
| 7 | |
| 8 | KindAttachment Kind = "attachment" |
| 9 | KindURL Kind = "url" |
| 10 | KindFiles Kind = "files" |
| 11 | ) |
| 12 | |
| 13 | // Kind selects one ImageInput arm. Empty is invalid. |
| 14 | type Kind string |
| 15 | |
| 16 | // AttachmentRef is the durable identity of one verified original image. |
| 17 | // Object identity is the SHA-256 of the original bytes (Content.Digest). |
| 18 | type AttachmentRef struct { |
| 19 | Version int `json:"v"` |
| 20 | Content sessioncontent.Ref `json:"content"` |
| 21 | Width int `json:"width,omitempty"` |
| 22 | Height int `json:"height,omitempty"` |
| 23 | DisplayName string `json:"name,omitempty"` |
| 24 | } |
| 25 | |
| 26 | // ImageInput is an ordered union stored on new messages. A message must not |
| 27 | // combine this field with legacy Images []string. |
| 28 | type ImageInput struct { |
| 29 | Kind Kind `json:"kind"` |
| 30 | Attachment *AttachmentRef `json:"attachment,omitempty"` |
| 31 | URL string `json:"url,omitempty"` |
| 32 | FilesID string `json:"filesId,omitempty"` |
| 33 | } |
| 34 | |
| 35 | // PreparedImage is one verified original, held only for the current submit. |
| 36 | type PreparedImage struct { |
| 37 | DisplayName string |
| 38 | MIME string |
| 39 | Width int |
| 40 | Height int |
| 41 | Bytes []byte |
| 42 | Existing *AttachmentRef |
| 43 | } |
| 44 | |
| 45 | // PreparedImages is an ordered, all-or-nothing batch. Commit publishes every |
| 46 | // object or none of the returned refs. |
| 47 | type PreparedImages struct { |
| 48 | Items []PreparedImage |
| 49 | } |
| 50 | |
| 51 | // DraftCredential is a host-issued handle. Clients cannot mint one from a digest. |
| 52 | type DraftCredential struct { |
| 53 | ID string `json:"id"` |
| 54 | Ref AttachmentRef `json:"-"` |
| 55 | DisplayName string `json:"displayName"` |
| 56 | MIME string `json:"mime"` |
| 57 | Width int `json:"width"` |
| 58 | Height int `json:"height"` |
| 59 | Bytes int64 `json:"bytes"` |
| 60 | } |
| 61 | |
| 62 | // Variant is a deterministic request encoding of one original. |
| 63 | type Variant struct { |
| 64 | Bytes []byte |
| 65 | MIME string |
| 66 | Width int |
| 67 | Height int |
| 68 | SourceWidth int |
| 69 | SourceHeight int |
| 70 | PolicyVersion int |
| 71 | Digest string |
| 72 | } |
| 73 | |
| 74 | func (r AttachmentRef) Digest() string { |
| 75 | return r.Content.Digest |
| 76 | } |
| 77 | |
| 78 | func (r AttachmentRef) MIME() string { |
| 79 | return r.Content.MediaType |
| 80 | } |
| 81 | |
| 82 | func (r AttachmentRef) Validate() error { |
| 83 | if r.Version != RefVersion { |
| 84 | return Error{Code: CodeUnsupported, Message: "unsupported attachment reference version"} |
| 85 | } |
| 86 | if r.Content.Digest == "" || r.Content.Bytes <= 0 { |
| 87 | return Error{Code: CodeCorrupt, Message: "attachment reference is incomplete"} |
| 88 | } |
| 89 | if r.Width <= 0 || r.Height <= 0 { |
| 90 | return Error{Code: CodeCorrupt, Message: "attachment reference is missing dimensions"} |
| 91 | } |
| 92 | return nil |
| 93 | } |
| 94 | |
| 95 | func (in ImageInput) Validate() error { |
| 96 | switch in.Kind { |
| 97 | case KindAttachment: |
| 98 | if in.Attachment == nil || in.URL != "" || in.FilesID != "" { |
| 99 | return Error{Code: CodeUnsupported, Message: "attachment image input is malformed"} |
| 100 | } |
| 101 | return in.Attachment.Validate() |
| 102 | case KindURL: |
| 103 | if in.Attachment != nil || in.FilesID != "" || in.URL == "" { |
| 104 | return Error{Code: CodeUnsupported, Message: "url image input is malformed"} |
| 105 | } |
| 106 | return nil |
| 107 | case KindFiles: |
| 108 | if in.Attachment != nil || in.URL != "" || in.FilesID == "" { |
| 109 | return Error{Code: CodeUnsupported, Message: "files image input is malformed"} |
| 110 | } |
| 111 | return nil |
| 112 | default: |
| 113 | return Error{Code: CodeUnsupported, Message: "unknown image input kind"} |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func CloneImageInputs(in []ImageInput) []ImageInput { |
| 118 | if len(in) == 0 { |
| 119 | return nil |
| 120 | } |
| 121 | out := make([]ImageInput, len(in)) |
| 122 | copy(out, in) |
| 123 | for i := range out { |
| 124 | if out[i].Attachment != nil { |
| 125 | ref := *out[i].Attachment |
| 126 | out[i].Attachment = &ref |
| 127 | } |
| 128 | } |
| 129 | return out |
| 130 | } |
| 131 | |
| 132 | func CollectContentRefs(inputs []ImageInput) []sessioncontent.Ref { |
| 133 | var refs []sessioncontent.Ref |
| 134 | for _, in := range inputs { |
| 135 | if in.Kind == KindAttachment && in.Attachment != nil && in.Attachment.Content.Digest != "" { |
| 136 | refs = append(refs, in.Attachment.Content) |
| 137 | } |
| 138 | } |
| 139 | return refs |
| 140 | } |
| 141 |