package rond import ( "archive/zip" "bytes" "encoding/binary" "hash/crc32" "os" "path/filepath" "reflect" "testing" ) // 导出包不许用「数据描述符」——zip 的 flag bit 3。 // // Go 的 zip.Writer 默认走流式写入:本地头里的长度与 CRC 留成 0,真值挪到条目尾部的描述符里, // 并把 bit 3 置上。真机的读取器只认**本地头里就写着长度**的条目,遇到这种包直接判成损坏, // 报「备份恢复失败,你的当前数据未被更改」。 // // 2026-09-16 起每一份 Go 写的导出都被拒,而唯一被真机接受过的那份(20260920-fixed-A)恰好是 // Python 重新打包的、flags 为 0x0000 —— 09-29 一整天「修内容却一直失败」的真凶就是这里。 func TestZipBackupWritesNoDataDescriptor(t *testing.T) { dir := t.TempDir() base := filepath.Join(dir, "LifeEasy.sqlite") content := bytes.Repeat([]byte("TamaBox-zip-check/"), 256) if err := os.WriteFile(base, content, 0o644); err != nil { t.Fatal(err) } var buf bytes.Buffer if err := zipBackup(base, &buf); err != nil { t.Fatalf("打包失败: %v", err) } // 1) 条目名与顺序必须与真机一致;-wal/-shm 缺失时要补 0 字节占位 zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) if err != nil { t.Fatalf("读回打包结果失败: %v", err) } var names []string for _, f := range zr.File { names = append(names, f.Name) } if want := []string{"LifeEasy.sqlite", "LifeEasy.sqlite-wal", "LifeEasy.sqlite-shm"}; !reflect.DeepEqual(names, want) { t.Errorf("条目名/顺序不对:%v,期望 %v", names, want) } // 2) 直接读**第一个条目的本地头**(真机看的就是它) b := buf.Bytes() if len(b) < 30 { t.Fatalf("打包结果太短(%d 字节)", len(b)) } flags := binary.LittleEndian.Uint16(b[6:8]) crc := binary.LittleEndian.Uint32(b[14:18]) csize := binary.LittleEndian.Uint32(b[18:22]) usize := binary.LittleEndian.Uint32(b[22:26]) if flags&0x8 != 0 { t.Errorf("本地头 flags=0x%04x 带了 data descriptor(bit3):真机会判成损坏数据", flags) } if csize != uint32(len(content)) || usize != uint32(len(content)) { t.Errorf("本地头里的长度是 0 或不对:压缩后 %d / 原始 %d,期望 %d", csize, usize, len(content)) } if crc != crc32.ChecksumIEEE(content) { t.Errorf("本地头里的 CRC 不对:0x%08x,期望 0x%08x", crc, crc32.ChecksumIEEE(content)) } }