mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 01:03:50 +08:00
- 补上 bzip2 / Zstandard 解压器(标准库 + 已有的 klauspost/compress,不引新依赖), 不再对压缩软件非默认档位打出的包抛 "zip: unsupported compression"; Deflate64/LZMA/XZ/PPMd 等真解不了的方式与加密包改为解压前给出中文提示, 点名文件、压缩方式与重新打包的办法 - 技能内文件路径校验从 [A-Za-z0-9-_./] 的 ASCII 白名单改为 Unicode 黑名单: 放行各语言文件名与空格,仍拒绝控制字符、非法 UTF-8、零宽与双向控制字符、 \ % # ? * : " < > | 及 .. / 绝对路径 / 空路径段,防目录穿越行为不变 - 技能名放开非 ASCII 字母数字(中文可直接作技能名),ASCII 仍守 agentskills.io 规范,空格、点与路径分隔符照旧拒绝 - Windows 压缩软件按 GBK 写的文件名兜底解码后再校验;frontmatter 的 name: "中文技能" 去引号 - 新增 8 个测试:zstd 包、GBK 包、不支持的压缩方式、加密包、目录穿越、名称与 路径校验
131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
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
|
||
}
|