First slice of the `banger update` package. No CLI yet — this just
defines the wire shape and parsers the rest of the flow will plug
into.
* internal/updater/manifest.go — Manifest / Release types,
ManifestSchemaVersion = 1, the hardcoded URL
https://releases.thaloco.com/banger/manifest.json (var instead
of const so tests can point at httptest), and FetchManifest /
ParseManifest / Manifest.LookupRelease / Manifest.Latest.
The manifest only references URLs (tarball, SHA256SUMS, optional
signature); actual binary hashes come from SHA256SUMS itself,
so manifest tampering can't substitute a hash for a known-good
tarball.
SchemaVersion gates forward-compat: a CLI that doesn't know its
server's schema_version refuses to update rather than guessing.
* internal/updater/sha256sums.go — ParseSHA256Sums tolerates both
GNU `<digest> <file>` (with optional `*` binary prefix) and
BSD `SHA256 (file) = <digest>` formats. Comments and blank
lines are skipped; malformed lines that LOOK like entries are
rejected (silent skipping is the wrong failure mode for a
security-relevant input). Digests are lowercased so the caller
can `==`-compare without worrying about case.
Caps: 1 MiB on the manifest body, 16 KiB on SHA256SUMS, 256 MiB on
release tarballs. Generous-but-bounded; bumping requires a code
change so a server-side mistake can't fill the disk.
Tests: ParseManifest happy path, schema-version-too-new rejection,
five malformed-input cases. ParseSHA256Sums covers GNU + BSD +
star-prefix + comments-and-blanks, six malformed-input rejections,
case-insensitive digest normalisation. FetchManifest end-to-end via
httptest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3.1 KiB
Go
98 lines
3.1 KiB
Go
package updater
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseSHA256SumsGNUFormat(t *testing.T) {
|
|
body := []byte(`# header comment
|
|
0000000000000000000000000000000000000000000000000000000000000001 banger-v0.1.0-linux-amd64.tar.gz
|
|
0000000000000000000000000000000000000000000000000000000000000002 banger-v0.1.0-linux-amd64.tar.gz.sig
|
|
`)
|
|
got, err := ParseSHA256Sums(body)
|
|
if err != nil {
|
|
t.Fatalf("ParseSHA256Sums: %v", err)
|
|
}
|
|
if got["banger-v0.1.0-linux-amd64.tar.gz"] != "0000000000000000000000000000000000000000000000000000000000000001" {
|
|
t.Fatalf("tarball digest = %q", got["banger-v0.1.0-linux-amd64.tar.gz"])
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("got %d entries, want 2", len(got))
|
|
}
|
|
}
|
|
|
|
func TestParseSHA256SumsBSDFormat(t *testing.T) {
|
|
body := []byte(`SHA256 (banger-v0.1.0-linux-amd64.tar.gz) = 0000000000000000000000000000000000000000000000000000000000000001
|
|
`)
|
|
got, err := ParseSHA256Sums(body)
|
|
if err != nil {
|
|
t.Fatalf("ParseSHA256Sums: %v", err)
|
|
}
|
|
if got["banger-v0.1.0-linux-amd64.tar.gz"] != "0000000000000000000000000000000000000000000000000000000000000001" {
|
|
t.Fatalf("digest = %q", got["banger-v0.1.0-linux-amd64.tar.gz"])
|
|
}
|
|
}
|
|
|
|
func TestParseSHA256SumsBinaryStarPrefix(t *testing.T) {
|
|
// `sha256sum -b` emits `<digest> *<filename>`.
|
|
body := []byte(`0000000000000000000000000000000000000000000000000000000000000001 *banger-v0.1.0-linux-amd64.tar.gz
|
|
`)
|
|
got, err := ParseSHA256Sums(body)
|
|
if err != nil {
|
|
t.Fatalf("ParseSHA256Sums: %v", err)
|
|
}
|
|
if _, ok := got["banger-v0.1.0-linux-amd64.tar.gz"]; !ok {
|
|
t.Fatalf("entries = %v, want star-prefix stripped", got)
|
|
}
|
|
}
|
|
|
|
func TestParseSHA256SumsTolerantOfBlankAndComments(t *testing.T) {
|
|
body := []byte(`
|
|
# top comment
|
|
0000000000000000000000000000000000000000000000000000000000000001 a
|
|
|
|
# inline comment
|
|
0000000000000000000000000000000000000000000000000000000000000002 b
|
|
`)
|
|
got, err := ParseSHA256Sums(body)
|
|
if err != nil {
|
|
t.Fatalf("ParseSHA256Sums: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("got %d, want 2", len(got))
|
|
}
|
|
}
|
|
|
|
func TestParseSHA256SumsRejectsMalformed(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
body string
|
|
}{
|
|
{name: "short_digest", body: "abc file"},
|
|
{name: "non_hex_digest", body: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz file"},
|
|
{name: "no_filename", body: "0000000000000000000000000000000000000000000000000000000000000001"},
|
|
{name: "empty_body", body: ""},
|
|
{name: "only_comments", body: "# comment\n# more\n"},
|
|
{name: "bsd_no_eq", body: "SHA256 (file) 0000000000000000000000000000000000000000000000000000000000000001"},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := ParseSHA256Sums([]byte(tc.body)); err == nil {
|
|
t.Fatalf("expected error for %s", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSHA256SumsLowercasesDigest(t *testing.T) {
|
|
body := []byte(`ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890 upper
|
|
`)
|
|
got, err := ParseSHA256Sums(body)
|
|
if err != nil {
|
|
t.Fatalf("ParseSHA256Sums: %v", err)
|
|
}
|
|
if got["upper"] != strings.ToLower("ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890") {
|
|
t.Fatalf("digest not lowercased: %q", got["upper"])
|
|
}
|
|
}
|