| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "path/filepath" |
| 6 | "reasonix/internal/attachment" |
| 7 | "slices" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | func submissionSourceFailure(item SubmissionAttachment, index int, err error) []ImageReferenceFailure { |
| 12 | failures := imageFailuresFromAttachment(err) |
| 13 | name := "image" |
| 14 | if item.Path != "" { |
| 15 | name = filepath.Base(filepath.FromSlash(item.Path)) |
| 16 | } else if item.Reference != nil { |
| 17 | name = item.Reference.DisplayName |
| 18 | } |
| 19 | for i := range failures { |
| 20 | failures[i].Index = index + 1 |
| 21 | failures[i].Name = attachment.NormalizeDisplayName(name) |
| 22 | } |
| 23 | return failures |
| 24 | } |
| 25 | |
| 26 | func withoutDraftID(ids []string, remove string) []string { |
| 27 | var out []string |
| 28 | for _, id := range ids { |
| 29 | if id != remove { |
| 30 | out = append(out, id) |
| 31 | } |
| 32 | } |
| 33 | return out |
| 34 | } |
| 35 | |
| 36 | func canonicalAttachmentToken(input, transport, logicalID string) string { |
| 37 | if slices.Contains(parseRefTokens(input), transport) { |
| 38 | return strings.ReplaceAll(input, "@"+EscapeRefPath(transport), "@attachment:"+logicalID) |
| 39 | } |
| 40 | return input |
| 41 | } |
| 42 | |
| 43 | func legacyRemoteImageInputs(line string) []attachment.ImageInput { |
| 44 | refs := bareVisionRefs(line) |
| 45 | for _, token := range parseRefTokens(line) { |
| 46 | if r, ok := classifyVisionToken(token); ok { |
| 47 | refs = append(refs, r) |
| 48 | } |
| 49 | } |
| 50 | seen := map[string]bool{} |
| 51 | var inputs []attachment.ImageInput |
| 52 | for _, r := range refs { |
| 53 | if seen[r.path] { |
| 54 | continue |
| 55 | } |
| 56 | seen[r.path] = true |
| 57 | switch r.kind { |
| 58 | case refRemoteImage: |
| 59 | inputs = append(inputs, attachment.ImageInput{Kind: attachment.KindURL, URL: r.path}) |
| 60 | case refFileID: |
| 61 | inputs = append(inputs, attachment.ImageInput{Kind: attachment.KindFiles, FilesID: r.path}) |
| 62 | } |
| 63 | } |
| 64 | return inputs |
| 65 | } |
| 66 | |
| 67 | // Keep ordinary image references when a submission also uses the new explicit |
| 68 | // attachment channel. Without this merge, the frozen channel would mask them. |
| 69 | func (c *Controller) appendOrdinaryImageSources(line string, sources []attachment.Source) []attachment.Source { |
| 70 | seen := make(map[string]bool) |
| 71 | for _, source := range sources { |
| 72 | seen[normalizedImageReferencePath(source.Path)] = true |
| 73 | } |
| 74 | for _, r := range c.detectRefsMode(line, true) { |
| 75 | imageFile := r.kind == refFile && isImageAttachmentRef(r.path) |
| 76 | if (r.kind != refImage && !imageFile) || isAttachmentRef(r.path) || seen[normalizedImageReferencePath(r.path)] { |
| 77 | continue |
| 78 | } |
| 79 | root := c.workspaceRoot |
| 80 | if r.baseDir != "" { |
| 81 | root = r.baseDir |
| 82 | } |
| 83 | sources = append(sources, attachment.Source{Path: r.path, WorkspaceRoot: root, Confine: ".", DisplayName: filepath.Base(r.path)}) |
| 84 | seen[normalizedImageReferencePath(r.path)] = true |
| 85 | } |
| 86 | return sources |
| 87 | } |
| 88 | |
| 89 | func (c *Controller) structuredImageSources(ctx context.Context, items []SubmissionAttachment) ([]attachment.Source, []ImageReferenceFailure) { |
| 90 | var sources []attachment.Source |
| 91 | svc := c.attachmentService() |
| 92 | logical := map[string]bool{} |
| 93 | for index, item := range items { |
| 94 | if item.ClientAttachmentID == "" || logical[item.ClientAttachmentID] { |
| 95 | return nil, submissionSourceFailure(item, index, attachment.Error{Code: attachment.CodeUnsupported, Message: "invalid logical attachment identity"}) |
| 96 | } |
| 97 | logical[item.ClientAttachmentID] = true |
| 98 | count := 0 |
| 99 | if item.DraftID != "" { |
| 100 | count++ |
| 101 | } |
| 102 | if item.Path != "" { |
| 103 | count++ |
| 104 | } |
| 105 | if item.Reference != nil { |
| 106 | count++ |
| 107 | } |
| 108 | if count != 1 { |
| 109 | return nil, submissionSourceFailure(item, index, attachment.Error{Code: attachment.CodeUnsupported, Message: "invalid attachment source"}) |
| 110 | } |
| 111 | if item.DraftID != "" { |
| 112 | draft, ok := svc.Drafts().Lookup(c.attachmentScope(), item.DraftID) |
| 113 | if !ok { |
| 114 | return nil, submissionSourceFailure(item, index, attachment.Error{Code: attachment.CodeMissing, Message: "draft credential is not valid"}) |
| 115 | } |
| 116 | sources = append(sources, attachment.Source{Existing: &draft.Ref, DisplayName: draft.DisplayName, Path: "draft:" + item.DraftID}) |
| 117 | } else if item.Path != "" { |
| 118 | sources = append(sources, attachment.Source{Path: item.Path, WorkspaceRoot: c.workspaceRoot, Confine: ".reasonix/attachments"}) |
| 119 | } else { |
| 120 | if _, _, err := c.ReadSessionAttachment(ctx, item.Reference.Content.Digest, 0, 1); err != nil { |
| 121 | return nil, submissionSourceFailure(item, index, err) |
| 122 | } |
| 123 | sources = append(sources, attachment.Source{Existing: item.Reference, DisplayName: item.Reference.DisplayName}) |
| 124 | } |
| 125 | } |
| 126 | return sources, nil |
| 127 | } |
| 128 |