panel — open-source game server manager (public release)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:19:43 -07:00
commit 658bda1d24
2160 changed files with 300413 additions and 0 deletions
+433
View File
@@ -0,0 +1,433 @@
package regionmedic
import (
"archive/tar"
"bytes"
"compress/flate"
"compress/gzip"
"crypto/sha256"
"encoding/binary"
"os"
"path/filepath"
"testing"
"time"
)
// --- fixture builders -------------------------------------------------------
func deflateBytes(b []byte) []byte {
var buf bytes.Buffer
w, _ := flate.NewWriter(&buf, flate.DefaultCompression)
_, _ = w.Write(b)
_ = w.Close()
return buf.Bytes()
}
// buildRegion synthesizes a valid .7rg image for the given region coordinate
// with the listed chunk indices present. Each chunk's decompressed payload
// carries its correct GLOBAL coords so deep validation passes.
func buildRegion(region RegionCoord, present []int) []byte {
header := make([]byte, SectorSize)
copy(header, regionMagic)
loctable := make([]byte, SectorSize)
var data []byte
nextSector := headerSectors // first data sector index = 2
for _, idx := range present {
lx, lz := idx%RegionSide, idx/RegionSide
gx := region.X*RegionSide + lx
gz := region.Z*RegionSide + lz
payload := make([]byte, 16)
binary.LittleEndian.PutUint32(payload[0:4], uint32(int32(gx)))
binary.LittleEndian.PutUint32(payload[8:12], uint32(int32(gz)))
payload[12] = 0xAB // arbitrary body
comp := deflateBytes(payload)
blockLen := 8 + len(comp)
need := (24 + len(comp) + SectorSize - 1) / SectorSize
blob := make([]byte, need*SectorSize)
binary.LittleEndian.PutUint32(blob[0:4], uint32(blockLen))
copy(blob[16:20], chunkMagic)
binary.LittleEndian.PutUint32(blob[20:24], uint32(chunkConstVal))
copy(blob[24:], comp)
off := nextSector
e := loctable[idx*4 : idx*4+4]
e[0] = byte(off & 0xff)
e[1] = byte((off >> 8) & 0xff)
e[2] = byte((off >> 16) & 0xff)
e[3] = byte(need)
data = append(data, blob...)
nextSector += need
}
out := append(append(append([]byte{}, header...), loctable...), data...)
return out
}
// chunkBlobOffset returns the file byte offset of the "ttc" magic for a present
// chunk index — i.e. the exact byte the corruption recipe overwrites.
func chunkMagicOffset(data []byte, idx int) int {
e := data[locTableOffset+idx*4 : locTableOffset+idx*4+4]
off := int(e[0]) | int(e[1])<<8 | int(e[2])<<16
return off*SectorSize + 16
}
// --- validation tests -------------------------------------------------------
func TestValidateGoodRegionOrigin(t *testing.T) {
data := buildRegion(RegionCoord{0, 0}, []int{0, 1, 33, 1023})
rep := ValidateRegionBytes(data, true)
if !rep.Healthy() {
t.Fatalf("expected healthy, got err=%q bad=%+v", rep.Err, rep.BadChunks)
}
if rep.PresentChunks != 4 {
t.Fatalf("present=%d want 4", rep.PresentChunks)
}
}
func TestValidateGoodRegionNegative(t *testing.T) {
// Region r.-2.0 exercises floored-modulo coord checking for negative globals.
data := buildRegion(RegionCoord{-2, 0}, []int{0, 1, 31, 500})
rep := ValidateRegionBytes(data, true)
if !rep.Healthy() {
t.Fatalf("expected healthy negative region, got err=%q bad=%+v", rep.Err, rep.BadChunks)
}
}
func TestWrongChunkHeaderDetected(t *testing.T) {
data := buildRegion(RegionCoord{0, 0}, []int{0, 5})
// Faithful corruption: clobber the "ttc" magic of chunk index 5.
off := chunkMagicOffset(data, 5)
data[off] = 0x00 // 't' -> 0x00
rep := ValidateRegionBytes(data, true)
if rep.Healthy() {
t.Fatal("expected corruption to be detected")
}
if len(rep.BadChunks) != 1 || rep.BadChunks[0].Index != 5 || rep.BadChunks[0].Reason != ReasonWrongHeader {
t.Fatalf("want one wrong-header bad chunk idx5, got %+v", rep.BadChunks)
}
// The other chunk (0) must still be fine and counted present.
if rep.PresentChunks != 2 {
t.Fatalf("present=%d want 2", rep.PresentChunks)
}
}
func TestBadMagic(t *testing.T) {
data := buildRegion(RegionCoord{0, 0}, []int{0})
data[1] = 'X'
if ValidateRegionBytes(data, false).Err == "" {
t.Fatal("expected fatal err on bad magic")
}
}
func TestNonSectorAligned(t *testing.T) {
if ValidateRegionBytes(make([]byte, 5000), false).Err == "" {
t.Fatal("expected fatal err on non-aligned size")
}
}
func TestOutOfBoundsEntry(t *testing.T) {
data := buildRegion(RegionCoord{0, 0}, []int{0})
// Point chunk index 1's entry far past EOF.
e := data[locTableOffset+1*4 : locTableOffset+1*4+4]
e[0], e[1], e[2], e[3] = 0xFF, 0xFF, 0x00, 1 // offset 0xFFFF sectors
rep := ValidateRegionBytes(data, false)
if rep.Healthy() || len(rep.BadChunks) != 1 || rep.BadChunks[0].Reason != ReasonOutOfBounds {
t.Fatalf("want out-of-bounds bad chunk, got err=%q bad=%+v", rep.Err, rep.BadChunks)
}
}
func TestOverlapDetected(t *testing.T) {
data := buildRegion(RegionCoord{0, 0}, []int{0, 1})
// Make chunk 1's entry point at chunk 0's sector (overlap).
e0 := data[locTableOffset : locTableOffset+4]
e1 := data[locTableOffset+4 : locTableOffset+8]
copy(e1, e0)
rep := ValidateRegionBytes(data, false)
foundOverlap := false
for _, bc := range rep.BadChunks {
if bc.Reason == ReasonOverlap {
foundOverlap = true
}
}
if !foundOverlap {
t.Fatalf("expected overlap detection, got %+v", rep.BadChunks)
}
}
// --- localize tests ---------------------------------------------------------
func TestParseErrorBackup(t *testing.T) {
cases := []struct {
name string
cx, cz int
ok bool
}{
{"error_backup_-28_12.comp.bak", -28, 12, true},
{"error_backup_-29_-13.uncomp.bak", -29, -13, true},
{"error_backup_5_7.comp.bak", 5, 7, true},
{"r.0.0.7rg", 0, 0, false},
{"error_backup_5_7.txt", 0, 0, false},
}
for _, c := range cases {
cx, cz, ok := ParseErrorBackup(c.name)
if ok != c.ok || (ok && (cx != c.cx || cz != c.cz)) {
t.Errorf("%s -> (%d,%d,%v) want (%d,%d,%v)", c.name, cx, cz, ok, c.cx, c.cz, c.ok)
}
}
}
func TestParseRegionFileName(t *testing.T) {
cases := []struct {
name string
rc RegionCoord
ok bool
}{
{"r.-2.0.7rg", RegionCoord{-2, 0}, true},
{"r.10.-5.7rg", RegionCoord{10, -5}, true},
{"r.0.0.7rg", RegionCoord{0, 0}, true},
{"error_backup_5_7.comp.bak", RegionCoord{}, false},
}
for _, c := range cases {
rc, ok := ParseRegionFileName(c.name)
if ok != c.ok || (ok && rc != c.rc) {
t.Errorf("%s -> (%v,%v) want (%v,%v)", c.name, rc, ok, c.rc, c.ok)
}
}
}
func TestRegionForChunk(t *testing.T) {
cases := []struct {
cx, cz int
rc RegionCoord
}{
{0, 0, RegionCoord{0, 0}},
{31, 31, RegionCoord{0, 0}},
{-1, -1, RegionCoord{-1, -1}},
{-32, 0, RegionCoord{-1, 0}},
{-33, 12, RegionCoord{-2, 0}},
{-64, 0, RegionCoord{-2, 0}},
}
for _, c := range cases {
if got := RegionForChunk(c.cx, c.cz); got != c.rc {
t.Errorf("RegionForChunk(%d,%d)=%v want %v", c.cx, c.cz, got, c.rc)
}
}
}
// --- scan test --------------------------------------------------------------
func TestScanRegionDir(t *testing.T) {
dir := t.TempDir()
// 3 salvage files in r.-2.0 (chunks -33..-64 / 0..31), 1 in r.2.1.
writeEmpty(t, dir, "error_backup_-33_12.comp.bak")
writeEmpty(t, dir, "error_backup_-33_12.uncomp.bak") // same chunk, dedup
writeEmpty(t, dir, "error_backup_-40_20.comp.bak")
writeEmpty(t, dir, "error_backup_70_40.comp.bak") // region r.2.1
// a live region file for r.-2.0
os.WriteFile(filepath.Join(dir, "r.-2.0.7rg"), buildRegion(RegionCoord{-2, 0}, []int{0}), 0o644)
scan, err := ScanRegionDir(dir)
if err != nil {
t.Fatal(err)
}
if scan.ErrorBackups != 4 {
t.Errorf("error_backups=%d want 4", scan.ErrorBackups)
}
if len(scan.Affected) != 2 {
t.Fatalf("affected=%d want 2", len(scan.Affected))
}
top := scan.Affected[0]
if top.Region != (RegionCoord{-2, 0}) || top.ErrorBackups != 3 {
t.Errorf("top affected=%v files=%d want r.-2.0 files=3", top.Region, top.ErrorBackups)
}
if len(top.Chunks) != 2 {
t.Errorf("distinct chunks=%d want 2", len(top.Chunks))
}
if !top.FilePresent {
t.Error("expected r.-2.0 live file present")
}
}
// --- backup extract + find-last-good ---------------------------------------
const navSave = "Saves/Navezgane/MyGame/Region"
// writeBackupTar writes a tarball whose Navezgane Region/<regionName> entry is
// regionBytes. If decoyBytes is non-nil it ALSO writes a second world
// (West Apeeni Mountains/MapRender) with the SAME region filename but different
// bytes — to prove world-qualified extraction picks the right one.
func writeBackupTar(t *testing.T, dir, name, regionName string, regionBytes, decoyBytes []byte) string {
t.Helper()
p := filepath.Join(dir, name)
f, err := os.Create(p)
if err != nil {
t.Fatal(err)
}
defer f.Close()
gw := gzip.NewWriter(f)
tw := tar.NewWriter(gw)
add := func(entry string, b []byte) {
tw.WriteHeader(&tar.Header{Name: entry, Mode: 0o644, Size: int64(len(b)), Typeflag: tar.TypeReg})
tw.Write(b)
}
if decoyBytes != nil {
// Decoy world FIRST in tar order, so a naive bare-name match would grab it.
add(".local/share/7DaysToDie/Saves/West Apeeni Mountains/MapRender/Region/"+regionName, decoyBytes)
}
if regionBytes != nil {
add(".local/share/7DaysToDie/"+navSave+"/"+regionName, regionBytes)
}
add("serverconfig.xml", []byte("xml"))
tw.Close()
gw.Close()
return p
}
func TestExtractRegionFromBackup(t *testing.T) {
dir := t.TempDir()
region := buildRegion(RegionCoord{0, 0}, []int{0, 1})
decoy := buildRegion(RegionCoord{0, 0}, []int{0, 1, 2, 3}) // different bytes
p := writeBackupTar(t, dir, "20260101-120000-bkp_aaaa.tar.gz", "r.0.0.7rg", region, decoy)
// World-qualified entry must return the Navezgane copy, NOT the decoy.
got, err := ExtractRegionFromBackup(p, navSave+"/r.0.0.7rg")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, region) {
t.Fatal("world-qualified extract returned wrong world's region")
}
if bytes.Equal(got, decoy) {
t.Fatal("extracted the decoy world's region — disambiguation failed")
}
if _, err := ExtractRegionFromBackup(p, navSave+"/r.9.9.7rg"); err == nil {
t.Fatal("expected not-found error for absent region")
}
}
func TestSaveRelEntry(t *testing.T) {
got := SaveRelEntry("/var/lib/docker/volumes/x/_data/.local/share/7DaysToDie/Saves/Navezgane/MyGame/Region", "r.-2.0.7rg")
if got != "Saves/Navezgane/MyGame/Region/r.-2.0.7rg" {
t.Fatalf("got %q", got)
}
}
func TestFindLastGoodRegion(t *testing.T) {
dir := t.TempDir()
region := RegionCoord{-2, 0}
good := buildRegion(region, []int{0, 1, 2})
corrupt := buildRegion(region, []int{0, 1, 2})
corrupt[chunkMagicOffset(corrupt, 1)] = 0x00 // break chunk 1
// oldest clean, middle clean, newest corrupt (each also carries a decoy world)
writeBackupTar(t, dir, "20260101-010000-bkp_old.tar.gz", region.FileName(), good, good)
writeBackupTar(t, dir, "20260102-010000-bkp_mid.tar.gz", region.FileName(), good, good)
writeBackupTar(t, dir, "20260103-010000-bkp_new.tar.gz", region.FileName(), corrupt, good)
refs, err := ListPanelBackups(dir)
if err != nil {
t.Fatal(err)
}
if len(refs) != 3 {
t.Fatalf("refs=%d want 3", len(refs))
}
// newest first
if !refs[0].CreatedAt.After(refs[1].CreatedAt) {
t.Fatal("backups not sorted newest-first")
}
best, evaluated, err := FindLastGoodRegion(refs, navSave+"/"+region.FileName(), true, time.Time{})
if err != nil {
t.Fatal(err)
}
if !best.Found || !best.Clean() {
t.Fatalf("expected a clean best, got found=%v clean=%v", best.Found, best.Clean())
}
// Best must be the middle (newest CLEAN), not the corrupt newest.
if best.Backup.ID != "bkp_mid" {
t.Fatalf("best=%s want bkp_mid (newest clean)", best.Backup.ID)
}
if len(evaluated) != 3 {
t.Fatalf("evaluated=%d want 3", len(evaluated))
}
}
// --- full plan + apply heal -------------------------------------------------
func TestPlanAndApplyHeal(t *testing.T) {
base := t.TempDir()
// Realistic region dir so SaveRelEntry yields a world-qualified suffix.
regionDir := filepath.Join(base, ".local/share/7DaysToDie", navSave)
backupDir := filepath.Join(base, "backups")
os.MkdirAll(regionDir, 0o755)
os.MkdirAll(backupDir, 0o755)
region := RegionCoord{-2, 0}
clean := buildRegion(region, []int{0, 1, 2, 33})
// Live (corrupt) region file: break one chunk header.
corrupt := append([]byte{}, clean...)
corrupt[chunkMagicOffset(corrupt, 2)] = 0x00
liveFile := filepath.Join(regionDir, region.FileName())
if err := os.WriteFile(liveFile, corrupt, 0o644); err != nil {
t.Fatal(err)
}
// Salvage files for this region.
writeEmpty(t, regionDir, "error_backup_-64_0.comp.bak")
writeEmpty(t, regionDir, "error_backup_-64_0.uncomp.bak")
// A clean backup containing the good region (plus a decoy world that must
// NOT be chosen).
decoy := buildRegion(RegionCoord{99, 99}, []int{0})
writeBackupTar(t, backupDir, "20260101-120000-bkp_clean.tar.gz", region.FileName(), clean, decoy)
refs, _ := ListPanelBackups(backupDir)
plan, err := PlanHeal(regionDir, region, refs, true, time.Time{})
if err != nil {
t.Fatal(err)
}
if plan.LiveReport.Healthy() {
t.Fatal("expected live report to flag corruption")
}
if !plan.Ready() {
t.Fatalf("plan not ready: source found=%v clean=%v file=%q", plan.Source.Found, plan.Source.Clean(), plan.RegionFile)
}
if len(plan.ErrorBackups) != 2 {
t.Fatalf("error backups in plan=%d want 2", len(plan.ErrorBackups))
}
snapDir := filepath.Join(base, "snap")
qDir := filepath.Join(base, "quarantine")
res, err := ApplyHeal(plan, snapDir, qDir)
if err != nil {
t.Fatal(err)
}
// Live file now equals the clean bytes.
healed, _ := os.ReadFile(liveFile)
if sha256.Sum256(healed) != sha256.Sum256(clean) {
t.Fatal("healed live file does not match clean source")
}
if !ValidateRegionFile(liveFile, true).Healthy() {
t.Fatal("healed file failed revalidation")
}
// Snapshot holds the corrupt original.
snap, _ := os.ReadFile(res.SnapshotPath)
if sha256.Sum256(snap) != sha256.Sum256(corrupt) {
t.Fatal("snapshot does not match the corrupt original")
}
// Salvage files moved out of the region dir.
if _, err := os.Stat(filepath.Join(regionDir, "error_backup_-64_0.comp.bak")); !os.IsNotExist(err) {
t.Fatal("error_backup still present in region dir after heal")
}
if len(res.QuarantinedFiles) != 2 {
t.Fatalf("quarantined=%d want 2", len(res.QuarantinedFiles))
}
}
func writeEmpty(t *testing.T, dir, name string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, name), []byte{}, 0o644); err != nil {
t.Fatal(err)
}
}