porting all github actions from bluesky-social/indigo to tangled CI
1package data
2
3import (
4 "encoding"
5 "encoding/base64"
6 "fmt"
7 "reflect"
8
9 "github.com/ipfs/go-cid"
10)
11
12func parseFloat(f float64) (int64, error) {
13 if f != float64(int64(f)) {
14 return 0, fmt.Errorf("number is not a safe integer: %f", f)
15 }
16 return int64(f), nil
17}
18
19func parseAtom(atom any) (any, error) {
20 switch v := atom.(type) {
21 case nil:
22 return v, nil
23 case bool:
24 return v, nil
25 case *bool:
26 return *v, nil
27 case int64:
28 return v, nil
29 case *int64:
30 return *v, nil
31 case int:
32 return int64(v), nil
33 case *int:
34 return int64(*v), nil
35 case float64:
36 return parseFloat(v)
37 case *float64:
38 return parseFloat(*v)
39 case string:
40 if len(v) > MAX_RECORD_STRING_LEN {
41 return nil, fmt.Errorf("string too long: %d", len(v))
42 }
43 return v, nil
44 case *string:
45 return parseAtom(*v)
46 case cid.Cid:
47 return CIDLink(v), nil
48 case *cid.Cid:
49 return CIDLink(*v), nil
50 case []byte:
51 return Bytes(v), nil
52 case *[]byte:
53 return Bytes(*v), nil
54 case []any:
55 return parseArray(v)
56 case *[]any:
57 return parseArray(*v)
58 case map[string]any:
59 return parseMap(v)
60 case *map[string]any:
61 return parseMap(*v)
62 case encoding.TextMarshaler:
63 s, err := v.MarshalText()
64 if err != nil {
65 return nil, fmt.Errorf("failed to marshal text (%s): %w", reflect.TypeOf(v), err)
66 }
67 return s, nil
68 default:
69 return nil, fmt.Errorf("unexpected type: %s", reflect.TypeOf(v))
70 }
71}
72
73func parseArray(l []any) ([]any, error) {
74 if len(l) > MAX_CBOR_CONTAINER_LEN {
75 return nil, fmt.Errorf("data array length too long: %d", len(l))
76 }
77 out := make([]any, len(l))
78 for i, v := range l {
79 p, err := parseAtom(v)
80 if err != nil {
81 return nil, err
82 }
83 out[i] = p
84 }
85 return out, nil
86}
87
88func parseMap(obj map[string]any) (any, error) {
89 if len(obj) > MAX_CBOR_CONTAINER_LEN {
90 return nil, fmt.Errorf("data object has too many fields: %d", len(obj))
91 }
92 if _, ok := obj["$link"]; ok {
93 return parseLink(obj)
94 }
95 if _, ok := obj["$bytes"]; ok {
96 return parseBytes(obj)
97 }
98 if typeVal, ok := obj["$type"]; ok {
99 if typeStr, ok := typeVal.(string); ok {
100 if typeStr == "blob" {
101 b, err := parseBlob(obj)
102 if err != nil {
103 return nil, err
104 }
105 return *b, nil
106 }
107 if len(typeStr) == 0 {
108 return nil, fmt.Errorf("$type field must contain a non-empty string")
109 }
110 } else {
111 return nil, fmt.Errorf("$type field must contain a non-empty string")
112 }
113 }
114 // legacy blob type
115 if len(obj) == 2 {
116 if _, ok := obj["mimeType"]; ok {
117 if _, ok := obj["cid"]; ok {
118 b, err := parseLegacyBlob(obj)
119 if err != nil {
120 return nil, err
121 }
122 return *b, nil
123 }
124 }
125 }
126 out := make(map[string]any, len(obj))
127 for k, val := range obj {
128 if len(k) > MAX_OBJECT_KEY_LEN {
129 return nil, fmt.Errorf("data object key too long: %d", len(k))
130 }
131 atom, err := parseAtom(val)
132 if err != nil {
133 return nil, err
134 }
135 out[k] = atom
136 }
137 return out, nil
138}
139
140func parseLink(obj map[string]any) (CIDLink, error) {
141 var zero CIDLink
142 if len(obj) != 1 {
143 return zero, fmt.Errorf("$link objects must have a single field")
144 }
145 v, ok := obj["$link"].(string)
146 if !ok {
147 return zero, fmt.Errorf("$link field missing or not a string")
148 }
149 c, err := cid.Parse(v)
150 if err != nil {
151 return zero, fmt.Errorf("invalid $link CID: %w", err)
152 }
153 if !c.Defined() {
154 return zero, fmt.Errorf("undefined (null) CID in $link")
155 }
156 return CIDLink(c), nil
157}
158
159func parseBytes(obj map[string]any) (Bytes, error) {
160 if len(obj) != 1 {
161 return nil, fmt.Errorf("$bytes objects must have a single field")
162 }
163 v, ok := obj["$bytes"].(string)
164 if !ok {
165 return nil, fmt.Errorf("$bytes field missing or not a string")
166 }
167 b, err := base64.RawStdEncoding.DecodeString(v)
168 if err != nil {
169 return nil, fmt.Errorf("decoding $byte value: %w", err)
170 }
171 return Bytes(b), nil
172}
173
174// NOTE: doesn't handle legacy blobs yet!
175func parseBlob(obj map[string]any) (*Blob, error) {
176 if len(obj) != 4 {
177 return nil, fmt.Errorf("blobs expected to have 4 fields")
178 }
179 if obj["$type"] != "blob" {
180 return nil, fmt.Errorf("blobs expected to have $type=blob")
181 }
182 var size int64
183 var err error
184 switch v := obj["size"].(type) {
185 case int:
186 size = int64(v)
187 case int64:
188 size = v
189 case float64:
190 size, err = parseFloat(v)
191 if err != nil {
192 return nil, err
193 }
194 default:
195 return nil, fmt.Errorf("blob 'size' missing or not a number")
196 }
197 mimeType, ok := obj["mimeType"].(string)
198 if !ok {
199 return nil, fmt.Errorf("blob 'mimeType' missing or not a string")
200 }
201 rawRef, ok := obj["ref"]
202 if !ok {
203 return nil, fmt.Errorf("blob 'ref' missing")
204 }
205 var ref CIDLink
206 switch v := rawRef.(type) {
207 case map[string]any:
208 cl, err := parseLink(v)
209 if err != nil {
210 return nil, err
211 }
212 ref = cl
213 case cid.Cid:
214 ref = CIDLink(v)
215 case CIDLink:
216 ref = v
217 default:
218 return nil, fmt.Errorf("blob 'ref' unexpected type")
219 }
220
221 return &Blob{
222 Size: size,
223 MimeType: mimeType,
224 Ref: ref,
225 }, nil
226}
227
228func parseLegacyBlob(obj map[string]any) (*Blob, error) {
229 if len(obj) != 2 {
230 return nil, fmt.Errorf("legacy blobs expected to have 2 fields")
231 }
232 var err error
233 mimeType, ok := obj["mimeType"].(string)
234 if !ok {
235 return nil, fmt.Errorf("blob 'mimeType' missing or not a string")
236 }
237 cidStr, ok := obj["cid"]
238 if !ok {
239 return nil, fmt.Errorf("blob 'cid' missing")
240 }
241 c, err := cid.Parse(cidStr)
242 if err != nil {
243 return nil, fmt.Errorf("invalid CID: %w", err)
244 }
245 return &Blob{
246 Size: -1,
247 MimeType: mimeType,
248 Ref: CIDLink(c),
249 }, nil
250}
251
252func parseObject(obj map[string]any) (map[string]any, error) {
253 out, err := parseMap(obj)
254 if err != nil {
255 return nil, err
256 }
257 if outObj, ok := out.(map[string]any); ok {
258 return outObj, nil
259 }
260 return nil, fmt.Errorf("top-level datum was not an object")
261}