1package snapshotcodec
2
3import (
4 "fmt"
5 "math"
6
7 vt "github.com/bnema/vev-vt"
8 "github.com/bnema/vev/internal/domain/layout"
9)
10
11func preflightSession(body []byte) error {
12 if err := preflightSessionStructure(body); err != nil {
13 return err
14 }
15 return preflightSessionReferences(body)
16}
17
18func preflightSessionStructure(body []byte) error {
19 r := payloadReader{b: body}
20 if err := skipString(&r); err != nil {
21 return err
22 }
23 if _, err := r.getUint64(); err != nil {
24 return err
25 }
26 active, err := r.getUint16()
27 if err != nil {
28 return err
29 }
30 tabs, err := r.getUint16()
31 if err != nil {
32 return err
33 }
34 if err := validateActive(active, int(tabs)); err != nil {
35 return err
36 }
37 if uint64(tabs) > uint64(len(r.b))/2 {
38 return ErrShortPayload
39 }
40
41 var totals vt.DecodeStats
42 blobs := uint64(0)
43 budget := preflightBudget{}
44 for range tabs {
45 if err := preflightTabStructure(&r, &totals, &blobs, &budget); err != nil {
46 return fmt.Errorf("preflight tab: %w", err)
47 }
48 }
49 if err := r.done(); err != nil {
50 return err
51 }
52 if totals.Rows > maxSnapshotRows || totals.Cells > maxSnapshotCells || totals.Bytes > maxSnapshotBytes {
53 return fmt.Errorf("%w: global VT budget", ErrInvalidData)
54 }
55 return nil
56}
57
58func skipString(r *payloadReader) error {
59 n, err := r.getUint16()
60 if err != nil {
61 return err
62 }
63 if uint64(n) > uint64(len(r.b)) {
64 return ErrShortPayload
65 }
66 r.b = r.b[n:]
67 return nil
68}
69
70type preflightBudget struct {
71 nodes uint64
72 alloc uint64
73}
74
75func (b *preflightBudget) addAllocation(amount uint64) bool {
76 if amount > maxSnapshotDecodedAllocation-b.alloc {
77 return false
78 }
79 b.alloc += amount
80 return true
81}
82
83func (b *preflightBudget) addProduct(count, size uint64) bool {
84 if count != 0 && size > math.MaxUint64/count {
85 return false
86 }
87 return b.addAllocation(count * size)
88}
89
90func (b *preflightBudget) addNode() bool {
91 if b.nodes >= maxSnapshotTreeNodes || !b.addAllocation(128) {
92 return false
93 }
94 b.nodes++
95 return true
96}
97
98func preflightTabStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
99 if !budget.addAllocation(256) {
100 return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
101 }
102 if err := skipString(r); err != nil {
103 return err
104 }
105 if _, err := r.getUint16(); err != nil {
106 return err
107 }
108 if _, err := r.getUint16(); err != nil {
109 return err
110 }
111 if _, err := r.getUint64(); err != nil {
112 return err
113 }
114 if err := skipString(r); err != nil {
115 return err
116 }
117 if err := preflightNodeStructure(r, 0, true, budget); err != nil {
118 return err
119 }
120 panes, err := r.getUint16()
121 if err != nil {
122 return err
123 }
124 if uint64(panes) > uint64(len(r.b))/2 {
125 return ErrShortPayload
126 }
127 for range panes {
128 if err := preflightPaneStructure(r, totals, blobs, budget); err != nil {
129 return err
130 }
131 }
132 return nil
133}
134
135func preflightNodeStructure(r *payloadReader, depth int, root bool, budget *preflightBudget) error {
136 if depth > 64 {
137 return fmt.Errorf("%w: tree depth", ErrInvalidData)
138 }
139 if !budget.addNode() {
140 return fmt.Errorf("%w: tree node or decoded allocation budget", ErrInvalidData)
141 }
142 kind, err := r.getUint8()
143 if err != nil {
144 return err
145 }
146 switch kind {
147 case 0:
148 return skipString(r)
149 case 1:
150 dir, err := r.getUint8()
151 if err != nil {
152 return err
153 }
154 if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
155 return fmt.Errorf("%w: split dir", ErrInvalidData)
156 }
157 case 2:
158 if err := skipString(r); err != nil {
159 return err
160 }
161 case 3:
162 if !root {
163 return fmt.Errorf("%w: nil child node", ErrInvalidData)
164 }
165 return nil
166 default:
167 return fmt.Errorf("%w: node kind", ErrInvalidData)
168 }
169 children, err := r.getUint16()
170 if err != nil {
171 return err
172 }
173 if uint64(children) > uint64(len(r.b)) {
174 return ErrShortPayload
175 }
176 for range children {
177 if err := preflightNodeStructure(r, depth+1, false, budget); err != nil {
178 return err
179 }
180 }
181 return nil
182}
183
184func preflightPaneStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
185 if !budget.addAllocation(256) {
186 return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
187 }
188 if err := skipString(r); err != nil {
189 return err
190 }
191 if err := skipString(r); err != nil {
192 return err
193 }
194 if err := skipString(r); err != nil {
195 return err
196 }
197 n, err := r.getUint32()
198 if err != nil {
199 return err
200 }
201 if n > maxSnapshotObjects || uint64(n) > uint64(len(r.b))/4 {
202 return fmt.Errorf("%w: sealed chunks", ErrInvalidData)
203 }
204 for range n {
205 stats, err := preflightBlob(r, totals, blobs, budget)
206 if err != nil {
207 return err
208 }
209 if stats.Chunks != 1 || stats.Rows == 0 {
210 return fmt.Errorf("%w: sealed blob role", ErrInvalidData)
211 }
212 }
213 tail, err := preflightBlob(r, totals, blobs, budget)
214 if err != nil {
215 return err
216 }
217 // A capture keeps the live mutable tail separate from immutable sealed
218 // chunks. It may therefore contain one partial chunk (or the canonical
219 // empty tail), but never a sequence of sealed chunks.
220 if tail.Chunks > 1 {
221 return fmt.Errorf("%w: tail blob role", ErrInvalidData)
222 }
223 transcript, err := preflightBlob(r, totals, blobs, budget)
224 if err != nil {
225 return err
226 }
227 // Transcripts are canonical history; they may contain multiple chunks or be
228 // empty, so no blob-role assertion applies.
229 _ = transcript
230 return preflightProcess(r)
231}
232
233func preflightBlob(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) (vt.DecodeStats, error) {
234 n, err := r.getUint32()
235 if err != nil {
236 return vt.DecodeStats{}, err
237 }
238 if n == 0 || uint64(n) > uint64(len(r.b)) || uint64(n) > maxSnapshotBytes {
239 return vt.DecodeStats{}, fmt.Errorf("%w: VT blob length", ErrInvalidData)
240 }
241 if *blobs >= maxSnapshotObjects {
242 return vt.DecodeStats{}, fmt.Errorf("%w: too many VT blobs", ErrInvalidData)
243 }
244 blob := r.b[:n]
245 r.b = r.b[n:]
246 stats, err := vt.MeasureHistoryBlob(blob)
247 if err != nil {
248 return vt.DecodeStats{}, fmt.Errorf("%w: VT blob", ErrInvalidData)
249 }
250 if !totals.Add(stats) {
251 return vt.DecodeStats{}, fmt.Errorf("%w: VT budget overflow", ErrInvalidData)
252 }
253 if !budget.addAllocation(uint64(n)) || !budget.addProduct(stats.Chunks, 64) || !budget.addProduct(stats.Rows, 32) || !budget.addProduct(stats.Cells, 64) {
254 return vt.DecodeStats{}, fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
255 }
256 *blobs++
257 return stats, nil
258}
259
260func preflightProcess(r *payloadReader) error {
261 present, err := r.getUint8()
262 if err != nil {
263 return err
264 }
265 if present == 0 {
266 return nil
267 }
268 if present != 1 {
269 return fmt.Errorf("%w: process presence", ErrInvalidData)
270 }
271 n, err := r.getUint16()
272 if err != nil {
273 return err
274 }
275 if n == 0 {
276 return fmt.Errorf("%w: process argv empty", ErrInvalidData)
277 }
278 for range n {
279 if err := skipString(r); err != nil {
280 return err
281 }
282 }
283 if err := skipString(r); err != nil {
284 return err
285 }
286 return skipString(r)
287}
288
289// preflightSessionReferences validates duplicate, empty, and dangling pane
290// references only after the structural pass has admitted the complete payload.
291func preflightSessionReferences(body []byte) error {
292 r := payloadReader{b: body}
293 if err := skipString(&r); err != nil {
294 return err
295 }
296 if _, err := r.getUint64(); err != nil {
297 return err
298 }
299 if _, err := r.getUint16(); err != nil {
300 return err
301 }
302 tabs, err := r.getUint16()
303 if err != nil {
304 return err
305 }
306 for range tabs {
307 if err := preflightTabReferences(&r); err != nil {
308 return fmt.Errorf("preflight tab references: %w", err)
309 }
310 }
311 return r.done()
312}
313
314func preflightTabReferences(r *payloadReader) error {
315 if err := skipString(r); err != nil {
316 return err
317 }
318 if _, err := r.getUint16(); err != nil {
319 return err
320 }
321 if _, err := r.getUint16(); err != nil {
322 return err
323 }
324 if _, err := r.getUint64(); err != nil {
325 return err
326 }
327 focus, err := r.getString()
328 if err != nil {
329 return err
330 }
331 references := preflightTreeReferences{}
332 if err := preflightNodeReferences(r, &references); err != nil {
333 return err
334 }
335 panes, err := r.getUint16()
336 if err != nil {
337 return err
338 }
339 ids := make(map[layout.PaneID]struct{}, panes)
340 for range panes {
341 id, err := r.getString()
342 if err != nil {
343 return err
344 }
345 if id == "" {
346 return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
347 }
348 paneID := layout.PaneID(id)
349 if _, exists := ids[paneID]; exists {
350 return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
351 }
352 ids[paneID] = struct{}{}
353 if err := skipString(r); err != nil {
354 return err
355 }
356 if err := skipString(r); err != nil {
357 return err
358 }
359 n, err := r.getUint32()
360 if err != nil {
361 return err
362 }
363 for range n {
364 if err := validateBlob(r); err != nil {
365 return err
366 }
367 }
368 if err := validateBlob(r); err != nil {
369 return err
370 }
371 if err := validateBlob(r); err != nil {
372 return err
373 }
374 if err := preflightProcess(r); err != nil {
375 return err
376 }
377 }
378 if focus != "" {
379 references.ids = append(references.ids, layout.PaneID(focus))
380 }
381 for _, id := range references.ids {
382 if _, ok := ids[id]; !ok {
383 return ErrUnknownPane
384 }
385 }
386 return nil
387}
388
389// validateBlob runs only after the complete collection passes resource sizing.
390// Dictionary/identity validation may allocate, but cannot run once per pane in
391// an over-budget collection. Sizing alone never authorizes session construction.
392func validateBlob(r *payloadReader) error {
393 n, err := r.getUint32()
394 if err != nil {
395 return err
396 }
397 if uint64(n) > uint64(len(r.b)) {
398 return ErrShortPayload
399 }
400 if _, err := vt.PreflightHistoryBlob(r.b[:n]); err != nil {
401 return fmt.Errorf("%w: VT blob", ErrInvalidData)
402 }
403 r.b = r.b[n:]
404 return nil
405}
406
407type preflightTreeReferences struct{ ids []layout.PaneID }
408
409func (r *preflightTreeReferences) add(id layout.PaneID) {
410 r.ids = append(r.ids, id)
411}
412
413func preflightNodeReferences(r *payloadReader, references *preflightTreeReferences) error {
414 kind, err := r.getUint8()
415 if err != nil {
416 return err
417 }
418 switch kind {
419 case 0:
420 leaf, err := r.getString()
421 if err != nil {
422 return err
423 }
424 references.add(layout.PaneID(leaf))
425 return nil
426 case 1:
427 if _, err := r.getUint8(); err != nil {
428 return err
429 }
430 case 2:
431 expanded, err := r.getString()
432 if err != nil {
433 return err
434 }
435 if expanded != "" {
436 references.add(layout.PaneID(expanded))
437 }
438 case 3:
439 return nil
440 default:
441 return fmt.Errorf("%w: node kind", ErrInvalidData)
442 }
443 children, err := r.getUint16()
444 if err != nil {
445 return err
446 }
447 for range children {
448 if err := preflightNodeReferences(r, references); err != nil {
449 return err
450 }
451 }
452 return nil
453}