151 lines
3.8 KiB
Go
151 lines
3.8 KiB
Go
package releasearchive
|
|
|
|
import (
|
|
"bytes"
|
|
"debug/buildinfo"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
// Verify 检查构建标记及本机构建路径,不将被禁止的路径写入发布清单。
|
|
func Verify(source string, binaries, forbiddenPaths []string) error {
|
|
if source == "" || len(binaries) == 0 || len(forbiddenPaths) == 0 {
|
|
return errors.New("发布检查必须提供源目录、二进制文件和本机路径")
|
|
}
|
|
info, err := os.Lstat(source)
|
|
if err != nil {
|
|
return fmt.Errorf("检查发布源目录失败:%w", err)
|
|
}
|
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
return errors.New("发布源路径必须是普通目录")
|
|
}
|
|
patterns := pathPatterns(forbiddenPaths)
|
|
if len(patterns) == 0 {
|
|
return errors.New("本机路径不能为空")
|
|
}
|
|
if err := filepath.WalkDir(source, func(path string, entry fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() {
|
|
return nil
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return errors.New("发布检查不接受符号链接或特殊文件")
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
if err := scanPaths(file, patterns); err != nil {
|
|
relative, _ := filepath.Rel(source, path)
|
|
return fmt.Errorf("发布文件 %s 检查失败:%w", relative, err)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
for _, name := range binaries {
|
|
if !filepath.IsLocal(name) {
|
|
return errors.New("二进制文件必须位于发布目录内")
|
|
}
|
|
info, err := buildinfo.ReadFile(filepath.Join(source, name))
|
|
if err != nil {
|
|
return fmt.Errorf("读取 %s 的 Go 构建信息失败:%w", name, err)
|
|
}
|
|
trimmed := false
|
|
for _, setting := range info.Settings {
|
|
if setting.Key == "-trimpath" && setting.Value == "true" {
|
|
trimmed = true
|
|
}
|
|
}
|
|
if !trimmed {
|
|
return fmt.Errorf("二进制文件 %s 未启用 -trimpath", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func pathPatterns(paths []string) [][]byte {
|
|
var patterns [][]byte
|
|
seen := make(map[string]bool)
|
|
for _, path := range paths {
|
|
if strings.TrimSpace(path) == "" {
|
|
continue
|
|
}
|
|
slash := strings.ReplaceAll(path, `\`, "/")
|
|
backslash := strings.ReplaceAll(slash, "/", `\`)
|
|
for _, variant := range []string{slash, backslash, strings.ReplaceAll(backslash, `\`, `\\`), strings.ReplaceAll(slash, "/", `\/`)} {
|
|
utf8Pattern := []byte(variant)
|
|
lowerASCII(utf8Pattern)
|
|
if seen[string(utf8Pattern)] {
|
|
continue
|
|
}
|
|
seen[string(utf8Pattern)] = true
|
|
patterns = append(patterns, utf8Pattern)
|
|
var little, big []byte
|
|
for _, unit := range utf16.Encode([]rune(variant)) {
|
|
little = append(little, byte(unit), byte(unit>>8))
|
|
big = append(big, byte(unit>>8), byte(unit))
|
|
}
|
|
lowerASCII(little)
|
|
lowerASCII(big)
|
|
patterns = append(patterns, little, big)
|
|
}
|
|
}
|
|
return patterns
|
|
}
|
|
|
|
func scanPaths(reader io.Reader, patterns [][]byte) error {
|
|
longest := 0
|
|
for _, pattern := range patterns {
|
|
if len(pattern) > longest {
|
|
longest = len(pattern)
|
|
}
|
|
}
|
|
if longest == 0 {
|
|
return errors.New("路径检查模式不能为空")
|
|
}
|
|
const chunkSize = 64 * 1024
|
|
buffer := make([]byte, chunkSize+longest-1)
|
|
retained := 0
|
|
for {
|
|
n, err := reader.Read(buffer[retained:])
|
|
data := buffer[:retained+n]
|
|
lowerASCII(data)
|
|
for _, pattern := range patterns {
|
|
if bytes.Contains(data, pattern) {
|
|
return errors.New("发现本机构建路径,拒绝发布")
|
|
}
|
|
}
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 保留跨读取边界的前缀,避免路径被分块后漏检。
|
|
retained = min(longest-1, len(data))
|
|
copy(buffer, data[len(data)-retained:])
|
|
}
|
|
}
|
|
|
|
func lowerASCII(data []byte) {
|
|
for i, value := range data {
|
|
if value >= 'A' && value <= 'Z' {
|
|
data[i] = value + ('a' - 'A')
|
|
}
|
|
}
|
|
}
|