Explorar o código

feat(analyzer): 同步 HeaderCrc — 展示 + 整页 XXH32 审计 + 修复 Comment 读取残留

- rHdrInfo 修复 Comment 切片残留 (b[332:844] → b[332:4096], Comment 扩为
  3764B 后旧切片只显示前 512B); 新增 HeaderCrc 字段展示 + 校验结论
- 结构审计新增头页整页 XXH32 复核 (checkHeaderCrc): 与 core.PrepareWrite
  写入侧同源 (复用 core.XXH32); v11 且 CRC==0 旧库跳过, 失配报 "CRC 不符"
- layout_assert 钉死偏移 84, 与 core 断言互锁
- web 头页面板加 Header CRC 行 (值 + 结论)
- 新增 audit_crc_test: 自洽/损坏/旧库跳过/v12 零 CRC 失配/输入不可变
yl32940 hai 3 semanas
pai
achega
5433871a41
Modificáronse 5 ficheiros con 109 adicións e 4 borrados
  1. 34 1
      analyzer/audit.go
  2. 63 0
      analyzer/audit_crc_test.go
  3. 1 0
      analyzer/layout_assert.go
  4. 10 3
      analyzer/main.go
  5. 1 0
      analyzer/web/index.html

+ 34 - 1
analyzer/audit.go

@@ -14,22 +14,51 @@ package main
 // ============================================================================
 
 import (
+	"cmp"
 	"encoding/binary"
 	"fmt"
 	"log"
 	"os"
 	"runtime/debug"
-	"cmp"
 	"slices"
 	"strings"
 	"sync"
 	"sync/atomic"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core"
 )
 
 // ---- HeaderPage 额外字段 ----
 // v11 起头页重排: FreePageStart(56) 与 FreePageEnd(60) 紧邻, 尾指针不再排在 Comment 之后。
 const HDR_FREE_END = 60
 
