detached d6a5676

fix(snapshot): encode strict generation heads

commit
d6a5676f844c3ff7ac56f10bd8f76132885da3b5
author
brice · · 2026-07-22 02:13 UTC
parent
fc94e20
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +76 -0
A internal/usecase/snapshot/head_test.go +44 -0
@@ -0,0 +1,44 @@
1+package snapshot
2+
3+import (
4+ "errors"
5+ "testing"
6+)
7+
8+func TestHeadCodecIsDeterministicAndStrict(t *testing.T) {
9+ head := Head{Generation: 7, ManifestDigest: SnapshotDigest{1}}
10+ encoded, err := MarshalHead(head)
11+ if err != nil {
12+ t.Fatal(err)
13+ }
14+ encodedAgain, err := MarshalHead(head)
15+ if err != nil {
16+ t.Fatal(err)
17+ }
18+ if string(encoded) != string(encodedAgain) {
19+ t.Fatal("head encoding is not deterministic")
20+ }
21+ got, err := UnmarshalHead(encoded)
22+ if err != nil || got != head {
23+ t.Fatalf("head round trip = %#v, %v", got, err)
24+ }
25+ for n := range len(encoded) {
26+ if _, err := UnmarshalHead(encoded[:n]); err == nil {
27+ t.Fatalf("prefix %d accepted", n)
28+ }
29+ }
30+ if _, err := UnmarshalHead(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) {
31+ t.Fatalf("trailing data error = %v", err)
32+ }
33+}
34+
35+func TestHeadCodecRejectsInvalidDeclarations(t *testing.T) {
36+ for _, head := range []Head{
37+ {ManifestDigest: SnapshotDigest{1}},
38+ {Generation: 1},
39+ } {
40+ if _, err := MarshalHead(head); !errors.Is(err, ErrInvalidData) {
41+ t.Fatalf("MarshalHead(%#v) error = %v, want invalid data", head, err)
42+ }
43+ }
44+}
@@ -0,0 +1,44 @@
1+package snapshot
2+
3+import (
4+ "errors"
5+ "testing"
6+)
7+
8+func TestHeadCodecIsDeterministicAndStrict(t *testing.T) {
9+ head := Head{Generation: 7, ManifestDigest: SnapshotDigest{1}}
10+ encoded, err := MarshalHead(head)
11+ if err != nil {
12+ t.Fatal(err)
13+ }
14+ encodedAgain, err := MarshalHead(head)
15+ if err != nil {
16+ t.Fatal(err)
17+ }
18+ if string(encoded) != string(encodedAgain) {
19+ t.Fatal("head encoding is not deterministic")
20+ }
21+ got, err := UnmarshalHead(encoded)
22+ if err != nil || got != head {
23+ t.Fatalf("head round trip = %#v, %v", got, err)
24+ }
25+ for n := range len(encoded) {
26+ if _, err := UnmarshalHead(encoded[:n]); err == nil {
27+ t.Fatalf("prefix %d accepted", n)
28+ }
29+ }
30+ if _, err := UnmarshalHead(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) {
31+ t.Fatalf("trailing data error = %v", err)
32+ }
33+}
34+
35+func TestHeadCodecRejectsInvalidDeclarations(t *testing.T) {
36+ for _, head := range []Head{
37+ {ManifestDigest: SnapshotDigest{1}},
38+ {Generation: 1},
39+ } {
40+ if _, err := MarshalHead(head); !errors.Is(err, ErrInvalidData) {
41+ t.Fatalf("MarshalHead(%#v) error = %v, want invalid data", head, err)
42+ }
43+ }
44+}
M internal/usecase/snapshot/manifest.go +32 -0
@@ -13,6 +13,7 @@ import (
1313
1414 const (
1515 manifestMagic = "VEVM"
16+ headMagic = "VEVH"
1617 objectMagic = "VEVO"
1718 ManifestVersion = uint16(1)
1819 manifestHeaderSize = 16
@@ -59,6 +60,37 @@ type Head struct {
5960 ManifestDigest SnapshotDigest
6061 }
6162
63+// MarshalHead encodes the deterministic, strict VEVH generation pointer.
64+func MarshalHead(h Head) ([]byte, error) {
65+ if h.Generation == 0 || isZeroDigestBytes(h.ManifestDigest[:]) {
66+ return nil, fmt.Errorf("%w: head", ErrInvalidData)
67+ }
68+ var w payloadWriter
69+ w.putUint64(h.Generation)
70+ w.b = append(w.b, h.ManifestDigest[:]...)
71+ return marshalManifestEnvelope(headMagic, w.b)
72+}
73+
74+// UnmarshalHead decodes a complete VEVH generation pointer.
75+func UnmarshalHead(encoded []byte) (Head, error) {
76+ body, err := unmarshalManifestEnvelope(encoded, headMagic)
77+ if err != nil {
78+ return Head{}, err
79+ }
80+ if len(body) < 8+sha256.Size {
81+ return Head{}, ErrShortPayload
82+ }
83+ if len(body) != 8+sha256.Size {
84+ return Head{}, ErrTrailingBytes
85+ }
86+ head := Head{Generation: binary.BigEndian.Uint64(body[:8])}
87+ copy(head.ManifestDigest[:], body[8:])
88+ if head.Generation == 0 || isZeroDigestBytes(head.ManifestDigest[:]) {
89+ return Head{}, fmt.Errorf("%w: head", ErrInvalidData)
90+ }
91+ return head, nil
92+}
93+
6294 // Manifest is the complete VEVM payload carried directly in a publication.
6395 type Manifest struct {
6496 Generation uint64
@@ -13,6 +13,7 @@ import (
13 13
14 const (14 const (
15 manifestMagic = "VEVM"15 manifestMagic = "VEVM"
16+ headMagic = "VEVH"
16 objectMagic = "VEVO"17 objectMagic = "VEVO"
17 ManifestVersion = uint16(1)18 ManifestVersion = uint16(1)
18 manifestHeaderSize = 1619 manifestHeaderSize = 16
@@ -59,6 +60,37 @@ type Head struct {
59 ManifestDigest SnapshotDigest60 ManifestDigest SnapshotDigest
60 }61 }
61 62
63+// MarshalHead encodes the deterministic, strict VEVH generation pointer.
64+func MarshalHead(h Head) ([]byte, error) {
65+ if h.Generation == 0 || isZeroDigestBytes(h.ManifestDigest[:]) {
66+ return nil, fmt.Errorf("%w: head", ErrInvalidData)
67+ }
68+ var w payloadWriter
69+ w.putUint64(h.Generation)
70+ w.b = append(w.b, h.ManifestDigest[:]...)
71+ return marshalManifestEnvelope(headMagic, w.b)
72+}
73+
74+// UnmarshalHead decodes a complete VEVH generation pointer.
75+func UnmarshalHead(encoded []byte) (Head, error) {
76+ body, err := unmarshalManifestEnvelope(encoded, headMagic)
77+ if err != nil {
78+ return Head{}, err
79+ }
80+ if len(body) < 8+sha256.Size {
81+ return Head{}, ErrShortPayload
82+ }
83+ if len(body) != 8+sha256.Size {
84+ return Head{}, ErrTrailingBytes
85+ }
86+ head := Head{Generation: binary.BigEndian.Uint64(body[:8])}
87+ copy(head.ManifestDigest[:], body[8:])
88+ if head.Generation == 0 || isZeroDigestBytes(head.ManifestDigest[:]) {
89+ return Head{}, fmt.Errorf("%w: head", ErrInvalidData)
90+ }
91+ return head, nil
92+}
93+
62 // Manifest is the complete VEVM payload carried directly in a publication.94 // Manifest is the complete VEVM payload carried directly in a publication.
63 type Manifest struct {95 type Manifest struct {
64 Generation uint6496 Generation uint64

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