package regionmedic import ( "bytes" "compress/flate" "encoding/binary" "fmt" "io" "os" ) // Format constants for the 7DTD ".7rg" region file. const ( SectorSize = 4096 RegionChunks = 1024 // 32 x 32 RegionSide = 32 headerSectors = 2 // sector 0 = header, sector 1 = location table locTableOffset = 1 * SectorSize chunkConstVal = 47 ) var ( regionMagic = []byte{0x37, 0x72, 0x67, 0x01} // "7rg" + version 1 chunkMagic = []byte{0x74, 0x74, 0x63, 0x00} // "ttc\0" ) // RegionReport is the outcome of validating a single .7rg image. type RegionReport struct { Path string `json:"path,omitempty"` SizeBytes int64 `json:"size_bytes"` TotalSectors int `json:"total_sectors"` PresentChunks int `json:"present_chunks"` BadChunks []BadChunk `json:"bad_chunks,omitempty"` // Err is set for a fatal, file-level problem (bad magic, non-sector-aligned // size, truncated tables) that makes the image unusable as a region at all. Err string `json:"err,omitempty"` } // Healthy reports whether the region image is structurally sound: no fatal // error and no individually bad chunk. func (r RegionReport) Healthy() bool { return r.Err == "" && len(r.BadChunks) == 0 } // BadChunk describes one structurally invalid chunk within a region. type BadChunk struct { Index int `json:"index"` LocalX int `json:"local_x"` LocalZ int `json:"local_z"` Reason string `json:"reason"` } // Reasons reported on BadChunk.Reason. const ( ReasonWrongHeader = "wrong chunk header" // the "Wrong chunk header!" trigger ReasonBadConst = "bad format constant" ReasonOutOfBounds = "sector out of bounds" ReasonOverlap = "overlapping sectors" ReasonShortRead = "blob truncated" ReasonBadLength = "implausible block length" ReasonDeflate = "deflate error" ReasonCoordMismatch = "embedded coord mismatch" ) // ValidateRegionFile reads path and validates it. deep=true additionally // raw-inflates every present chunk and checks its embedded coordinates. func ValidateRegionFile(path string, deep bool) RegionReport { data, err := os.ReadFile(path) if err != nil { return RegionReport{Path: path, Err: fmt.Sprintf("read: %v", err)} } r := ValidateRegionBytes(data, deep) r.Path = path return r } // ValidateRegionBytes validates a .7rg image held in memory. // // Structural checks (always): sector-aligned size; "7rg" magic; empty header // remainder; every present chunk's location entry in-bounds and non-overlapping; // each chunk blob's "ttc\0" magic + format constant + plausible block length. // deep=true also inflates each chunk and verifies the decompressed payload's // chunkX/chunkZ match the table index. func ValidateRegionBytes(data []byte, deep bool) RegionReport { rep := RegionReport{SizeBytes: int64(len(data))} if len(data) == 0 || len(data)%SectorSize != 0 { rep.Err = fmt.Sprintf("size %d is not a positive multiple of %d", len(data), SectorSize) return rep } total := len(data) / SectorSize rep.TotalSectors = total if total < headerSectors { rep.Err = "file shorter than header+location table" return rep } if !bytes.Equal(data[0:4], regionMagic) { rep.Err = fmt.Sprintf("bad magic % x (want % x)", data[0:4], regionMagic) return rep } // Header remainder must be empty (cheap corruption tripwire). for _, b := range data[4:SectorSize] { if b != 0 { rep.Err = "non-zero bytes in header block" return rep } } used := make([]bool, total) used[0] = true // header used[1] = true // location table for idx := 0; idx < RegionChunks; idx++ { e := data[locTableOffset+idx*4 : locTableOffset+idx*4+4] off := int(e[0]) | int(e[1])<<8 | int(e[2])<<16 cnt := int(e[3]) if off == 0 && cnt == 0 { continue // absent chunk — legitimate } rep.PresentChunks++ lx, lz := idx%RegionSide, idx/RegionSide bad := func(reason string) { rep.BadChunks = append(rep.BadChunks, BadChunk{Index: idx, LocalX: lx, LocalZ: lz, Reason: reason}) } if off < headerSectors || cnt < 1 || off+cnt > total { bad(ReasonOutOfBounds) continue } overlap := false for s := off; s < off+cnt; s++ { if used[s] { overlap = true break } } if overlap { bad(ReasonOverlap) continue } for s := off; s < off+cnt; s++ { used[s] = true } base := off * SectorSize capacity := cnt * SectorSize // Need at least the 24-byte blob header. if capacity < 24 { bad(ReasonShortRead) continue } blockLen := int(binary.LittleEndian.Uint32(data[base : base+4])) // blob occupies 16 + blockLen bytes; must fit the allocated sectors. if blockLen < 8 || 16+blockLen > capacity { bad(ReasonBadLength) continue } if !bytes.Equal(data[base+16:base+20], chunkMagic) { bad(ReasonWrongHeader) // <-- the "Wrong chunk header!" failure continue } if binary.LittleEndian.Uint32(data[base+20:base+24]) != chunkConstVal { bad(ReasonBadConst) continue } if deep { // Deep check: confirm the chunk's raw DEFLATE stream actually // inflates. A torn chunk whose "ttc" header happens to survive but // whose compressed body is garbage is caught here. The DEFLATE // stream is exactly blockLen-8 bytes starting at base+24. // // NOTE: we deliberately do NOT compare the decompressed payload's // embedded coordinates. Real pregenerated (RWG-baked) regions store // those in a layout that does not match a naive global-coord // assumption, so a coord comparison false-positives on perfectly // healthy regions — which would make every backup look corrupt and // block healing. The "ttc" magic check above is exactly what 7DTD // itself validates for "Wrong chunk header!", so structural validity // is the trustworthy signal. compLen := blockLen - 8 start := base + 24 if start+compLen > len(data) { bad(ReasonShortRead) continue } if _, err := io.ReadAll(flate.NewReader(bytes.NewReader(data[start : start+compLen]))); err != nil { bad(ReasonDeflate) continue } } } return rep }