导出:zip 用 CreateRaw 写长度/CRC,别再带 data descriptor(真机判整包损坏)
真正的病根(09-16 起每一份站点导出都被拒都是它):Go 的 zip.Writer 默认走流式写入——本地头里 的长度与 CRC 留 0、真值挪到条目尾部的数据描述符里,并在 flag 里置 bit3。**真机的读取器只认 「本地头里就写着长度」的条目**,遇到这种包直接判成损坏,报「备份恢复失败,你的当前数据未被更改」。 铁证(直接读文件头): 站点 Go 写的每一份导出 本地头 flags=0x0008 csize/usize=0/0 → 全部被拒 20260920-fixed-A-数据修复 本地头 flags=0x0000 有长度 → 真机接受(Python 重打包) 真机自己的包 本地头 flags=0x0800 有长度 → 正常(只有 UTF-8 位) 这也解释了之前为什么全都修错了方向:09-20 那次「修悬空引用后就能导入」其实是 Python 重打包顺手把 zip 写法修对了;而 09-29 一整天修的内容问题(活动颜色为 NULL、交通方式重名)都不是拦路虎。 修复:zipBackup 改用 zw.CreateRaw,先把三个文件读进内存算出 CRC32 与长度写进本地头, 不再用会置 bit3 的 zw.CreateHeader。 用例:TestZipBackupWritesNoDataDescriptor —— 直接读第 0 个条目的**本地头**,断言 bit3 为 0、 压缩/原始长度非 0、CRC 正确(变异验证:改回 CreateHeader → 立刻变红,且精确复现 0x0008 + 长度 0)。 真实数据验证:修复后新导出的本地头 flags=0x0000、长度正确。 全量验收 296 PASS / 0 FAIL / 0 SKIP。
This commit is contained in:
+18
-13
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
@@ -1476,19 +1477,23 @@ func zipBackup(sqlitePath string, dst io.Writer) error {
|
||||
}
|
||||
}
|
||||
name := "LifeEasy.sqlite" + suffix
|
||||
if err := func() error {
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w, err := zw.CreateHeader(&zip.FileHeader{Name: name, Method: zip.Store})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, f)
|
||||
return err
|
||||
}(); err != nil {
|
||||
// 必须用 CreateRaw 把长度与 CRC 先写进本地头:zip.Writer 默认走流式写入,会把这三项留成 0
|
||||
// 并在 flag 里置 bit3(data descriptor)、把真值挪到条目尾部——真机的读取器只认本地头里就
|
||||
// 写着长度的条目,遇到这种包会直接判成损坏(实测 2026-09-16 起每一份 Go 写的导出都被拒,
|
||||
// 而唯一被接受过的那份恰好是 Python 重新打包的,它的 flags 是 0x0000)。
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读 %s 失败: %w", name, err)
|
||||
}
|
||||
fh := &zip.FileHeader{Name: name, Method: zip.Store}
|
||||
fh.CRC32 = crc32.ChecksumIEEE(data)
|
||||
fh.CompressedSize64 = uint64(len(data))
|
||||
fh.UncompressedSize64 = uint64(len(data))
|
||||
w, err := zw.CreateRaw(fh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打包 %s 失败: %w", name, err)
|
||||
}
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return fmt.Errorf("打包 %s 失败: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user