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