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's archive/zip ships only Store(0) and Deflate(8) decompressors; any other // method returns "zip: unsupported compression algorithm". Archivers at // non-default settings often write other methods (7-Zip's bzip2, WinZip's // zstd), so the two a pure-Go build can decode are registered here; the ones we // truly cannot decode (Deflate64 / LZMA / XZ / PPMd / encrypted archives) get a // clear error message before extraction instead of surfacing the raw // underlying error to the user. const ( zipMethodStore = 0 zipMethodDeflate = 8 zipMethodDeflate64 = 9 zipMethodBzip2 = 12 zipMethodLZMA = 14 zipMethodZstdPKW = 20 // the number PKWARE originally assigned to 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 encrypted", } func zipMethodName(m uint16) string { if n, ok := zipMethodNames[m]; ok { return n } return "unknown" } // 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("cannot parse archive (must be zip format): %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 archiving leftovers } out = append(out, skillZipEntry{f: f, name: name}) } return out } // zipEntryName returns the entry path as UTF-8. 7-Zip / WinRAR / Explorer on // Windows write Chinese filenames as GBK bytes when the UTF-8 flag is not set, // and Go keeps those bytes verbatim, leaving a name that is neither valid UTF-8 // nor able to pass path validation — fall back to GBK decoding here. 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("archive is encrypted (%s); please upload an unencrypted zip", e.name) } switch e.f.Method { case zipMethodStore, zipMethodDeflate, zipMethodBzip2, zipMethodZstd, zipMethodZstdPKW: default: return fmt.Errorf("archive uses unsupported compression method %s (method %d): %s. "+ "Please repack with \"Store\" or \"Deflate\" (choose Deflate as the compression "+ "method in 7-Zip/WinRAR, use the built-in \"Compress/Send to compressed folder\", "+ "or the zip -r command)", zipMethodName(e.f.Method), e.f.Method, e.name) } } return nil }