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: len(chunk.rows)})
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 canonical tail blob.
57func MarshalSealedHistory(view HistoryView) ([][]byte, []byte, error) {
58 if view.rows != historyViewRowCount(view) {
59 return nil, nil, fmt.Errorf("marshal sealed history: %w", errInvalidHistory)
60 }
61 sealed := make([][]byte, len(view.chunks))
62 for i, chunk := range view.chunks {
63 blob, err := MarshalHistoryChunk(chunk)
64 if err != nil {
65 return nil, nil, err
66 }
67 sealed[i] = blob
68 }
69 tail, err := MarshalEmptyHistoryTail()
70 if err != nil {
71 return nil, nil, err
72 }
73 return sealed, tail, nil
74}
75
76// HistoryFromBlobs restores history directly from sealed, oldest-first blobs
77// and the mandatory tail blob. It never feeds decoded rows through Append.
78func HistoryFromBlobs(config HistoryConfig, sealed [][]byte, tail []byte) (*History, error) {
79 h := NewHistory(config)
80 for _, blob := range sealed {
81 view, err := UnmarshalHistory(blob)
82 if err != nil || len(view.chunks) != 1 || len(view.chunks[0].rows) == 0 {
83 return nil, fmt.Errorf("restore sealed history: %w", errInvalidHistory)
84 }
85 h.evictUntil(len(view.chunks[0].rows), view.Cells())
86 h.chunks = append(h.chunks, view.chunks[0])
87 h.rows += len(view.chunks[0].rows)
88 h.cells += view.Cells()
89 }
90 view, err := UnmarshalHistory(tail)
91 if err != nil || len(view.chunks) > 1 {
92 return nil, fmt.Errorf("restore history tail: %w", errInvalidHistory)
93 }
94 if len(view.chunks) == 1 {
95 h.evictUntil(len(view.chunks[0].rows), view.Cells())
96 h.tail = view.chunks[0].rows
97 h.tailBounds = growBounds(view.chunks[0].bounds, len(h.tail))
98 h.rows += len(h.tail)
99 h.cells += view.Cells()
100 }
101 h.evict()
102 h.normalizeTail()
103 return h, nil
104}
105
106// NewScreenWithRecoveryTranscript constructs a fresh blank screen whose
107// history contains the restored bounded history followed by the recovery
108// transcript. The transcript is decoded in full before history is restored.
109func NewScreenWithRecoveryTranscript(width, height int, config HistoryConfig, sealed [][]byte, tail, transcript []byte) (*Screen, error) {
110 transcriptView, err := UnmarshalHistory(transcript)
111 if err != nil {
112 return nil, fmt.Errorf("restore recovery transcript: %w", err)
113 }
114
115 history, err := HistoryFromBlobs(config, sealed, tail)
116 if err != nil {
117 return nil, err
118 }
119 remainingRows := transcriptView.rows
120 for _, chunk := range transcriptView.chunks {
121 for i, row := range chunk.rows {
122 remainingRows--
123 bound := chunk.bounds[i]
124 if remainingRows == 0 {
125 bound.Soft = false
126 }
127 if err := history.Append(row, bound); err != nil {
128 return nil, fmt.Errorf("restore recovery transcript: %w", err)
129 }
130 }
131 }
132
133 screen := NewScreenWithHistory(width, height, config)
134 screen.history = history
135 return screen, nil
136}