tar.NewReader传入gzip.Reader会错乱,因gzip.NewReader预读至少10字节破坏tar header起始位置;必须用未被预读的原始io.Reader(如*os.File或bytes.Reader)创建gzip.Reader,再将其直接传入tar.NewReader。

tar.NewReader 传入 gzip.Reader 就错乱?必须控制预读时机
直接把 gzip.NewReader(r) 的返回值塞给 tar.NewReader() 是常见错误,解压后文件名乱码、权限丢失、甚至读到空 Header。根本原因是 gzip.NewReader 内部会预读至少 10 字节识别 gzip header,破坏了 tar 流的起始偏移——tar.NewReader 要求输入流严格从 tar header 的第 0 字节开始。
正确做法是:用原始可重放的 io.Reader(如 *os.File 或 bytes.Reader)创建 gzip.Reader,再把这个 *gzip.Reader 直接传给 tar.NewReader,中间不取字段、不二次包装、不调 Peek 或 Read。
- HTTP 响应体(
http.Response.Body)不可重放,必须先用bytes.Buffer缓存前几个字节做 magic check,再构造gzip.Reader - 若源是
*os.File,记得在gzip.NewReader前调f.Seek(0, io.SeekStart)确保位置归零 - 别手动调
tr.Next()后再读数据——每次Next()都隐式跳过 padding,后续io.CopyN才能对齐
解压时路径遍历攻击怎么拦?filepath.Clean 不够用
tar.Header.Name 是用户可控输入,filepath.Clean 在 Windows 上对 ".. oo" 这类空格绕过无效,也无法拦截 "C:\windows\system32" 这种盘符路径。必须分步校验。
- 先用
filepath.FromSlash(header.Name)统一分隔符为本地格式 - 再用
filepath.Clean归一化,得到cleaned - 检查
cleaned是否以目标根目录(如"tmp")开头,且!strings.Contains(cleaned, "..") - 拒绝空名、以
/或\开头、含":"(Windows 盘符)的Name
大文件解压内存暴涨?永远别用 header.Size 做 make
看到 header.Size == 2147483647 就 make([]byte, header.Size),等于主动申请 2GB 内存——OOM 是必然结果。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 永远用
io.CopyN(dst, tr, header.Size),它内部按 32KB 分块读写,内存恒定 - 每个文件打开后立刻
defer f.Close(),别等整个 tar 流结束 - 创建文件用
os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, header.FileInfo().Mode()),不自动建父目录 - 不要自己算 512 字节对齐:tar.Reader 的
Next()已处理 padding,手动Read才需对齐
限速或中断解压?别碰 Size 字段做预分配
想限速就套 io.LimitReader(tr, n),想支持中断就用自定义 io.Reader 实现带计数的 Read 方法——但绝不能靠 header.Size 预估总大小再分配缓冲区。
因为 header.Size 只表示当前文件内容长度,不包括 tar padding、后续文件头、或 gzip trailer;而且流式场景下你根本不知道总共有多少文件、每个文件多大。预分配只会让逻辑变脆、内存失控、中断点难定位。
真正容易被忽略的是:HTTP body 这类一次性流,一旦 gzip.NewReader 预读失败,整个 stream 就废了,没机会重试。所以 magic check 和 buffer 缓存必须前置,不能省。

















