导出: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:
tamakyi
2026-09-29 15:04:07 +08:00
parent 9c1413bb39
commit f98d3864b8
2 changed files with 84 additions and 13 deletions
+18 -13
View File
@@ -8,6 +8,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"hash/crc32"
"io" "io"
"math" "math"
"os" "os"
@@ -1476,19 +1477,23 @@ func zipBackup(sqlitePath string, dst io.Writer) error {
} }
} }
name := "LifeEasy.sqlite" + suffix name := "LifeEasy.sqlite" + suffix
if err := func() error { // 必须用 CreateRaw 把长度与 CRC 先写进本地头:zip.Writer 默认走流式写入,会把这三项留成 0
f, err := os.Open(p) // 并在 flag 里置 bit3(data descriptor)、把真值挪到条目尾部——真机的读取器只认本地头里就
if err != nil { // 写着长度的条目,遇到这种包会直接判成损坏(实测 2026-09-16 起每一份 Go 写的导出都被拒,
return err // 而唯一被接受过的那份恰好是 Python 重新打包的,它的 flags 是 0x0000)。
} data, err := os.ReadFile(p)
defer f.Close() if err != nil {
w, err := zw.CreateHeader(&zip.FileHeader{Name: name, Method: zip.Store}) return fmt.Errorf("读 %s 失败: %w", name, err)
if err != nil { }
return err fh := &zip.FileHeader{Name: name, Method: zip.Store}
} fh.CRC32 = crc32.ChecksumIEEE(data)
_, err = io.Copy(w, f) fh.CompressedSize64 = uint64(len(data))
return err fh.UncompressedSize64 = uint64(len(data))
}(); err != nil { 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) return fmt.Errorf("打包 %s 失败: %w", name, err)
} }
} }
+66
View File
@@ -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))
}
}