1package snapshot
2
3import (
4 "fmt"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/usecase/layout"
8)
9
10// preflightManifest rejects hostile declarations before UnmarshalManifest
11// allocates tab, pane, string, or reference slices.
12func preflightManifest(body []byte) error {
13 r := payloadReader{b: body}
14 budget := manifestPreflightBudget{}
15 generation, err := r.getUint64()
16 if err != nil {
17 return err
18 }
19 if generation == 0 {
20 return fmt.Errorf("%w: manifest generation", ErrInvalidData)
21 }
22 incarnationBytes, err := r.getBytes(len(domain.IncarnationID{}))
23 if err != nil {
24 return err
25 }
26 var incarnationID domain.IncarnationID
27 copy(incarnationID[:], incarnationBytes)
28 if incarnationID == (domain.IncarnationID{}) {
29 return fmt.Errorf("%w: manifest incarnation", ErrInvalidData)
30 }
31 parentPresent, err := r.getUint8()
32 if err != nil {
33 return err
34 }
35 switch parentPresent {
36 case 0:
37 case 1:
38 parentGeneration, err := r.getUint64()
39 if err != nil {
40 return err
41 }
42 digest, err := r.getBytes(len(SnapshotDigest{}))
43 if err != nil {
44 return err
45 }
46 if parentGeneration == 0 || parentGeneration >= generation || isZeroDigestBytes(digest) {
47 return fmt.Errorf("%w: parent checkpoint", ErrInvalidData)
48 }
49 default:
50 return fmt.Errorf("%w: parent checkpoint presence", ErrInvalidData)
51 }
52 name, err := preflightManifestStringBytes(&r, &budget)
53 if err != nil {
54 return err
55 }
56 if len(name) == 0 {
57 return fmt.Errorf("%w: manifest name", ErrInvalidData)
58 }
59 if _, err := r.getUint64(); err != nil {
60 return err
61 }
62 active, err := r.getUint16()
63 if err != nil {
64 return err
65 }
66 tabs, err := r.getUint16()
67 if err != nil {
68 return err
69 }
70 if err := validateActive(active, int(tabs)); err != nil {
71 return err
72 }
73 if uint64(tabs) > uint64(len(r.b))/2 || !budget.add(uint64(tabs)*128) {
74 return fmt.Errorf("%w: tab allocation", ErrInvalidData)
75 }
76 for range tabs {
77 if err := preflightManifestTab(&r, &budget); err != nil {
78 return err
79 }
80 }
81 return preflightManifestWeights(&r, budget.nodes)
82}
83
84type manifestPreflightBudget struct {
85 allocation uint64
86 nodes uint32
87}
88
89func (b *manifestPreflightBudget) add(n uint64) bool {
90 if n > maxSnapshotDecodedAllocation-b.allocation {
91 return false
92 }
93 b.allocation += n
94 return true
95}
96
97func preflightManifestStringBytes(r *payloadReader, budget *manifestPreflightBudget) ([]byte, error) {
98 n, err := r.getUint16()
99 if err != nil {
100 return nil, err
101 }
102 if uint64(n) > uint64(len(r.b)) {
103 return nil, ErrShortPayload
104 }
105 if !budget.add(uint64(n) + 16) {
106 return nil, fmt.Errorf("%w: string allocation", ErrInvalidData)
107 }
108 b := r.b[:n]
109 r.b = r.b[n:]
110 return b, nil
111}
112
113// skipManifestString accounts for the decoded string without materializing it.
114func skipManifestString(r *payloadReader, budget *manifestPreflightBudget) error {
115 _, err := preflightManifestStringBytes(r, budget)
116 return err
117}
118
119func preflightManifestString(r *payloadReader, budget *manifestPreflightBudget) (string, error) {
120 b, err := preflightManifestStringBytes(r, budget)
121 if err != nil {
122 return "", err
123 }
124 return string(b), nil
125}
126
127func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) error {
128 if err := skipManifestString(r, budget); err != nil {
129 return err
130 }
131 cols, err := r.getUint16()
132 if err != nil {
133 return err
134 }
135 rows, err := r.getUint16()
136 if err != nil {
137 return err
138 }
139 if !safeDimensions(cols, rows) {
140 return fmt.Errorf("%w: dimensions", ErrInvalidData)
141 }
142 if _, err := r.getUint64(); err != nil {
143 return err
144 }
145 focus, err := preflightManifestString(r, budget)
146 if err != nil {
147 return err
148 }
149 references := make([]layout.PaneID, 0, 8)
150 if err := preflightManifestNode(r, 0, true, budget, &references); err != nil {
151 return err
152 }
153 panes, err := r.getUint16()
154 if err != nil {
155 return err
156 }
157 if uint64(panes) > uint64(len(r.b))/2 || !budget.add(uint64(panes)*256) {
158 return fmt.Errorf("%w: pane allocation", ErrInvalidData)
159 }
160 ids := make(map[layout.PaneID]struct{}, panes)
161 for range panes {
162 id, err := preflightManifestPane(r, budget)
163 if err != nil {
164 return err
165 }
166 if id == "" {
167 return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
168 }
169 if _, duplicate := ids[id]; duplicate {
170 return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
171 }
172 ids[id] = struct{}{}
173 }
174 if focus != "" {
175 references = append(references, layout.PaneID(focus))
176 }
177 for _, id := range references {
178 if _, ok := ids[id]; !ok {
179 return ErrUnknownPane
180 }
181 }
182 return nil
183}
184
185func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manifestPreflightBudget, refs *[]layout.PaneID) error {
186 if depth > 64 || !budget.add(128) {
187 return fmt.Errorf("%w: tree resource declaration", ErrInvalidData)
188 }
189 kind, err := r.getUint8()
190 if err != nil {
191 return err
192 }
193 switch kind {
194 case manifestNodeLeaf:
195 leaf, err := preflightManifestString(r, budget)
196 if err != nil {
197 return err
198 }
199 budget.nodes++
200 *refs = append(*refs, layout.PaneID(leaf))
201 return nil
202 case manifestNodeSplit:
203 dir, err := r.getUint8()
204 if err != nil {
205 return err
206 }
207 if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
208 return fmt.Errorf("%w: split dir", ErrInvalidData)
209 }
210 case manifestNodeStack:
211 expanded, err := preflightManifestString(r, budget)
212 if err != nil {
213 return err
214 }
215 if expanded != "" {
216 *refs = append(*refs, layout.PaneID(expanded))
217 }
218 case manifestNodeNil:
219 if !root {
220 return fmt.Errorf("%w: nil child node", ErrInvalidData)
221 }
222 return nil
223 default:
224 return fmt.Errorf("%w: node kind", ErrInvalidData)
225 }
226 budget.nodes++
227 n, err := r.getUint16()
228 if err != nil {
229 return err
230 }
231 if uint64(n) > uint64(len(r.b)) {
232 return ErrShortPayload
233 }
234 for range n {
235 if err := preflightManifestNode(r, depth+1, false, budget, refs); err != nil {
236 return err
237 }
238 }
239 return nil
240}
241
242func preflightManifestWeights(r *payloadReader, wantNodes uint32) error {
243 return readManifestWeightExtension(r, uint64(wantNodes), nil)
244}
245
246func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {
247 id, err := preflightManifestString(r, budget)
248 if err != nil {
249 return "", err
250 }
251 if err := skipManifestString(r, budget); err != nil {
252 return "", err
253 }
254 if err := skipManifestString(r, budget); err != nil {
255 return "", err
256 }
257 sealed, err := r.getUint16()
258 if err != nil {
259 return "", err
260 }
261 if uint64(sealed) > uint64(len(r.b))/(objectRefDigestSize+objectRefSizeFieldSize+1) || !budget.add(uint64(sealed)*48) {
262 return "", fmt.Errorf("%w: sealed allocation", ErrInvalidData)
263 }
264 for range sealed {
265 if err := preflightObjectRef(r, HistoryChunk); err != nil {
266 return "", err
267 }
268 }
269 if err := preflightObjectRef(r, HistoryTail); err != nil {
270 return "", err
271 }
272 if err := preflightObjectRef(r, RecoveryTranscript); err != nil {
273 return "", err
274 }
275 if err := preflightManifestProcess(r, budget); err != nil {
276 return "", err
277 }
278 return layout.PaneID(id), nil
279}
280
281func preflightObjectRef(r *payloadReader, want ObjectKind) error {
282 kind, digest, size, err := parseObjectRef(r)
283 if err != nil {
284 return err
285 }
286 return validateObjectRefFields(kind, digest, size, want)
287}
288func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) error {
289 present, err := r.getUint8()
290 if err != nil {
291 return err
292 }
293 if present == processAbsent {
294 return nil
295 }
296 if present != processPresent {
297 return fmt.Errorf("%w: process presence", ErrInvalidData)
298 }
299 n, err := r.getUint16()
300 if err != nil {
301 return err
302 }
303 if n == 0 || !budget.add(uint64(n)*16) {
304 return fmt.Errorf("%w: process argv", ErrInvalidData)
305 }
306 for range n {
307 if err := skipManifestString(r, budget); err != nil {
308 return err
309 }
310 }
311 if err := skipManifestString(r, budget); err != nil {
312 return err
313 }
314 return skipManifestString(r, budget)
315}