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