| 1 | package attachment |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | |
| 6 | "reasonix/internal/sessioncontent" |
| 7 | ) |
| 8 | |
| 9 | // CollectJSONRefs extracts attachment objects from any JSON value that may |
| 10 | // contain ImageInput or AttachmentRef objects. It is used by export, import, |
| 11 | // and authorization rebuilds. |
| 12 | func CollectJSONRefs(raw []byte) []sessioncontent.Ref { |
| 13 | if len(raw) == 0 { |
| 14 | return nil |
| 15 | } |
| 16 | var value any |
| 17 | if err := json.Unmarshal(raw, &value); err != nil { |
| 18 | return nil |
| 19 | } |
| 20 | seen := map[string]sessioncontent.Ref{} |
| 21 | walkValue(value, seen) |
| 22 | if len(seen) == 0 { |
| 23 | return nil |
| 24 | } |
| 25 | out := make([]sessioncontent.Ref, 0, len(seen)) |
| 26 | for _, ref := range seen { |
| 27 | out = append(out, ref) |
| 28 | } |
| 29 | return out |
| 30 | } |
| 31 | |
| 32 | func walkValue(value any, seen map[string]sessioncontent.Ref) { |
| 33 | switch item := value.(type) { |
| 34 | case map[string]any: |
| 35 | if ref, ok := attachmentRefFromMap(item); ok { |
| 36 | key := ref.Digest + ":" + itoa(ref.Bytes) + ":" + ref.IndexDigest |
| 37 | seen[key] = ref |
| 38 | } |
| 39 | for _, nested := range item { |
| 40 | walkValue(nested, seen) |
| 41 | } |
| 42 | case []any: |
| 43 | for _, nested := range item { |
| 44 | walkValue(nested, seen) |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func attachmentRefFromMap(item map[string]any) (sessioncontent.Ref, bool) { |
| 50 | content, _ := item["content"].(map[string]any) |
| 51 | if content == nil { |
| 52 | if _, ok := item["digest"].(string); ok && item["v"] == nil { |
| 53 | content = item |
| 54 | } |
| 55 | } |
| 56 | if content == nil { |
| 57 | return sessioncontent.Ref{}, false |
| 58 | } |
| 59 | digest, _ := content["digest"].(string) |
| 60 | if digest == "" { |
| 61 | return sessioncontent.Ref{}, false |
| 62 | } |
| 63 | ref := sessioncontent.Ref{ |
| 64 | Digest: digest, |
| 65 | MediaType: stringField(content, "mediaType"), |
| 66 | Name: stringField(content, "name"), |
| 67 | IndexDigest: stringField(content, "indexDigest"), |
| 68 | } |
| 69 | switch bytes := content["bytes"].(type) { |
| 70 | case float64: |
| 71 | ref.Bytes = int64(bytes) |
| 72 | case json.Number: |
| 73 | n, _ := bytes.Int64() |
| 74 | ref.Bytes = n |
| 75 | } |
| 76 | switch block := content["integrityBlockBytes"].(type) { |
| 77 | case float64: |
| 78 | ref.IntegrityBlock = int64(block) |
| 79 | case json.Number: |
| 80 | n, _ := block.Int64() |
| 81 | ref.IntegrityBlock = n |
| 82 | } |
| 83 | if ref.Bytes <= 0 { |
| 84 | return sessioncontent.Ref{}, false |
| 85 | } |
| 86 | return ref, true |
| 87 | } |
| 88 | |
| 89 | func stringField(item map[string]any, key string) string { |
| 90 | value, _ := item[key].(string) |
| 91 | return value |
| 92 | } |
| 93 | |
| 94 | func itoa(n int64) string { |
| 95 | if n == 0 { |
| 96 | return "0" |
| 97 | } |
| 98 | var buf [20]byte |
| 99 | i := len(buf) |
| 100 | neg := n < 0 |
| 101 | if neg { |
| 102 | n = -n |
| 103 | } |
| 104 | for n > 0 { |
| 105 | i-- |
| 106 | buf[i] = byte('0' + n%10) |
| 107 | n /= 10 |
| 108 | } |
| 109 | if neg { |
| 110 | i-- |
| 111 | buf[i] = '-' |
| 112 | } |
| 113 | return string(buf[i:]) |
| 114 | } |
| 115 |