main ece7682

record.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close