1// Command systemviews-assets generates and checks the content manifest
2// for served systemview assets (Plan 05 P3.1).
3//
4// Generation (default) reads the uncompressed servables from -dir,
5// requires the compressed WASM to decompress to the same bytes under a
6// bounded read, and writes asset-manifest.json. It fails loudly on stale
7// or missing inputs instead of writing a partial manifest.
8//
9// Check mode (-check) validates the generated bundle manifest against the
10// artifacts on disk and exits nonzero on any mismatch, for CI and
11// release verification.
12package main
13
14import (
15 "bytes"
16 "crypto/sha256"
17 "encoding/hex"
18 "flag"
19 "fmt"
20 "io"
21 "io/fs"
22 "os"
23 "path/filepath"
24
25 "github.com/andybalholm/brotli"
26
27 "github.com/bnema/dumber/assets"
28)
29
30// maxManifestWASMBytes bounds manifest-time decompression, mirroring the
31// serving bound so a manifest can never bless what serving would reject.
32const maxManifestWASMBytes = 64 * 1024 * 1024
33
34// manifestFilePerm matches repo asset files.
35const manifestFilePerm = 0o644
36
37func main() {
38 dir := flag.String("dir", "assets/systemviews", "directory holding the built systemview assets")
39 check := flag.Bool("check", false, "verify the generated manifest against disk artifacts instead of regenerating")
40 checkIfPresent := flag.Bool("check-if-present", false, "like -check, but pass with a notice when no built WASM exists (for quick builds)")
41 flag.Parse()
42
43 if err := run(*dir, *check, *checkIfPresent); err != nil {
44 fmt.Fprintf(os.Stderr, "systemviews-assets: %v\n", err)
45 os.Exit(1)
46 }
47}
48
49func run(dir string, check, checkIfPresent bool) error {
50 manifestPath := filepath.Join(dir, assets.ManifestName)
51 if checkIfPresent {
52 // Serving treats the compressed WASM as authoritative, so a
53 // stale .br without raw (or vice versa) is a partial bundle
54 // that must fail validation, never skip it.
55 _, rawErr := os.Stat(filepath.Join(dir, "systemviews.wasm"))
56 _, brErr := os.Stat(filepath.Join(dir, "systemviews.wasm.br"))
57 if os.IsNotExist(rawErr) && os.IsNotExist(brErr) {
58 fmt.Printf("no built systemviews WASM in %s; skipping manifest check\n", dir)
59 return nil
60 }
61 return checkTree(dir, manifestPath)
62 }
63 if check {
64 return checkTree(dir, manifestPath)
65 }
66 m, err := collectManifest(os.DirFS(dir))
67 if err != nil {
68 return err
69 }
70 data, err := m.Bytes()
71 if err != nil {
72 return err
73 }
74 if err := os.WriteFile(manifestPath, data, manifestFilePerm); err != nil {
75 return fmt.Errorf("write %s: %w", manifestPath, err)
76 }
77 fmt.Printf("wrote %s pinning %d assets\n", manifestPath, len(assets.ManifestFiles))
78 return nil
79}
80
81// checkTree validates the generated manifest against disk artifacts:
82// pins, raw bytes, and raw/compressed WASM agreement. Generation checks
83// agreement at write time, but only the check path guards a later
84// compressed-only swap, which serving would treat as authoritative.
85func checkTree(dir, manifestPath string) error {
86 data, err := os.ReadFile(manifestPath)
87 if err != nil {
88 return fmt.Errorf("read %s: run 'make build-systemviews' first: %w", manifestPath, err)
89 }
90 m, err := assets.ParseManifest(data)
91 if err != nil {
92 return err
93 }
94 fsys := os.DirFS(dir)
95 if verr := m.Verify(fsys); verr != nil {
96 return verr
97 }
98 raw, err := readBoundedManifestFile(fsys, "systemviews.wasm", maxManifestWASMBytes)
99 if err != nil {
100 return fmt.Errorf("read servable systemviews.wasm: %w", err)
101 }
102 if err := requireCompressedAgreement(fsys, raw, maxManifestWASMBytes); err != nil {
103 return err
104 }
105 fmt.Printf("manifest %s matches %d pinned assets with agreeing compressed WASM\n", manifestPath, len(assets.ManifestFiles))
106 return nil
107}
108
109// collectManifest pins every manifest file from fsys. The raw and
110// compressed WASM must agree under bounded decompression; any missing,
111// corrupt, or oversized input is a hard error.
112func collectManifest(fsys fs.FS) (assets.Manifest, error) {
113 files := make(map[string]assets.FileEntry, len(assets.ManifestFiles))
114 for _, name := range assets.ManifestFiles {
115 data, err := readBoundedManifestFile(fsys, name, maxManifestWASMBytes)
116 if err != nil {
117 return assets.Manifest{}, fmt.Errorf("read servable asset %s: %w", name, err)
118 }
119 if name == "systemviews.wasm" {
120 if err := requireCompressedAgreement(fsys, data, maxManifestWASMBytes); err != nil {
121 return assets.Manifest{}, err
122 }
123 }
124 sum := sha256.Sum256(data)
125 files[name] = assets.FileEntry{SHA256: hex.EncodeToString(sum[:]), Size: int64(len(data))}
126 }
127 return assets.Manifest{Version: assets.ManifestVersion, Files: files}, nil
128}
129
130// readBoundedManifestFile reads a manifest input with the pre-allocation
131// bound the runtime loader enforces, so generation and checks cannot be
132// pushed into unbounded allocation by a corrupt or hostile artifact.
133func readBoundedManifestFile(fsys fs.FS, name string, maxBytes int) ([]byte, error) {
134 f, err := fsys.Open(name)
135 if err != nil {
136 return nil, err
137 }
138 defer func() { _ = f.Close() }()
139 data, err := io.ReadAll(io.LimitReader(f, int64(maxBytes)+1))
140 if err != nil {
141 return nil, fmt.Errorf("read %s: %w", name, err)
142 }
143 if len(data) > maxBytes {
144 return nil, fmt.Errorf("%s exceeds %d bytes", name, maxBytes)
145 }
146 return data, nil
147}
148
149// requireCompressedAgreement ensures the shipped .br decompresses to
150// exactly the raw bytes being pinned. Both inputs arrive bounded, and the
151// decoded output is rejected past the bound before comparison, mirroring
152// the runtime loader: without the sentinel check a max+1-byte bundle
153// would bless here what serving refuses there.
154func requireCompressedAgreement(fsys fs.FS, raw []byte, maxBytes int) error {
155 compressed, err := readBoundedManifestFile(fsys, "systemviews.wasm.br", maxBytes)
156 if err != nil {
157 return fmt.Errorf("read compressed systemviews.wasm.br: %w", err)
158 }
159 data, err := io.ReadAll(io.LimitReader(brotli.NewReader(bytes.NewReader(compressed)), int64(maxBytes)+1))
160 if err != nil {
161 return fmt.Errorf("decompress systemviews.wasm.br: %w", err)
162 }
163 if len(data) > maxBytes {
164 return fmt.Errorf("systemviews.wasm.br decompresses past %d bytes", maxBytes)
165 }
166 if !bytes.Equal(data, raw) {
167 return fmt.Errorf("systemviews.wasm.br decompresses to %d bytes, raw file has %d", len(data), len(raw))
168 }
169 return nil
170}