+// HDR_CRC HeaderCrc 字段偏移 (84): 整页 XXH32, 覆盖除该字段自身外的 4096 字节。
+// 与 core/header_page.go 的编译期断言互相独立地钉住同一布局 —— 任一侧改了都编译失败。
+const HDR_CRC = 84
+
+// checkHeaderCrc 校验头页整页 XXH32。返回 (结论, 说明):
+//   - v11 且 CRC==0: 旧库无校验, ("跳过", "旧 v11 库, 首次写头页自动生成 CRC")
+//   - 匹配: ("OK", "整页 XXH32 与 HeaderCrc 一致")
+//   - 失配: ("CRC 不符", "期望/实际值 —— 头页写坏或文件非 VRTX")
+//
+// b 必须是完整的头页字节 (>= 4096); 计算在副本上进行, 不改动调用方数据。
+// 算法直接复用 core.XXH32, 与引擎写入侧 (PrepareWrite) 同源, 不存在两套实现漂移。
+func checkHeaderCrc(b []byte, version uint32) (string, string) {
+	stored := le32(b[HDR_CRC : HDR_CRC+4])
+	if version == 11 && stored == 0 {
+		return "跳过", "旧 v11 库 (HeaderCrc=0 为 ExtConfig 预留区), 首次写头页自动生成 CRC"
+	}
+	page := make([]byte, 4096)
+	copy(page, b[:4096])
+	for i := HDR_CRC; i < HDR_CRC+4; i++ {
+		page[i] = 0 // CRC 覆盖范围: 除自身外的整页
+	}
+	if got := core.XXH32(page); got != stored {
+		return "CRC 不符", fmt.Sprintf("HeaderCrc=%08x, 实算=%08x —— 头页损坏或文件非 VRTX", stored, got)
+	}
+	return "OK", "整页 XXH32 与 HeaderCrc 一致"
+}
+
 // ---- IndexHead (索引页头) 字段偏移 ----
 const (
 	IX_TYP  = 20 // IndexType
@@ -679,6 +708,8 @@ type AuditResult struct {
 	Reserved     int            `json:"reserved"`  // 页 1: 引擎预留, 从不分配
 	NeverUsed    int            `json:"neverUsed"` // 从未分配过的尾页
 	Free         *FreeChainInfo `json:"free"`
+	HeaderCrc    string         `json:"hdrCrc"`        // 头页整页 XXH32 校验结论 (OK/CRC 不符/跳过)
+	HeaderCrcD   string         `json:"hdrCrcDetail"`  // 校验说明 (期望/实算值等)
 	Tables       []TableAudit   `json:"tables"`
 	SumRows      uint64         `json:"sumRows"`
 	SumHeapDecl  uint64         `json:"sumHeapDecl"`
@@ -735,6 +766,8 @@ func runAuditStructural() (*AuditResult, *walker, map[uint32]bool, []TInfo, bool
 	}
 	R.TotalPages = totalPages()
 	R.Free = rFreeChain()
+	// 头页整页 XXH32 校验 (core.PrepareWrite 写入侧的同源复核)
+	R.HeaderCrc, R.HeaderCrcD = checkHeaderCrc(hdr[:], le32(hdr[4:8]))
 	freeSet := make(map[uint32]bool, R.Free.Total)
 	for _, p := range R.Free.Pages {
 		freeSet[p] = true

+ 63 - 0
analyzer/audit_crc_test.go

@@ -0,0 +1,63 @@
+package main
+
+import (
+	"encoding/binary"
+	"testing"
+
+	"git.ycl.pub/yl32940/TsDB2/DB/core"
+)
+
+// 构造一个 CRC 自洽的头页字节块 (Magic + Version + Comment + 正确 CRC)。
+func buildHeaderPage(t *testing.T, version uint32, comment string) []byte {
+	t.Helper()
+	b := make([]byte, 4096)
+	copy(b[0:4], "VRTX")
+	binary.LittleEndian.PutUint32(b[4:8], version)
+	copy(b[332:], comment)
+	// 与 core.PrepareWrite 同法: 清零 CRC 字段后整页散列
+	for i := 84; i < 88; i++ {
+		b[i] = 0
+	}
+	binary.LittleEndian.PutUint32(b[84:88], core.XXH32(b))
+	return b
+}
+
+func TestCheckHeaderCrc(t *testing.T) {
+	// 正常: CRC 自洽 → OK
+	ok := buildHeaderPage(t, 11, "hello")
+	if v, d := checkHeaderCrc(ok, 11); v != "OK" {
+		t.Fatalf("自洽头页应 OK, 得到 %s (%s)", v, d)
+	}
+
+	// 损坏: 改 Comment 区一字节 → CRC 不符
+	bad := buildHeaderPage(t, 11, "hello")
+	bad[400] ^= 0x01
+	if v, _ := checkHeaderCrc(bad, 11); v != "CRC 不符" {
+		t.Fatalf("损坏头页应 CRC 不符, 得到 %s", v)
+	}
+
+	// 旧库: v11 且 CRC==0 → 跳过
+	legacy := buildHeaderPage(t, 11, "old")
+	for i := 84; i < 88; i++ {
+		legacy[i] = 0
+	}
+	if v, _ := checkHeaderCrc(legacy, 11); v != "跳过" {
+		t.Fatalf("旧 v11 库应跳过, 得到 %s", v)
+	}
+
+	// 未来版本 (v12): CRC==0 也要校验 → 不符
+	if v, _ := checkHeaderCrc(legacy, 12); v != "CRC 不符" {
+		t.Fatalf("v12 零 CRC 应按失配处理, 得到 %s", v)
+	}
+
+	// checkHeaderCrc 不得改动调用方的字节
+	mine := buildHeaderPage(t, 11, "immutable")
+	snapshot := make([]byte, len(mine))
+	copy(snapshot, mine)
+	checkHeaderCrc(mine, 11)
+	for i := range mine {
+		if mine[i] != snapshot[i] {
+			t.Fatalf("checkHeaderCrc 改动了输入字节 @ %d", i)
+		}
+	}
+}

+ 1 - 0
analyzer/layout_assert.go

@@ -53,6 +53,7 @@ var (
 	// HeaderPage 字段偏移 (v11 重排): audit.go / main.go 里所有 hdr[...] 手写的
 	// 读盘偏移。漏改的后果与其它结构体一样 —— 页面照常渲染, 读出来的全是错值。
 	_ [HDR_FREE_END]byte = [unsafe.Offsetof(core.HeaderPage{}.FreePageEnd)]byte{}
+	_ [HDR_CRC]byte      = [unsafe.Offsetof(core.HeaderPage{}.HeaderCrc)]byte{}
 	_ [12]byte           = [unsafe.Offsetof(core.HeaderPage{}.NextFreePage)]byte{}
 	_ [48]byte           = [unsafe.Offsetof(core.HeaderPage{}.TablePage)]byte{}
 	_ [56]byte           = [unsafe.Offsetof(core.HeaderPage{}.FreePageStart)]byte{}

+ 10 - 3
analyzer/main.go

@@ -240,6 +240,9 @@ type HdrInfo struct {
 	WALLast     uint32 `json:"wal1"`
 	TxnID       uint64 `json:"txn"`
 	Comment     string `json:"cm"`
+	HeaderCrc   uint32 `json:"crc"` // 头页声明的整页 XXH32 (旧库为 0)
+	CrcVerdict  string `json:"cv"`  // 校验结论: OK / CRC 不符 / 跳过
+	CrcDetail   string `json:"cd"`  // 校验说明
 
 	// 实算值 (与上面的声明值互校)
 	FreeWalked  int    `json:"fw"` // 实走空闲链长度
@@ -1078,7 +1081,8 @@ func rHdrInfo() *HdrInfo {
 	//   16 CreateTime | 24 ModifyTime | 32 NextTxnID | 40 GlobalFlags
 	//   48 TablePage | 52 TableCount | 56 FreePageStart | 60 FreePageEnd
 	//   64 FreePageCount | 68 FirstWALPage | 72 LastWALPage
-	//   76 ExtConfig[256] | 332 Comment[3764] (占满整页余下空间)
+	//   76 ExtConfigHdr[8] | 84 HeaderCrc | 88 ExtConfig[244]
+	//   332 Comment[3764] (占满整页余下空间)
 	magic := string(b[:4])
 	ver := binary.LittleEndian.Uint32(b[4:8])
 	pgsz := binary.LittleEndian.Uint32(b[8:12])
@@ -1094,7 +1098,9 @@ func rHdrInfo() *HdrInfo {
 	fc := binary.LittleEndian.Uint32(b[64:68])
 	wal0 := binary.LittleEndian.Uint32(b[68:72])
 	wal1 := binary.LittleEndian.Uint32(b[72:76])
-	cm := strings.TrimRight(string(b[332:844]), "\x00")
+	cm := strings.TrimRight(string(b[332:4096]), "\x00")
+	crc := le32(b[HDR_CRC : HDR_CRC+4])
+	crcV, crcD := checkHeaderCrc(b, ver)
 	fcW := rFreeChain()
 	total := totalPages()
 	h := &HdrInfo{
@@ -1103,7 +1109,8 @@ func rHdrInfo() *HdrInfo {
 		CreateTime: time.Unix(0, ct).Format("2006-01-02 15:04:05"),
 		ModifyTime: time.Unix(0, mt).Format("2006-01-02 15:04:05"),
 		FreeStart:  fs, FreeEnd: fe, FreeCount: fc,
-		GlobalFlags: gf, WALFirst: wal0, WALLast: wal1, TxnID: txn, Comment: cm,
+	GlobalFlags: gf, WALFirst: wal0, WALLast: wal1, TxnID: txn, Comment: cm,
+		HeaderCrc: crc, CrcVerdict: crcV, CrcDetail: crcD,
 		FreeWalked: fcW.Total, Pages: total,
 		FreeVerdict: fcW.Verdict, FreeDetail: fcW.Detail,
 	}

+ 1 - 0
analyzer/web/index.html

@@ -629,6 +629,7 @@ function showHeader(){
    ['Create Time',d.ct],['Modify Time',d.mt],
    ['Free Chain Start',d.fs],['Free Chain End',d.fe],['Free Count (declared)',d.fc],['Free Walked (actual)',d.fw],
    ['Global Flags',d.gf],['WAL First Page',d.wal0],['WAL Last Page',d.wal1],['Next Txn ID',d.txn],
+   ['Header CRC',(d.crc?('0x'+d.crc.toString(16).toUpperCase().padStart(8,'0')):'(none)')+' — '+d.cv],
    ['Comment',d.cm||'(empty)']];
   let t='<table class="dtbl"><thead><tr><th style="width:200px">Field</th><th>Value</th></tr></thead><tbody>';
   rows.forEach(r=>{t+='<tr><td style="color:#5b9bd5;font-weight:600">'+r[0]+'</td><td class="mono" style="color:#c8ccd4">'+r[1]+'</td></tr>'});