package server import ( "archive/zip" "bytes" "compress/bzip2" "fmt" "io" "path" "strings" "unicode/utf8" "github.com/klauspost/compress/zstd" "golang.org/x/text/encoding/simplifiedchinese" ) // Go 的 archive/zip 只内置 Store(0) 和 Deflate(8) 两种解压器,遇到别的方法会返回 // "zip: unsupported compression algorithm"。压缩软件在非默认档位下经常写出别的方法 // (7-Zip 的 bzip2、WinZip 的 zstd),所以这里把纯 Go 能解的两种补上;真的解不了的 // (Deflate64 / LZMA / XZ / PPMd / 加密包) 在解压前就报出中文提示,而不是把底层 // 错误原样甩给用户。 const ( zipMethodStore = 0 zipMethodDeflate = 8 zipMethodDeflate64 = 9 zipMethodBzip2 = 12 zipMethodLZMA = 14 zipMethodZstdPKW = 20 // PKWARE 早期给 zstd 分配的编号 zipMethodZstd = 93 zipMethodXZ = 95 zipMethodJPEG = 96 zipMethodWavPack = 97 zipMethodPPMd = 98 zipMethodAES = 99 ) var zipMethodNames = map[uint16]string{ zipMethodStore: "Store", zipMethodDeflate: "Deflate", zipMethodDeflate64: "Deflate64", zipMethodBzip2: "bzip2", zipMethodLZMA: "LZMA", zipMethodZstdPKW: "Zstandard", zipMethodZstd: "Zstandard", zipMethodXZ: "XZ", zipMethodJPEG: "JPEG", zipMethodWavPack: "WavPack", zipMethodPPMd: "PPMd", zipMethodAES: "AES 加密", } func zipMethodName(m uint16) string { if n, ok := zipMethodNames[m]; ok { return n } return "未知" } // newSkillZipReader parses an uploaded archive and registers the extra decompressors // we can support beyond the stdlib's Store/Deflate. func newSkillZipReader(buf []byte) (*zip.Reader, error) { zr, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf))) if err != nil { return nil, fmt.Errorf("无法解析压缩包(需为 zip 格式):%w", err) } zr.RegisterDecompressor(zipMethodBzip2, func(r io.Reader) io.ReadCloser { return io.NopCloser(bzip2.NewReader(r)) }) zdec := zstd.ZipDecompressor(zstd.WithDecoderConcurrency(1)) zr.RegisterDecompressor(zipMethodZstd, zdec) zr.RegisterDecompressor(zipMethodZstdPKW, zdec) return zr, nil } // skillZipEntry pairs a zip entry with its decoded (UTF-8) name — f.Name may hold // raw GBK bytes, see zipEntryName. type skillZipEntry struct { f *zip.File name string } // skillZipEntries lists the archive's real files (no directory entries, no archiver // junk) with their names decoded to UTF-8. func skillZipEntries(zr *zip.Reader) []skillZipEntry { out := make([]skillZipEntry, 0, len(zr.File)) for _, f := range zr.File { if f.FileInfo().IsDir() { continue } name := zipEntryName(f) if strings.HasPrefix(name, "__MACOSX/") || strings.Contains(name, "/__MACOSX/") || path.Base(name) == ".DS_Store" { continue // macOS 打包残留 } out = append(out, skillZipEntry{f: f, name: name}) } return out } // zipEntryName returns the entry path as UTF-8. Windows 上的 7-Zip / WinRAR / 资源管理器 // 在不置 UTF-8 标志位时会把中文文件名按 GBK 写进 zip,Go 原样保留这些字节,于是名字 // 既不是合法 UTF-8 也过不了路径校验 —— 这里按 GBK 兜底解码。 func zipEntryName(f *zip.File) string { if utf8.ValidString(f.Name) { return f.Name } if dec, err := simplifiedchinese.GBK.NewDecoder().String(f.Name); err == nil && utf8.ValidString(dec) { return dec } return f.Name } // checkSkillZipMethods rejects archives we cannot extract, naming the offending // entry and method instead of letting f.Open() fail with an opaque English error. func checkSkillZipMethods(entries []skillZipEntry) error { for _, e := range entries { if e.f.Flags&0x1 != 0 || e.f.Method == zipMethodAES { return fmt.Errorf("压缩包已加密(%s),请上传未加密的 zip", e.name) } switch e.f.Method { case zipMethodStore, zipMethodDeflate, zipMethodBzip2, zipMethodZstd, zipMethodZstdPKW: default: return fmt.Errorf("压缩包使用了不支持的压缩方式 %s(method %d):%s。"+ "请改用「存储」或「Deflate」重新打包(7-Zip/WinRAR 的压缩方式选 Deflate,"+ "或直接用系统自带的“压缩/发送到压缩文件夹”、命令行 zip -r)", zipMethodName(e.f.Method), e.f.Method, e.name) } } return nil }