1package vt
2
3import (
4 "math"
5
6 "github.com/bnema/vev-vt/core"
7)
8
9// MeasureHistoryBlob scans resource declarations without allocating. It is a
10// sizing pass, NOT semantic validation: callers must still use
11// PreflightHistoryBlob or UnmarshalHistory after accepting their aggregate
12// budget. In particular, duplicate IDs/dictionary entries are not checked here.
13func MeasureHistoryBlob(data []byte) (DecodeStats, error) {
14 invalid := func() (DecodeStats, error) { return DecodeStats{}, errInvalidHistory }
15 if len(data) < historyHeaderBytes || string(data[:4]) != historyMagic {
16 return invalid()
17 }
18 p := historyParser{data: data[4:]}
19 chunks, _ := p.uint32()
20 next, _ := p.uint64()
21 if chunks > maxHistoryChunks || uint64(chunks)*historyChunkHeaderBytes > uint64(len(p.data)) || next == 0 || next == math.MaxUint64 {
22 return invalid()
23 }
24 stats := historyDecodeStats{chunks: uint64(chunks)}
25 for range chunks {
26 width, wok := p.uint32()
27 rows, rok := p.uint32()
28 styles, sok := p.uint32()
29 payloads, pok := p.uint32()
30 cells := uint64(width) * uint64(rows)
31 if !wok || !rok || !sok || !pok || rows == 0 || rows > maxHistoryChunkRows || uint64(width) > uint64(math.MaxInt) || styles == 0 || styles > maxHistoryStylesPerChunk || uint64(styles) > cells+1 || uint64(payloads) > cells {
32 return invalid()
33 }
34 if !addHistoryDecodeBudget(&stats.rows, uint64(rows), maxHistoryRows) || !addHistoryDecodeBudget(&stats.cells, cells, maxHistoryCells) || !addHistoryDecodeBudget(&stats.styles, uint64(styles), maxHistoryDecodeStyles) || !addHistoryLogicalBytes(&stats, cells, uint64(rows), uint64(styles)) {
35 return invalid()
36 }
37 styleBytes := uint64(styles-1) * historyStyleBytes
38 if styleBytes > uint64(len(p.data)) {
39 return invalid()
40 }
41 p.data = p.data[styleBytes:]
42 for range payloads {
43 grapheme, gok := p.uint32()
44 link, lok := p.uint32()
45 length := uint64(grapheme) + uint64(link)
46 if !gok || !lok || grapheme > core.MaxGraphemeBytes || link > core.MaxHyperlinkBytes || length == 0 || length > uint64(len(p.data)) {
47 return invalid()
48 }
49 bytes := length + core.PayloadRecordLogicalBytes
50 if !addHistoryDecodeBudget(&stats.payloadBytes, bytes, maxHistoryPayloadBytes) || !addHistoryDecodeBudget(&stats.bytes, bytes, maxHistoryDecodedBytes) {
51 return invalid()
52 }
53 p.data = p.data[length:]
54 }
55 rowBytes := uint64(rows) * historyRowBytes
56 if rowBytes > uint64(len(p.data)) {
57 return invalid()
58 }
59 p.data = p.data[rowBytes:]
60 for range cells {
61 if _, _, _, _, ok := p.storedCell(styles, payloads); !ok {
62 return invalid()
63 }
64 }
65 }
66 if len(p.data) != 0 {
67 return invalid()
68 }
69 return DecodeStats{Chunks: stats.chunks, Rows: stats.rows, Cells: stats.cells, Styles: stats.styles, Bytes: stats.bytes}, nil
70}