main 5041353

manifest.go RawHistoryFolder

1package assets
2
3// Content manifest for served systemview assets (Plan 05 P3.1).
4//
5// The manifest pins the SHA-256 and size of every uncompressed immutable
6// asset the CEF scheme handler may serve with long-lived cache headers.
7// It is generated by cmd/systemviews-assets after the WASM build as an
8// untracked build artifact; verification compares it against the actually
9// embedded artifacts, never against Git state. Encoding is canonical JSON with
10// sorted keys and no timestamps or paths, so identical inputs always
11// produce identical bytes.
12
13import (
14	"bytes"
15	"crypto/sha256"
16	"encoding/hex"
17	"encoding/json"
18	"fmt"
19	"io"
20	"io/fs"
21)
22
23// ManifestVersion is the only accepted manifest schema version.
24const ManifestVersion = 1
25
26// ManifestName is the manifest file name inside assets/systemviews.
27const ManifestName = "asset-manifest.json"
28
29// ManifestFiles lists every served uncompressed asset pinned by the
30// manifest, in canonical order.
31var ManifestFiles = []string{"systemviews.css", "systemviews.wasm", "wasm_exec.js"}
32
33// FileEntry pins one asset's content identity.
34type FileEntry struct {
35	SHA256 string `json:"sha256"`
36	Size   int64  `json:"size"`
37}
38
39// Manifest pins the content identity of servable immutable assets.
40type Manifest struct {
41	Version int                  `json:"version"`
42	Files   map[string]FileEntry `json:"files"`
43}
44
45// ParseManifest decodes and validates manifest bytes. Unknown files,
46// missing pins, malformed digests, and version mismatches are all hard
47// errors: a trust root must fail closed, never serve unverified immutable
48// content. Every pin must be exactly 64 lowercase hex characters: pins
49// are interpolated into shell HTML/JS, so anything else is rejected
50// before any shell use rather than escaped after it.
51func ParseManifest(data []byte) (Manifest, error) {
52	var m Manifest
53	dec := json.NewDecoder(bytes.NewReader(data))
54	dec.DisallowUnknownFields()
55	if err := dec.Decode(&m); err != nil {
56		return Manifest{}, fmt.Errorf("decode asset manifest: %w", err)
57	}
58	var extra json.RawMessage
59	if err := dec.Decode(&extra); err != io.EOF {
60		return Manifest{}, fmt.Errorf("asset manifest has trailing data")
61	}
62	if m.Version != ManifestVersion {
63		return Manifest{}, fmt.Errorf("unsupported asset manifest version %d", m.Version)
64	}
65	if len(m.Files) != len(ManifestFiles) {
66		return Manifest{}, fmt.Errorf("asset manifest pins %d files, want %d", len(m.Files), len(ManifestFiles))
67	}
68	for _, name := range ManifestFiles {
69		entry, ok := m.Files[name]
70		if !ok {
71			return Manifest{}, fmt.Errorf("asset manifest missing pin for %s", name)
72		}
73		if !validDigest(entry.SHA256) {
74			return Manifest{}, fmt.Errorf("asset manifest has invalid pin for %s", name)
75		}
76		if entry.Size < 0 {
77			return Manifest{}, fmt.Errorf("asset manifest has invalid size for %s", name)
78		}
79	}
80	return m, nil
81}
82
83// validDigest reports whether s is a SHA-256 hex digest: exactly 64
84// lowercase hexadecimal characters, nothing else.
85func validDigest(s string) bool {
86	if len(s) != 64 {
87		return false
88	}
89	for i := 0; i < len(s); i++ {
90		digit := s[i] >= '0' && s[i] <= '9'
91		lowerHex := s[i] >= 'a' && s[i] <= 'f'
92		if !digit && !lowerHex {
93			return false
94		}
95	}
96	return true
97}
98
99// Bytes renders the canonical manifest encoding: indented JSON with sorted
100// keys, no timestamps or paths, terminated by a single newline.
101func (m Manifest) Bytes() ([]byte, error) {
102	data, err := json.MarshalIndent(m, "", "  ")
103	if err != nil {
104		return nil, fmt.Errorf("encode asset manifest: %w", err)
105	}
106	return append(data, '\n'), nil
107}
108
109// Verify checks every pinned file against its actual bytes in fsys. Names
110// resolve at the filesystem root; callers pass fs.Sub when artifacts live
111// under a prefix (e.g. the embedded systemviews directory).
112func (m Manifest) Verify(fsys fs.FS) error {
113	for _, name := range ManifestFiles {
114		want := m.Files[name]
115		data, err := fs.ReadFile(fsys, name)
116		if err != nil {
117			return fmt.Errorf("read pinned asset %s: %w", name, err)
118		}
119		sum := sha256.Sum256(data)
120		if got := hex.EncodeToString(sum[:]); got != want.SHA256 {
121			return fmt.Errorf("pinned asset %s digest mismatch", name)
122		}
123		if int64(len(data)) != want.Size {
124			return fmt.Errorf("pinned asset %s size mismatch: got %d, want %d", name, len(data), want.Size)
125		}
126	}
127	return nil
128}

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 8

Tags 42

↑↓ moveenter openesc close