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