fix(skills): 修复技能压缩包上传失败与中文命名被判非法
- 补上 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 包、不支持的压缩方式、加密包、目录穿越、名称与 路径校验
This commit is contained in:
1 parent
99fb78df1c
commit
338e77c064
5 files changed
+483
-51
No files matched your search
@@ -0,0 +1,130 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user