1package persist
2
3import (
4 "encoding/binary"
5 "errors"
6 "fmt"
7 "math"
8
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"
11)
12
13const (
14 catalogueRecordVersion uint16 = 5
15 protocolLessCatalogueRecordVersion uint16 = 4
16 legacyCatalogueRecordVersion uint16 = 3
17)
18
19var errMalformedRecord = errors.New("persist: malformed catalogue record")
20
21var catalogueMagic = [4]byte{'V', 'E', 'V', 'C'}
22
23// Record is the durable catalogue record used by daemon code.
24type Record = domain.CatalogueRecord
25
26func encodeRecordValue(record domain.CatalogueRecord) ([]byte, error) {
27 return encodeRecordValueForProtocol(record, ports.ProtocolVersion)
28}
29
30func encodeRecordValueForProtocol(record domain.CatalogueRecord, protocolVersion uint16) ([]byte, error) {
31 if err := record.Validate(); err != nil {
32 return nil, fmt.Errorf("persist: invalid catalogue record: %w", err)
33 }
34 buf := make([]byte, 0, 128)
35 buf = append(buf, catalogueMagic[:]...)
36 buf = binary.BigEndian.AppendUint16(buf, catalogueRecordVersion)
37 buf = binary.BigEndian.AppendUint16(buf, protocolVersion)
38 buf = append(buf, record.IncarnationID[:]...)
39 var err error
40 buf, err = appendCheckedString(buf, record.Cwd)
41 if err != nil {
42 return nil, err
43 }
44 buf = binary.BigEndian.AppendUint64(buf, uint64(record.CreatedAt))
45 buf = binary.BigEndian.AppendUint64(buf, uint64(record.UpdatedAt))
46 buf = binary.BigEndian.AppendUint64(buf, record.LastUsedSeq)
47 if uint64(len(record.TabNames)) > math.MaxUint32 {
48 return nil, errors.New("persist: too many tab names")
49 }
50 buf = binary.BigEndian.AppendUint32(buf, uint32(len(record.TabNames)))
51 for _, name := range record.TabNames {
52 buf, err = appendCheckedString(buf, name)
53 if err != nil {
54 return nil, err
55 }
56 }
57 if uint64(len(record.TabRecords)) > math.MaxUint32 {
58 return nil, errors.New("persist: too many tab records")
59 }
60 buf = binary.BigEndian.AppendUint32(buf, uint32(len(record.TabRecords)))
61 for _, tab := range record.TabRecords {
62 buf, err = appendCheckedString(buf, string(tab.StableID))
63 if err != nil {
64 return nil, err
65 }
66 buf, err = appendCheckedString(buf, tab.Name)
67 if err != nil {
68 return nil, err
69 }
70 }
71 buf = appendRef(buf, record.Committed)
72 buf, err = appendCheckedString(buf, record.DegradedReason)
73 return buf, err
74}
75
76type storedRecord struct {
77 record domain.CatalogueRecord
78 protocolVersion uint16
79}
80
81func decodeRecordValue(name string, value []byte) (domain.CatalogueRecord, error) {
82 stored, err := decodeStoredRecordValue(name, value)
83 return stored.record, err
84}
85
86func decodeStoredRecordValue(name string, value []byte) (storedRecord, error) {
87 if err := domain.ValidateSessionName(name); err != nil {
88 return storedRecord{}, err
89 }
90 reader := valueReader{data: value}
91 magic, ok := reader.take(4)
92 if !ok || string(magic) != string(catalogueMagic[:]) {
93 return storedRecord{}, errMalformedRecord
94 }
95 version, ok := reader.u16()
96 if !ok || (version != catalogueRecordVersion && version != protocolLessCatalogueRecordVersion && version != legacyCatalogueRecordVersion) {
97 return storedRecord{}, errMalformedRecord
98 }
99 var protocolVersion uint16
100 if version == catalogueRecordVersion {
101 protocolVersion, ok = reader.u16()
102 if !ok {
103 return storedRecord{}, errMalformedRecord
104 }
105 }
106 id, ok := reader.take(16)
107 if !ok {
108 return storedRecord{}, errMalformedRecord
109 }
110 record := domain.CatalogueRecord{Name: name}
111 copy(record.IncarnationID[:], id)
112 var valid bool
113 if record.Cwd, valid = reader.str(); !valid {
114 return storedRecord{}, errMalformedRecord
115 }
116 created, ok := reader.u64()
117 if !ok {
118 return storedRecord{}, errMalformedRecord
119 }
120 record.CreatedAt = int64(created)
121 updated, ok := reader.u64()
122 if !ok {
123 return storedRecord{}, errMalformedRecord
124 }
125 record.UpdatedAt = int64(updated)
126 if record.LastUsedSeq, ok = reader.u64(); !ok {
127 return storedRecord{}, errMalformedRecord
128 }
129 count, ok := reader.u32()
130 if !ok || uint64(count) > uint64(reader.remaining())/4 {
131 return storedRecord{}, errMalformedRecord
132 }
133 if count > 0 {
134 record.TabNames = make([]string, 0, int(count))
135 }
136 for range count {
137 tab, ok := reader.str()
138 if !ok {
139 return storedRecord{}, errMalformedRecord
140 }
141 record.TabNames = append(record.TabNames, tab)
142 }
143 if version == legacyCatalogueRecordVersion {
144 if len(record.TabNames) != 0 {
145 record.TabRecords = make([]domain.CatalogueTabRecord, len(record.TabNames))
146 for i, name := range record.TabNames {
147 record.TabRecords[i] = domain.CatalogueTabRecord{Name: name}
148 }
149 }
150 } else {
151 tabRecordCount, ok := reader.u32()
152 if !ok || uint64(tabRecordCount) > uint64(reader.remaining())/8 {
153 return storedRecord{}, errMalformedRecord
154 }
155 if tabRecordCount > 0 {
156 record.TabRecords = make([]domain.CatalogueTabRecord, 0, int(tabRecordCount))
157 }
158 for range tabRecordCount {
159 stableID, stableOK := reader.str()
160 name, nameOK := reader.str()
161 if !stableOK || !nameOK {
162 return storedRecord{}, errMalformedRecord
163 }
164 record.TabRecords = append(record.TabRecords, domain.CatalogueTabRecord{StableID: domain.TabStableID(stableID), Name: name})
165 }
166 }
167 if record.Committed, ok = reader.ref(); !ok {
168 return storedRecord{}, errMalformedRecord
169 }
170 if record.DegradedReason, ok = reader.str(); !ok || reader.remaining() != 0 {
171 return storedRecord{}, errMalformedRecord
172 }
173 if err := record.Validate(); err != nil {
174 return storedRecord{}, errors.Join(errMalformedRecord, fmt.Errorf("persist: invalid catalogue record: %w", err))
175 }
176 return storedRecord{record: record, protocolVersion: protocolVersion}, nil
177}
178
179func appendCheckedString(buf []byte, value string) ([]byte, error) {
180 if uint64(len(value)) > math.MaxUint32 {
181 return nil, errors.New("persist: string too large")
182 }
183 buf = binary.BigEndian.AppendUint32(buf, uint32(len(value)))
184 return append(buf, value...), nil
185}
186func appendRef(buf []byte, ref *domain.CheckpointRef) []byte {
187 if ref == nil {
188 return append(buf, 0)
189 }
190 buf = append(buf, 1)
191 buf = binary.BigEndian.AppendUint64(buf, ref.Generation)
192 return append(buf, ref.ManifestDigest[:]...)
193}
194
195type valueReader struct {
196 data []byte
197 off int
198}
199
200func (r *valueReader) remaining() int { return len(r.data) - r.off }
201func (r *valueReader) take(n int) ([]byte, bool) {
202 if n < 0 || n > r.remaining() {
203 return nil, false
204 }
205 value := r.data[r.off : r.off+n]
206 r.off += n
207 return value, true
208}
209func (r *valueReader) byte() (byte, bool) {
210 value, ok := r.take(1)
211 if !ok {
212 return 0, false
213 }
214 return value[0], true
215}
216func (r *valueReader) u16() (uint16, bool) {
217 value, ok := r.take(2)
218 if !ok {
219 return 0, false
220 }
221 return binary.BigEndian.Uint16(value), true
222}
223func (r *valueReader) u32() (uint32, bool) {
224 value, ok := r.take(4)
225 if !ok {
226 return 0, false
227 }
228 return binary.BigEndian.Uint32(value), true
229}
230func (r *valueReader) u64() (uint64, bool) {
231 value, ok := r.take(8)
232 if !ok {
233 return 0, false
234 }
235 return binary.BigEndian.Uint64(value), true
236}
237func (r *valueReader) str() (string, bool) {
238 size, ok := r.u32()
239 if !ok || uint64(size) > uint64(r.remaining()) {
240 return "", false
241 }
242 value, _ := r.take(int(size))
243 return string(value), true
244}
245func (r *valueReader) ref() (*domain.CheckpointRef, bool) {
246 present, ok := r.byte()
247 if !ok {
248 return nil, false
249 }
250 if present == 0 {
251 return nil, true
252 }
253 if present != 1 {
254 return nil, false
255 }
256 generation, ok := r.u64()
257 if !ok {
258 return nil, false
259 }
260 digest, ok := r.take(32)
261 if !ok {
262 return nil, false
263 }
264 ref := &domain.CheckpointRef{Generation: generation}
265 copy(ref.ManifestDigest[:], digest)
266 return ref, true
267}