package busarchive import ( "archive/tar" "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "github.com/klauspost/compress/s2" ) func sum(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) } // streamArchive is a stream's archive as the server sends it: a tar of one file, s2-compressed. func streamArchive(t *testing.T) []byte { t.Helper() var raw bytes.Buffer tw := tar.NewWriter(&raw) body := []byte(strings.Repeat("a message of the stream\n", 50)) if err := tw.WriteHeader(&tar.Header{Name: "msgs.blk", Mode: 0o600, Size: int64(len(body))}); err != nil { t.Fatal(err) } tw.Write(body) tw.Close() var out bytes.Buffer w := s2.NewWriter(&out) w.Write(raw.Bytes()) w.Close() return out.Bytes() } // snapshotTar builds a snapshot of two streams; the mutations change what the manifest says or what is // stored, so each fault is made in the one place it is found. func snapshotTar(t *testing.T, mutate func(m *Manifest, files map[string][]byte)) []byte { t.Helper() files := map[string][]byte{} m := Manifest{Format: 1, Server: "2.11.17", Finished: time.Date(2026, 10, 22, 3, 0, 0, 0, time.UTC)} for i, name := range []string{"EVENTS", "KV_conditions"} { meta := []byte(`{"config":{"name":"` + name + `"}}`) arc := streamArchive(t) e := StreamEntry{Name: name, LastSeq: uint64(10 * (i + 1)), Meta: "streams/" + name + "/backup.json", MetaSHA256: sum(meta), Archive: "streams/" + name + "/stream.tar.s2", ArchiveBytes: int64(len(arc)), ArchiveSHA256: sum(arc)} m.Streams = append(m.Streams, e) files[e.Meta], files[e.Archive] = meta, arc } if mutate != nil { mutate(&m, files) } var out bytes.Buffer tw := tar.NewWriter(&out) put := func(name string, b []byte) { if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0o600, Size: int64(len(b))}); err != nil { t.Fatal(err) } tw.Write(b) } raw, _ := json.Marshal(m) put(ManifestName, raw) for _, s := range m.Streams { put(s.Meta, files[s.Meta]) put(s.Archive, files[s.Archive]) } tw.Close() return out.Bytes() } func put(t *testing.T, dir, name string, b []byte, at time.Time) { t.Helper() p := filepath.Join(dir, name) if err := os.WriteFile(p, b, 0o600); err != nil { t.Fatal(err) } os.Chtimes(p, at, at) } func TestAGoodSnapshotPassesAndSaysItsStreams(t *testing.T) { dir := t.TempDir() good := snapshotTar(t, nil) put(t, dir, "bus.tar", good, time.Now()) r, err := VerifyFile(dir, "") if err != nil { t.Fatal(err) } if !r.Passed || r.Fault != "" || r.File != "bus.tar" || r.Size != int64(len(good)) { t.Fatalf("%+v", r) } if r.Taken == nil || !r.Taken.Equal(time.Date(2026, 10, 22, 3, 0, 0, 0, time.UTC)) { t.Errorf("taken %v", r.Taken) } if len(r.Streams) != 2 || r.Streams[0] != (StreamReport{"EVENTS", 10}) || r.Streams[1] != (StreamReport{"KV_conditions", 20}) { t.Errorf("streams %+v", r.Streams) } } func TestACorruptedChecksumFailsNamingTheFile(t *testing.T) { dir := t.TempDir() put(t, dir, "bus.tar", snapshotTar(t, func(m *Manifest, files map[string][]byte) { m.Streams[1].ArchiveSHA256 = sum([]byte("something else")) }), time.Now()) r, err := VerifyFile(dir, "bus.tar") if err != nil { t.Fatal(err) } if r.Passed || !strings.Contains(r.Fault, "streams/KV_conditions/stream.tar.s2") || !strings.Contains(r.Fault, "checksum") { t.Fatalf("%+v", r) } if len(r.Streams) != 2 { t.Errorf("a failed snapshot still says its streams: %+v", r.Streams) } } func TestATruncatedSnapshotFails(t *testing.T) { dir := t.TempDir() good := snapshotTar(t, nil) put(t, dir, "bus.tar", good[:len(good)*2/3], time.Now()) r, err := VerifyFile(dir, "") if err != nil { t.Fatal(err) } if r.Passed || r.Fault == "" { t.Fatalf("a truncated snapshot passed: %+v", r) } } func TestAnArchiveThatDoesNotReadToItsEndFailsEvenWithMatchingChecksums(t *testing.T) { dir := t.TempDir() put(t, dir, "bus.tar", snapshotTar(t, func(m *Manifest, files map[string][]byte) { bad := []byte("not an s2 stream at all") s := &m.Streams[0] files[s.Archive] = bad s.ArchiveSHA256, s.ArchiveBytes = sum(bad), int64(len(bad)) }), time.Now()) r, _ := VerifyFile(dir, "") if r.Passed || !strings.Contains(r.Fault, "does not read") { t.Fatalf("%+v", r) } } func TestTheNewestTarIsVerifiedAndOnlyInsideTheDirectory(t *testing.T) { dir := t.TempDir() now := time.Now() put(t, dir, "old.tar", []byte("junk"), now.Add(-48*time.Hour)) put(t, dir, "bus.tar", snapshotTar(t, nil), now) put(t, dir, "bus.tar.partial", []byte("half"), now.Add(time.Hour)) r, err := VerifyFile(dir, "") if err != nil || r.File != "bus.tar" || !r.Passed { t.Fatalf("%+v %v", r, err) } if r, _ := VerifyFile(dir, "old.tar"); r.Passed || r.File != "old.tar" { t.Errorf("a named file is the one checked: %+v", r) } outside := t.TempDir() put(t, outside, "x.tar", snapshotTar(t, nil), now) for _, name := range []string{"../" + filepath.Base(outside) + "/x.tar", outside + "/x.tar", ".."} { if _, err := VerifyFile(dir, name); err == nil { t.Errorf("%q was read", name) } } os.Symlink(filepath.Join(outside, "x.tar"), filepath.Join(dir, "link.tar")) if _, err := VerifyFile(dir, "link.tar"); err == nil { t.Error("a symbolic link out of the directory was followed") } if _, err := VerifyFile(t.TempDir(), ""); err == nil { t.Error("an empty directory is not a snapshot") } }