1package vt
2
3import (
4 "fmt"
5 "math"
6)
7
8// DecodeStats describes resources declared by one canonical VT blob.
9type DecodeStats struct {
10 Chunks uint64
11 Rows uint64
12 Cells uint64
13 Styles uint64
14 Bytes uint64
15}
16
17// Add adds another preflight result with overflow checking.
18func (s *DecodeStats) Add(other DecodeStats) bool {
19 for _, totals := range [][2]*uint64{{&s.Chunks, &other.Chunks}, {&s.Rows, &other.Rows}, {&s.Cells, &other.Cells}, {&s.Styles, &other.Styles}, {&s.Bytes, &other.Bytes}} {
20 if math.MaxUint64-*totals[0] < *totals[1] {
21 return false
22 }
23 *totals[0] += *totals[1]
24 }
25 return true
26}
27
28// PreflightHistoryBlob validates one self-contained history blob without
29// allocating decoded rows.
30func PreflightHistoryBlob(data []byte) (DecodeStats, error) {
31 stats, ok := preflightHistory(data)
32 if !ok {
33 return DecodeStats{}, fmt.Errorf("preflight history: %w", errInvalidHistory)
34 }
35 return DecodeStats{Chunks: stats.chunks, Rows: stats.rows, Cells: stats.cells, Styles: stats.styles, Bytes: stats.bytes}, nil
36}
37
38// MarshalHistoryChunk encodes one immutable chunk as a self-contained blob.
39func MarshalHistoryChunk(chunk *HistoryChunk) ([]byte, error) {
40 if chunk == nil {
41 return nil, fmt.Errorf("marshal history chunk: %w", errInvalidHistory)
42 }
43 return MarshalHistory(HistoryView{chunks: []*HistoryChunk{chunk}, rows: chunk.len(), cells: chunk.cells()})
44}
45
46// MarshalEmptyHistoryTail returns the mandatory canonical empty tail blob.
47func MarshalEmptyHistoryTail() ([]byte, error) { return MarshalHistory(HistoryView{}) }
48
49// MarshalHistoryTail encodes the copied mutable tail of a snapshot view as one
50// canonical history blob. It does not seal or otherwise mutate live history.
51func MarshalHistoryTail(view HistorySnapshotView) ([]byte, error) {
52 return MarshalHistory(view.Tail())
53}
54
55// MarshalSealedHistory serializes a SealAndView result as oldest-first,
56// self-contained sealed blobs plus a mandatory empty tail blob. The empty tail
57// carries NextRowID and is not the canonical default empty encoding.
58func MarshalSealedHistory(view HistoryView) ([][]byte, []byte, error) {
59 if view.rows != historyViewRowCount(view) {
60 return nil, nil, fmt.Errorf("marshal sealed history: %w", errInvalidHistory)
61 }
62 sealed := make([][]byte, len(view.chunks))
63 for i, chunk := range view.chunks {
64 blob, err := MarshalHistoryChunk(chunk)
65 if err != nil {
66 return nil, nil, err
67 }
68 sealed[i] = blob
69 }
70 tail, err := MarshalHistory(HistoryView{nextRowID: view.NextRowID()})
71 if err != nil {
72 return nil, nil, err
73 }
74 return sealed, tail, nil
75}
76
77// HistoryFromBlobs restores history directly from sealed, oldest-first blobs
78// and the mandatory tail blob. It never feeds decoded rows through Append.
79func HistoryFromBlobs(config HistoryConfig, sealed [][]byte, tail []byte) (*History, error) {
80 if err := config.Validate(); err != nil {
81 return nil, err
82 }
83 sealedViews, tailView, err := decodeRestoredHistoryBlobs(sealed, tail)
84 if err != nil {
85 return nil, err
86 }
87 return restoreHistoryViews(config, sealedViews, tailView), nil
88}
89
90func decodeRestoredHistoryBlobs(sealed [][]byte, tail []byte, extra ...HistoryView) ([]HistoryView, HistoryView, error) {
91 seen := make(map[RowID]struct{})
92 sealedViews := make([]HistoryView, len(sealed))
93 for i, blob := range sealed {
94 view, err := UnmarshalHistory(blob)
95 if err != nil || len(view.chunks) != 1 || view.chunks[0].len() == 0 || !validateRestoredHistoryView(view, seen) {
96 return nil, HistoryView{}, fmt.Errorf("restore sealed history: %w", errInvalidHistory)
97 }
98 sealedViews[i] = view
99 }
100
101 tailView, err := UnmarshalHistory(tail)
102 if err != nil || !validateRestoredHistoryView(tailView, seen) {
103 return nil, HistoryView{}, fmt.Errorf("restore history tail: %w", errInvalidHistory)
104 }
105 for _, view := range extra {
106 if !validateRestoredHistoryView(view, seen) {
107 return nil, HistoryView{}, fmt.Errorf("restore recovery transcript: %w", errInvalidHistory)
108 }
109 }
110 return sealedViews, tailView, nil
111}
112
113func restoreHistoryViews(config HistoryConfig, sealed []HistoryView, tail HistoryView) *History {
114 h := NewHistory(config)
115 for _, view := range sealed {
116 h.nextRowID = max(h.nextRowID, view.nextRowID)
117 chunk := h.boundedRestoredChunk(view.chunks[0])
118 if chunk == nil {
119 continue
120 }
121 chunkBytes := historyChunkLogicalBytes(chunk)
122 for h.rows > h.maxRows-chunk.len() || h.logicalBytes > h.maxBytes-chunkBytes {
123 h.evictOldest()
124 }
125 h.chunks = append(h.chunks, chunk)
126 h.rows += chunk.len()
127 h.cells += chunk.cells()
128 h.logicalBytes += chunkBytes
129 }
130 h.nextRowID = max(h.nextRowID, tail.nextRowID)
131 if h.maxRows == 0 {
132 return h
133 }
134 for _, chunk := range tail.chunks {
135 for row := range chunk.len() {
136 if h.prepareChunkRowStyles(chunk, row) > h.maxBytes {
137 continue
138 }
139 for {
140 delta := h.tailAppendDelta(chunk.width)
141 if h.rows < h.maxRows && h.logicalBytes <= h.maxBytes-delta {
142 h.appendTailChunkRow(chunk, row)
143 h.recordTailRowStyles(chunk.width)
144 h.tailBytes += delta
145 h.logicalBytes += delta
146 h.tailBounds = append(h.tailBounds, chunk.bounds[row])
147 h.tailIDs = append(h.tailIDs, chunk.rowIDs[row])
148 h.rows++
149 h.cells += chunk.width
150 if len(h.tail) == h.chunkRows {
151 h.sealTail()
152 }
153 break
154 }
155 h.evictOldest()
156 h.prepareChunkRowStyles(chunk, row)
157 }
158 }
159 }
160 return h
161}
162
163func (h *History) boundedRestoredChunk(chunk *HistoryChunk) *HistoryChunk {
164 if h.maxRows == 0 || chunk == nil {
165 return nil
166 }
167 bounded := chunk
168 for bounded.count > h.maxRows || historyChunkLogicalBytes(bounded) > h.maxBytes {
169 if bounded.count == 1 {
170 return nil
171 }
172 bounded = bounded.withoutFirstRow()
173 }
174 return bounded
175}
176
177func validateRestoredHistoryView(view HistoryView, seen map[RowID]struct{}) bool {
178 if view.nextRowID == 0 || view.nextRowID == ^RowID(0) {
179 return false
180 }
181 var maxID RowID
182 for _, chunk := range view.chunks {
183 if chunk == nil || chunk.len() != len(chunk.rowIDs) || chunk.len() != len(chunk.bounds) {
184 return false
185 }
186 for _, id := range chunk.rowIDs {
187 if id == 0 || id >= ^RowID(0)-1 {
188 return false
189 }
190 if _, duplicate := seen[id]; duplicate {
191 return false
192 }
193 seen[id] = struct{}{}
194 maxID = max(maxID, id)
195 }
196 }
197 return view.nextRowID > maxID
198}
199
200// NewScreenWithRecoveryTranscript constructs a fresh blank screen whose
201// history contains the restored bounded history followed by the recovery
202// transcript. The transcript is decoded in full before history is restored.
203func NewScreenWithRecoveryTranscript(width, height int, config HistoryConfig, sealed [][]byte, tail, transcript []byte) (*Screen, error) {
204 if err := config.Validate(); err != nil {
205 return nil, err
206 }
207 transcriptView, err := UnmarshalHistory(transcript)
208 if err != nil {
209 return nil, fmt.Errorf("restore recovery transcript: %w", err)
210 }
211
212 sealedViews, tailView, err := decodeRestoredHistoryBlobs(sealed, tail, transcriptView)
213 if err != nil {
214 return nil, err
215 }
216 history := restoreHistoryViews(config, sealedViews, tailView)
217 remainingRows := transcriptView.rows
218 for _, chunk := range transcriptView.chunks {
219 for i := range chunk.len() {
220 row := chunk.row(i)
221 remainingRows--
222 bound := chunk.bounds[i]
223 if remainingRows == 0 {
224 bound.Soft = false
225 }
226 if err := history.AppendWithID(row, bound, chunk.rowIDs[i]); err != nil {
227 return nil, fmt.Errorf("restore recovery transcript: %w", err)
228 }
229 }
230 }
231
232 screen := NewScreenWithHistory(width, height, config)
233 screen.history = history
234 counter := max(history.nextRowID, transcriptView.nextRowID, RowID(1))
235 if counter == 0 || counter >= ^RowID(0) || uint64(height) > uint64(^RowID(0)-1-counter) {
236 return nil, fmt.Errorf("restore recovery transcript: %w", errInvalidHistory)
237 }
238 screen.nextRowID = counter - 1
239 for i := range screen.buffer.rowIDs {
240 screen.buffer.rowIDs[i] = screen.nextRowIDValue()
241 }
242 screen.frame = screen.buffer.frame
243 return screen, nil
244}